diff --git a/docs/modules/ROOT/pages/api-core.adoc b/docs/modules/ROOT/pages/api-core.adoc index 20d8b07f1..929359f92 100644 --- a/docs/modules/ROOT/pages/api-core.adoc +++ b/docs/modules/ROOT/pages/api-core.adoc @@ -122,6 +122,7 @@ If any errors are found, the command will exit with a non-zero exit code and pri * `--unsafeAllowRenames` - Configure storage layout check to allow variable renaming. * `--unsafeSkipStorageCheck` - Skips checking for storage layout compatibility errors. This is a dangerous option meant to be used as a last resort. +[[high-level-api]] == High-Level API The high-level API is a programmatic equivalent to the <>. Use this API if you want to validate all of your project's upgradeable contracts from a JavaScript or TypeScript environment. @@ -193,6 +194,8 @@ An object that represents the result of upgrade safety checks and storage layout == Low-Level API +NOTE: This low-level API is deprecated. Use the <> instead. + The low-level API works with https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description[Solidity input and output JSON objects] and lets you perform upgrade safety checks and storage layout comparisons on individual contracts. Use this API if you want to validate specific contracts rather than a whole project. === Prerequisites @@ -229,6 +232,7 @@ constructor UpgradeableContract( unsafeSkipStorageCheck?: boolean, kind?: 'uups' | 'transparent' | 'beacon', }, + solcVersion?: string, ): UpgradeableContract ---- @@ -244,6 +248,7 @@ Creates a new instance of `UpgradeableContract`. ** `unsafeAllow` ** `unsafeAllowRenames` ** `unsafeSkipStorageCheck` +* `solcVersion` - the Solidity version used to compile the implementation contract. TIP: In Hardhat, `solcInput` and `solcOutput` can be obtained from the Build Info file, which itself can be retrieved with `hre.artifacts.getBuildInfo`. diff --git a/docs/modules/ROOT/pages/writing-upgradeable.adoc b/docs/modules/ROOT/pages/writing-upgradeable.adoc index df555c39f..33eae9d9e 100644 --- a/docs/modules/ROOT/pages/writing-upgradeable.adoc +++ b/docs/modules/ROOT/pages/writing-upgradeable.adoc @@ -454,3 +454,14 @@ contract Base { ---- To help determine the proper storage gap size in the new version of your contract, you can simply attempt an upgrade using `upgradeProxy` or just run the validations with `validateUpgrade` (see docs for xref:api-hardhat-upgrades.adoc[Hardhat] or xref:api-truffle-upgrades.adoc[Truffle]). If a storage gap is not being reduced properly, you will see an error message indicating the expected size of the storage gap. + +[[namespaced-storage-layout]] +=== Namespaced Storage Layout + +https://eips.ethereum.org/EIPS/eip-7201[ERC-7201: Namespaced Storage Layout] is another convention that can be used to avoid storage layout errors when modifying base contracts or when changing the inheritance order of contracts. This convention is used in the upgradeable variant of OpenZeppelin Contracts starting with version 5.0. + +This convention involves placing all storage variables of a contract into one or more structs and annotating those structs with `@custom:storage-location erc7201:`. A namespace id is a string that uniquely identifies each namespace in a contract, so the same id must not be defined more than once in a contract or any of its base contracts. + +When using namespaced storage layouts, the OpenZeppelin Upgrades plugins will automatically detect the namespace ids and validate that each change within a namespace during an upgrade is safe according to the same rules as described in <>. + +NOTE: Solidity version 0.8.20 or higher is required in order to use the Upgrades plugins with namespaced storage layouts. The plugins will give an error if they detect `@custom:storage-location` annotations with an older version of Solidity, because older versions of the compiler do not produce sufficient information for validation of namespaced storage layouts. \ No newline at end of file diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 0aecc420d..4784c37ab 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Add validations for namespaced storage layout. ([#876](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/876)) +- Deprecate low-level API. Use [CLI or high-level API](https://docs.openzeppelin.com/upgrades-plugins/1.x/api-core) instead. + ## 1.29.0 (2023-09-19) - Support implementations with upgradeTo or upgradeToAndCall. ([#880](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/880)) diff --git a/packages/core/contracts/test/Namespaced.sol b/packages/core/contracts/test/Namespaced.sol new file mode 100644 index 000000000..8f33d4f67 --- /dev/null +++ b/packages/core/contracts/test/Namespaced.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Example { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesY() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y; + } +} + +contract MultipleNamespaces { + /// @custom:storage-location erc7201:one + struct S1 { + uint256 a; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + } +} + +contract ExampleV2_Ok { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + uint256 z; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesYPlusZ() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y + $.z; + } +} + +contract ExampleV2_Bad { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 y; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getYSquared() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.y * $.y; + } +} + +contract RecursiveStruct { + struct MyStruct { + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + } +} + +contract RecursiveStructV2_Outer_Ok { + struct MyStruct { + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + uint256 z; + } +} + +contract RecursiveStructV2_Bad { + struct MyStruct { + uint128 a; + uint256 b; + uint256 c; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + } +} + +contract MultipleNamespacesAndRegularVariables { + /// @custom:storage-location erc7201:one + struct S1 { + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + uint256 b; + } + + uint128 public a; + uint256 public b; +} + +contract MultipleNamespacesAndRegularVariablesV2_Ok { + /// @custom:storage-location erc7201:one + struct S1 { + uint128 a; + uint256 b; + uint256 c; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + uint256 b; + uint256 c; + } + + uint128 public a; + uint256 public b; + uint256 public c; +} + +contract MultipleNamespacesAndRegularVariablesV2_Bad { + /// @custom:storage-location erc7201:one + struct S1 { + uint256 c; + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint256 c; + uint128 a; + uint256 b; + } + + uint256 public c; + uint128 public a; + uint256 public b; +} \ No newline at end of file diff --git a/packages/core/contracts/test/NamespacedConflicts.sol b/packages/core/contracts/test/NamespacedConflicts.sol new file mode 100644 index 000000000..ca734abc0 --- /dev/null +++ b/packages/core/contracts/test/NamespacedConflicts.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract DuplicateNamespace { + function foo() public pure returns (uint256) { + return 0; + } + + /// @custom:storage-location erc7201:conflicting + struct Conflicting1 { + uint256 b; + } + + /// @custom:storage-location erc7201:conflicting + struct Conflicting2 { + uint256 c; + } + + function foo2() public pure returns (uint256) { + return 0; + } +} + +contract Parent { + function foo5() public pure returns (uint256) { + return 0; + } + + /// @custom:storage-location erc7201:conflicting + struct Conflicting0 { + uint256 a; + } + + function foo6() public pure returns (uint256) { + return 0; + } +} + +contract ConflictsWithParent is Parent { + function foo3() public pure returns (uint256) { + return 0; + } + + /// @custom:storage-location erc7201:conflicting + struct Conflicting { + uint256 a; + } + + function foo4() public pure returns (uint256) { + return 0; + } +} + +contract ConflictsInBothParents is DuplicateNamespace, ConflictsWithParent { + uint256 a; +} + +contract InheritsDuplicate is DuplicateNamespace { +} \ No newline at end of file diff --git a/packages/core/contracts/test/NamespacedConflictsLayout.sol b/packages/core/contracts/test/NamespacedConflictsLayout.sol new file mode 100644 index 000000000..e2fd7b623 --- /dev/null +++ b/packages/core/contracts/test/NamespacedConflictsLayout.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract DuplicateNamespace { + /// @custom:storage-location erc7201:conflicting + struct Conflicting1 { + uint256 b; + } Conflicting1 $Conflicting1; + + /// @custom:storage-location erc7201:conflicting + struct Conflicting2 { + uint256 c; + } Conflicting2 $Conflicting2; +} + +contract Parent { + /// @custom:storage-location erc7201:conflicting + struct Conflicting0 { + uint256 a; + } Conflicting0 $Conflicting0; +} + +contract ConflictsWithParent is Parent { + /// @custom:storage-location erc7201:conflicting + struct Conflicting { + uint256 a; + } Conflicting $Conflicting; +} + +contract ConflictsInBothParents is DuplicateNamespace, ConflictsWithParent { + uint256 a; +} + +contract InheritsDuplicate is DuplicateNamespace { +} \ No newline at end of file diff --git a/packages/core/contracts/test/NamespacedLayout.sol b/packages/core/contracts/test/NamespacedLayout.sol new file mode 100644 index 000000000..bf297f673 --- /dev/null +++ b/packages/core/contracts/test/NamespacedLayout.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Example { + MainStorage $MainStorage; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; +} diff --git a/packages/core/contracts/test/NamespacedOutsideContract.sol b/packages/core/contracts/test/NamespacedOutsideContract.sol new file mode 100644 index 000000000..fdc0a23ea --- /dev/null +++ b/packages/core/contracts/test/NamespacedOutsideContract.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// This is not valid according to ERC-7201 because the namespaced struct is outside of a contract. + +/// @custom:storage-location erc7201:example.main +struct MainStorage { + uint256 x; + uint256 y; +} + +contract Example { + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesY() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y; + } +} diff --git a/packages/core/contracts/test/NamespacedToModify.sol b/packages/core/contracts/test/NamespacedToModify.sol new file mode 100644 index 000000000..91863fc3b --- /dev/null +++ b/packages/core/contracts/test/NamespacedToModify.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Example { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + } + + /// @notice some natspec + function foo() public {} + + /// @param a docs + function foo1(uint a) public {} + + /// @param a docs + function foo2(uint a) internal {} + struct MyStruct { uint b; } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesY() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y; + } + + /// @notice standlone natspec + + /// @notice natspec for var + uint256 normalVar; + + // standalone doc + + /** + * standlone doc block + */ + + /** + * doc block without natspec + */ + function foo3() public {} + + /** + * doc block with natspec + * + * @notice some natspec + */ + function foo4() public {} +} + +contract HasFunction { + constructor(uint) {} + function foo() pure public returns (uint) {} +} + +contract UsingFunction is HasFunction(1) { + uint x = foo(); +} + +function FreeFunctionUsingSelector() pure returns (bytes4) { + return HasFunction.foo.selector; +} + +bytes4 constant CONSTANT_USING_SELECTOR = HasFunction.foo.selector; + +library Lib { + function usingSelector() pure public returns (bytes4) { + return HasFunction.foo.selector; + } + + function plusOne(uint x) pure public returns (uint) { + return x + 1; + } +} + +contract Consumer { + function usingFreeFunction() pure public returns (bytes4) { + return FreeFunctionUsingSelector(); + } + + function usingConstant() pure public returns (bytes4) { + return CONSTANT_USING_SELECTOR; + } + + function usingLibraryFunction() pure public returns (bytes4) { + return Lib.usingSelector(); + } +} + +function plusTwo(uint x) pure returns (uint) { + return x + 2; +} + +using {plusTwo} for uint; + +contract UsingForDirectives { + using {Lib.plusOne} for uint; + + function usingFor(uint x) pure public returns (uint) { + return x.plusOne() + x.plusTwo(); + } +} \ No newline at end of file diff --git a/packages/core/contracts/test/NamespacedToModify07.sol b/packages/core/contracts/test/NamespacedToModify07.sol new file mode 100644 index 000000000..c3fa7058a --- /dev/null +++ b/packages/core/contracts/test/NamespacedToModify07.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; + +contract HasFunction { + constructor(uint) {} + function foo() pure public returns (uint) {} +} + +contract UsingFunction is HasFunction(1) { + uint x = foo(); +} diff --git a/packages/core/contracts/test/NamespacedUDVT.sol b/packages/core/contracts/test/NamespacedUDVT.sol new file mode 100644 index 000000000..b83aebe12 --- /dev/null +++ b/packages/core/contracts/test/NamespacedUDVT.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract NamespacedUDVT { + type MyUserValueType is uint128; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + } +} + +contract NamespacedUDVT_V2_Ok { + type MyUserValueType is uint128; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + uint x; + } +} + +contract NamespacedUDVT_V2_Resize { + type MyUserValueType is uint8; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + uint x; + } +} + +contract NamespacedUDVT_MappingKey_V1 { + type MyUserValueType is bool; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } +} + +contract NamespacedUDVT_MappingKey_V2_Ok { + type MyUserValueType is uint8; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } +} + +contract NamespacedUDVT_MappingKey_V2_Bad { + type MyUserValueType is uint16; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } +} diff --git a/packages/core/contracts/test/NamespacedUDVTLayout.sol b/packages/core/contracts/test/NamespacedUDVTLayout.sol new file mode 100644 index 000000000..e4ecd9773 --- /dev/null +++ b/packages/core/contracts/test/NamespacedUDVTLayout.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract NamespacedUDVT { + type MyUserValueType is uint128; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + } + + MainStorage $MainStorage; +} + +contract NamespacedUDVT_V2_Ok { + type MyUserValueType is uint128; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + uint x; + } + + MainStorage $MainStorage; +} + +contract NamespacedUDVT_V2_Resize { + type MyUserValueType is uint8; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyUserValueType my_user_value; + uint x; + } + + MainStorage $MainStorage; +} + +contract NamespacedUDVT_MappingKey_V1 { + type MyUserValueType is bool; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } + + MainStorage $MainStorage; +} + +contract NamespacedUDVT_MappingKey_V2_Ok { + type MyUserValueType is uint8; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } + + MainStorage $MainStorage; +} + +contract NamespacedUDVT_MappingKey_V2_Bad { + type MyUserValueType is uint16; + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + mapping (MyUserValueType => uint) m1; + mapping (MyUserValueType => uint) m2; + mapping (uint8 => uint) m3; + } + + MainStorage $MainStorage; +} diff --git a/packages/core/hardhat.config.js b/packages/core/hardhat.config.js index 7c049bdd6..4e28f5f8c 100644 --- a/packages/core/hardhat.config.js +++ b/packages/core/hardhat.config.js @@ -21,6 +21,18 @@ const settings = { }, }; +function getNamespacedOverrides() { + const contracts = fs.readdirSync(path.join(__dirname, 'contracts', 'test')); + const namespacedContracts = contracts.filter(c => c.startsWith('Namespaced')); + const overrides = {}; + for (const c of namespacedContracts) { + if (c !== 'NamespacedToModify07.sol') { + overrides[`contracts/test/${c}`] = { version: '0.8.20', settings }; + } + } + return overrides; +} + /** * @type import('hardhat/config').HardhatUserConfig */ @@ -38,6 +50,7 @@ module.exports = { { version: '0.8.8', settings }, { version: '0.8.9', settings }, ], + overrides: getNamespacedOverrides(), }, etherscan: { apiKey: { diff --git a/packages/core/package.json b/packages/core/package.json index b194e9627..fdbbe6d59 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -62,6 +62,6 @@ "ethereumjs-util": "^7.0.3", "minimist": "^1.2.7", "proper-lockfile": "^4.1.1", - "solidity-ast": "^0.4.26" + "solidity-ast": "^0.4.51" } } diff --git a/packages/core/src/cli/validate/build-info-file.test.ts b/packages/core/src/cli/validate/build-info-file.test.ts index 4cbcfaac3..27e7d3501 100644 --- a/packages/core/src/cli/validate/build-info-file.test.ts +++ b/packages/core/src/cli/validate/build-info-file.test.ts @@ -15,6 +15,7 @@ test.afterEach(async () => { }); const BUILD_INFO = { + solcVersion: '0.8.9', input: { language: 'Solidity', sources: { @@ -41,6 +42,7 @@ const BUILD_INFO = { }; const BUILD_INFO_2 = { + solcVersion: '0.8.9', input: { language: 'Solidity', sources: { @@ -67,6 +69,7 @@ const BUILD_INFO_2 = { }; const BUILD_INFO_NO_LAYOUT = { + solcVersion: '0.8.9', input: { language: 'Solidity', sources: { @@ -165,7 +168,7 @@ test.serial('invalid build info file', async t => { await fs.writeFile('invalid-build-info/invalid.json', JSON.stringify({ output: {} })); const error = await t.throwsAsync(getBuildInfoFiles('invalid-build-info')); - t.true(error?.message.includes('must contain Solidity compiler input and output')); + t.true(error?.message.includes('must contain Solidity compiler input, output, and solcVersion')); }); test.serial('dir does not exist', async t => { diff --git a/packages/core/src/cli/validate/build-info-file.ts b/packages/core/src/cli/validate/build-info-file.ts index b0ed0c88a..d98e1124b 100644 --- a/packages/core/src/cli/validate/build-info-file.ts +++ b/packages/core/src/cli/validate/build-info-file.ts @@ -31,6 +31,11 @@ Then recompile your contracts with '${FOUNDRY_COMPILE_COMMAND}' and try again.`; * A build info file containing Solidity compiler input and output JSON objects. */ export interface BuildInfoFile { + /** + * The Solidity compiler version. + */ + solcVersion: string; + /** * The Solidity compiler input JSON object. */ @@ -119,9 +124,13 @@ async function readBuildInfo(buildInfoFilePaths: string[]) { for (const buildInfoFilePath of buildInfoFilePaths) { const buildInfoJson = await readJSON(buildInfoFilePath); - if (buildInfoJson.input === undefined || buildInfoJson.output === undefined) { + if ( + buildInfoJson.input === undefined || + buildInfoJson.output === undefined || + buildInfoJson.solcVersion === undefined + ) { throw new ValidateCommandError( - `Build info file ${buildInfoFilePath} must contain Solidity compiler input and output.`, + `Build info file ${buildInfoFilePath} must contain Solidity compiler input, output, and solcVersion.`, ); } else { if (!hasStorageLayoutSetting(buildInfoJson)) { @@ -134,6 +143,7 @@ async function readBuildInfo(buildInfoFilePaths: string[]) { buildInfoFiles.push({ input: buildInfoJson.input, output: buildInfoJson.output, + solcVersion: buildInfoJson.solcVersion, }); } } diff --git a/packages/core/src/cli/validate/upgradeability-assessment.ts b/packages/core/src/cli/validate/upgradeability-assessment.ts index 23ce7978f..5e2e7c806 100644 --- a/packages/core/src/cli/validate/upgradeability-assessment.ts +++ b/packages/core/src/cli/validate/upgradeability-assessment.ts @@ -1,4 +1,4 @@ -import { getAnnotationArgs, getDocumentation } from '../../utils/annotations'; +import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../../utils/annotations'; import { inferInitializable, inferUUPS } from '../../validate/query'; import { ValidateCommandError } from './error'; import { findContract } from './find-contract'; @@ -89,11 +89,6 @@ function getAndValidateAnnotationArgs(doc: string, tag: string, contract: Source return annotationArgs; } -function hasAnnotationTag(doc: string, tag: string): boolean { - const regex = new RegExp(`^\\s*(@custom:${tag})(\\s|$)`, 'm'); - return regex.test(doc); -} - function getUpgradesFrom(doc: string, contract: SourceContract): string | undefined { const tag = 'oz-upgrades-from'; if (hasAnnotationTag(doc, tag)) { diff --git a/packages/core/src/cli/validate/validations.ts b/packages/core/src/cli/validate/validations.ts index 3d9f5f5a0..7cfe84ce3 100644 --- a/packages/core/src/cli/validate/validations.ts +++ b/packages/core/src/cli/validate/validations.ts @@ -1,4 +1,4 @@ -import { solcInputOutputDecoder, validate, SolcOutput, SolcInput, ValidationRunData } from '../..'; +import { solcInputOutputDecoder, validate, ValidationRunData } from '../..'; import debug from '../../utils/debug'; @@ -18,15 +18,16 @@ export interface SourceContract { export function validateBuildInfoContracts(buildInfoFiles: BuildInfoFile[]): SourceContract[] { const sourceContracts: SourceContract[] = []; for (const buildInfoFile of buildInfoFiles) { - const validations = runValidations(buildInfoFile.input, buildInfoFile.output); + const validations = runValidations(buildInfoFile); addContractsFromBuildInfo(buildInfoFile, validations, sourceContracts); } return sourceContracts; } -function runValidations(solcInput: SolcInput, solcOutput: SolcOutput) { - const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput); - const validation = validate(solcOutput, decodeSrc); +function runValidations(buildInfoFile: BuildInfoFile) { + const { input, output, solcVersion } = buildInfoFile; + const decodeSrc = solcInputOutputDecoder(input, output); + const validation = validate(output, decodeSrc, solcVersion, input); return validation; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 072e8a7b1..e8fe2ab06 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -57,3 +57,5 @@ export { } from './usage-error'; export { ValidateUpgradeSafetyOptions, validateUpgradeSafety, ProjectReport, ReferenceContractNotFound } from './cli'; + +export { makeNamespacedInput } from './utils/make-namespaced'; diff --git a/packages/core/src/levenshtein.ts b/packages/core/src/levenshtein.ts index d4716f0a9..fcfb3d37f 100644 --- a/packages/core/src/levenshtein.ts +++ b/packages/core/src/levenshtein.ts @@ -6,6 +6,13 @@ export type BasicOperation = | { kind: 'delete'; original: T; + } + | { + kind: 'delete-namespace'; + namespace: string; + original: { + contract: string; + }; }; export type Operation = C | BasicOperation; diff --git a/packages/core/src/solc-api.ts b/packages/core/src/solc-api.ts index 3605e239c..7b5cb92af 100644 --- a/packages/core/src/solc-api.ts +++ b/packages/core/src/solc-api.ts @@ -22,6 +22,13 @@ export interface SolcOutput { id: number; }; }; + errors?: { + severity: 'error' | 'warning'; + formattedMessage: string; + sourceLocation?: { + file: string; + }; + }[]; } export interface SolcInput { @@ -30,6 +37,13 @@ export interface SolcInput { content?: string; }; }; + settings?: { + outputSelection?: { + [file in string]: { + [contract in string]: string[]; + }; + }; + }; } export type SolcLinkReferences = { diff --git a/packages/core/src/standalone.test.ts b/packages/core/src/standalone.test.ts index 68957ad39..9a99dec43 100644 --- a/packages/core/src/standalone.test.ts +++ b/packages/core/src/standalone.test.ts @@ -61,3 +61,29 @@ test('dont report renamed version update', t => { const goodReport2 = v2.getStorageUpgradeReport(v3); t.true(goodReport2.ok); }); + +test('namespaced output', async t => { + const buildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example'); + if (buildInfo === undefined) { + throw new Error('Build info not found'); + } + + const impl = new UpgradeableContract('Example', buildInfo.input, buildInfo.output, {}, '0.8.20'); + const report = impl.getErrorReport(); + t.true(report.ok); +}); + +test('namespaced output without version', async t => { + const buildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example'); + if (buildInfo === undefined) { + throw new Error('Build info not found'); + } + + const error = t.throws(() => new UpgradeableContract('Example', buildInfo.input, buildInfo.output)); + t.assert( + error?.message.includes( + `contracts/test/Namespaced.sol: Namespace annotations require Solidity version >= 0.8.20, but no solcVersion parameter was provided`, + ), + error?.message, + ); +}); diff --git a/packages/core/src/standalone.ts b/packages/core/src/standalone.ts index b04b9d7b7..958d650d7 100644 --- a/packages/core/src/standalone.ts +++ b/packages/core/src/standalone.ts @@ -18,6 +18,9 @@ export interface Report { explain(color?: boolean): string; } +/** + * @deprecated Use `validateUpgradeSafety` instead. + */ export class UpgradeableContract { readonly version: Version; readonly errors: ValidationError[]; @@ -28,11 +31,12 @@ export class UpgradeableContract { solcInput: SolcInput, solcOutput: SolcOutput, opts: ValidationOptions = {}, + solcVersion?: string, ) { const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput); - const validation = validate(solcOutput, decodeSrc); + const validation = validate(solcOutput, decodeSrc, solcVersion, solcInput); this.version = getContractVersion(validation, name); - this.errors = getErrors(validation, this.version, opts); + this.errors = getErrors(validation, this.version, withValidationDefaults(opts)); this.layout = getStorageLayout(validation, this.version); } diff --git a/packages/core/src/storage-0.8.test.ts.md b/packages/core/src/storage-0.8.test.ts.md index fa8238226..e6a80337f 100644 --- a/packages/core/src/storage-0.8.test.ts.md +++ b/packages/core/src/storage-0.8.test.ts.md @@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Storage088', @@ -46,6 +47,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Storage089', @@ -83,6 +85,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Storage089', @@ -117,6 +120,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'StorageRenamedRetyped', diff --git a/packages/core/src/storage-0.8.test.ts.snap b/packages/core/src/storage-0.8.test.ts.snap index 46d74fd7a..e4c55d8de 100644 Binary files a/packages/core/src/storage-0.8.test.ts.snap and b/packages/core/src/storage-0.8.test.ts.snap differ diff --git a/packages/core/src/storage-memory-0.5.test.ts.md b/packages/core/src/storage-memory-0.5.test.ts.md index 88178cc19..3bfb5ce09 100644 --- a/packages/core/src/storage-memory-0.5.test.ts.md +++ b/packages/core/src/storage-memory-0.5.test.ts.md @@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Memory05', @@ -94,6 +95,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Memory08', diff --git a/packages/core/src/storage-memory-0.5.test.ts.snap b/packages/core/src/storage-memory-0.5.test.ts.snap index 83fb54287..993bc161f 100644 Binary files a/packages/core/src/storage-memory-0.5.test.ts.snap and b/packages/core/src/storage-memory-0.5.test.ts.snap differ diff --git a/packages/core/src/storage-namespaced-conflicts-layout.test.ts b/packages/core/src/storage-namespaced-conflicts-layout.test.ts new file mode 100644 index 000000000..aa33ddc7b --- /dev/null +++ b/packages/core/src/storage-namespaced-conflicts-layout.test.ts @@ -0,0 +1,96 @@ +import _test, { TestFn } from 'ava'; +import { ContractDefinition } from 'solidity-ast'; +import { findAll, astDereferencer } from 'solidity-ast/utils'; +import { artifacts } from 'hardhat'; + +import { SolcOutput } from './solc-api'; +import { StorageLayout } from './storage/layout'; +import { extractStorageLayout } from './storage/extract'; +import { solcInputOutputDecoder } from './src-decoder'; + +interface Context { + extractStorageLayout: (contract: string) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + const origBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedConflicts.sol:DuplicateNamespace'); + const namespacedBuildInfo = await artifacts.getBuildInfo( + 'contracts/test/NamespacedConflictsLayout.sol:DuplicateNamespace', + ); + + if (origBuildInfo === undefined || namespacedBuildInfo === undefined) { + throw new Error('Build info not found'); + } + + const origSolcOutput: SolcOutput = origBuildInfo.output; + const origContracts: Record = {}; + const origStorageLayouts: Record = {}; + + const namespacedSolcOutput: SolcOutput = namespacedBuildInfo.output; + const namespacedContracts: Record = {}; + const namespacedStorageLayouts: Record = {}; + + const origContractDefs = []; + for (const def of findAll( + 'ContractDefinition', + origSolcOutput.sources['contracts/test/NamespacedConflicts.sol'].ast, + )) { + origContractDefs.push(def); + } + const namespacedContractDefs = []; + for (const def of findAll( + 'ContractDefinition', + namespacedSolcOutput.sources['contracts/test/NamespacedConflictsLayout.sol'].ast, + )) { + namespacedContractDefs.push(def); + } + + for (let i = 0; i < origContractDefs.length; i++) { + const origContractDef = origContractDefs[i]; + const namespacedContractDef = namespacedContractDefs[i]; + + origContracts[origContractDef.name] = origContractDef; + namespacedContracts[namespacedContractDef.name] = namespacedContractDef; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + origStorageLayouts[origContractDef.name] = + origSolcOutput.contracts['contracts/test/NamespacedConflicts.sol'][origContractDef.name].storageLayout!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + namespacedStorageLayouts[namespacedContractDef.name] = + namespacedSolcOutput.contracts['contracts/test/NamespacedConflictsLayout.sol'][ + namespacedContractDef.name + ].storageLayout!; + } + const origDeref = astDereferencer(origSolcOutput); + const namespacedDeref = astDereferencer(namespacedSolcOutput); + + const decodeSrc = solcInputOutputDecoder(origBuildInfo.input, origSolcOutput); + t.context.extractStorageLayout = name => + extractStorageLayout(origContracts[name], decodeSrc, origDeref, origStorageLayouts[name], { + deref: namespacedDeref, + contractDef: namespacedContracts[name], + storageLayout: namespacedStorageLayouts[name], + }); +}); + +test('duplicate namespace', t => { + const error = t.throws(() => t.context.extractStorageLayout('DuplicateNamespace')); + t.snapshot(error?.message); +}); + +test('inherits duplicate', t => { + const error = t.throws(() => t.context.extractStorageLayout('InheritsDuplicate')); + t.snapshot(error?.message); +}); + +test('conflicts with parent', t => { + const error = t.throws(() => t.context.extractStorageLayout('ConflictsWithParent')); + t.snapshot(error?.message); +}); + +test('conflicts in both parents', t => { + const error = t.throws(() => t.context.extractStorageLayout('ConflictsInBothParents')); + t.snapshot(error?.message); +}); diff --git a/packages/core/src/storage-namespaced-conflicts-layout.test.ts.md b/packages/core/src/storage-namespaced-conflicts-layout.test.ts.md new file mode 100644 index 000000000..b10662cc3 --- /dev/null +++ b/packages/core/src/storage-namespaced-conflicts-layout.test.ts.md @@ -0,0 +1,53 @@ +# Snapshot report for `src/storage-namespaced-conflicts-layout.test.ts` + +The actual snapshot is saved in `storage-namespaced-conflicts-layout.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## duplicate namespace + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract DuplicateNamespace␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:10␊ + - contracts/test/NamespacedConflicts.sol:15␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## inherits duplicate + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract InheritsDuplicate␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:10␊ + - contracts/test/NamespacedConflicts.sol:15␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## conflicts with parent + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract ConflictsWithParent␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:45␊ + - contracts/test/NamespacedConflicts.sol:30␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## conflicts in both parents + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract ConflictsInBothParents␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:45␊ + - contracts/test/NamespacedConflicts.sol:30␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` diff --git a/packages/core/src/storage-namespaced-conflicts-layout.test.ts.snap b/packages/core/src/storage-namespaced-conflicts-layout.test.ts.snap new file mode 100644 index 000000000..1ac895a84 Binary files /dev/null and b/packages/core/src/storage-namespaced-conflicts-layout.test.ts.snap differ diff --git a/packages/core/src/storage-namespaced-conflicts.test.ts b/packages/core/src/storage-namespaced-conflicts.test.ts new file mode 100644 index 000000000..b4f7ba41a --- /dev/null +++ b/packages/core/src/storage-namespaced-conflicts.test.ts @@ -0,0 +1,54 @@ +import _test, { TestFn } from 'ava'; +import { ContractDefinition } from 'solidity-ast'; +import { findAll, astDereferencer } from 'solidity-ast/utils'; +import { artifacts } from 'hardhat'; + +import { SolcOutput } from './solc-api'; +import { StorageLayout } from './storage/layout'; +import { extractStorageLayout } from './storage/extract'; +import { solcInputOutputDecoder } from './src-decoder'; + +interface Context { + extractStorageLayout: (contract: string) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + const buildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedConflicts.sol:DuplicateNamespace'); + if (buildInfo === undefined) { + throw new Error('Build info not found'); + } + const solcOutput: SolcOutput = buildInfo.output; + const contracts: Record = {}; + const storageLayouts: Record = {}; + for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/NamespacedConflicts.sol'].ast)) { + contracts[def.name] = def; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + storageLayouts[def.name] = solcOutput.contracts['contracts/test/NamespacedConflicts.sol'][def.name].storageLayout!; + } + const deref = astDereferencer(solcOutput); + const decodeSrc = solcInputOutputDecoder(buildInfo.input, solcOutput); + t.context.extractStorageLayout = name => + extractStorageLayout(contracts[name], decodeSrc, deref, storageLayouts[name]); +}); + +test('duplicate namespace', t => { + const error = t.throws(() => t.context.extractStorageLayout('DuplicateNamespace')); + t.snapshot(error?.message); +}); + +test('inherits duplicate', t => { + const error = t.throws(() => t.context.extractStorageLayout('InheritsDuplicate')); + t.snapshot(error?.message); +}); + +test('conflicts with parent', t => { + const error = t.throws(() => t.context.extractStorageLayout('ConflictsWithParent')); + t.snapshot(error?.message); +}); + +test('conflicts in both parents', t => { + const error = t.throws(() => t.context.extractStorageLayout('ConflictsInBothParents')); + t.snapshot(error?.message); +}); diff --git a/packages/core/src/storage-namespaced-conflicts.test.ts.md b/packages/core/src/storage-namespaced-conflicts.test.ts.md new file mode 100644 index 000000000..9be0d4c26 --- /dev/null +++ b/packages/core/src/storage-namespaced-conflicts.test.ts.md @@ -0,0 +1,53 @@ +# Snapshot report for `src/storage-namespaced-conflicts.test.ts` + +The actual snapshot is saved in `storage-namespaced-conflicts.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## duplicate namespace + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract DuplicateNamespace␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:10␊ + - contracts/test/NamespacedConflicts.sol:15␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## inherits duplicate + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract InheritsDuplicate␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:10␊ + - contracts/test/NamespacedConflicts.sol:15␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## conflicts with parent + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract ConflictsWithParent␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:45␊ + - contracts/test/NamespacedConflicts.sol:30␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` + +## conflicts in both parents + +> Snapshot 1 + + `Namespace erc7201:conflicting is defined multiple times for contract ConflictsInBothParents␊ + ␊ + The namespace erc7201:conflicting was found in structs at the following locations:␊ + - contracts/test/NamespacedConflicts.sol:45␊ + - contracts/test/NamespacedConflicts.sol:30␊ + ␊ + Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.` diff --git a/packages/core/src/storage-namespaced-conflicts.test.ts.snap b/packages/core/src/storage-namespaced-conflicts.test.ts.snap new file mode 100644 index 000000000..1ac895a84 Binary files /dev/null and b/packages/core/src/storage-namespaced-conflicts.test.ts.snap differ diff --git a/packages/core/src/storage-namespaced-layout.test.ts b/packages/core/src/storage-namespaced-layout.test.ts new file mode 100644 index 000000000..232db729c --- /dev/null +++ b/packages/core/src/storage-namespaced-layout.test.ts @@ -0,0 +1,77 @@ +import _test, { TestFn } from 'ava'; +import { ContractDefinition } from 'solidity-ast'; +import { findAll, astDereferencer } from 'solidity-ast/utils'; +import { artifacts } from 'hardhat'; + +import { SolcOutput } from './solc-api'; +import { StorageLayout } from './storage/layout'; +import { extractStorageLayout } from './storage/extract'; +import { stabilizeStorageLayout } from './utils/stabilize-layout'; +import { solcInputOutputDecoder } from './src-decoder'; + +interface Context { + extractStorageLayout: (contract: string) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + // Tests extracting the storage layout (to include slot and offset) using Namespaced.sol's Example as the original contract, + // and NamespacedLayout.sol's Example as the modified contract with the storage layout. + const origBuildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example'); + const namespacedBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedLayout.sol:Example'); + + if (origBuildInfo === undefined || namespacedBuildInfo === undefined) { + throw new Error('Build info not found'); + } + + const origSolcOutput: SolcOutput = origBuildInfo.output; + const origContracts: Record = {}; + const origStorageLayouts: Record = {}; + + const namespacedSolcOutput: SolcOutput = namespacedBuildInfo.output; + const namespacedContracts: Record = {}; + const namespacedStorageLayouts: Record = {}; + + const origContractDefs = []; + for (const def of findAll('ContractDefinition', origSolcOutput.sources['contracts/test/Namespaced.sol'].ast)) { + origContractDefs.push(def); + } + const namespacedContractDefs = []; + for (const def of findAll( + 'ContractDefinition', + namespacedSolcOutput.sources['contracts/test/NamespacedLayout.sol'].ast, + )) { + namespacedContractDefs.push(def); + } + + // Expects the first contract in Namespaced.sol and NamespacedLayout.sol to be 'Example' + const origContractDef = origContractDefs[0]; + const namespacedContractDef = namespacedContractDefs[0]; + + origContracts[origContractDef.name] = origContractDef; + namespacedContracts[namespacedContractDef.name] = namespacedContractDef; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + origStorageLayouts[origContractDef.name] = + origSolcOutput.contracts['contracts/test/Namespaced.sol'][origContractDef.name].storageLayout!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + namespacedStorageLayouts[namespacedContractDef.name] = + namespacedSolcOutput.contracts['contracts/test/NamespacedLayout.sol'][namespacedContractDef.name].storageLayout!; + + const origDeref = astDereferencer(origSolcOutput); + const namespacedDeref = astDereferencer(namespacedSolcOutput); + + const decodeSrc = solcInputOutputDecoder(origBuildInfo.input, origSolcOutput); + t.context.extractStorageLayout = name => + extractStorageLayout(origContracts[name], decodeSrc, origDeref, origStorageLayouts[name], { + deref: namespacedDeref, + contractDef: namespacedContracts[name], + storageLayout: namespacedStorageLayouts[name], + }); +}); + +test('layout', t => { + const layout = t.context.extractStorageLayout('Example'); + t.snapshot(stabilizeStorageLayout(layout)); +}); diff --git a/packages/core/src/storage-namespaced-layout.test.ts.md b/packages/core/src/storage-namespaced-layout.test.ts.md new file mode 100644 index 000000000..3be2128b7 --- /dev/null +++ b/packages/core/src/storage-namespaced-layout.test.ts.md @@ -0,0 +1,67 @@ +# Snapshot report for `src/storage-namespaced-layout.test.ts` + +The actual snapshot is saved in `storage-namespaced-layout.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## layout + +> Snapshot 1 + + { + namespaces: [ + [ + 'erc7201:example.main', + [ + { + contract: 'Example', + label: 'x', + offset: 0, + slot: '0', + src: 'contracts/test/Namespaced.sol:7', + type: 't_uint256', + }, + { + contract: 'Example', + label: 'y', + offset: 0, + slot: '1', + src: 'contracts/test/Namespaced.sol:8', + type: 't_uint256', + }, + ], + ], + ], + storage: [], + types: [ + [ + 't_struct(MainStorage)storage', + { + label: 'struct Example.MainStorage', + members: [ + { + label: 'x', + offset: 0, + slot: '0', + type: 't_uint256', + }, + { + label: 'y', + offset: 0, + slot: '1', + type: 't_uint256', + }, + ], + numberOfBytes: '64', + }, + ], + [ + 't_uint256', + { + label: 'uint256', + members: undefined, + numberOfBytes: '32', + }, + ], + ], + } diff --git a/packages/core/src/storage-namespaced-layout.test.ts.snap b/packages/core/src/storage-namespaced-layout.test.ts.snap new file mode 100644 index 000000000..d2368085c Binary files /dev/null and b/packages/core/src/storage-namespaced-layout.test.ts.snap differ diff --git a/packages/core/src/storage-namespaced-outside-contract.test.ts b/packages/core/src/storage-namespaced-outside-contract.test.ts new file mode 100644 index 000000000..98f6a2662 --- /dev/null +++ b/packages/core/src/storage-namespaced-outside-contract.test.ts @@ -0,0 +1,24 @@ +import test from 'ava'; +import { artifacts } from 'hardhat'; + +import { validate } from './validate'; +import { solcInputOutputDecoder } from './src-decoder'; + +test('namespace outside contract', async t => { + const contract = 'contracts/test/NamespacedOutsideContract.sol:Example'; + + const buildInfo = await artifacts.getBuildInfo(contract); + if (buildInfo === undefined) { + throw new Error(`Build info not found for contract ${contract}`); + } + const solcOutput = buildInfo.output; + const solcInput = buildInfo.input; + const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput); + const error = t.throws(() => validate(solcOutput, decodeSrc)); + t.assert( + error?.message.includes( + 'contracts/test/NamespacedOutsideContract.sol:7: Namespace struct MainStorage is defined outside of a contract', + ), + error?.message, + ); +}); diff --git a/packages/core/src/storage-namespaced-solc-versions.test.ts b/packages/core/src/storage-namespaced-solc-versions.test.ts new file mode 100644 index 000000000..bb6cb8e24 --- /dev/null +++ b/packages/core/src/storage-namespaced-solc-versions.test.ts @@ -0,0 +1,59 @@ +import _test, { TestFn } from 'ava'; +import { artifacts } from 'hardhat'; + +import { validate } from './validate'; +import { solcInputOutputDecoder } from './src-decoder'; + +interface Context { + validate: (solcVersion?: string) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + const contract = 'contracts/test/Namespaced.sol:Example'; + + const buildInfo = await artifacts.getBuildInfo(contract); + if (buildInfo === undefined) { + throw new Error(`Build info not found for contract ${contract}`); + } + const solcOutput = buildInfo.output; + const solcInput = buildInfo.input; + const decodeSrc = solcInputOutputDecoder(solcInput, solcOutput); + + t.context.validate = solcVersion => validate(solcOutput, decodeSrc, solcVersion, solcInput); +}); + +test('namespace with older solc version', async t => { + const { validate } = t.context; + const error = t.throws(() => validate('0.8.19')); + t.assert( + error?.message.includes( + `contracts/test/Namespaced.sol: Namespace annotations require Solidity version >= 0.8.20, but 0.8.19 was used`, + ), + error?.message, + ); +}); + +test('namespace with correct solc version', async t => { + const { validate } = t.context; + validate('0.8.20'); + t.pass(); +}); + +test('namespace with newer solc version', async t => { + const { validate } = t.context; + validate('0.8.21'); + t.pass(); +}); + +test('namespace with no solc version', async t => { + const { validate } = t.context; + const error = t.throws(() => validate(undefined)); + t.assert( + error?.message.includes( + `contracts/test/Namespaced.sol: Namespace annotations require Solidity version >= 0.8.20, but no solcVersion parameter was provided`, + ), + error?.message, + ); +}); diff --git a/packages/core/src/storage-namespaced-udvt.test.ts b/packages/core/src/storage-namespaced-udvt.test.ts new file mode 100644 index 000000000..e9500b3b5 --- /dev/null +++ b/packages/core/src/storage-namespaced-udvt.test.ts @@ -0,0 +1,160 @@ +import _test, { TestFn } from 'ava'; +import { ContractDefinition } from 'solidity-ast'; +import { findAll, astDereferencer } from 'solidity-ast/utils'; +import { artifacts } from 'hardhat'; + +import { SolcOutput } from './solc-api'; +import { getStorageUpgradeErrors } from './storage'; +import { StorageLayout } from './storage/layout'; +import { extractStorageLayout } from './storage/extract'; +import { solcInputOutputDecoder } from './src-decoder'; + +interface Context { + extractStorageLayout: (contract: string, layoutInfo: boolean) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + const origBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedUDVT.sol:NamespacedUDVT'); + const namespacedBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedUDVTLayout.sol:NamespacedUDVT'); + + if (origBuildInfo === undefined || namespacedBuildInfo === undefined) { + throw new Error('Build info not found'); + } + + const origSolcOutput: SolcOutput = origBuildInfo.output; + const origContracts: Record = {}; + const origStorageLayouts: Record = {}; + + const namespacedSolcOutput: SolcOutput = namespacedBuildInfo.output; + const namespacedContracts: Record = {}; + const namespacedStorageLayouts: Record = {}; + + const origContractDefs = []; + for (const def of findAll('ContractDefinition', origSolcOutput.sources['contracts/test/NamespacedUDVT.sol'].ast)) { + origContractDefs.push(def); + } + const namespacedContractDefs = []; + for (const def of findAll( + 'ContractDefinition', + namespacedSolcOutput.sources['contracts/test/NamespacedUDVTLayout.sol'].ast, + )) { + namespacedContractDefs.push(def); + } + + for (let i = 0; i < origContractDefs.length; i++) { + const origContractDef = origContractDefs[i]; + const namespacedContractDef = namespacedContractDefs[i]; + + origContracts[origContractDef.name] = origContractDef; + namespacedContracts[namespacedContractDef.name] = namespacedContractDef; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + origStorageLayouts[origContractDef.name] = + origSolcOutput.contracts['contracts/test/NamespacedUDVT.sol'][origContractDef.name].storageLayout!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + namespacedStorageLayouts[namespacedContractDef.name] = + namespacedSolcOutput.contracts['contracts/test/NamespacedUDVTLayout.sol'][ + namespacedContractDef.name + ].storageLayout!; + } + const origDeref = astDereferencer(origSolcOutput); + const namespacedDeref = astDereferencer(namespacedSolcOutput); + + const decodeSrc = solcInputOutputDecoder(origBuildInfo.input, origSolcOutput); + t.context.extractStorageLayout = (name, layoutInfo) => + extractStorageLayout( + origContracts[name], + decodeSrc, + origDeref, + origStorageLayouts[name], + layoutInfo + ? { + deref: namespacedDeref, + contractDef: namespacedContracts[name], + storageLayout: namespacedStorageLayouts[name], + } + : undefined, + ); +}); + +test('user defined value types - layout info', async t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT', true); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_V2_Ok', true); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('user defined value types - no layout info', async t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT', false); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_V2_Ok', false); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('user defined value types - layout info - bad underlying type', async t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT', true); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_V2_Resize', true); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 1, + 0: { + kind: 'typechange', + change: { + kind: 'type resize', + }, + original: { label: 'my_user_value' }, + updated: { label: 'my_user_value' }, + }, + }); +}); + +test('user defined value types - no layout info - bad underlying type', async t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT', false); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_V2_Resize', false); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 1, + 0: { + kind: 'typechange', + change: { + kind: 'unknown', + }, + original: { label: 'my_user_value' }, + updated: { label: 'my_user_value' }, + }, + }); +}); + +test('mapping with user defined value type key - ok', t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT_MappingKey_V1', true); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_MappingKey_V2_Ok', true); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('mapping with user defined value type key - bad', t => { + const v1 = t.context.extractStorageLayout('NamespacedUDVT_MappingKey_V1', true); + const v2 = t.context.extractStorageLayout('NamespacedUDVT_MappingKey_V2_Bad', true); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 2, + 0: { + kind: 'typechange', + change: { + kind: 'mapping key', + }, + original: { label: 'm1' }, + updated: { label: 'm1' }, + }, + 1: { + kind: 'typechange', + change: { + kind: 'mapping key', + }, + original: { label: 'm2' }, + updated: { label: 'm2' }, + }, + }); +}); diff --git a/packages/core/src/storage-namespaced.test.ts b/packages/core/src/storage-namespaced.test.ts new file mode 100644 index 000000000..33204b0bc --- /dev/null +++ b/packages/core/src/storage-namespaced.test.ts @@ -0,0 +1,146 @@ +import _test, { TestFn } from 'ava'; +import { ContractDefinition } from 'solidity-ast'; +import { findAll, astDereferencer } from 'solidity-ast/utils'; +import { artifacts } from 'hardhat'; + +import { SolcOutput } from './solc-api'; +import { getStorageUpgradeErrors } from './storage'; +import { StorageLayout } from './storage/layout'; +import { extractStorageLayout } from './storage/extract'; +import { stabilizeStorageLayout } from './utils/stabilize-layout'; + +interface Context { + extractStorageLayout: (contract: string) => ReturnType; +} + +const test = _test as TestFn; + +test.before(async t => { + const buildInfo = await artifacts.getBuildInfo('contracts/test/Namespaced.sol:Example'); + if (buildInfo === undefined) { + throw new Error('Build info not found'); + } + const solcOutput: SolcOutput = buildInfo.output; + const contracts: Record = {}; + const storageLayouts: Record = {}; + for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/Namespaced.sol'].ast)) { + contracts[def.name] = def; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + storageLayouts[def.name] = solcOutput.contracts['contracts/test/Namespaced.sol'][def.name].storageLayout!; + } + const deref = astDereferencer(solcOutput); + t.context.extractStorageLayout = name => + extractStorageLayout(contracts[name], dummyDecodeSrc, deref, storageLayouts[name]); +}); + +const dummyDecodeSrc = () => 'file.sol:1'; + +test('layout', t => { + const layout = t.context.extractStorageLayout('Example'); + t.snapshot(stabilizeStorageLayout(layout)); +}); + +test('multiple namespaces', t => { + const layout = t.context.extractStorageLayout('MultipleNamespaces'); + t.snapshot(stabilizeStorageLayout(layout)); +}); + +test('namespaced upgrade ok', t => { + const v1 = t.context.extractStorageLayout('Example'); + const v2 = t.context.extractStorageLayout('ExampleV2_Ok'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('namespaced upgrade bad', t => { + const v1 = t.context.extractStorageLayout('Example'); + const v2 = t.context.extractStorageLayout('ExampleV2_Bad'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 1, + 0: { + kind: 'delete', + original: { + contract: 'Example', + label: 'x', + type: { + id: 't_uint256', + }, + }, + }, + }); +}); + +test('recursive struct outer ok', t => { + const v1 = t.context.extractStorageLayout('RecursiveStruct'); + const v2 = t.context.extractStorageLayout('RecursiveStructV2_Outer_Ok'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('recursive struct bad', t => { + const v1 = t.context.extractStorageLayout('RecursiveStruct'); + const v2 = t.context.extractStorageLayout('RecursiveStructV2_Bad'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 1, + 0: { + kind: 'typechange', + change: { + kind: 'struct members', + ops: { + length: 1, + 0: { kind: 'append' }, + }, + }, + original: { label: 's' }, + updated: { label: 's' }, + }, + }); +}); + +test('multiple namespaces and regular variables ok', t => { + const v1 = t.context.extractStorageLayout('MultipleNamespacesAndRegularVariables'); + const v2 = t.context.extractStorageLayout('MultipleNamespacesAndRegularVariablesV2_Ok'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.deepEqual(comparison, []); +}); + +test('multiple namespaces and regular variables bad', t => { + const v1 = t.context.extractStorageLayout('MultipleNamespacesAndRegularVariables'); + const v2 = t.context.extractStorageLayout('MultipleNamespacesAndRegularVariablesV2_Bad'); + const comparison = getStorageUpgradeErrors(v1, v2); + t.like(comparison, { + length: 5, + 0: { + kind: 'insert', + updated: { + label: 'c', + }, + }, + 1: { + kind: 'layoutchange', + updated: { + label: 'a', // layout available for regular variable outside of namespace + }, + }, + 2: { + kind: 'layoutchange', + updated: { + label: 'b', // layout available for regular variable outside of namespace + }, + }, + 3: { + kind: 'insert', + updated: { + label: 'c', + }, + }, + 4: { + kind: 'insert', + updated: { + label: 'c', + }, + }, + }); +}); diff --git a/packages/core/src/storage-namespaced.test.ts.md b/packages/core/src/storage-namespaced.test.ts.md new file mode 100644 index 000000000..4eecc971b --- /dev/null +++ b/packages/core/src/storage-namespaced.test.ts.md @@ -0,0 +1,89 @@ +# Snapshot report for `src/storage-namespaced.test.ts` + +The actual snapshot is saved in `storage-namespaced.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## layout + +> Snapshot 1 + + { + namespaces: [ + [ + 'erc7201:example.main', + [ + { + contract: 'Example', + label: 'x', + src: 'file.sol:1', + type: 't_uint256', + }, + { + contract: 'Example', + label: 'y', + src: 'file.sol:1', + type: 't_uint256', + }, + ], + ], + ], + storage: [], + types: [ + [ + 't_uint256', + { + label: 'uint256', + members: undefined, + }, + ], + ], + } + +## multiple namespaces + +> Snapshot 1 + + { + namespaces: [ + [ + 'erc7201:one', + [ + { + contract: 'MultipleNamespaces', + label: 'a', + src: 'file.sol:1', + type: 't_uint256', + }, + ], + ], + [ + 'erc7201:two', + [ + { + contract: 'MultipleNamespaces', + label: 'a', + src: 'file.sol:1', + type: 't_uint128', + }, + ], + ], + ], + storage: [], + types: [ + [ + 't_uint256', + { + label: 'uint256', + members: undefined, + }, + ], + [ + 't_uint128', + { + label: 'uint128', + members: undefined, + }, + ], + ], + } diff --git a/packages/core/src/storage-namespaced.test.ts.snap b/packages/core/src/storage-namespaced.test.ts.snap new file mode 100644 index 000000000..c15ed47aa Binary files /dev/null and b/packages/core/src/storage-namespaced.test.ts.snap differ diff --git a/packages/core/src/storage.test.ts.md b/packages/core/src/storage.test.ts.md index 6090e763a..edd60a9db 100644 --- a/packages/core/src/storage.test.ts.md +++ b/packages/core/src/storage.test.ts.md @@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Storage1', @@ -92,6 +93,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + namespaces: [], storage: [ { contract: 'Storage2', diff --git a/packages/core/src/storage.test.ts.snap b/packages/core/src/storage.test.ts.snap index b62568346..dd6917242 100644 Binary files a/packages/core/src/storage.test.ts.snap and b/packages/core/src/storage.test.ts.snap differ diff --git a/packages/core/src/storage/compare.ts b/packages/core/src/storage/compare.ts index 5de061f2a..2e3f67b4a 100644 --- a/packages/core/src/storage/compare.ts +++ b/packages/core/src/storage/compare.ts @@ -108,8 +108,40 @@ export class StorageLayoutComparator { readonly unsafeAllowRenames = false, ) {} - compareLayouts(original: StorageItem[], updated: StorageItem[]): LayoutCompatibilityReport { - return new LayoutCompatibilityReport(this.layoutLevenshtein(original, updated, { allowAppend: true })); + compareLayouts( + original: StorageItem[], + updated: StorageItem[], + originalNamespaces?: Record, + updatedNamespaces?: Record, + ): LayoutCompatibilityReport { + const ops = this.layoutLevenshtein(original, updated, { allowAppend: true }); + const namespacedOps = this.getNamespacedStorageOperations(originalNamespaces, updatedNamespaces); + + return new LayoutCompatibilityReport([...ops, ...namespacedOps]); + } + + private getNamespacedStorageOperations( + originalNamespaces?: Record, + updatedNamespaces?: Record, + ) { + const ops: StorageOperation[] = []; + if (originalNamespaces !== undefined) { + for (const [storageLocation, origNamespacedLayout] of Object.entries(originalNamespaces)) { + const updatedNamespacedLayout = updatedNamespaces?.[storageLocation]; + if (updatedNamespacedLayout !== undefined) { + ops.push(...this.layoutLevenshtein(origNamespacedLayout, updatedNamespacedLayout, { allowAppend: true })); + } else if (origNamespacedLayout.length > 0) { + ops.push({ + kind: 'delete-namespace', + namespace: storageLocation, + original: { + contract: origNamespacedLayout[0].contract, + }, + }); + } + } + } + return ops; } private layoutLevenshtein( diff --git a/packages/core/src/storage/extract.ts b/packages/core/src/storage/extract.ts index d5214914b..6bb35b82b 100644 --- a/packages/core/src/storage/extract.ts +++ b/packages/core/src/storage/extract.ts @@ -1,18 +1,20 @@ -import assert from 'assert'; +import assert from 'assert/strict'; import { ContractDefinition, StructDefinition, EnumDefinition, TypeDescriptions, VariableDeclaration, + TypeName, } from 'solidity-ast'; import { isNodeType, findAll, ASTDereferencer } from 'solidity-ast/utils'; -import { StorageLayout, TypeItem } from './layout'; +import { StorageLayout, StructMember, TypeItem, isStructMembers, EnumMember } from './layout'; import { normalizeTypeIdentifier } from '../utils/type-id'; import { SrcDecoder } from '../src-decoder'; import { mapValues } from '../utils/map-values'; import { pick } from '../utils/pick'; import { execall } from '../utils/execall'; +import { loadNamespaces } from './namespace'; const currentLayoutVersion = '1.2'; @@ -20,31 +22,53 @@ export function isCurrentLayoutVersion(layout: StorageLayout): boolean { return layout?.layoutVersion === currentLayoutVersion; } +export interface CompilationContext { + deref: ASTDereferencer; + contractDef: ContractDefinition; + storageLayout?: StorageLayout; +} + export function extractStorageLayout( contractDef: ContractDefinition, decodeSrc: SrcDecoder, deref: ASTDereferencer, - storageLayout?: StorageLayout | undefined, + storageLayout?: StorageLayout, + namespacedContext?: CompilationContext, ): StorageLayout { const layout: StorageLayout = { storage: [], types: {}, layoutVersion: currentLayoutVersion, flat: false }; - if (storageLayout !== undefined) { - layout.types = mapValues(storageLayout.types, m => { - return { - label: m.label, - members: m.members?.map(m => - typeof m === 'string' ? m : pick(m, ['label', 'type', 'offset', 'slot']), - ) as TypeItem['members'], - numberOfBytes: m.numberOfBytes, - }; - }); + // The namespaced context will contain the types of namespaces that may not be included + // in the original storage layout. + // Some types will be present in both and they must be exactly equivalent. + // If they are not, it fails an assertion because this may be a clash between different types. + const combinedTypes = { ...namespacedContext?.storageLayout?.types, ...storageLayout?.types }; + if (namespacedContext?.storageLayout?.types) { + for (const t in storageLayout?.types) { + if (t in namespacedContext.storageLayout.types) { + assert.deepEqual(namespacedContext.storageLayout.types[t], storageLayout?.types[t]); + } + } + } + + layout.types = mapValues(combinedTypes, m => { + return { + label: m.label, + members: + m.members && isStructMembers(m.members) + ? m.members.map(m => pick(m, ['label', 'type', 'offset', 'slot'])) + : m.members, + numberOfBytes: m.numberOfBytes, + }; + }); + + if (storageLayout !== undefined) { for (const storage of storageLayout.storage) { const origin = getOriginContract(contractDef, storage.astId, deref); assert(origin, `Did not find variable declaration node for '${storage.label}'`); const { varDecl, contract } = origin; const { renamedFrom, retypedFrom } = getRetypedRenamed(varDecl); // Solc layout doesn't bring members for enums so we get them using the ast method - loadLayoutType(varDecl, layout, deref); + loadLayoutType(varDecl.typeName, layout, deref); const { label, offset, slot, type } = storage; const src = decodeSrc(varDecl); layout.storage.push({ label, offset, slot, type, contract, src, retypedFrom, renamedFrom }); @@ -65,11 +89,15 @@ export function extractStorageLayout( renamedFrom, }); - loadLayoutType(varDecl, layout, deref); + loadLayoutType(varDecl.typeName, layout, deref); } } } } + + const origContext = { deref, contractDef, storageLayout }; + loadNamespaces(decodeSrc, layout, origContext, namespacedContext); + return layout; } @@ -92,14 +120,34 @@ function typeDescriptions(x: { typeDescriptions: TypeDescriptions }): RequiredTy return x.typeDescriptions as RequiredTypeDescriptions; } -function getTypeMembers(typeDef: StructDefinition | EnumDefinition): TypeItem['members'] { +type GotTypeMembers< + D extends EnumDefinition | StructDefinition, + F extends 'src' | 'typeName', +> = D extends EnumDefinition ? EnumMember[] : (StructMember & Pick)[]; + +export function getTypeMembers(typeDef: D): GotTypeMembers; +export function getTypeMembers( + typeDef: D, + includeFields: { [f in F]: true }, +): GotTypeMembers; +export function getTypeMembers( + typeDef: StructDefinition | EnumDefinition, + includeFields: { src?: boolean; typeName?: boolean } = {}, +): TypeItem['members'] { if (typeDef.nodeType === 'StructDefinition') { return typeDef.members.map(m => { assert(typeof m.typeDescriptions.typeIdentifier === 'string'); - return { + const member: StructMember & Partial> = { label: m.name, type: normalizeTypeIdentifier(m.typeDescriptions.typeIdentifier), }; + if (includeFields.src && m.src) { + member.src = m.src; + } + if (includeFields.typeName && m.typeName) { + member.typeName = m.typeName; + } + return member; }); } else { return typeDef.members.map(m => m.name); @@ -116,16 +164,16 @@ function getOriginContract(contract: ContractDefinition, astId: number | undefin } } -function loadLayoutType(varDecl: VariableDeclaration, layout: StorageLayout, deref: ASTDereferencer) { +export function loadLayoutType(typeName: TypeName | null | undefined, layout: StorageLayout, deref: ASTDereferencer) { // Note: A UserDefinedTypeName can also refer to a ContractDefinition but we won't care about those. const derefUserDefinedType = deref(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition']); - assert(varDecl.typeName != null); + assert(typeName != null); // We will recursively look for all types involved in this variable declaration in order to store their type // information. We iterate over a Map that is indexed by typeIdentifier to ensure we visit each type only once. // Note that there can be recursive types. - const typeNames = new Map([...findTypeNames(varDecl.typeName)].map(n => [typeDescriptions(n).typeIdentifier, n])); + const typeNames = new Map([...findTypeNames(typeName)].map(n => [typeDescriptions(n).typeIdentifier, n])); for (const typeName of typeNames.values()) { const { typeIdentifier, typeString: label } = typeDescriptions(typeName); diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts index b429147fb..3d7e343ac 100644 --- a/packages/core/src/storage/index.ts +++ b/packages/core/src/storage/index.ts @@ -43,8 +43,16 @@ export function getStorageUpgradeReport( ): LayoutCompatibilityReport { const originalDetailed = getDetailedLayout(original); const updatedDetailed = getDetailedLayout(updated); + const originalDetailedNamespaces = getDetailedNamespacedLayout(original); + const updatedDetailedNamespaces = getDetailedNamespacedLayout(updated); + const comparator = new StorageLayoutComparator(opts.unsafeAllowCustomTypes, opts.unsafeAllowRenames); - const report = comparator.compareLayouts(originalDetailed, updatedDetailed); + const report = comparator.compareLayouts( + originalDetailed, + updatedDetailed, + originalDetailedNamespaces, + updatedDetailedNamespaces, + ); if (comparator.hasAllowedUncheckedCustomTypes) { logWarning(`Potentially unsafe deployment`, [ @@ -58,6 +66,19 @@ export function getStorageUpgradeReport( return report; } +function getDetailedNamespacedLayout(layout: StorageLayout): Record { + const detailedNamespaces: Record = {}; + if (layout.namespaces !== undefined) { + for (const [storageLocation, namespacedLayout] of Object.entries(layout.namespaces)) { + detailedNamespaces[storageLocation] = getDetailedLayout({ + storage: namespacedLayout, + types: layout.types, + }); + } + } + return detailedNamespaces; +} + export class StorageUpgradeErrors extends UpgradesError { constructor(readonly report: LayoutCompatibilityReport) { super(`New storage layout is incompatible`, () => report.explain()); diff --git a/packages/core/src/storage/layout.ts b/packages/core/src/storage/layout.ts index 15440c879..7f9ef4153 100644 --- a/packages/core/src/storage/layout.ts +++ b/packages/core/src/storage/layout.ts @@ -11,6 +11,7 @@ export interface StorageLayout { storage: StorageItem[]; types: Record; flat?: boolean; + namespaces?: Record; } export type StorageField = StorageItem | StructMember; diff --git a/packages/core/src/storage/namespace.ts b/packages/core/src/storage/namespace.ts new file mode 100644 index 000000000..6d24326bc --- /dev/null +++ b/packages/core/src/storage/namespace.ts @@ -0,0 +1,262 @@ +import assert from 'assert'; +import { ContractDefinition, StructDefinition } from 'solidity-ast'; +import { isNodeType } from 'solidity-ast/utils'; +import { StorageItem, StorageLayout, TypeItem, isStructMembers } from './layout'; +import { SrcDecoder } from '../src-decoder'; +import { getAnnotationArgs, getDocumentation, hasAnnotationTag } from '../utils/annotations'; +import { Node } from 'solidity-ast/node'; +import { CompilationContext, getTypeMembers, loadLayoutType } from './extract'; +import { UpgradesError } from '../error'; + +/** + * Loads a contract's namespaces and namespaced type information into the storage layout. + * + * The provided compilation contexts must include both the original compilation and optionally + * a namespaced compilation where contracts have been modified to include namespaced type information. + * + * If namespaced compilation is included, storage slots and offsets will be included in the loaded namespaces and types. + * + * This function looks up namespaces and their members from the namespaced compilation context's AST if available + * (meaning node ids would be from the namespaced compilation), and looks up slots and offsets from the compiled type information. + * However, it saves the original source locations from the original context so that line numbers are + * consistent with the original source code. + * + * @param decodeSrc Source decoder for the original source code. + * @param layout The storage layout object to load namespaces into. + * @param origContext The original compilation context, which is used to lookup original source locations. + * @param namespacedContext The namespaced compilation context, which represents a namespaced compilation. + */ +export function loadNamespaces( + decodeSrc: SrcDecoder, + layout: StorageLayout, + origContext: CompilationContext, + namespacedContext?: CompilationContext, +) { + const namespacesWithSrc: Record = {}; + + const origLinearized = origContext.contractDef.linearizedBaseContracts.map(id => + getReferencedContract(origContext, id), + ); + const namespacedLinearized = namespacedContext?.contractDef.linearizedBaseContracts.map(id => + getReferencedContract(namespacedContext, id), + ); + assert(namespacedLinearized === undefined || origLinearized.length === namespacedLinearized.length); + const context = namespacedContext ?? origContext; + const linearized = namespacedLinearized ?? origLinearized; + for (const [i, contractDef] of linearized.entries()) { + const origContractDef = origLinearized[i]; + const contractContext = { ...context, contractDef }; + addContractNamespacesWithSrc( + namespacesWithSrc, + decodeSrc, + layout, + contractContext, + origContractDef, + origContext.contractDef.canonicalName ?? origContext.contractDef.name, + ); + } + + // Add to layout without the namespaced structs' src locations, since those are no longer needed + // as they were only used to give duplicate namespace errors above. + layout.namespaces = Object.fromEntries( + Object.entries(namespacesWithSrc).map(([id, namespaceWithSrc]) => [id, namespaceWithSrc.namespace]), + ); +} + +class DuplicateNamespaceError extends UpgradesError { + constructor(id: string, contractName: string, src1: string, src2: string) { + super( + `Namespace ${id} is defined multiple times for contract ${contractName}`, + () => `\ +The namespace ${id} was found in structs at the following locations: +- ${src1} +- ${src2} + +Use a unique namespace id for each struct annotated with '@custom:storage-location erc7201:' in your contract and its inherited contracts.`, + ); + } +} + +/** + * Namespaced storage items, along with the original source location of the namespace struct. + */ +interface NamespaceWithSrc { + namespace: StorageItem[]; + src: string; +} + +/** + * Gets the contract definition for the given referenced id. + */ +function getReferencedContract(context: CompilationContext, referencedId: number) { + // Optimization to avoid dereferencing if the referenced id is the same as the current contract + return context.contractDef.id === referencedId + ? context.contractDef + : context.deref(['ContractDefinition'], referencedId); +} + +/** + * Add namespaces and source locations for the given compilation context's contract. + * Does not include inherited contracts. + * + * @param namespacesWithSrc The record of namespaces with source locations to add to. + * @param decodeSrc Source decoder for the original source code. + * @param layout The storage layout object to load types into. + * @param contractContext The compilation context for this specific contract to load namespaces for. + * @param origContractDef The AST node for this specific contract but from the original compilation context. + * @param leastDerivedContractName The name of the least derived contract in the inheritance list. + * @throws DuplicateNamespaceError if a duplicate namespace is found when adding to the `namespaces` record. + */ +function addContractNamespacesWithSrc( + namespacesWithSrc: Record, + decodeSrc: SrcDecoder, + layout: StorageLayout, + contractContext: CompilationContext, + origContractDef: ContractDefinition, + leastDerivedContractName: string, +) { + for (const node of contractContext.contractDef.nodes) { + if (isNodeType('StructDefinition', node)) { + const storageLocation = getStorageLocationAnnotation(node); + if (storageLocation !== undefined) { + const origSrc = decodeSrc(getOriginalStruct(node.canonicalName, origContractDef)); + + if (namespacesWithSrc[storageLocation] !== undefined) { + throw new DuplicateNamespaceError( + storageLocation, + leastDerivedContractName, + namespacesWithSrc[storageLocation].src, + origSrc, + ); + } else { + namespacesWithSrc[storageLocation] = { + namespace: getNamespacedStorageItems(node, decodeSrc, layout, contractContext, origContractDef), + src: origSrc, + }; + } + } + } + } +} + +/** + * Gets the storage location string from the `@custom:storage-location` annotation. + * + * For example, when using ERC-7201 (https://eips.ethereum.org/EIPS/eip-7201), the result will be `erc7201:` + * + * @param node The node that may have a `@custom:storage-location` annotation. + * @returns The storage location string, or undefined if the node does not have a `@custom:storage-location` annotation. + * @throws Error if the node has the annotation `@custom:storage-location` but it does not have exactly one argument. + */ +export function getStorageLocationAnnotation(node: Node) { + const doc = getDocumentation(node); + if (hasAnnotationTag(doc, 'storage-location')) { + const storageLocationArgs = getAnnotationArgs(doc, 'storage-location'); + if (storageLocationArgs.length !== 1) { + throw new Error('@custom:storage-location annotation must have exactly one argument'); + } + return storageLocationArgs[0]; + } +} + +/** + * Gets the storage items for the given struct node. + * Includes loading recursive type information, and adds slot and offset if they are available in the given compilation context's layout. + */ +function getNamespacedStorageItems( + node: StructDefinition, + decodeSrc: SrcDecoder, + layout: StorageLayout, + context: CompilationContext, + origContractDef: ContractDefinition, +): StorageItem[] { + const storageItems: StorageItem[] = []; + + for (const astMember of getTypeMembers(node, { typeName: true })) { + const item: StorageItem = { + contract: context.contractDef.name, + label: astMember.label, + type: astMember.type, + src: decodeSrc({ src: getOriginalMemberSrc(node.canonicalName, astMember.label, origContractDef) }), + }; + + const layoutMember = findLayoutStructMember( + context.storageLayout?.types ?? {}, + node.canonicalName, + astMember.label, + ); + + if (layoutMember?.offset !== undefined && layoutMember?.slot !== undefined) { + item.offset = layoutMember.offset; + item.slot = layoutMember.slot; + } + + storageItems.push(item); + + // If context is namespaced, we have storage layout, and this will fill in enum members just like in extractStorageLayout. + // If context is original, this will add the types from the namespace structs to the layout. + loadLayoutType(astMember.typeName, layout, context.deref); + } + return storageItems; +} + +/** + * Gets the struct definition matching the given canonical name from the original contract definition. + */ +function getOriginalStruct(structCanonicalName: string, origContractDef: ContractDefinition) { + for (const node of origContractDef.nodes) { + if (isNodeType('StructDefinition', node)) { + if (node.canonicalName === structCanonicalName) { + return node; + } + } + } + throw new Error( + `Could not find original source location for namespace struct with name ${structCanonicalName} from contract ${origContractDef.name}`, + ); +} + +/** + * Gets the original source location for the given struct canonical name and struct member label. + */ +function getOriginalMemberSrc(structCanonicalName: string, memberLabel: string, origContractDef: ContractDefinition) { + const node = getOriginalStruct(structCanonicalName, origContractDef); + if (node !== undefined) { + for (const member of getTypeMembers(node, { src: true })) { + if (member.label === memberLabel) { + return member.src; + } + } + } + + throw new Error( + `Could not find original source location for namespace struct with name ${structCanonicalName} and member ${memberLabel}`, + ); +} + +/** + * From the given layout types, gets the struct member matching the given struct canonical name and struct member label. + */ +function findLayoutStructMember( + types: Record>, + structCanonicalName: string, + memberLabel: string, +) { + const structType = findTypeWithLabel(types, `struct ${structCanonicalName}`); + const structMembers = structType?.members; + if (structMembers !== undefined) { + assert(isStructMembers(structMembers)); + for (const structMember of structMembers) { + if (structMember.label === memberLabel) { + return structMember; + } + } + } +} + +/** + * From the given layout types, gets the type matching the given type label. + */ +function findTypeWithLabel(types: Record, label: string) { + return Object.values(types).find(type => type.label === label); +} diff --git a/packages/core/src/storage/report.ts b/packages/core/src/storage/report.ts index 50140209d..1baa7f234 100644 --- a/packages/core/src/storage/report.ts +++ b/packages/core/src/storage/report.ts @@ -154,6 +154,11 @@ function explainStorageOperation(op: StorageOperation, ctx: Storag hints.push('Keep the variable even if unused'); break; } + + case 'delete-namespace': { + hints.push(`Keep the struct with annotation '@custom:storage-location ${op.namespace}' even if unused`); + break; + } } return printWithHints({ title, hints }); @@ -299,6 +304,9 @@ function explainBasicOperation(op: BasicOperation, getName: (t: T) => stri case 'append': return `Added \`${getName(op.updated)}\``; + + case 'delete-namespace': + return `Deleted namespace \`${op.namespace}\``; } } diff --git a/packages/core/src/utils/annotations.ts b/packages/core/src/utils/annotations.ts index b09537970..3a5e9d2cf 100644 --- a/packages/core/src/utils/annotations.ts +++ b/packages/core/src/utils/annotations.ts @@ -1,6 +1,14 @@ import { execall } from './execall'; import { Node } from 'solidity-ast/node'; +/** + * Whether the given doc string has an annotation tag. + */ +export function hasAnnotationTag(doc: string, tag: string): boolean { + const regex = new RegExp(`^\\s*(@custom:${tag})(\\s|$)`, 'm'); + return regex.test(doc); +} + /** * Get args from the doc string matching the given tag. * diff --git a/packages/core/src/utils/make-namespaced.test.ts b/packages/core/src/utils/make-namespaced.test.ts new file mode 100644 index 000000000..da02b8418 --- /dev/null +++ b/packages/core/src/utils/make-namespaced.test.ts @@ -0,0 +1,74 @@ +import test, { ExecutionContext } from 'ava'; +import { artifacts, run } from 'hardhat'; +import { + TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD, + TASK_COMPILE_SOLIDITY_RUN_SOLC, + TASK_COMPILE_SOLIDITY_RUN_SOLCJS, +} from 'hardhat/builtin-tasks/task-names'; + +import { makeNamespacedInput } from './make-namespaced'; +import { SolcBuild } from 'hardhat/types/builtin-tasks'; +import { SolcInput, SolcOutput } from '../solc-api'; +import { BuildInfo } from 'hardhat/types'; + +test('make namespaced input', async t => { + const origBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedToModify.sol:Example'); + await testMakeNamespaced(origBuildInfo, t, '0.8.20'); +}); + +test('make namespaced input - solc 0.7', async t => { + // The nameNamespacedInput function must work for different solc versions, since it is called before we check whether namespaces are used with solc >= 0.8.20 + const origBuildInfo = await artifacts.getBuildInfo('contracts/test/NamespacedToModify07.sol:HasFunction'); + await testMakeNamespaced(origBuildInfo, t, '0.7.6'); +}); + +async function testMakeNamespaced( + origBuildInfo: BuildInfo | undefined, + t: ExecutionContext, + solcVersion: string, +) { + if (origBuildInfo === undefined) { + throw new Error('Build info not found'); + } + + // Inefficient, but we want to test that we don't actually modify the original input object + const origInput = JSON.parse(JSON.stringify(origBuildInfo.input)); + + const modifiedInput = makeNamespacedInput(origBuildInfo.input, origBuildInfo.output); + normalizeStateVariableNames(modifiedInput); + t.snapshot(modifiedInput); + + t.deepEqual(origBuildInfo.input, origInput); + t.notDeepEqual(modifiedInput, origInput); + + // Run hardhat compile on the modified input and make sure it has no errors + const modifiedOutput = await hardhatCompile(modifiedInput, solcVersion); + t.is(modifiedOutput.errors, undefined); +} + +function normalizeStateVariableNames(input: SolcInput): void { + for (const source of Object.values(input.sources)) { + if (source.content !== undefined) { + source.content = source.content.replace(/\$MainStorage_\d{1,6};/g, '$MainStorage_random;'); + } + } +} + +async function hardhatCompile(input: SolcInput, solcVersion: string): Promise { + const solcBuild: SolcBuild = await run(TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD, { + quiet: true, + solcVersion, + }); + + if (solcBuild.isSolcJs) { + return await run(TASK_COMPILE_SOLIDITY_RUN_SOLCJS, { + input, + solcJsPath: solcBuild.compilerPath, + }); + } else { + return await run(TASK_COMPILE_SOLIDITY_RUN_SOLC, { + input, + solcPath: solcBuild.compilerPath, + }); + } +} diff --git a/packages/core/src/utils/make-namespaced.test.ts.md b/packages/core/src/utils/make-namespaced.test.ts.md new file mode 100644 index 000000000..bdcf08b29 --- /dev/null +++ b/packages/core/src/utils/make-namespaced.test.ts.md @@ -0,0 +1,154 @@ +# Snapshot report for `src/utils/make-namespaced.test.ts` + +The actual snapshot is saved in `make-namespaced.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## make namespaced input + +> Snapshot 1 + + { + language: 'Solidity', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + outputSelection: { + '*': { + '': [ + 'ast', + ], + '*': [ + 'storageLayout', + ], + }, + }, + }, + sources: { + 'contracts/test/NamespacedToModify.sol': { + content: `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.20;␊ + ␊ + contract Example {␊ + /// @custom:storage-location erc7201:example.main␊ + struct MainStorage {␊ + uint256 x;␊ + uint256 y;␊ + } MainStorage $MainStorage_random;␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + struct MyStruct { uint b; }␊ + ␊ + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff));␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + /// @notice standlone natspec␊ + ␊ + ␊ + ␊ + ␊ + // standalone doc␊ + ␊ + /**␊ + * standlone doc block␊ + */␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + }␊ + ␊ + contract HasFunction {␊ + ␊ + ␊ + }␊ + ␊ + contract UsingFunction is HasFunction {␊ + ␊ + }␊ + ␊ + uint256 constant FreeFunctionUsingSelector = 0;␊ + ␊ + uint256 constant CONSTANT_USING_SELECTOR = 0;␊ + ␊ + library Lib {␊ + ␊ + ␊ + ␊ + }␊ + ␊ + contract Consumer {␊ + ␊ + ␊ + ␊ + ␊ + ␊ + }␊ + ␊ + uint256 constant plusTwo = 0;␊ + ␊ + ␊ + ␊ + contract UsingForDirectives {␊ + ␊ + ␊ + ␊ + }`, + }, + }, + } + +## make namespaced input - solc 0.7 + +> Snapshot 1 + + { + language: 'Solidity', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + outputSelection: { + '*': { + '': [ + 'ast', + ], + '*': [ + 'storageLayout', + ], + }, + }, + }, + sources: { + 'contracts/test/NamespacedToModify07.sol': { + content: `// SPDX-License-Identifier: MIT␊ + pragma solidity 0.7.6;␊ + ␊ + contract HasFunction {␊ + ␊ + ␊ + }␊ + ␊ + contract UsingFunction is HasFunction {␊ + ␊ + }␊ + `, + }, + }, + } diff --git a/packages/core/src/utils/make-namespaced.test.ts.snap b/packages/core/src/utils/make-namespaced.test.ts.snap new file mode 100644 index 000000000..760ce4135 Binary files /dev/null and b/packages/core/src/utils/make-namespaced.test.ts.snap differ diff --git a/packages/core/src/utils/make-namespaced.ts b/packages/core/src/utils/make-namespaced.ts new file mode 100644 index 000000000..282c2dde5 --- /dev/null +++ b/packages/core/src/utils/make-namespaced.ts @@ -0,0 +1,154 @@ +import { isNodeType } from 'solidity-ast/utils'; +import { Node } from 'solidity-ast/node'; +import { SolcInput, SolcOutput } from '../solc-api'; +import { getStorageLocationAnnotation } from '../storage/namespace'; +import { assert } from './assert'; + +const OUTPUT_SELECTION = { + '*': { + '*': ['storageLayout'], + '': ['ast'], + }, +}; + +/** + * Makes a modified version of the solc input to add state variables in each contract for namespaced struct definitions, + * so that the compiler will generate their types in the storage layout. + * + * This makes the following modifications to the input: + * - Adds a state variable for each namespaced struct definition + * - Deletes all contracts' functions since they are not needed for storage layout + * - Deletes all contracts' modifiers, variables, and parent constructor invocations to avoid compilation errors due to deleted functions and constructors + * - Deletes all using for directives (at file level and in contracts) since they may reference deleted functions + * - Converts all free functions and constants (at file level) to dummy variables (do not delete them since they might be imported by other files) + * + * Also sets the outputSelection to only include storageLayout and ast, since the other outputs are not needed. + * + * @param input The original solc input. + * @param output The original solc output. + * @returns The modified solc input with storage layout that includes namespaced type information. + */ +export function makeNamespacedInput(input: SolcInput, output: SolcOutput): SolcInput { + const modifiedSources: Record = {}; + + for (const [sourcePath] of Object.entries(input.sources)) { + const source = input.sources[sourcePath]; + + if (source.content === undefined) { + modifiedSources[sourcePath] = source; + continue; + } + + const orig = Buffer.from(source.content, 'utf8'); + + const modifications: Modification[] = []; + + for (const node of output.sources[sourcePath].ast.nodes) { + if (isNodeType('ContractDefinition', node)) { + const contractDef = node; + + // Remove any calls to parent constructors from the inheritance list + const inherits = contractDef.baseContracts; + for (const inherit of inherits) { + if (Array.isArray(inherit.arguments)) { + assert(inherit.baseName.name !== undefined); + modifications.push(makeReplace(inherit, orig, inherit.baseName.name)); + } + } + + const contractNodes = contractDef.nodes; + for (const contractNode of contractNodes) { + if ( + isNodeType('FunctionDefinition', contractNode) || + isNodeType('ModifierDefinition', contractNode) || + isNodeType('VariableDeclaration', contractNode) + ) { + if (contractNode.documentation) { + modifications.push(makeDelete(contractNode.documentation, orig)); + } + modifications.push(makeDelete(contractNode, orig)); + } else if (isNodeType('UsingForDirective', contractNode)) { + modifications.push(makeDelete(contractNode, orig)); + } else if (isNodeType('StructDefinition', contractNode)) { + const storageLocation = getStorageLocationAnnotation(contractNode); + if (storageLocation !== undefined) { + const structName = contractNode.name; + const variableName = `$${structName}_${(Math.random() * 1e6).toFixed(0)}`; + const insertText = ` ${structName} ${variableName};`; + + modifications.push(makeInsertAfter(contractNode, insertText)); + } + } + } + } else if (isNodeType('FunctionDefinition', node) || isNodeType('VariableDeclaration', node)) { + if (node.documentation) { + modifications.push(makeDelete(node.documentation, orig)); + } + // Replace with a dummy variable of arbitrary type + const name = node.name; + const insertText = `uint256 constant ${name} = 0;`; + modifications.push(makeReplace(node, orig, insertText)); + } else if (isNodeType('UsingForDirective', node)) { + modifications.push(makeDelete(node, orig)); + } + } + + modifiedSources[sourcePath] = { ...source, content: getModifiedSource(orig, modifications) }; + } + + return { ...input, sources: modifiedSources, settings: { ...input.settings, outputSelection: OUTPUT_SELECTION } }; +} + +interface Modification { + start: number; + end: number; + text?: string; +} + +function getPositions(node: Node) { + const [start, length] = node.src.split(':').map(Number); + const end = start + length; + return { start, end }; +} + +function makeReplace(node: Node, orig: Buffer, text: string): Modification { + // Replace is a delete and insert + const { start, end } = makeDelete(node, orig); + return { start, end, text }; +} + +function makeInsertAfter(node: Node, text: string): Modification { + const { end } = getPositions(node); + return { start: end, end, text }; +} + +function makeDelete(node: Node, orig: Buffer): Modification { + const positions = getPositions(node); + let end = positions.end; + // If the next character is a semicolon for variables, skip over it + if (isNodeType('VariableDeclaration', node) && end + 1 < orig.length && orig.toString('utf8', end, end + 1) === ';') { + end += 1; + } + return { start: positions.start, end }; +} + +function getModifiedSource(orig: Buffer, modifications: Modification[]): string { + let result = ''; + let copyFromIndex = 0; + + for (const modification of modifications) { + assert(modification.start >= copyFromIndex); + result += orig.toString('utf8', copyFromIndex, modification.start); + + if (modification.text !== undefined) { + result += modification.text; + } + + copyFromIndex = modification.end; + } + + assert(copyFromIndex <= orig.length); + result += orig.toString('utf8', copyFromIndex); + + return result; +} diff --git a/packages/core/src/utils/stabilize-layout.ts b/packages/core/src/utils/stabilize-layout.ts index 5d4df7d21..bc277d0f2 100644 --- a/packages/core/src/utils/stabilize-layout.ts +++ b/packages/core/src/utils/stabilize-layout.ts @@ -13,5 +13,10 @@ export function stabilizeStorageLayout(layout: StorageLayout) { : item.members.map(m => ({ ...m, type: stabilizeTypeIdentifier(m.type) }))); return [stabilizeTypeIdentifier(type), { ...item, members }]; }), + namespaces: layout.namespaces + ? Object.entries(layout.namespaces).map(([ns, items]) => { + return [ns, items.map(item => ({ ...item, type: stabilizeTypeIdentifier(item.type) }))]; + }) + : undefined, }; } diff --git a/packages/core/src/validate/query.ts b/packages/core/src/validate/query.ts index dc73c7dd5..62f4706d1 100644 --- a/packages/core/src/validate/query.ts +++ b/packages/core/src/validate/query.ts @@ -93,9 +93,11 @@ export function unfoldStorageLayout(runData: ValidationRunData, fullContractName solcVersion, storage: c.layout.storage, types: c.layout.types, + namespaces: c.layout.namespaces, }; } else { - const layout: StorageLayout = { solcVersion, storage: [], types: {} }; + // Namespaces are pre-flattened + const layout: StorageLayout = { solcVersion, storage: [], types: {}, namespaces: c.layout.namespaces }; for (const name of [fullContractName].concat(c.inherit)) { layout.storage.unshift(...runData[name].layout.storage); Object.assign(layout.types, runData[name].layout.types); diff --git a/packages/core/src/validate/run.ts b/packages/core/src/validate/run.ts index df545eb1f..5afe6db1b 100644 --- a/packages/core/src/validate/run.ts +++ b/packages/core/src/validate/run.ts @@ -1,8 +1,10 @@ import { Node } from 'solidity-ast/node'; import { isNodeType, findAll, ASTDereferencer, astDereferencer } from 'solidity-ast/utils'; import type { ContractDefinition, FunctionDefinition } from 'solidity-ast'; +import debug from '../utils/debug'; +import * as versions from 'compare-versions'; -import { SolcOutput, SolcBytecode } from '../solc-api'; +import { SolcOutput, SolcBytecode, SolcInput } from '../solc-api'; import { SrcDecoder } from '../src-decoder'; import { isNullish } from '../utils/is-nullish'; import { getFunctionSignature } from '../utils/function'; @@ -12,6 +14,8 @@ import { extractStorageLayout } from '../storage/extract'; import { StorageLayout } from '../storage/layout'; import { getFullyQualifiedName } from '../utils/contract-name'; import { getAnnotationArgs as getSupportedAnnotationArgs, getDocumentation } from '../utils/annotations'; +import { getStorageLocationAnnotation } from '../storage/namespace'; +import { UpgradesError } from '../error'; export type ValidationRunData = Record; @@ -116,7 +120,29 @@ function skipCheck(error: string, node: Node): boolean { return getAllowed(node, false).includes(error) || getAllowed(node, true).includes(error); } -export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVersion?: string): ValidationRunData { +/** + * Runs validations on the given solc output. + * + * If `namespacedOutput` is provided, it is used to extract storage layout information for namespaced types. + * It must be from a compilation with the same sources as `solcInput` and `solcOutput`, but with storage variables + * injected for each namespaced struct so that the types are available in the storage layout. This can be obtained by + * calling the `makeNamespacedInput` function from this package to create modified solc input, then compiling + * that modified solc input to get the namespaced output. + * + * @param solcOutput Solc output to validate + * @param decodeSrc Source decoder for the original source code + * @param solcVersion The version of solc used to compile the contracts + * @param solcInput Solc input that the compiler was invoked with + * @param namespacedOutput Namespaced solc output to extract storage layout information for namespaced types + * @returns A record of validation results for each fully qualified contract name + */ +export function validate( + solcOutput: SolcOutput, + decodeSrc: SrcDecoder, + solcVersion?: string, + solcInput?: SolcInput, + namespacedOutput?: SolcOutput, +): ValidationRunData { const validation: ValidationRunData = {}; const fromId: Record = {}; const inheritIds: Record = {}; @@ -128,6 +154,9 @@ export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVers const selfDestructCache = initOpcodeCache(); for (const source in solcOutput.contracts) { + checkNamespaceSolidityVersion(source, solcVersion, solcInput); + checkNamespacesOutsideContract(source, solcOutput, decodeSrc); + for (const contractName in solcOutput.contracts[source]) { const bytecode = solcOutput.contracts[source][contractName].evm.bytecode; const version = bytecode.object === '' ? undefined : getVersion(bytecode.object); @@ -176,7 +205,9 @@ export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVers decodeSrc, deref, solcOutput.contracts[source][contractDef.name].storageLayout, + getNamespacedCompilationContext(source, contractDef, namespacedOutput), ); + validation[key].methods = [...findAll('FunctionDefinition', contractDef)] .filter(fnDef => ['external', 'public'].includes(fnDef.visibility)) .map(fnDef => getFunctionSignature(fnDef, deref)); @@ -195,6 +226,82 @@ export function validate(solcOutput: SolcOutput, decodeSrc: SrcDecoder, solcVers return validation; } +function checkNamespaceSolidityVersion(source: string, solcVersion?: string, solcInput?: SolcInput) { + if (solcInput === undefined) { + // This should only be missing if using an old version of the Hardhat or Truffle plugin. + // Even without this param, namespace layout checks would still occur if compiled with solc version >= 0.8.20 + debug('Cannot check Solidity version for namespaces because solcInput is undefined'); + } else { + // Solc versions older than 0.8.20 do not have documentation for structs. + // Use a regex to check for strings that look like namespace annotations, and if found, check that the compiler version is >= 0.8.20 + const content = solcInput.sources[source].content; + const hasMatch = content !== undefined && content.match(/@custom:storage-location/); + if (hasMatch) { + if (solcVersion === undefined) { + throw new UpgradesError( + `${source}: Namespace annotations require Solidity version >= 0.8.20, but no solcVersion parameter was provided`, + () => + `Structs with the @custom:storage-location annotation can only be used with Solidity version 0.8.20 or higher. Pass the solcVersion parameter to the validate function, or remove the annotation if the struct is not used for namespaced storage.`, + ); + } else if (versions.compare(solcVersion, '0.8.20', '<')) { + throw new UpgradesError( + `${source}: Namespace annotations require Solidity version >= 0.8.20, but ${solcVersion} was used`, + () => + `Structs with the @custom:storage-location annotation can only be used with Solidity version 0.8.20 or higher. Use a newer version of Solidity, or remove the annotation if the struct is not used for namespaced storage.`, + ); + } + } + } +} + +function checkNamespacesOutsideContract(source: string, solcOutput: SolcOutput, decodeSrc: SrcDecoder) { + for (const node of solcOutput.sources[source].ast.nodes) { + if (isNodeType('StructDefinition', node)) { + const storageLocation = getStorageLocationAnnotation(node); + if (storageLocation !== undefined) { + throw new UpgradesError( + `${decodeSrc(node)}: Namespace struct ${node.name} is defined outside of a contract`, + () => + `Structs with the @custom:storage-location annotation must be defined within a contract. Move the struct definition into a contract, or remove the annotation if the struct is not used for namespaced storage.`, + ); + } + } + } +} + +function getNamespacedCompilationContext( + source: string, + contractDef: ContractDefinition, + namespacedOutput?: SolcOutput, +) { + if (namespacedOutput === undefined || contractDef.canonicalName === undefined) { + return undefined; + } + + if (namespacedOutput.sources[source] === undefined) { + throw new Error(`Source ${source} not found in namespaced solc output`); + } + + const namespacedContractDef = namespacedOutput.sources[source].ast.nodes + .filter(isNodeType('ContractDefinition')) + .find(c => c.canonicalName === contractDef.canonicalName); + + if (namespacedContractDef === undefined) { + throw new Error(`Contract definition with name ${contractDef.canonicalName} not found in namespaced solc output`); + } + + const storageLayout = namespacedOutput.contracts[source][contractDef.name].storageLayout; + if (storageLayout === undefined) { + throw new Error(`Storage layout for contract ${contractDef.canonicalName} not found in namespaced solc output`); + } + + return { + contractDef: namespacedContractDef, + deref: astDereferencer(namespacedOutput), + storageLayout: storageLayout, + }; +} + function* getConstructorErrors(contractDef: ContractDefinition, decodeSrc: SrcDecoder): Generator { for (const fnDef of findAll('FunctionDefinition', contractDef, node => skipCheck('constructor', node))) { if (fnDef.kind === 'constructor' && ((fnDef.body?.statements?.length ?? 0) > 0 || fnDef.modifiers.length > 0)) { diff --git a/packages/plugin-hardhat/CHANGELOG.md b/packages/plugin-hardhat/CHANGELOG.md index 0a911ff22..22a7819ea 100644 --- a/packages/plugin-hardhat/CHANGELOG.md +++ b/packages/plugin-hardhat/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add validations for namespaced storage layout. ([#876](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/876)) + ## 2.2.1 (2023-08-18) - Allow using proxy with different admin address than manifest. ([#859](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/859)) diff --git a/packages/plugin-hardhat/ava.config.js b/packages/plugin-hardhat/ava.config.js index 7d7e0e6de..92db61f76 100644 --- a/packages/plugin-hardhat/ava.config.js +++ b/packages/plugin-hardhat/ava.config.js @@ -4,4 +4,5 @@ module.exports = { ignoredByWatcher: ['**/*.ts', '.openzeppelin'], serial: true, failWithoutAssertions: false, + snapshotDir: '.', }; diff --git a/packages/plugin-hardhat/contracts/Namespaced.sol b/packages/plugin-hardhat/contracts/Namespaced.sol new file mode 100644 index 000000000..c2556356e --- /dev/null +++ b/packages/plugin-hardhat/contracts/Namespaced.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract Example { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesY() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y; + } +} + +contract ExampleV2_Ok { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 x; + uint256 y; + uint256 z; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getXTimesYPlusZ() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.x * $.y + $.z; + } +} + +contract ExampleV2_Bad { + /// @custom:storage-location erc7201:example.main + struct MainStorage { + uint256 y; + } + + // keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MAIN_STORAGE_LOCATION = + 0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500; + + function _getMainStorage() private pure returns (MainStorage storage $) { + assembly { + $.slot := MAIN_STORAGE_LOCATION + } + } + + function _getYSquared() internal view returns (uint256) { + MainStorage storage $ = _getMainStorage(); + return $.y * $.y; + } +} + +contract RecursiveStruct { + struct MyStruct { + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + } +} + +contract RecursiveStructV2_Ok { + struct MyStruct { + uint128 a; + uint128 a2; + uint256 b; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + } +} + +contract RecursiveStructV2_Bad { + struct MyStruct { + uint128 a; + uint256 b; + uint256 c; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + MyStruct s; + uint256 y; + } +} + +contract TripleStruct { + struct Inner { + uint128 a; + uint256 b; + } + + struct Outer { + Inner i; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + Outer s; + uint256 y; + } +} + +contract TripleStructV2_Ok { + struct Inner { + uint128 a; + uint128 a2; + uint256 b; + } + + struct Outer { + Inner i; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + Outer s; + uint256 y; + } +} + +contract TripleStructV2_Bad { + struct Inner { + uint128 a; + uint256 b; + uint256 c; + } + + struct Outer { + Inner i; + } + + /// @custom:storage-location erc7201:example.main + struct MainStorage { + Outer s; + uint256 y; + } +} + +contract MultipleNamespacesAndRegularVariables { + /// @custom:storage-location erc7201:one + struct S1 { + uint128 a; + uint256 b; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + uint256 b; + } + + uint128 public a; + uint256 public b; +} + +contract MultipleNamespacesAndRegularVariablesV2_Ok { + /// @custom:storage-location erc7201:one + struct S1 { + uint128 a; + uint128 a2; + uint256 b; + uint256 c; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + uint128 a2; + uint256 b; + uint256 c; + } + + uint128 public a; + uint128 public a2; + uint256 public b; + uint256 public c; +} + +contract MultipleNamespacesAndRegularVariablesV2_Bad { + /// @custom:storage-location erc7201:one + struct S1 { + uint128 a; + uint256 a2; + uint256 b; + uint256 c; + } + + /// @custom:storage-location erc7201:two + struct S2 { + uint128 a; + uint256 a2; + uint256 b; + uint256 c; + } + + uint128 public a; + uint256 public a2; + uint256 public b; + uint256 public c; +} + +contract NoNamespace { + // not annotated as a namespace + struct MainStorage { + uint256 x; + uint256 y; + } +} + +contract InheritsNamespace is Example { +} + +contract InheritsNamespaceV2_Ok is ExampleV2_Ok { +} + +contract InheritsNamespaceV2_Bad is ExampleV2_Bad { +} + +contract InheritsNamespaceV2_BadAndHasLayout is ExampleV2_Bad { + uint256 public a; +} diff --git a/packages/plugin-hardhat/hardhat.config.js b/packages/plugin-hardhat/hardhat.config.js index 98a1e9473..96239448e 100644 --- a/packages/plugin-hardhat/hardhat.config.js +++ b/packages/plugin-hardhat/hardhat.config.js @@ -12,6 +12,9 @@ const override = { module.exports = { solidity: { compilers: [ + { + version: '0.8.20', + }, { version: '0.8.9', }, diff --git a/packages/plugin-hardhat/src/index.ts b/packages/plugin-hardhat/src/index.ts index 78d40b512..c34b8ec91 100644 --- a/packages/plugin-hardhat/src/index.ts +++ b/packages/plugin-hardhat/src/index.ts @@ -6,12 +6,7 @@ import { subtask, extendEnvironment, extendConfig } from 'hardhat/config'; import { TASK_COMPILE_SOLIDITY, TASK_COMPILE_SOLIDITY_COMPILE } from 'hardhat/builtin-tasks/task-names'; import { lazyObject } from 'hardhat/plugins'; import { HardhatConfig, HardhatRuntimeEnvironment } from 'hardhat/types'; -import { - getImplementationAddressFromBeacon, - logWarning, - silenceWarnings, - SolcInput, -} from '@openzeppelin/upgrades-core'; +import type { silenceWarnings, SolcInput, SolcOutput } from '@openzeppelin/upgrades-core'; import type { DeployFunction } from './deploy-proxy'; import type { PrepareUpgradeFunction } from './prepare-upgrade'; import type { UpgradeFunction } from './upgrade-proxy'; @@ -23,12 +18,12 @@ import type { ChangeAdminFunction, TransferProxyAdminOwnershipFunction, GetInsta import type { ValidateImplementationFunction } from './validate-implementation'; import type { ValidateUpgradeFunction } from './validate-upgrade'; import type { DeployImplementationFunction } from './deploy-implementation'; -import { DeployAdminFunction, makeDeployProxyAdmin } from './deploy-proxy-admin'; +import type { DeployAdminFunction } from './deploy-proxy-admin'; import type { DeployContractFunction } from './deploy-contract'; import type { ProposeUpgradeWithApprovalFunction } from './defender/propose-upgrade-with-approval'; import type { GetDefaultApprovalProcessFunction } from './defender/get-default-approval-process'; -import { ProposeUpgradeFunction } from './defender-v1/propose-upgrade'; -import { +import type { ProposeUpgradeFunction } from './defender-v1/propose-upgrade'; +import type { VerifyDeployFunction, VerifyDeployWithUploadedArtifactFunction, GetVerifyDeployArtifactFunction, @@ -82,6 +77,7 @@ export interface DefenderHardhatUpgrades extends HardhatUpgrades, DefenderV1Hard interface RunCompilerArgs { input: SolcInput; solcVersion: string; + quiet: boolean; } subtask(TASK_COMPILE_SOLIDITY, async (args: { force: boolean }, hre, runSuper) => { @@ -101,7 +97,7 @@ subtask(TASK_COMPILE_SOLIDITY, async (args: { force: boolean }, hre, runSuper) = }); subtask(TASK_COMPILE_SOLIDITY_COMPILE, async (args: RunCompilerArgs, hre, runSuper) => { - const { validate, solcInputOutputDecoder } = await import('@openzeppelin/upgrades-core'); + const { validate, solcInputOutputDecoder, makeNamespacedInput } = await import('@openzeppelin/upgrades-core'); const { writeValidations } = await import('./utils/validations'); // TODO: patch input @@ -110,13 +106,41 @@ subtask(TASK_COMPILE_SOLIDITY_COMPILE, async (args: RunCompilerArgs, hre, runSup const { isFullSolcOutput } = await import('./utils/is-full-solc-output'); if (isFullSolcOutput(output)) { const decodeSrc = solcInputOutputDecoder(args.input, output); - const validations = validate(output, decodeSrc, args.solcVersion); + + const namespacedInput = makeNamespacedInput(args.input, output); + const { output: namespacedOutput } = await runSuper({ ...args, quiet: true, input: namespacedInput }); + await checkNamespacedCompileErrors(namespacedOutput); + + const validations = validate(output, decodeSrc, args.solcVersion, args.input, namespacedOutput); await writeValidations(hre, validations); } return { output, solcBuild }; }); +/** + * Checks for compile errors in the modified contracts for namespaced storage. + * If errors are found, throws an error with the compile error messages. + */ +async function checkNamespacedCompileErrors(namespacedOutput: SolcOutput) { + const errors = []; + if (namespacedOutput.errors !== undefined) { + for (const error of namespacedOutput.errors) { + if (error.severity === 'error') { + errors.push(error.formattedMessage); + } + } + } + if (errors.length > 0) { + const { UpgradesError } = await import('@openzeppelin/upgrades-core'); + throw new UpgradesError( + `Failed to compile modified contracts for namespaced storage:\n\n${errors.join('\n')}`, + () => + 'Please report this at https://zpl.in/upgrades/report. If possible, include the source code for the contracts mentioned in the errors above.', + ); + } +} + extendEnvironment(hre => { hre.upgrades = lazyObject((): HardhatUpgrades => { return makeUpgradesFunctions(hre); @@ -131,6 +155,7 @@ extendEnvironment(hre => { function warnOnHardhatDefender() { if (tryRequire('@openzeppelin/hardhat-defender', true)) { + const { logWarning } = require('@openzeppelin/upgrades-core'); logWarning('The @openzeppelin/hardhat-defender package is deprecated.', [ 'Uninstall the @openzeppelin/hardhat-defender package.', 'OpenZeppelin Defender integration is included as part of the Hardhat Upgrades plugin.', @@ -175,6 +200,7 @@ function makeFunctions(hre: HardhatRuntimeEnvironment, defender: boolean) { getAdminAddress, getImplementationAddress, getBeaconAddress, + getImplementationAddressFromBeacon, } = require('@openzeppelin/upgrades-core'); const { makeDeployProxy } = require('./deploy-proxy'); const { makeUpgradeProxy } = require('./upgrade-proxy'); @@ -187,6 +213,7 @@ function makeFunctions(hre: HardhatRuntimeEnvironment, defender: boolean) { const { makeUpgradeBeacon } = require('./upgrade-beacon'); const { makeForceImport } = require('./force-import'); const { makeChangeProxyAdmin, makeTransferProxyAdminOwnership, makeGetInstanceFunction } = require('./admin'); + const { makeDeployProxyAdmin } = require('./deploy-proxy-admin'); return { silenceWarnings, diff --git a/packages/plugin-hardhat/test/namespaced.js b/packages/plugin-hardhat/test/namespaced.js new file mode 100644 index 000000000..85812290d --- /dev/null +++ b/packages/plugin-hardhat/test/namespaced.js @@ -0,0 +1,233 @@ +const test = require('ava'); + +const { ethers, upgrades } = require('hardhat'); + +test.before(async t => { + t.context.Example = await ethers.getContractFactory('Example'); + t.context.ExampleV2_Ok = await ethers.getContractFactory('ExampleV2_Ok'); + t.context.ExampleV2_Bad = await ethers.getContractFactory('ExampleV2_Bad'); + t.context.RecursiveStruct = await ethers.getContractFactory('RecursiveStruct'); + t.context.RecursiveStructV2_Ok = await ethers.getContractFactory('RecursiveStructV2_Ok'); + t.context.RecursiveStructV2_Bad = await ethers.getContractFactory('RecursiveStructV2_Bad'); + t.context.TripleStruct = await ethers.getContractFactory('TripleStruct'); + t.context.TripleStructV2_Ok = await ethers.getContractFactory('TripleStructV2_Ok'); + t.context.TripleStructV2_Bad = await ethers.getContractFactory('TripleStructV2_Bad'); + t.context.MultipleNamespacesAndRegularVariables = await ethers.getContractFactory( + 'MultipleNamespacesAndRegularVariables', + ); + t.context.MultipleNamespacesAndRegularVariablesV2_Ok = await ethers.getContractFactory( + 'MultipleNamespacesAndRegularVariablesV2_Ok', + ); + t.context.MultipleNamespacesAndRegularVariablesV2_Bad = await ethers.getContractFactory( + 'MultipleNamespacesAndRegularVariablesV2_Bad', + ); + t.context.NoNamespace = await ethers.getContractFactory('NoNamespace'); + t.context.InheritsNamespace = await ethers.getContractFactory('InheritsNamespace'); + t.context.InheritsNamespaceV2_Ok = await ethers.getContractFactory('InheritsNamespaceV2_Ok'); + t.context.InheritsNamespaceV2_Bad = await ethers.getContractFactory('InheritsNamespaceV2_Bad'); + t.context.InheritsNamespaceV2_BadAndHasLayout = await ethers.getContractFactory( + 'InheritsNamespaceV2_BadAndHasLayout', + ); +}); + +test('validate namespace - ok', async t => { + const { Example, ExampleV2_Ok } = t.context; + + await upgrades.validateUpgrade(Example, ExampleV2_Ok); +}); + +test('validate namespace - bad', async t => { + const { Example, ExampleV2_Bad } = t.context; + + try { + await upgrades.validateUpgrade(Example, ExampleV2_Bad); + } catch (e) { + const comparison = e.report.ops; + + // Ensure the layout change is detected, in addition to the deletion. This is not normally reported since it has lower cost. + t.like(comparison, { + length: 2, + 0: { + kind: 'delete', + original: { + contract: 'Example', + label: 'x', + type: { + id: 't_uint256', + }, + }, + }, + 1: { + kind: 'layoutchange', + original: { + label: 'y', + type: { + id: 't_uint256', + }, + slot: '1', + }, + updated: { + label: 'y', + type: { + id: 't_uint256', + }, + slot: '0', + }, + }, + }); + + t.snapshot(e.message); + } +}); + +test('validate namespace - recursive - ok', async t => { + const { RecursiveStruct, RecursiveStructV2_Ok } = t.context; + + await upgrades.validateUpgrade(RecursiveStruct, RecursiveStructV2_Ok); +}); + +test('validate namespace - recursive - bad', async t => { + const { RecursiveStruct, RecursiveStructV2_Bad } = t.context; + + try { + await upgrades.validateUpgrade(RecursiveStruct, RecursiveStructV2_Bad); + } catch (e) { + const comparison = e.report.ops; + + t.like(comparison, { + length: 2, + 0: { + kind: 'typechange', + change: { + kind: 'struct members', + ops: { + length: 1, + 0: { kind: 'append' }, + }, + }, + original: { label: 's' }, + updated: { label: 's' }, + }, + 1: { + kind: 'layoutchange', + original: { + label: 'y', + slot: '2', + }, + updated: { + label: 'y', + slot: '3', + }, + }, + }); + + t.snapshot(e.message); + } +}); + +test('validate namespace - triple struct - ok', async t => { + const { TripleStruct, TripleStructV2_Ok } = t.context; + + await upgrades.validateUpgrade(TripleStruct, TripleStructV2_Ok); +}); + +test('validate namespace - triple struct - bad', async t => { + const { TripleStruct, TripleStructV2_Bad } = t.context; + + try { + await upgrades.validateUpgrade(TripleStruct, TripleStructV2_Bad); + } catch (e) { + const comparison = e.report.ops; + + t.like(comparison, { + length: 2, + 0: { + kind: 'typechange', + change: { + kind: 'struct members', + ops: { + length: 1, + 0: { + kind: 'typechange', + change: { + kind: 'struct members', + ops: { + length: 1, + 0: { kind: 'append' }, + }, + }, + }, + }, + }, + }, + 1: { + kind: 'layoutchange', + original: { + label: 'y', + slot: '2', + }, + updated: { + label: 'y', + slot: '3', + }, + }, + }); + + t.snapshot(e.message); + } +}); + +test('multiple namespaces and regular variables - ok', async t => { + const { MultipleNamespacesAndRegularVariables, MultipleNamespacesAndRegularVariablesV2_Ok } = t.context; + + await upgrades.validateUpgrade(MultipleNamespacesAndRegularVariables, MultipleNamespacesAndRegularVariablesV2_Ok); +}); + +test('multiple namespaces and regular variables - bad', async t => { + const { MultipleNamespacesAndRegularVariables, MultipleNamespacesAndRegularVariablesV2_Bad } = t.context; + + const error = await t.throwsAsync(() => + upgrades.validateUpgrade(MultipleNamespacesAndRegularVariables, MultipleNamespacesAndRegularVariablesV2_Bad), + ); + t.snapshot(error.message); +}); + +test('add namespace - ok', async t => { + const { NoNamespace, Example } = t.context; + + await upgrades.validateUpgrade(NoNamespace, Example); +}); + +test('delete namespace - bad', async t => { + const { Example, NoNamespace } = t.context; + + const error = await t.throwsAsync(() => upgrades.validateUpgrade(Example, NoNamespace)); + t.snapshot(error.message); +}); + +test('moving namespace between inherited contract - ok', async t => { + const { InheritsNamespace, Example } = t.context; + + await upgrades.validateUpgrade(Example, InheritsNamespace); + await upgrades.validateUpgrade(InheritsNamespace, Example); +}); + +test('moving namespace to inherited contract - add variable - ok', async t => { + const { Example, InheritsNamespaceV2_Ok } = t.context; + + await upgrades.validateUpgrade(Example, InheritsNamespaceV2_Ok); +}); + +test('moving namespace to inherited contract - delete variable - bad', async t => { + const { Example, InheritsNamespaceV2_Bad } = t.context; + + const error = await t.throwsAsync(() => upgrades.validateUpgrade(Example, InheritsNamespaceV2_Bad)); + t.snapshot(error.message); +}); + +test('moving namespace to inherited contract - delete variable and has layout - bad', async t => { + const { Example, InheritsNamespaceV2_BadAndHasLayout } = t.context; + + const error = await t.throwsAsync(() => upgrades.validateUpgrade(Example, InheritsNamespaceV2_BadAndHasLayout)); + t.snapshot(error.message); +}); diff --git a/packages/plugin-hardhat/test/namespaced.js.md b/packages/plugin-hardhat/test/namespaced.js.md new file mode 100644 index 000000000..44199f0ac --- /dev/null +++ b/packages/plugin-hardhat/test/namespaced.js.md @@ -0,0 +1,81 @@ +# Snapshot report for `test/namespaced.js` + +The actual snapshot is saved in `namespaced.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## validate namespace - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + Example: Deleted \`x\`␊ + > Keep the variable even if unused` + +## validate namespace - recursive - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + contracts/Namespaced.sol:109: Upgraded \`s\` to an incompatible type␊ + - Bad upgrade from struct RecursiveStruct.MyStruct to struct RecursiveStructV2_Bad.MyStruct␊ + - In struct RecursiveStructV2_Bad.MyStruct␊ + - Added \`c\`` + +## validate namespace - triple struct - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + contracts/Namespaced.sol:162: Upgraded \`s\` to an incompatible type␊ + - Bad upgrade from struct TripleStruct.Outer to struct TripleStructV2_Bad.Outer␊ + - In struct TripleStructV2_Bad.Outer␊ + - Upgraded \`i\` to an incompatible type␊ + - Bad upgrade from struct TripleStruct.Inner to struct TripleStructV2_Bad.Inner␊ + - In struct TripleStructV2_Bad.Inner␊ + - Added \`c\`` + +## multiple namespaces and regular variables - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + contracts/Namespaced.sol:225: Inserted \`a2\`␊ + > New variables should be placed after all existing inherited variables␊ + ␊ + contracts/Namespaced.sol:211: Inserted \`a2\`␊ + > New variables should be placed after all existing inherited variables␊ + ␊ + contracts/Namespaced.sol:219: Inserted \`a2\`␊ + > New variables should be placed after all existing inherited variables` + +## delete namespace - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + Example: Deleted namespace \`erc7201:example.main\`␊ + > Keep the struct with annotation '@custom:storage-location erc7201:example.main' even if unused` + +## moving namespace to inherited contract - delete variable - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + Example: Deleted \`x\`␊ + > Keep the variable even if unused` + +## moving namespace to inherited contract - delete variable and has layout - bad + +> Snapshot 1 + + `New storage layout is incompatible␊ + ␊ + Example: Deleted \`x\`␊ + > Keep the variable even if unused` diff --git a/packages/plugin-hardhat/test/namespaced.js.snap b/packages/plugin-hardhat/test/namespaced.js.snap new file mode 100644 index 000000000..edaec0930 Binary files /dev/null and b/packages/plugin-hardhat/test/namespaced.js.snap differ diff --git a/packages/plugin-truffle/CHANGELOG.md b/packages/plugin-truffle/CHANGELOG.md index c39b3948a..e1412c994 100644 --- a/packages/plugin-truffle/CHANGELOG.md +++ b/packages/plugin-truffle/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add validations for namespaced storage layout. ([#876](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/876)) + ## 1.20.1 (2023-08-18) - Allow using proxy with different admin address than manifest. ([#859](https://github.com/OpenZeppelin/openzeppelin-upgrades/pull/859)) diff --git a/packages/plugin-truffle/src/utils/validations.ts b/packages/plugin-truffle/src/utils/validations.ts index 0e912e41e..c789b6c9e 100644 --- a/packages/plugin-truffle/src/utils/validations.ts +++ b/packages/plugin-truffle/src/utils/validations.ts @@ -18,7 +18,7 @@ export async function validateArtifacts(artifactsPath: string, sourcesPath: stri const artifacts = await readArtifacts(artifactsPath); const { input, output, solcVersion } = reconstructSolcInputOutput(artifacts); const srcDecoder = solcInputOutputDecoder(input, output, sourcesPath); - return validate(output, srcDecoder, solcVersion); + return validate(output, srcDecoder, solcVersion, input); } async function readArtifacts(artifactsPath: string): Promise { diff --git a/yarn.lock b/yarn.lock index 6411ef2cd..8ca9950a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3773,6 +3773,14 @@ argsarray@0.0.1: resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -3798,6 +3806,29 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +array.prototype.findlast@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.2.tgz#134ef6b7215f131a8884fafe6af46846a032c518" + integrity sha512-p1YDNPNqA+P6cPX9ATsxg7DKir7gOmJ+jh5dEP3LlumMNYVC1F2Jgnyh6oI3n/qD9FeIkqR2jXfd73G68ImYUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" + +arraybuffer.prototype.slice@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" + integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrgv@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/arrgv/-/arrgv-1.0.2.tgz#025ed55a6a433cad9b604f8112fc4292715a6ec0" @@ -5381,6 +5412,14 @@ define-lazy-prop@^3.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" + integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -5687,6 +5726,76 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.22.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" + integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.1" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.0" + safe-array-concat "^1.0.0" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.10" + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es5-ext@^0.10.35, es5-ext@^0.10.50: version "0.10.62" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" @@ -6582,11 +6691,26 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== +functions-have-names@^1.2.2, functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + ganache@7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/ganache/-/ganache-7.9.0.tgz#561deceb376b1c4e8998ac8e5a842574507d3295" @@ -6637,7 +6761,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.3: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== @@ -6686,6 +6810,14 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -6834,6 +6966,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globby@^11.0.2, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -7007,6 +7146,11 @@ hardhat@^2.0.2: uuid "^8.3.2" ws "^7.4.6" +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -7017,6 +7161,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -7373,6 +7524,15 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" +internal-slot@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -7408,11 +7568,27 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -7420,6 +7596,14 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-buffer@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" @@ -7432,7 +7616,7 @@ is-builtin-module@^3.2.1: dependencies: builtin-modules "^3.3.0" -is-callable@^1.1.3: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -7451,6 +7635,13 @@ is-core-module@^2.11.0, is-core-module@^2.5.0, is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -7541,6 +7732,18 @@ is-lower-case@^1.1.0: dependencies: lower-case "^1.1.0" +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -7593,6 +7796,21 @@ is-promise@^4.0.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-ssh@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" @@ -7615,6 +7833,20 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-text-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" @@ -7622,7 +7854,7 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.3: +is-typed-array@^1.1.10, is-typed-array@^1.1.3, is-typed-array@^1.1.9: version "1.1.12" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== @@ -7656,6 +7888,13 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7678,6 +7917,11 @@ isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -9488,11 +9732,26 @@ object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: +object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + obliterator@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" @@ -10681,6 +10940,15 @@ regexp-tree@^0.1.27: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== +regexp.prototype.flags@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" + regjsparser@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.10.0.tgz#b1ed26051736b436f22fdec1c8f72635f9f44892" @@ -10912,6 +11180,16 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -10922,6 +11200,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -11242,6 +11529,13 @@ solidity-ast@^0.4.26: resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.49.tgz#ecba89d10c0067845b7848c3a3e8cc61a4fc5b82" integrity sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ== +solidity-ast@^0.4.51: + version "0.4.51" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.51.tgz#ae21eab3f6d8b91f5cbd08cfe1dae2d9c0b90415" + integrity sha512-Mql4HTY3ce2t8YW6cGjq8dKKRT9D38D3TB/lOfIhgbfXx/cCFG2clXgqWuOfXGX9t6fhOPFvcVZhj2b6n30VBA== + dependencies: + array.prototype.findlast "^1.2.2" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -11425,6 +11719,33 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -11979,6 +12300,45 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -12025,6 +12385,16 @@ ultron@~1.1.0: resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + undici@^5.14.0: version "5.22.1" resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" @@ -12537,6 +12907,17 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -12547,7 +12928,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.11, which-typed-array@^1.1.2: +which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.2: version "1.1.11" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==