├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── Initializable.sol ├── LICENSE ├── README.md ├── ava.config.js ├── contracts ├── GenerateWithInit.sol ├── TransformAddGap-0.8.sol ├── TransformAddGap.sol ├── TransformAllowedImmutable.sol ├── TransformConstructor.sol ├── TransformConstructorWithArgs.sol ├── TransformCustomSize.sol ├── TransformImmutable.sol ├── TransformImport2-Imported.sol ├── TransformImport2.sol ├── TransformInheritanceArgs.sol ├── TransformInitializable.sol ├── TransformNew.sol ├── TransformNewVarInit.sol ├── TransformRemove.sol ├── TransformUtf8Chars.sol ├── find-already-init │ ├── AlreadyMixed.sol │ ├── AlreadyOk.sol │ ├── Init1.sol │ ├── Init2.sol │ ├── InitPlus.sol │ └── NoInit.sol ├── invalid │ ├── TransformConstructorDupExpr.sol │ ├── TransformConstructorVarSubexpr.sol │ └── TransformConstructorVarSubexprVar.sol ├── namespaces-error-storage-size.sol ├── namespaces.sol ├── project │ ├── ISomeInterface.sol │ ├── SomeContract.sol │ ├── SomeLibrary.sol │ ├── SomeOtherContract.sol │ └── SomeStatelessContract.sol ├── rename-0.8.sol └── solc-0.6 │ ├── AbstractContract.sol │ ├── AlreadyUpgradeable.sol │ ├── ClassInheritance.sol │ ├── DiamondInheritance.sol │ ├── ElementaryTypes.sol │ ├── ElementaryTypesWithConstructor.sol │ ├── Imported.sol │ ├── Interface.sol │ ├── Library.sol │ ├── Local.sol │ ├── Override.sol │ ├── Rename.sol │ ├── SimpleInheritance.sol │ ├── StringConstructor.sol │ └── one │ └── two │ └── three │ └── Deep.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── ast-resolver.ts ├── cli.ts ├── find-already-initializable.test.ts ├── find-already-initializable.ts ├── generate-with-init.test.ts ├── generate-with-init.test.ts.md ├── generate-with-init.test.ts.snap ├── generate-with-init.ts ├── index.test.ts ├── index.test.ts.md ├── index.test.ts.snap ├── index.ts ├── prepare-peer-project.test.ts ├── prepare-peer-project.test.ts.md ├── prepare-peer-project.test.ts.snap ├── prepare-peer-project.ts ├── rename.ts ├── shifts.test.ts ├── shifts.ts ├── solc │ ├── ast-utils.ts │ ├── input-output.ts │ ├── layout-getter.ts │ └── src-decoder.ts ├── test-utils │ └── get-build-info.ts ├── transform-0.8.test.ts ├── transform-0.8.test.ts.md ├── transform-0.8.test.ts.snap ├── transform-namespaces.test.ts ├── transform-namespaces.test.ts.md ├── transform-namespaces.test.ts.snap ├── transform.test.ts ├── transform.test.ts.md ├── transform.test.ts.snap ├── transform.ts ├── transformations │ ├── add-namespace-struct.ts │ ├── add-required-public-initializers.ts │ ├── add-storage-gaps.ts │ ├── append-initializable-import.ts │ ├── apply.test.ts │ ├── apply.ts │ ├── compare.test.ts │ ├── compare.ts │ ├── fix-import-directives.ts │ ├── fix-new-statement.ts │ ├── peer-import.ts │ ├── prepend-initializable-base.ts │ ├── purge-var-inits.ts │ ├── remove-immutable.ts │ ├── remove-inheritance-list-args.ts │ ├── rename-contract-definition.ts │ ├── rename-identifiers.ts │ ├── rename-inheritdoc.ts │ ├── transform-constructor.ts │ ├── type.ts │ └── utils │ │ ├── build-pulic-initialize.ts │ │ ├── build-super-calls-for-chain.ts │ │ ├── contract-start-position.ts │ │ ├── format-lines.ts │ │ ├── get-initializer-items.ts │ │ ├── is-storage-variable.ts │ │ └── new-function-position.ts └── utils │ ├── erc7201.ts │ ├── execall.ts │ ├── match.ts │ ├── matcher.test.ts │ ├── matcher.ts │ ├── natspec.ts │ ├── new-expression.ts │ ├── parse-type-id.ts │ ├── relative-path.ts │ ├── type-id.ts │ └── upgrades-overrides.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 7 | env: { 8 | node: true, 9 | }, 10 | rules: { 11 | curly: 'warn', 12 | 'prettier/prettier': 'warn', 13 | }, 14 | overrides: [ 15 | { 16 | files: ['*.ts'], 17 | parser: '@typescript-eslint/parser', 18 | plugins: ['@typescript-eslint'], 19 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 20 | rules: { 21 | '@typescript-eslint/no-non-null-assertion': 'off', 22 | }, 23 | }, 24 | { 25 | files: ['buidler.config.js'], 26 | globals: { 27 | usePlugin: 'readonly', 28 | }, 29 | }, 30 | { 31 | files: ['ava.config.js'], 32 | parserOptions: { 33 | sourceType: 'module', 34 | }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 16.x 14 | 15 | - run: npm ci 16 | - run: npm run lint 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist 3 | artifacts 4 | cache 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | printWidth: 100, 5 | arrowParens: 'avoid', 6 | }; 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.33 (2023-12-8) 4 | 5 | - Improve code compatibility with older versions of node. 6 | 7 | ## 0.3.32 (2023-09-30) 8 | 9 | - Revert 0.3.31 changes. 10 | 11 | ## 0.3.31 (2023-09-30) 12 | 13 | - (Reverted in 0.3.32) ~Simplify peer project mode by renaming imported peer symbols.~ 14 | 15 | ## 0.3.30 (2023-09-27) 16 | 17 | - Fix `@inheritdoc` in peer project mode. 18 | 19 | ## 0.3.29 (2023-09-26) 20 | 21 | - Add a NatSpec annotation `@custom:stateless` to skip transpiling annotated contracts in peer project mode. 22 | 23 | ## 0.3.28 (2023-09-25) 24 | 25 | - Add an option to skip transpilation of items that don't need it (e.g. interfaces), by fetching them from a "peer project". This is set by using the new `-q` flag. 26 | 27 | ## 0.3.27 (2023-08-29) 28 | 29 | - Throw error when using `@custom:storage-size` along with namespaced storage. 30 | 31 | ## 0.3.26 (2023-08-28) 32 | 33 | - Change location of initializer functions when the original contract doesn't have a constructor. Previously it would be the start of the contract, before state variables. It is now placed immediately before the first function of the contract, if the contract has functions. 34 | - Add namespaced storage as an alternative to gaps `-n` enables namespaces, and `-N` excludes specific files from namespaces. 35 | 36 | ## 0.3.25 (2023-07-05) 37 | 38 | - Allow immutable variable assignment given `unsafe-allow state-variable-immutable`. Previously `unsafe-allow state-variable-assignment` was required as well. 39 | 40 | ## 0.3.24 (2023-05-04) 41 | 42 | - Allow constructor override at contract level. 43 | 44 | ## 0.3.23 (2023-05-04) 45 | 46 | - Switch AST resolver to faster implementation. 47 | - Consider unsafe-allow at contract level. 48 | 49 | ## 0.3.22 (2023-04-26) 50 | 51 | - Add `-W` option to skip `WithInit` generation. 52 | 53 | ## 0.3.21 (2023-02-17) 54 | 55 | - Generate `WithInit` contract variant for abstract but fully implemented contracts. 56 | 57 | ## 0.3.20 (2023-02-11) 58 | 59 | - Fix support for immutable variables of user defined value types. 60 | 61 | ## 0.3.19 (2022-09-24) 62 | 63 | - Add license header to WithInit.sol. 64 | 65 | ## 0.3.18 (2022-09-23) 66 | 67 | - Fix ignored `-b` flag. 68 | 69 | ## 0.3.17 (2022-09-23) 70 | 71 | - Add info to error message. 72 | 73 | ## 0.3.16 (2022-09-23) 74 | 75 | - Add -b flag to manually pass build info file path. 76 | 77 | ## 0.3.13 (2022-03-31) 78 | 79 | - Transform new expressions in variable initializations. 80 | - Fix WithInit contracts when there is a constructor override. 81 | 82 | ## 0.3.12 (2022-03-31) 83 | 84 | - Fix transpilation of new statements when immediately cast to address. 85 | 86 | ## 0.3.11 (2022-03-11) 87 | 88 | - Fix evaluation of the size of value type variables that are not documented in the layout, adding support for enum, contracts and payable addresses. 89 | 90 | ## 0.3.10 (2022-03-08) 91 | 92 | - Fix gap size when immutable variables are transpiled to storage, and add `@custom:storage-size` override to customize gap size. 93 | 94 | ## 0.3.9 (2022-02-11) 95 | 96 | - Add `@dev` tag to gap variable natspec. 97 | 98 | ## 0.3.8 (2022-02-08) 99 | 100 | - Fix wrong assumption that Identifier maps to VariableDeclaration. 101 | 102 | ## 0.3.7 (2022-02-08) 103 | 104 | - Fix bug when removing abstract parents too eagerly. 105 | 106 | ## 0.3.6 (2022-01-31) 107 | 108 | - Remove calls to empty self unchained method from init method body, and empty parent unchained methods calls without parameters. 109 | - Persist modifiers on constructors. 110 | - Add docstring to gap variable. 111 | 112 | ## 0.3.5 (2022-01-13) 113 | 114 | - Delete unused parameter names in unchained initializers. Removes compiler warnings. 115 | - Remove visibility from constructor in WithInit.sol file. 116 | 117 | ## 0.3.4 (2022-01-11) 118 | 119 | - Fix @inheritdoc tags by renaming the contract name. 120 | 121 | ## 0.3.3 (2021-12-27) 122 | 123 | - Fix bug in constructor transformation. 124 | 125 | ## 0.3.2 (2021-12-27) 126 | 127 | - Add handling of 'unsafe-allow override'. 128 | 129 | ## 0.3.1 (2021-12-14) 130 | 131 | - Add `initializer` modifier to WithInit constructors. 132 | 133 | ## 0.3.0 (2021-12-10) 134 | 135 | - Breaking change: Use `onlyInitializing` modifier for internal initializer functions. 136 | 137 | ## 0.1.1 (2020-11-16) 138 | 139 | - Support Solidity 0.7. 140 | 141 | ## 0.1.0 (2020-11-06) 142 | 143 | - First release to npm. 144 | -------------------------------------------------------------------------------- /Initializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.6.0 <0.7.0; 3 | 4 | /** 5 | * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed 6 | * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an 7 | * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer 8 | * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. 9 | * 10 | * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as 11 | * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. 12 | * 13 | * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure 14 | * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. 15 | */ 16 | abstract contract Initializable { 17 | /** 18 | * @dev Indicates that the contract has been initialized. 19 | */ 20 | bool private _initialized; 21 | 22 | /** 23 | * @dev Indicates that the contract is in the process of being initialized. 24 | */ 25 | bool private _initializing; 26 | 27 | /** 28 | * @dev Modifier to protect an initializer function from being invoked twice. 29 | */ 30 | modifier initializer() { 31 | // If the contract is initializing we ignore whether _initialized is set in order to support multiple 32 | // inheritance patterns, but we only do this in the context of a constructor, because in other contexts the 33 | // contract may have been reentered. 34 | require(_initializing ? _isConstructor() : !_initialized, "Initializable: contract is already initialized"); 35 | 36 | bool isTopLevelCall = !_initializing; 37 | if (isTopLevelCall) { 38 | _initializing = true; 39 | _initialized = true; 40 | } 41 | 42 | _; 43 | 44 | if (isTopLevelCall) { 45 | _initializing = false; 46 | } 47 | } 48 | 49 | /** 50 | * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the 51 | * {initializer} modifier, directly or indirectly. 52 | */ 53 | modifier onlyInitializing() { 54 | require(_initializing, "Initializable: contract is not initializing"); 55 | _; 56 | } 57 | 58 | /// @dev Returns true if and only if the function is running in the constructor 59 | function _isConstructor() private view returns (bool) { 60 | // extcodesize checks the size of the code stored in an address, and 61 | // address returns the current address. Since the code is still not 62 | // deployed when running a constructor, any checks on its code size will 63 | // yield zero, making it an effective way to detect if a contract is 64 | // under construction or not. 65 | address self = address(this); 66 | uint256 cs; 67 | assembly { cs := extcodesize(self) } 68 | return cs == 0; 69 | } 70 | 71 | // Reserved storage space to allow for layout changes in the future. 72 | uint256[50] private ______gap; 73 | } 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) zOS Global Limited 2018-2020. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenZeppelin Upgradeability Transpiler 2 | 3 | :warning: **Warning** :warning: 4 | 5 | This tool was built and tested specifically for [OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts), and might produce incorrect results if used in any other context. 6 | 7 | We do not recommend using it in a different context. If you're interested in doing so, get in touch with us over at [forum.openzeppelin.com](https://forum.openzeppelin.com). 8 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extensions: ['ts'], 3 | require: ['ts-node/register'], 4 | ignoredByWatcher: ['**/cache/*', '**/artifacts/*'], 5 | }; 6 | -------------------------------------------------------------------------------- /contracts/GenerateWithInit.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo1 { 4 | } 5 | 6 | contract Foo2 { 7 | constructor(uint x, string memory y) public {} 8 | } 9 | -------------------------------------------------------------------------------- /contracts/TransformAddGap-0.8.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8; 3 | 4 | type ShortString is bytes32; 5 | 6 | contract Foo { 7 | ShortString immutable s = ShortString.wrap(0); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/TransformAddGap.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo0 { 4 | } 5 | 6 | contract Foo1 { 7 | uint x; 8 | } 9 | 10 | contract Foo2 { 11 | uint x; 12 | bool b1; 13 | bool b2; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/TransformAllowedImmutable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | contract T1 { 5 | uint immutable a = 1; 6 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 7 | uint immutable b = 4; 8 | uint immutable c = 3; 9 | } 10 | 11 | contract T2 { 12 | uint immutable a = 1; 13 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 14 | uint immutable b = 4; 15 | uint immutable c; 16 | /// @custom:oz-upgrades-unsafe-allow constructor 17 | constructor(uint _c) { 18 | c = _c; 19 | } 20 | } 21 | 22 | abstract contract T3 is T2 { 23 | } 24 | -------------------------------------------------------------------------------- /contracts/TransformConstructor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo1 { 4 | constructor() public { 5 | } 6 | } 7 | 8 | contract Foo2 { 9 | event Ev(); 10 | constructor() public { 11 | emit Ev(); 12 | } 13 | } 14 | 15 | contract Foo3 { 16 | uint x = 1; 17 | } 18 | 19 | contract Foo4 { 20 | uint x = 1; 21 | event Ev(); 22 | constructor() public { 23 | emit Ev(); 24 | } 25 | } 26 | 27 | contract Bar1 { 28 | 29 | } 30 | 31 | contract Bar2 is Bar1 { 32 | 33 | } 34 | 35 | contract Bar3 is Bar2 { 36 | 37 | } 38 | 39 | contract Foo5 { 40 | constructor(function () external f) public { 41 | f(); 42 | } 43 | } 44 | 45 | contract Foo6 { 46 | constructor( 47 | uint a, 48 | uint b 49 | ) public { 50 | a = a + b; 51 | } 52 | } 53 | 54 | contract Foo7 { 55 | uint a; 56 | constructor( 57 | uint _a 58 | ) public { 59 | a = _a; 60 | } 61 | } 62 | 63 | contract Foo8 { 64 | 65 | modifier hasModifier() { 66 | _; 67 | } 68 | 69 | constructor() public hasModifier { 70 | } 71 | } 72 | 73 | contract Foo9 { 74 | constructor( 75 | uint a, 76 | uint b 77 | ) public { 78 | a = 0; 79 | b = 0; 80 | } 81 | } 82 | 83 | contract Foo10 is Foo7(123) { 84 | 85 | uint bar = 1; 86 | } 87 | 88 | contract Foo11 is Foo7 { 89 | 90 | modifier hasModifier() { 91 | _; 92 | } 93 | 94 | constructor(uint a) Foo7(a) public hasModifier { 95 | } 96 | } 97 | 98 | contract Foo12 is Foo7 { 99 | 100 | constructor(uint a) Foo7(a) public { 101 | } 102 | } 103 | 104 | contract Foo13 is Foo4 { 105 | constructor() public { } 106 | } 107 | 108 | contract Foo14 is Foo7 { 109 | 110 | modifier hasModifier(uint b) { 111 | _; 112 | } 113 | 114 | constructor(uint a, uint b) Foo7(a) public hasModifier(b) { 115 | } 116 | } 117 | 118 | contract Foo15 is Foo7 { 119 | uint x; 120 | constructor(uint _x) Foo7(_x) public { 121 | x = _x; 122 | } 123 | } 124 | 125 | abstract contract Foo16 is Foo15 { 126 | 127 | } 128 | 129 | contract Foo17 is Foo4 { 130 | constructor() public {} 131 | } 132 | 133 | abstract contract Foo18 is Foo17 { 134 | 135 | } 136 | 137 | contract Foo19 { 138 | constructor(uint x) public {} 139 | } 140 | 141 | contract Foo20 is Foo19 { 142 | constructor(uint x) Foo19(x) public {} 143 | } 144 | 145 | contract Foo21 is Foo20 { 146 | constructor() Foo20(4) public {} 147 | } 148 | 149 | contract Foo22 is Foo19 { 150 | constructor(uint y) Foo19(y + 1) public {} 151 | } 152 | 153 | contract Foo23 is Foo9, Foo20 { 154 | constructor(uint x, uint y) Foo9(x, y) Foo20(y) public {} 155 | } 156 | 157 | contract Foo24 is Foo23 { 158 | constructor() Foo23(1, 2) public {} 159 | } 160 | 161 | contract Foo25 is Foo19 { 162 | constructor() public Foo19(1+2) {} 163 | } 164 | 165 | interface IFoo { 166 | function mint() external returns (uint); 167 | } 168 | 169 | contract Foo26 is Foo19 { 170 | constructor(IFoo t) Foo19(t.mint()) public {} 171 | } 172 | 173 | contract Foo27 is Foo26 { 174 | constructor(IFoo t) Foo26(t) public {} 175 | } 176 | 177 | contract Foo28 { 178 | constructor(uint x) public {} 179 | } 180 | 181 | abstract contract Foo29 is Foo28 { 182 | constructor(uint y) public {} 183 | } 184 | 185 | abstract contract Foo30 is Foo28, Foo29 { 186 | constructor(uint z) Foo28(z) public {} 187 | } 188 | 189 | abstract contract Foo31 is Foo19 { 190 | constructor(uint b) public {} 191 | } 192 | 193 | abstract contract Foo32 is Foo20, Foo31 { 194 | constructor(uint b) Foo20(b) public {} 195 | } -------------------------------------------------------------------------------- /contracts/TransformConstructorWithArgs.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract FooWithArgs { 4 | constructor(uint x, uint y) public {} 5 | } 6 | 7 | contract FooWithArgs2 { 8 | modifier hasModifierArguments(uint x) { 9 | _; 10 | } 11 | constructor(uint x, uint y) public hasModifierArguments(x) {} 12 | } 13 | -------------------------------------------------------------------------------- /contracts/TransformCustomSize.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8; 3 | 4 | enum SomeEnum { 5 | One, 6 | Two, 7 | Three 8 | } 9 | 10 | struct OneAndAHalfSlot { 11 | uint256 x; 12 | uint128 y; 13 | } 14 | 15 | contract SizeDefault { 16 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 17 | uint immutable w1 = block.number; 18 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 19 | uint immutable w2 = block.timestamp; 20 | uint immutable x; // slot 0 (after conversion to private) 21 | uint constant y = 1; 22 | uint224 z0; // slot 1 23 | uint256 z1; // slot 2 24 | uint32 z2; // slot 3 25 | OneAndAHalfSlot s1; // slot 4&5 26 | OneAndAHalfSlot s2; // slot 6&7 27 | uint32 z3; // slot 8 28 | uint32 z4; // slot 8 29 | uint32 z5; // slot 8 30 | uint64[5] a1; // slot 9&10 31 | uint64[3] a2; // slot 11 32 | SizeDefault immutable c = this; // slot 12 33 | SomeEnum immutable e1 = SomeEnum.One; // slot 12 34 | SomeEnum immutable e2 = SomeEnum.Two; // slot 12 35 | SomeEnum immutable e3 = SomeEnum.Three; // slot 12 36 | address payable immutable blockhole = payable(0); // slot 13 37 | 38 | constructor(uint _x) { 39 | x = _x; 40 | } 41 | // gap should be 36 = 50 - 14 42 | } 43 | 44 | /// @custom:storage-size 128 45 | contract SizeOverride { 46 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 47 | uint immutable w1 = block.number; 48 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 49 | uint immutable w2 = block.timestamp; 50 | uint immutable x; // slot 0 (after conversion to private) 51 | uint constant y = 1; 52 | uint224 z0; // slot 1 53 | uint256 z1; // slot 2 54 | uint32 z2; // slot 3 55 | OneAndAHalfSlot s1; // slot 4&5 56 | OneAndAHalfSlot s2; // slot 6&7 57 | uint32 z3; // slot 8 58 | uint32 z4; // slot 8 59 | uint32 z5; // slot 8 60 | uint64[5] a1; // slot 9&10 61 | uint64[3] a2 ; // slot 11 62 | SizeOverride immutable c = this; // slot 12 63 | SomeEnum immutable e1 = SomeEnum.One; // slot 12 64 | SomeEnum immutable e2 = SomeEnum.Two; // slot 12 65 | SomeEnum immutable e3 = SomeEnum.Three; // slot 12 66 | address payable immutable blockhole = payable(0); // slot 13 67 | 68 | constructor(uint _x) { 69 | x = _x; 70 | } 71 | // gap should be 114 = 128 - 14 72 | } 73 | -------------------------------------------------------------------------------- /contracts/TransformImmutable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.5; 2 | 3 | contract TransformImmutable { 4 | uint immutable x; 5 | constructor() public { 6 | x = 3; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/TransformImport2-Imported.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library Imp1 { 4 | struct S { 5 | uint val; 6 | } 7 | } 8 | 9 | library Imp2 { 10 | struct S { 11 | uint val; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/TransformImport2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import { Imp1 } from './TransformImport2-Imported.sol'; 4 | import { Imp2 as ImpX } from './TransformImport2-Imported.sol'; 5 | 6 | contract Foo { 7 | using Imp1 for Imp1.S; 8 | using ImpX for ImpX.S; 9 | } 10 | -------------------------------------------------------------------------------- /contracts/TransformInheritanceArgs.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract A { 4 | constructor(uint) public {} 5 | } 6 | 7 | contract B is A(4) { 8 | 9 | modifier hasModifier (){ 10 | _; 11 | } 12 | 13 | modifier hasModifierArgument(uint b) { 14 | _; 15 | } 16 | 17 | constructor (uint b) public hasModifier hasModifierArgument(b) {} 18 | } 19 | -------------------------------------------------------------------------------- /contracts/TransformInitializable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo { 4 | } 5 | 6 | contract Bar is Foo { 7 | } 8 | -------------------------------------------------------------------------------- /contracts/TransformNew.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo { 4 | constructor(uint x) public {} 5 | } 6 | 7 | contract Bar { 8 | } 9 | 10 | contract TransformNew { 11 | function test1() external { 12 | Foo foo; 13 | foo = new Foo(1); 14 | } 15 | 16 | function test2() external { 17 | Bar bar; 18 | bar = new Bar(); 19 | } 20 | 21 | function test3() external { 22 | address bar; 23 | bar = address(new Bar()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/TransformNewVarInit.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Foo { 4 | constructor(uint x) public {} 5 | } 6 | 7 | contract Bar { 8 | } 9 | 10 | contract TransformNew { 11 | Foo foo = new Foo(1); 12 | Bar bar = new Bar(); 13 | address baz = address(new Bar()); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/TransformRemove.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract RemoveFunctions { 4 | function foo() external {} 5 | function bar() external {} 6 | } 7 | -------------------------------------------------------------------------------- /contracts/TransformUtf8Chars.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | library Strings { 6 | function toString(uint256) internal pure returns (string memory) { 7 | return ""; 8 | } 9 | } 10 | 11 | library ECDSA { 12 | enum RecoverError { 13 | InvalidSignature 14 | } 15 | 16 | function tryRecover(bytes32, uint8, bytes32, bytes32) internal pure returns (address, RecoverError) { 17 | // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28} 18 | return (address(0), RecoverError.InvalidSignature); 19 | } 20 | 21 | function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { 22 | return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); 23 | } 24 | } 25 | 26 | /** 27 | * - https://docs.tokenbridge.net/eth-xdai-amb-bridge/about-the-eth-xdai-amb[ETH ⇌ xDai] 28 | * - https://docs.tokenbridge.net/eth-qdai-bridge/about-the-eth-qdai-amb[ETH ⇌ qDai] 29 | * - https://docs.tokenbridge.net/eth-etc-amb-bridge/about-the-eth-etc-amb[ETH ⇌ ETC] 30 | */ 31 | contract CrossChainEnabledAMB { 32 | } 33 | 34 | -------------------------------------------------------------------------------- /contracts/find-already-init/AlreadyMixed.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | import "./Init2.sol"; 4 | 5 | contract Already is Initializable {} 6 | contract NotReady {} 7 | -------------------------------------------------------------------------------- /contracts/find-already-init/AlreadyOk.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | import "./Init1.sol"; 4 | 5 | contract Already1 is Initializable {} 6 | -------------------------------------------------------------------------------- /contracts/find-already-init/Init1.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Initializable {} 4 | -------------------------------------------------------------------------------- /contracts/find-already-init/Init2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Initializable {} 4 | -------------------------------------------------------------------------------- /contracts/find-already-init/InitPlus.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract Initializable {} 4 | contract Plus {} 5 | -------------------------------------------------------------------------------- /contracts/find-already-init/NoInit.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract NoInit {} 4 | -------------------------------------------------------------------------------- /contracts/invalid/TransformConstructorDupExpr.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract AA { 4 | constructor(uint x) public {} 5 | } 6 | 7 | contract BB is AA { 8 | constructor(uint x) AA(x) public {} 9 | } 10 | 11 | interface IFoo { 12 | function mint() external returns (uint); 13 | } 14 | 15 | contract E is BB { 16 | constructor(IFoo t) BB(t.mint()) public {} 17 | } 18 | -------------------------------------------------------------------------------- /contracts/invalid/TransformConstructorVarSubexpr.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract A { 4 | constructor(uint x) public {} 5 | } 6 | 7 | contract B is A { 8 | constructor(uint y) A(y + 1) public {} 9 | } 10 | 11 | contract C is B { 12 | constructor() B(4) public {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/invalid/TransformConstructorVarSubexprVar.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6; 2 | 3 | contract A { 4 | constructor(uint x) public {} 5 | } 6 | 7 | contract B is A { 8 | constructor(uint y) A(y + 1) public {} 9 | } 10 | 11 | contract C is B { 12 | constructor(uint z) B(z) public {} 13 | } 14 | -------------------------------------------------------------------------------- /contracts/namespaces-error-storage-size.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | /// @custom:storage-size 52 5 | contract C1 { 6 | } 7 | -------------------------------------------------------------------------------- /contracts/namespaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.20; 3 | 4 | contract C1 { 5 | } 6 | 7 | contract C2 { 8 | event B(); 9 | 10 | struct A { uint z; } 11 | 12 | uint constant C = 3; 13 | // a 14 | uint x; 15 | // b 16 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 17 | uint immutable y = 2; 18 | uint private z; 19 | 20 | string private s1 = ""; 21 | 22 | function f() public { 23 | z = 3; 24 | } 25 | 26 | function g() public { 27 | } 28 | } 29 | 30 | contract C3 { 31 | address private x; 32 | } 33 | 34 | contract C4 { 35 | address private x; 36 | constructor() { 37 | x = msg.sender; 38 | } 39 | } 40 | 41 | contract C5 { 42 | address private x = msg.sender; 43 | } 44 | 45 | contract C6 { 46 | } 47 | 48 | contract C7 { 49 | uint x; // a comment 50 | 51 | uint y; 52 | // a separate comment 53 | } 54 | 55 | contract C8 { 56 | address private x; 57 | address private y = address(this); 58 | 59 | constructor() { 60 | x = msg.sender; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/project/ISomeInterface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | interface ISomeInterface { 5 | function someFunction() external returns (bool); 6 | function someOtherFunction() external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/project/SomeContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import { ISomeInterface } from "./ISomeInterface.sol"; 5 | import { SomeLibrary } from "./SomeLibrary.sol"; 6 | import { SomeStatelessContract } from "./SomeStatelessContract.sol"; 7 | 8 | interface ISomeContract is ISomeInterface {} 9 | 10 | error Error1(uint256); 11 | 12 | function freeFn_1(uint256 x) pure { revert Error1(x); } 13 | 14 | struct SomeStruct { 15 | uint member; 16 | } 17 | 18 | contract SomeBaseContract { 19 | function test(ISomeInterface other) public virtual returns (bool) { 20 | return SomeLibrary.bothFunctions(other); 21 | } 22 | } 23 | 24 | contract SomeContract is ISomeContract, SomeBaseContract { 25 | SomeStruct s; 26 | 27 | /// @inheritdoc ISomeInterface 28 | function someFunction() public pure override returns (bool) { 29 | return false; 30 | } 31 | 32 | /// @inheritdoc ISomeInterface 33 | function someOtherFunction() public pure override returns (bool) { 34 | return true; 35 | } 36 | 37 | /// @inheritdoc SomeBaseContract 38 | function test(ISomeInterface other) public override returns (bool) { 39 | return SomeLibrary.bothFunctions(this) && super.test(other); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/project/SomeLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import { ISomeInterface } from "./ISomeInterface.sol"; 5 | 6 | library SomeLibrary { 7 | function bothFunctions(ISomeInterface instance) internal returns (bool) { 8 | return instance.someFunction() && instance.someOtherFunction(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/project/SomeOtherContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import { ISomeInterface } from "./ISomeInterface.sol"; 5 | import { SomeLibrary } from "./SomeLibrary.sol"; 6 | import { ISomeContract, SomeContract } from "./SomeContract.sol"; 7 | 8 | contract SomeOtherContract is SomeContract { 9 | function extraFunction() public returns (uint256) { 10 | return 42; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/project/SomeStatelessContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | /// @custom:stateless 5 | contract SomeStatelessContract {} 6 | -------------------------------------------------------------------------------- /contracts/rename-0.8.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8; 3 | 4 | contract A { 5 | function foo() external virtual {} 6 | } 7 | 8 | contract B is A { 9 | /// @inheritdoc A 10 | function foo() external override {} 11 | } 12 | 13 | library L { 14 | struct S { 15 | uint x; 16 | } 17 | } 18 | 19 | contract U { 20 | using L for L.S; 21 | } 22 | -------------------------------------------------------------------------------- /contracts/solc-0.6/AbstractContract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | abstract contract AbstractContract { 4 | } 5 | -------------------------------------------------------------------------------- /contracts/solc-0.6/AlreadyUpgradeable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract AlreadyUpgradeable { 4 | } 5 | -------------------------------------------------------------------------------- /contracts/solc-0.6/ClassInheritance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | interface CIC { 4 | function fooBar(uint a) external; 5 | } 6 | 7 | contract CIA { 8 | uint256 public foo; 9 | event log(string); 10 | constructor(uint bar) public { 11 | foo = bar; 12 | emit log("SIA"); 13 | } 14 | } 15 | 16 | contract CIB is CIA(324) { 17 | uint256 public val = 123; 18 | } 19 | 20 | contract CID is CIC { 21 | uint256 public val = 123; 22 | 23 | function fooBar(uint a) override external{ 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/solc-0.6/DiamondInheritance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract DA { 4 | event Log(string); 5 | uint256 public foo = 42; 6 | constructor() public { 7 | emit Log("DA"); 8 | } 9 | } 10 | 11 | contract DB1 is DA { 12 | string public hello = "hello"; 13 | constructor() public { 14 | emit Log("DB1"); 15 | } 16 | } 17 | 18 | contract DB2 is DA { 19 | bool public bar = true; 20 | constructor() public { 21 | emit Log("DB2"); 22 | } 23 | } 24 | 25 | contract DC is DB2, DB1 { 26 | address public owner = address(0x123); 27 | constructor() public { 28 | emit Log("DC"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/solc-0.6/ElementaryTypes.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract ElementaryTypes { 4 | address public owner = address(0x123); 5 | bool active = true; 6 | string hello = "hello"; 7 | int count = -123; 8 | uint ucount = 123; 9 | bytes32 samevar = "stringliteral"; 10 | uint x = 11 | 5; 12 | uint y=4; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/solc-0.6/ElementaryTypesWithConstructor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract ElementaryTypesWithConstructor { 4 | address public owner; 5 | bool active; 6 | string hello; 7 | int count; 8 | uint ucount; 9 | bytes32 samevar; 10 | 11 | constructor() public { 12 | owner = address(0x123); 13 | active = true; 14 | hello = "hello"; 15 | count = -123; 16 | ucount = 123; 17 | samevar = "stringliteral"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Imported.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract Imported1 { 4 | constructor(uint256 x, uint256 y) public { } 5 | } 6 | 7 | contract Imported2 is Imported1 { 8 | constructor(uint256 x, uint256 y) Imported1(x, y) public { } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Interface.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | interface Interface { 4 | } 5 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Library.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library Library { 4 | } 5 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Local.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import './Imported.sol'; 4 | 5 | contract Local is Imported2 { 6 | constructor(uint x, uint y) Imported2(x, y) public { } 7 | } 8 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Override.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract Parent1 { 4 | function foo() public virtual { 5 | } 6 | 7 | function bar() public virtual { 8 | } 9 | } 10 | 11 | contract Parent2 { 12 | function foo() public virtual { 13 | } 14 | } 15 | 16 | contract Child is Parent1, Parent2 { 17 | function foo() public override(Parent1, Parent2) { 18 | } 19 | 20 | function bar() public override { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/solc-0.6/Rename.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library RenameLibrary { 4 | function test() external { 5 | } 6 | } 7 | 8 | contract RenameContract { 9 | } 10 | 11 | contract RenameDeployer { 12 | RenameContract rc = RenameContract(0); 13 | 14 | constructor() public { 15 | new RenameContract(); 16 | } 17 | 18 | function deploy() external returns (RenameContract) { 19 | return new RenameContract(); 20 | } 21 | 22 | function test() external { 23 | RenameLibrary.test(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/solc-0.6/SimpleInheritance.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract SIA { 4 | uint256 public foo; 5 | event log(string); 6 | constructor() public { 7 | emit log("SIA"); 8 | } 9 | } 10 | 11 | contract SIB is SIA { 12 | uint256 public val = 123; 13 | } 14 | 15 | contract SIC is SIB { 16 | string public bar = "hello"; 17 | constructor() public { 18 | bar = "changed"; 19 | emit log("SIC"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/solc-0.6/StringConstructor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | contract StringConstructor { 4 | 5 | modifier hasModifier() { 6 | _; 7 | } 8 | 9 | constructor(string memory message) public hasModifier { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/solc-0.6/one/two/three/Deep.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "../../../ElementaryTypes.sol"; 4 | 5 | contract Deep { 6 | } 7 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | const { internalTask } = require('hardhat/config'); 2 | const { TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT } = require('hardhat/builtin-tasks/task-names'); 3 | 4 | require('hardhat-ignore-warnings'); 5 | 6 | internalTask(TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, async (args, hre, runSuper) => { 7 | const input = await runSuper(); 8 | input.settings.outputSelection['*']['*'].push('storageLayout'); 9 | return input; 10 | }); 11 | 12 | module.exports = { 13 | solidity: { 14 | compilers: ['0.6.7', '0.8.8', '0.8.20'].map(version => ({ version })), 15 | }, 16 | warnings: 'off', 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openzeppelin/upgrade-safe-transpiler", 3 | "version": "0.3.33", 4 | "description": "Solidity preprocessor used to generate OpenZeppelin Contracts Upgrade Safe.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "src", 9 | "dist", 10 | "!*.tsbuildinfo", 11 | "Initializable.sol" 12 | ], 13 | "bin": { 14 | "upgrade-safe-transpiler": "dist/cli.js" 15 | }, 16 | "scripts": { 17 | "test": "hardhat compile && ava", 18 | "test:watch": "hardhat compile && ava -w", 19 | "compile": "tsc", 20 | "prepare": "rimraf dist && npm run compile", 21 | "watch": "tsc -w", 22 | "lint": "eslint . --ignore-path .gitignore --max-warnings 0" 23 | }, 24 | "repository": "github:OpenZeppelin/openzeppelin-transpiler", 25 | "keywords": [ 26 | "solidity", 27 | "openzeppelin", 28 | "smart-contarcts", 29 | "ethereum", 30 | "upgradeability", 31 | "openzeppelin-sdk" 32 | ], 33 | "author": "Igor Yalovoy ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/OpenZeppelin/openzeppelin-sdk/issues" 37 | }, 38 | "homepage": "https://github.com/OpenZeppelin/openzeppelin-sdk#readme", 39 | "dependencies": { 40 | "ajv": "^8.0.0", 41 | "compare-versions": "^6.0.0", 42 | "ethereum-cryptography": "^2.0.0", 43 | "lodash": "^4.17.20", 44 | "minimatch": "^9.0.0", 45 | "minimist": "^1.2.5", 46 | "solidity-ast": "^0.4.51" 47 | }, 48 | "devDependencies": { 49 | "@types/lodash": "^4.14.165", 50 | "@types/minimist": "^1.2.1", 51 | "@types/mocha": "^7.0.2", 52 | "@types/node": "^10.17.44", 53 | "@typescript-eslint/eslint-plugin": "^6.0.0", 54 | "@typescript-eslint/parser": "^6.0.0", 55 | "ava": "^5.0.0", 56 | "eslint": "^8.0.0", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-plugin-prettier": "^5.0.0", 59 | "hardhat": "^2.0.9", 60 | "hardhat-ignore-warnings": "^0.2.9", 61 | "prettier": "^3.0.0", 62 | "rimraf": "^5.0.0", 63 | "ts-node": "^10.4.0", 64 | "typescript": "^4.0.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>OpenZeppelin/configs" 4 | ], 5 | "lockFileMaintenance": { 6 | "extends": ["schedule:monthly"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ast-resolver.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | import { 3 | astDereferencer, 4 | ASTDereferencer, 5 | ASTDereferencerError, 6 | ExtendedNodeType, 7 | ExtendedNodeTypeMap, 8 | isNodeType, 9 | } from 'solidity-ast/utils'; 10 | import { NodeType, NodeTypeMap } from 'solidity-ast/node'; 11 | 12 | import { SolcOutput } from './solc/input-output'; 13 | 14 | export class ASTResolver { 15 | private deref: ASTDereferencer; 16 | 17 | constructor( 18 | readonly output: SolcOutput, 19 | readonly exclude?: (source: string) => boolean, 20 | ) { 21 | this.deref = astDereferencer(output); 22 | } 23 | 24 | resolveContract(id: number): ContractDefinition | undefined { 25 | return this.tryResolveNode('ContractDefinition', id); 26 | } 27 | 28 | resolveNode(nodeType: T, id: number): ExtendedNodeTypeMap[T] { 29 | const { node, sourceUnit } = this.deref.withSourceUnit(nodeType, id); 30 | const source = sourceUnit.absolutePath; 31 | if (this.exclude?.(source)) { 32 | throw new Error(`Symbol #${id} was imported from an excluded file (${source})`); 33 | } else { 34 | return node; 35 | } 36 | } 37 | 38 | tryResolveNode(nodeType: T, id: number): NodeTypeMap[T] | undefined { 39 | try { 40 | const node = this.resolveNode('*', id); 41 | if (isNodeType(nodeType, node)) { 42 | return node; 43 | } 44 | } catch (e) { 45 | if (e instanceof ASTDereferencerError) { 46 | return undefined; 47 | } else { 48 | throw e; 49 | } 50 | } 51 | } 52 | } 53 | 54 | export const ASTResolverError = ASTDereferencerError; 55 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { promises as fs } from 'fs'; 4 | import path from 'path'; 5 | import minimist from 'minimist'; 6 | import type { HardhatRuntimeEnvironment } from 'hardhat/types'; 7 | 8 | import { transpile } from '.'; 9 | import { SolcOutput, SolcInput } from './solc/input-output'; 10 | import { findAlreadyInitializable } from './find-already-initializable'; 11 | 12 | async function getPaths() { 13 | const hardhat = require.resolve('hardhat', { paths: [process.cwd()] }); 14 | const hre: HardhatRuntimeEnvironment = (await import(hardhat)).default; 15 | return hre.config.paths; 16 | } 17 | 18 | interface Options { 19 | initializablePath?: string; 20 | buildInfo?: string; 21 | deleteOriginals: boolean; 22 | skipWithInit: boolean; 23 | exclude: string[]; 24 | publicInitializers: string[]; 25 | namespaced: boolean; 26 | namespaceExclude: string[]; 27 | peerProject?: string; 28 | } 29 | 30 | function readCommandFlags(resolveRootRelative: (p: string) => string): Options { 31 | const { 32 | b: buildInfo, 33 | i: initializablePath, 34 | p: publicInitializers = [], 35 | D: deleteOriginals = false, 36 | x: exclude = [], 37 | W: skipWithInit = false, 38 | n: namespaced = false, 39 | N: namespaceExclude = [], 40 | q: peerProject, 41 | } = minimist(process.argv.slice(2)); 42 | return { 43 | buildInfo, 44 | deleteOriginals, 45 | skipWithInit, 46 | namespaced, 47 | peerProject, 48 | namespaceExclude: ensureArray(namespaceExclude).map(resolveRootRelative), 49 | initializablePath: initializablePath && resolveRootRelative(initializablePath), 50 | publicInitializers: ensureArray(publicInitializers).map(resolveRootRelative), 51 | exclude: ensureArray(exclude).map(p => 52 | p.replace( 53 | /^(!*)(.*)/, 54 | (_: string, neg: string, pat: string) => neg + resolveRootRelative(pat), 55 | ), 56 | ), 57 | }; 58 | } 59 | 60 | function ensureArray(arr: T | T[]): T[] { 61 | if (Array.isArray(arr)) { 62 | return arr; 63 | } else { 64 | return [arr]; 65 | } 66 | } 67 | 68 | async function getVersion() { 69 | // eslint-disable-next-line @typescript-eslint/no-var-requires 70 | const pkg = require('../package.json'); 71 | return pkg.name + '@' + pkg.version; 72 | } 73 | 74 | async function main() { 75 | console.error(await getVersion()); 76 | 77 | const paths = await getPaths(); 78 | const resolveRootRelative = (p: string) => path.relative(paths.root, path.resolve(p)); 79 | const options = readCommandFlags(resolveRootRelative); 80 | 81 | let buildInfo = options.buildInfo; 82 | 83 | if (buildInfo === undefined) { 84 | const buildInfoDir = path.join(paths.artifacts, 'build-info'); 85 | const filenames = await fs.readdir(buildInfoDir); 86 | if (filenames.length != 1) { 87 | throw new Error(`Expected ${buildInfoDir} to contain only one file`); 88 | } 89 | buildInfo = path.join(buildInfoDir, filenames[0]); 90 | } 91 | 92 | const { 93 | input: solcInput, 94 | output: solcOutput, 95 | solcVersion, 96 | }: { 97 | input: SolcInput; 98 | output: SolcOutput; 99 | solcVersion: string; 100 | } = JSON.parse(await fs.readFile(buildInfo, 'utf8')); 101 | 102 | const transpiled = await transpile(solcInput, solcOutput, paths, { solcVersion, ...options }); 103 | 104 | await Promise.all( 105 | transpiled.map(async t => { 106 | const outputPath = path.join(paths.root, t.path); 107 | await fs.mkdir(path.dirname(outputPath), { recursive: true }); 108 | await fs.writeFile(outputPath, t.source); 109 | }), 110 | ); 111 | 112 | if (options.deleteOriginals) { 113 | const keep = new Set( 114 | [ 115 | ...transpiled.map(t => t.path), 116 | ...findAlreadyInitializable(solcOutput, options.initializablePath), 117 | ].map(p => path.join(paths.root, p)), 118 | ); 119 | if (options.initializablePath) { 120 | keep.add(path.join(paths.root, options.initializablePath)); 121 | } 122 | const originals = Object.keys(solcOutput.sources) 123 | .map(s => path.join(paths.root, s)) 124 | .filter(p => !keep.has(p)); 125 | 126 | await Promise.all(originals.map(p => fs.unlink(p).catch(() => undefined))); 127 | } 128 | } 129 | 130 | main().catch(e => { 131 | console.error(e); 132 | process.exit(1); 133 | }); 134 | -------------------------------------------------------------------------------- /src/find-already-initializable.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | 3 | import { getBuildInfo } from './test-utils/get-build-info'; 4 | 5 | import { SolcOutput } from './solc/input-output'; 6 | import { findAlreadyInitializable } from './find-already-initializable'; 7 | 8 | const test = _test as TestFn; 9 | 10 | interface Context { 11 | solcOutput: SolcOutput; 12 | } 13 | 14 | test.serial.before('compile', async t => { 15 | t.context.solcOutput = (await getBuildInfo('0.6')).output as SolcOutput; 16 | }); 17 | 18 | test('ok', t => { 19 | const alreadyInit = findAlreadyInitializable( 20 | t.context.solcOutput, 21 | 'contracts/find-already-init/Init1.sol', 22 | ); 23 | t.deepEqual(['contracts/find-already-init/AlreadyOk.sol'], alreadyInit); 24 | }); 25 | 26 | test('mixed', t => { 27 | t.throws( 28 | () => findAlreadyInitializable(t.context.solcOutput, 'contracts/find-already-init/Init2.sol'), 29 | { message: /contains both Initializable and non-Initializable/ }, 30 | ); 31 | }); 32 | 33 | test('no Initializable contract', t => { 34 | t.throws( 35 | () => findAlreadyInitializable(t.context.solcOutput, 'contracts/find-already-init/NoInit.sol'), 36 | { message: /does not contain Initializable/ }, 37 | ); 38 | }); 39 | 40 | test('more than Initializable contract', t => { 41 | t.throws( 42 | () => 43 | findAlreadyInitializable(t.context.solcOutput, 'contracts/find-already-init/InitPlus.sol'), 44 | { message: /contains contracts other than Initializable/ }, 45 | ); 46 | }); 47 | -------------------------------------------------------------------------------- /src/find-already-initializable.ts: -------------------------------------------------------------------------------- 1 | import { findAll } from 'solidity-ast/utils'; 2 | 3 | import { SolcOutput } from './solc/input-output'; 4 | 5 | export function findAlreadyInitializable( 6 | solcOutput: SolcOutput, 7 | initializablePath?: string, 8 | ): string[] { 9 | if (initializablePath === undefined) { 10 | return []; 11 | } 12 | 13 | const initSourceUnit = solcOutput.sources[initializablePath]?.ast; 14 | 15 | if (initSourceUnit === undefined) { 16 | throw new Error(`File ${initializablePath} is not found or has not been compiled`); 17 | } 18 | 19 | const [initContract, ...otherContracts] = findAll('ContractDefinition', initSourceUnit); 20 | 21 | if (otherContracts.length > 0) { 22 | throw new Error(`File ${initializablePath} contains contracts other than Initializable`); 23 | } 24 | 25 | if (initContract === undefined || initContract.name !== 'Initializable') { 26 | throw new Error(`File ${initializablePath} does not contain Initializable`); 27 | } 28 | 29 | const initializableSources = []; 30 | 31 | for (const source in solcOutput.sources) { 32 | if (source === initializablePath) { 33 | continue; 34 | } 35 | 36 | const { ast } = solcOutput.sources[source]; 37 | const initializable = new Set(); 38 | 39 | for (const contract of findAll('ContractDefinition', ast)) { 40 | if (contract.linearizedBaseContracts.includes(initContract.id)) { 41 | initializable.add(true); 42 | } else if (contract.contractKind === 'contract') { 43 | initializable.add(false); 44 | } 45 | } 46 | 47 | if (initializable.has(true)) { 48 | if (initializable.has(false)) { 49 | throw new Error( 50 | `File ${source} contains both Initializable and non-Initializable contracts`, 51 | ); 52 | } else { 53 | initializableSources.push(source); 54 | } 55 | } 56 | } 57 | 58 | return initializableSources; 59 | } 60 | -------------------------------------------------------------------------------- /src/generate-with-init.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | import { mapValues, pick } from 'lodash'; 3 | 4 | import { getBuildInfo } from './test-utils/get-build-info'; 5 | 6 | import { SolcInput, SolcOutput } from './solc/input-output'; 7 | import { Transform } from './transform'; 8 | 9 | import { generateWithInit } from './generate-with-init'; 10 | 11 | const test = _test as TestFn; 12 | 13 | interface Context { 14 | solcInputOutput(...paths: string[]): [SolcInput, SolcOutput]; 15 | } 16 | 17 | test.before('gather solc input output', async t => { 18 | const buildInfo = await getBuildInfo('0.6'); 19 | const solcInput = buildInfo.input; 20 | const solcOutput = buildInfo.output as SolcOutput; 21 | 22 | t.context.solcInputOutput = (...paths) => { 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | return [solcInput, solcOutput].map(x => mapValues(x, y => pick(y, paths))) as any; 25 | }; 26 | }); 27 | 28 | test('simple', t => { 29 | const transform = new Transform(...t.context.solcInputOutput('contracts/GenerateWithInit.sol')); 30 | t.snapshot(generateWithInit(transform, 'contracts/WithInit.sol')); 31 | }); 32 | -------------------------------------------------------------------------------- /src/generate-with-init.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/generate-with-init.test.ts` 2 | 3 | The actual snapshot is saved in `generate-with-init.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## simple 8 | 9 | > Snapshot 1 10 | 11 | `// SPDX-License-Identifier: UNLICENSED␊ 12 | pragma solidity >=0.6 <0.9;␊ 13 | pragma experimental ABIEncoderV2;␊ 14 | ␊ 15 | import "./GenerateWithInitUpgradeable.sol";␊ 16 | ␊ 17 | contract Foo1UpgradeableWithInit is Foo1Upgradeable {␊ 18 | constructor() public payable initializer {␊ 19 | __Foo1_init();␊ 20 | }␊ 21 | }␊ 22 | import "./GenerateWithInitUpgradeable.sol";␊ 23 | ␊ 24 | contract Foo2UpgradeableWithInit is Foo2Upgradeable {␊ 25 | constructor(uint x, string memory y) public payable initializer {␊ 26 | __Foo2_init(x, y);␊ 27 | }␊ 28 | }␊ 29 | ` 30 | -------------------------------------------------------------------------------- /src/generate-with-init.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/generate-with-init.test.ts.snap -------------------------------------------------------------------------------- /src/generate-with-init.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { satisfies } from 'compare-versions'; 3 | import { Transform } from './transform'; 4 | import { formatLines, Line } from './transformations/utils/format-lines'; 5 | import { findAll } from 'solidity-ast/utils'; 6 | import { getConstructor } from './solc/ast-utils'; 7 | import { renameContract, renamePath } from './rename'; 8 | import { relativePath } from './utils/relative-path'; 9 | import { hasConstructorOverride } from './utils/upgrades-overrides'; 10 | 11 | export function generateWithInit( 12 | transform: Transform, 13 | destPath: string, 14 | solcVersion = '0.6.0', 15 | ): string { 16 | const pragmaVersion = satisfies(solcVersion, '>=0.7') ? '0.7' : '0.6'; 17 | const res: Line[] = [ 18 | `// SPDX-License-Identifier: UNLICENSED`, 19 | `pragma solidity >=${pragmaVersion} <0.9;`, 20 | `pragma experimental ABIEncoderV2;`, 21 | ``, 22 | ]; 23 | 24 | for (const sourceUnit of transform.asts()) { 25 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 26 | if (contract.contractKind !== 'contract' || !contract.fullyImplemented) { 27 | continue; 28 | } 29 | 30 | const constructorNode = getConstructor(contract); 31 | 32 | let argNames = ''; 33 | if (constructorNode) { 34 | argNames = constructorNode.parameters.parameters.map(p => p.name).join(', '); 35 | } 36 | 37 | let argsList = ''; 38 | 39 | if (constructorNode) { 40 | const source = transform.read(constructorNode); 41 | const argsMatch = source.match(/\((.*?)\)/s); 42 | if (argsMatch === null) { 43 | throw new Error(`Could not find constructor arguments for ${contract.name}`); 44 | } 45 | [, argsList] = argsMatch; 46 | } 47 | 48 | const renamedContract = renameContract(contract.name); 49 | 50 | let parents: string[] = []; 51 | let statements: string[] = []; 52 | if (hasConstructorOverride(contract)) { 53 | parents = [`${renameContract(contract.name)}(${argNames})`]; 54 | } else { 55 | statements = [`__${contract.name}_init(${argNames});`]; 56 | } 57 | 58 | res.push( 59 | `import "${relativePath(path.dirname(destPath), renamePath(sourceUnit.absolutePath))}";`, 60 | ``, 61 | `contract ${renamedContract}WithInit is ${renamedContract} {`, 62 | [ 63 | [ 64 | `constructor(${argsList})`, 65 | ...parents, 66 | ...(satisfies(pragmaVersion, '>=0.7') ? [] : [`public`]), 67 | `payable initializer {`, 68 | ].join(' '), 69 | statements, 70 | `}`, 71 | ], 72 | `}`, 73 | ); 74 | } 75 | } 76 | 77 | return formatLines(0, res); 78 | } 79 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | import { promises as fs } from 'fs'; 3 | import hre from 'hardhat'; 4 | 5 | import { getBuildInfo } from './test-utils/get-build-info'; 6 | import { OutputFile, transpile } from '.'; 7 | import { SolcOutput } from './solc/input-output'; 8 | 9 | const test = _test as TestFn; 10 | 11 | interface Context { 12 | files: OutputFile[]; 13 | } 14 | 15 | const fileNames = [ 16 | 'ClassInheritance.sol', 17 | 'Override.sol', 18 | 'DiamondInheritance.sol', 19 | 'Deep.sol', 20 | 'ElementaryTypes.sol', 21 | 'ElementaryTypesWithConstructor.sol', 22 | 'Imported.sol', 23 | 'Local.sol', 24 | 'SimpleInheritance.sol', 25 | 'StringConstructor.sol', 26 | 'Library.sol', 27 | 'AbstractContract.sol', 28 | 'Interface.sol', 29 | 'Rename.sol', 30 | ]; 31 | 32 | const excludeDir = 'contracts/invalid/'; 33 | 34 | test.serial.before('compile', async t => { 35 | const buildInfo = await getBuildInfo('0.6'); 36 | const solcInput = buildInfo.input; 37 | const solcOutput = buildInfo.output as SolcOutput; 38 | const exclude = (await fs.readdir(excludeDir)).map(f => excludeDir + f); 39 | 40 | t.context.files = await transpile(solcInput, solcOutput, hre.config.paths, { exclude }); 41 | }); 42 | 43 | for (const fileName of fileNames) { 44 | test(fileName, t => { 45 | const file = t.context.files.find(f => f.fileName === fileName); 46 | t.not(file, undefined, 'file not found'); 47 | t.snapshot(file); 48 | }); 49 | } 50 | 51 | test('AlreadyUpgradeable.sol', t => { 52 | const file = t.context.files.find(f => f.fileName === 'AlreadyUpgradeable.sol'); 53 | t.is(file, undefined); 54 | }); 55 | -------------------------------------------------------------------------------- /src/index.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/index.test.ts.snap -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { mapValues } from 'lodash'; 4 | import { minimatch } from 'minimatch'; 5 | 6 | import { matcher } from './utils/matcher'; 7 | import { renamePath, isRenamed } from './rename'; 8 | import { SolcOutput, SolcInput } from './solc/input-output'; 9 | import { Transform } from './transform'; 10 | import { generateWithInit } from './generate-with-init'; 11 | import { findAlreadyInitializable } from './find-already-initializable'; 12 | import { preparePeerProject } from './prepare-peer-project'; 13 | 14 | import { fixImportDirectives } from './transformations/fix-import-directives'; 15 | import { renameIdentifiers } from './transformations/rename-identifiers'; 16 | import { prependInitializableBase } from './transformations/prepend-initializable-base'; 17 | import { removeStateVarInits } from './transformations/purge-var-inits'; 18 | import { removeImmutable } from './transformations/remove-immutable'; 19 | import { peerImport } from './transformations/peer-import'; 20 | import { removeInheritanceListArguments } from './transformations/remove-inheritance-list-args'; 21 | import { renameContractDefinition } from './transformations/rename-contract-definition'; 22 | import { appendInitializableImport } from './transformations/append-initializable-import'; 23 | import { fixNewStatement } from './transformations/fix-new-statement'; 24 | import { addRequiredPublicInitializer } from './transformations/add-required-public-initializers'; 25 | import { addStorageGaps } from './transformations/add-storage-gaps'; 26 | import { addNamespaceStruct } from './transformations/add-namespace-struct'; 27 | import { renameInheritdoc } from './transformations/rename-inheritdoc'; 28 | import { 29 | transformConstructor, 30 | removeLeftoverConstructorHead, 31 | } from './transformations/transform-constructor'; 32 | 33 | interface Paths { 34 | root: string; 35 | sources: string; 36 | } 37 | 38 | export interface OutputFile { 39 | fileName: string; 40 | source: string; 41 | path: string; 42 | } 43 | 44 | interface TranspileOptions { 45 | initializablePath?: string; 46 | exclude?: string[]; 47 | publicInitializers?: string[]; 48 | solcVersion?: string; 49 | skipWithInit?: boolean; 50 | namespaced?: boolean; 51 | namespaceExclude?: string[]; 52 | peerProject?: string; 53 | } 54 | 55 | function getExtraOutputPaths( 56 | paths: Paths, 57 | options: TranspileOptions = {}, 58 | ): Record<'initializable' | 'withInit', string> { 59 | const outputPaths = mapValues( 60 | { 61 | initializable: 'Initializable.sol', 62 | withInit: 'mocks/WithInit.sol', 63 | }, 64 | s => path.relative(paths.root, path.join(paths.sources, s)), 65 | ); 66 | 67 | if (options.initializablePath) { 68 | outputPaths.initializable = options.initializablePath; 69 | } 70 | 71 | return outputPaths; 72 | } 73 | 74 | export async function transpile( 75 | solcInput: SolcInput, 76 | solcOutput: SolcOutput, 77 | paths: Paths, 78 | options: TranspileOptions = {}, 79 | ): Promise { 80 | const outputPaths = getExtraOutputPaths(paths, options); 81 | const alreadyInitializable = findAlreadyInitializable(solcOutput, options.initializablePath); 82 | 83 | const excludeSet = new Set([...alreadyInitializable, ...Object.values(outputPaths)]); 84 | const excludeMatch = matcher(options.exclude ?? []); 85 | 86 | const namespaceInclude = (source: string) => { 87 | const namespaced = options.namespaced ?? false; 88 | const namespaceExclude = options.namespaceExclude ?? []; 89 | return namespaced && !namespaceExclude.some(p => minimatch(source, p)); 90 | }; 91 | 92 | const transform = new Transform(solcInput, solcOutput, { 93 | exclude: source => excludeSet.has(source) || (excludeMatch(source) ?? isRenamed(source)), 94 | }); 95 | 96 | if (options.peerProject !== undefined) { 97 | preparePeerProject(transform, options.peerProject); 98 | } 99 | 100 | transform.apply(renameIdentifiers); 101 | transform.apply(renameContractDefinition); 102 | transform.apply(renameInheritdoc); 103 | transform.apply(prependInitializableBase); 104 | transform.apply(fixImportDirectives(options.peerProject !== undefined)); 105 | transform.apply(appendInitializableImport(outputPaths.initializable)); 106 | transform.apply(fixNewStatement); 107 | transform.apply(transformConstructor(namespaceInclude)); 108 | transform.apply(removeLeftoverConstructorHead); 109 | transform.apply(addRequiredPublicInitializer(options.publicInitializers)); 110 | transform.apply(removeInheritanceListArguments); 111 | transform.apply(removeStateVarInits); 112 | transform.apply(removeImmutable); 113 | transform.apply(peerImport); 114 | 115 | if (options.namespaced) { 116 | transform.apply(addNamespaceStruct(namespaceInclude)); 117 | } else { 118 | transform.apply(addStorageGaps); 119 | } 120 | 121 | // build a final array of files to return 122 | const outputFiles: OutputFile[] = []; 123 | 124 | const results = transform.results(); 125 | 126 | for (const file in results) { 127 | const transformedSource = results[file]; 128 | 129 | outputFiles.push({ 130 | source: transformedSource, 131 | path: renamePath(file), 132 | fileName: path.basename(file), 133 | }); 134 | } 135 | 136 | const initializableSource = 137 | options.initializablePath !== undefined 138 | ? transpileInitializable(solcInput, solcOutput, paths, { 139 | ...options, 140 | initializablePath: options.initializablePath, 141 | }) 142 | : fs.readFileSync(require.resolve('../Initializable.sol'), 'utf8'); 143 | 144 | outputFiles.push({ 145 | source: initializableSource, 146 | path: outputPaths.initializable, 147 | fileName: path.basename(outputPaths.initializable), 148 | }); 149 | 150 | if (!options.skipWithInit) { 151 | outputFiles.push({ 152 | source: generateWithInit(transform, outputPaths.withInit, options.solcVersion), 153 | path: outputPaths.withInit, 154 | fileName: path.basename(outputPaths.withInit), 155 | }); 156 | } 157 | 158 | return outputFiles; 159 | } 160 | 161 | function transpileInitializable( 162 | solcInput: SolcInput, 163 | solcOutput: SolcOutput, 164 | paths: Paths, 165 | options: TranspileOptions & Required>, 166 | ): string { 167 | const transform = new Transform(solcInput, solcOutput); 168 | 169 | transform.apply(function* (ast, tools) { 170 | if (ast.absolutePath === options.initializablePath) { 171 | yield* renameIdentifiers(ast, tools); 172 | yield* fixImportDirectives(options.peerProject !== undefined)(ast, tools); 173 | } 174 | }); 175 | 176 | return transform.results()[options.initializablePath]; 177 | } 178 | -------------------------------------------------------------------------------- /src/prepare-peer-project.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | import hre from 'hardhat'; 3 | import path from 'path'; 4 | 5 | import { getBuildInfo } from './test-utils/get-build-info'; 6 | import { OutputFile, transpile } from '.'; 7 | import { SolcOutput } from './solc/input-output'; 8 | 9 | const test = _test as TestFn; 10 | 11 | interface Context { 12 | inputs: string[]; 13 | files: OutputFile[]; 14 | } 15 | 16 | const projectDir = 'contracts/project'; 17 | const peerProject = '@openzeppelin/test/..'; 18 | 19 | test.serial.before('compile', async t => { 20 | const buildInfo = await getBuildInfo('0.8'); 21 | const solcInput = buildInfo.input; 22 | const solcOutput = buildInfo.output as SolcOutput; 23 | const exclude = Object.keys(solcOutput.sources).filter(path => !path.startsWith(projectDir)); 24 | 25 | t.context.inputs = Object.keys(solcInput.sources); 26 | t.context.files = await transpile(solcInput, solcOutput, hre.config.paths, { 27 | exclude, 28 | peerProject, 29 | }); 30 | }); 31 | 32 | for (const fileName of ['ISomeInterface.sol', 'SomeLibrary.sol', 'SomeStatelessContract.sol']) { 33 | test(`do not transpile ${fileName}`, t => { 34 | const file = t.context.files.find(f => f.fileName === fileName); 35 | // source file exists 36 | t.true(t.context.inputs.includes(path.join(projectDir, fileName))); 37 | // transpiled file does not exist 38 | t.is(file, undefined, 'file should not be transpiled'); 39 | }); 40 | } 41 | 42 | for (const fileName of ['SomeContract.sol', 'SomeOtherContract.sol']) { 43 | test(`transpile ${fileName}`, t => { 44 | const file = t.context.files.find(f => f.fileName === fileName); 45 | // source file exists 46 | t.true(t.context.inputs.includes(path.join(projectDir, fileName))); 47 | // transpiled file exists 48 | t.not(file, undefined, 'file not found'); 49 | // snapshot 50 | t.snapshot(file); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/prepare-peer-project.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/prepare-peer-project.test.ts` 2 | 3 | The actual snapshot is saved in `prepare-peer-project.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## transpile SomeContract.sol 8 | 9 | > Snapshot 1 10 | 11 | { 12 | fileName: 'SomeContract.sol', 13 | path: 'contracts/project/SomeContractUpgradeable.sol', 14 | source: `// SPDX-License-Identifier: UNLICENSED␊ 15 | pragma solidity ^0.8.0;␊ 16 | ␊ 17 | import {ISomeInterface} from "@openzeppelin/contracts/project/ISomeInterface.sol";␊ 18 | import {SomeLibrary} from "@openzeppelin/contracts/project/SomeLibrary.sol";␊ 19 | import {SomeStatelessContract} from "@openzeppelin/contracts/project/SomeStatelessContract.sol";␊ 20 | import {Initializable} from "../Initializable.sol";␊ 21 | ␊ 22 | import { ISomeContract } from "@openzeppelin/contracts/project/SomeContract.sol";␊ 23 | ␊ 24 | import { Error1 } from "@openzeppelin/contracts/project/SomeContract.sol";␊ 25 | ␊ 26 | import { freeFn_1 } from "@openzeppelin/contracts/project/SomeContract.sol";␊ 27 | ␊ 28 | import { SomeStruct } from "@openzeppelin/contracts/project/SomeContract.sol";␊ 29 | ␊ 30 | contract SomeBaseContractUpgradeable is Initializable {␊ 31 | function __SomeBaseContract_init() internal onlyInitializing {␊ 32 | }␊ 33 | ␊ 34 | function __SomeBaseContract_init_unchained() internal onlyInitializing {␊ 35 | }␊ 36 | function test(ISomeInterface other) public virtual returns (bool) {␊ 37 | return SomeLibrary.bothFunctions(other);␊ 38 | }␊ 39 | ␊ 40 | /**␊ 41 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 42 | * variables without shifting down storage in the inheritance chain.␊ 43 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 44 | */␊ 45 | uint256[50] private __gap;␊ 46 | }␊ 47 | ␊ 48 | contract SomeContractUpgradeable is Initializable, ISomeContract, SomeBaseContractUpgradeable {␊ 49 | SomeStruct s;␊ 50 | ␊ 51 | function __SomeContract_init() internal onlyInitializing {␊ 52 | }␊ 53 | ␊ 54 | function __SomeContract_init_unchained() internal onlyInitializing {␊ 55 | }␊ 56 | /// @inheritdoc ISomeInterface␊ 57 | function someFunction() public pure override returns (bool) {␊ 58 | return false;␊ 59 | }␊ 60 | ␊ 61 | /// @inheritdoc ISomeInterface␊ 62 | function someOtherFunction() public pure override returns (bool) {␊ 63 | return true;␊ 64 | }␊ 65 | ␊ 66 | /// @inheritdoc SomeBaseContractUpgradeable␊ 67 | function test(ISomeInterface other) public override returns (bool) {␊ 68 | return SomeLibrary.bothFunctions(this) && super.test(other);␊ 69 | }␊ 70 | ␊ 71 | /**␊ 72 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 73 | * variables without shifting down storage in the inheritance chain.␊ 74 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 75 | */␊ 76 | uint256[49] private __gap;␊ 77 | }␊ 78 | `, 79 | } 80 | 81 | ## transpile SomeOtherContract.sol 82 | 83 | > Snapshot 1 84 | 85 | { 86 | fileName: 'SomeOtherContract.sol', 87 | path: 'contracts/project/SomeOtherContractUpgradeable.sol', 88 | source: `// SPDX-License-Identifier: UNLICENSED␊ 89 | pragma solidity ^0.8.0;␊ 90 | ␊ 91 | import {ISomeInterface} from "@openzeppelin/contracts/project/ISomeInterface.sol";␊ 92 | import {SomeLibrary} from "@openzeppelin/contracts/project/SomeLibrary.sol";␊ 93 | import {ISomeContract} from "@openzeppelin/contracts/project/SomeContract.sol";␊ 94 | import {SomeContractUpgradeable} from "./SomeContractUpgradeable.sol";␊ 95 | import {Initializable} from "../Initializable.sol";␊ 96 | ␊ 97 | contract SomeOtherContractUpgradeable is Initializable, SomeContractUpgradeable {␊ 98 | function __SomeOtherContract_init() internal onlyInitializing {␊ 99 | }␊ 100 | ␊ 101 | function __SomeOtherContract_init_unchained() internal onlyInitializing {␊ 102 | }␊ 103 | function extraFunction() public returns (uint256) {␊ 104 | return 42;␊ 105 | }␊ 106 | ␊ 107 | /**␊ 108 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 109 | * variables without shifting down storage in the inheritance chain.␊ 110 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 111 | */␊ 112 | uint256[50] private __gap;␊ 113 | }␊ 114 | `, 115 | } 116 | -------------------------------------------------------------------------------- /src/prepare-peer-project.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/prepare-peer-project.test.ts.snap -------------------------------------------------------------------------------- /src/prepare-peer-project.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { Transform } from './transform'; 4 | import { extractContractStorageSize, extractContractStateless } from './utils/natspec'; 5 | import { isStorageVariable } from './transformations/utils/is-storage-variable'; 6 | 7 | export function preparePeerProject(transform: Transform, peerProject: string) { 8 | for (const ast of transform.asts()) { 9 | let shouldExclude = true; 10 | for (const node of ast.nodes) { 11 | switch (node.nodeType) { 12 | case 'ContractDefinition': { 13 | if (node.contractKind === 'contract') { 14 | if (!extractContractStateless(node)) { 15 | shouldExclude = false; 16 | break; 17 | } 18 | if (extractContractStorageSize(node) !== undefined) { 19 | throw transform.error( 20 | node, 21 | 'Contract marked as stateless should not have an associated storage size', 22 | ); 23 | } 24 | for (const decl of node.nodes) { 25 | if ( 26 | decl.nodeType == 'VariableDeclaration' && 27 | isStorageVariable(decl, transform.resolver) 28 | ) { 29 | throw transform.error( 30 | node, 31 | 'Contract marked as stateless should not contain storage variable declarations', 32 | ); 33 | } 34 | if (decl.nodeType == 'FunctionDefinition' && decl.kind == 'constructor') { 35 | throw transform.error( 36 | node, 37 | 'Contract marked as stateless should not have a constructor', 38 | ); 39 | } 40 | } 41 | } 42 | transform.getData(node).importFromPeer = path.join(peerProject, ast.absolutePath); 43 | break; 44 | } 45 | case 'EnumDefinition': 46 | case 'ErrorDefinition': 47 | case 'FunctionDefinition': 48 | case 'StructDefinition': 49 | case 'UserDefinedValueTypeDefinition': 50 | case 'VariableDeclaration': { 51 | transform.getData(node).importFromPeer = path.join(peerProject, ast.absolutePath); 52 | break; 53 | } 54 | case 'ImportDirective': 55 | case 'PragmaDirective': 56 | case 'UsingForDirective': { 57 | break; 58 | } 59 | } 60 | } 61 | if (shouldExclude) { 62 | transform.exclude(ast.absolutePath); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/rename.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const suffix = 'Upgradeable'; 4 | 5 | export function isRenamed(name: string): boolean { 6 | return path.basename(name, '.sol').endsWith(suffix); 7 | } 8 | 9 | export function renameContract(name: string): string { 10 | if (name.endsWith(suffix)) { 11 | return name; 12 | } else { 13 | return name + suffix; 14 | } 15 | } 16 | 17 | export function renamePath(filePath: string): string { 18 | const { dir, name, ext } = path.parse(filePath); 19 | return path.format({ dir, ext, name: renameContract(name) }); 20 | } 21 | -------------------------------------------------------------------------------- /src/shifts.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { shiftBounds } from './shifts'; 4 | 5 | test('shift start', t => { 6 | const original = { start: 1, length: 2 }; 7 | const offsets = [{ location: 0, amount: 1, lengthZero: false }]; 8 | const shifted = { start: 2, length: 2 }; 9 | t.deepEqual(shifted, shiftBounds(offsets, original)); 10 | }); 11 | 12 | test('shift start overlapping', t => { 13 | const original = { start: 1, length: 2 }; 14 | const offsets = [{ location: 1, amount: 1, lengthZero: false }]; 15 | const shifted = { start: 2, length: 2 }; 16 | t.deepEqual(shifted, shiftBounds(offsets, original)); 17 | }); 18 | 19 | test('shift length', t => { 20 | const original = { start: 1, length: 2 }; 21 | const offsets = [{ location: 2, amount: 1, lengthZero: false }]; 22 | const shifted = { start: 1, length: 3 }; 23 | t.deepEqual(shifted, shiftBounds(offsets, original)); 24 | }); 25 | 26 | test('shift length overlapping length nonzero', t => { 27 | const original = { start: 1, length: 2 }; 28 | const offsets = [{ location: 3, amount: 1, lengthZero: false }]; 29 | const shifted = { start: 1, length: 3 }; 30 | t.deepEqual(shifted, shiftBounds(offsets, original)); 31 | }); 32 | 33 | test('shift length overlapping length zero', t => { 34 | const original = { start: 1, length: 2 }; 35 | const offsets = [{ location: 3, amount: 1, lengthZero: true }]; 36 | const shifted = { start: 1, length: 2 }; 37 | t.deepEqual(shifted, shiftBounds(offsets, original)); 38 | }); 39 | 40 | test('shift start and length', t => { 41 | const original = { start: 1, length: 2 }; 42 | const offsets = [ 43 | { location: 0, amount: 1, lengthZero: false }, 44 | { location: 2, amount: 1, lengthZero: false }, 45 | ]; 46 | const shifted = { start: 2, length: 3 }; 47 | t.deepEqual(shifted, shiftBounds(offsets, original)); 48 | }); 49 | -------------------------------------------------------------------------------- /src/shifts.ts: -------------------------------------------------------------------------------- 1 | import { Bounds } from './transformations/type'; 2 | 3 | export interface Shift { 4 | amount: number; 5 | location: number; 6 | lengthZero: boolean; 7 | } 8 | 9 | export function shiftBounds(shifts: Shift[], b: Bounds): Bounds { 10 | const end = b.start + b.length; 11 | 12 | let startOffset = 0; 13 | let lengthOffset = 0; 14 | 15 | for (const s of shifts) { 16 | if (s.location <= b.start) { 17 | startOffset += s.amount; 18 | } else if (s.location < end || (s.location === end && !s.lengthZero)) { 19 | lengthOffset += s.amount; 20 | } 21 | } 22 | 23 | return { start: b.start + startOffset, length: b.length + lengthOffset }; 24 | } 25 | -------------------------------------------------------------------------------- /src/solc/ast-utils.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import util from 'util'; 3 | import astNodeSchema from 'solidity-ast/schema.json'; 4 | import { Node } from 'solidity-ast/node'; 5 | import { findAll } from 'solidity-ast/utils'; 6 | import { ContractDefinition, FunctionDefinition } from 'solidity-ast'; 7 | 8 | import { Bounds, WithSrc } from '../transformations/type'; 9 | 10 | const nodeSchemaValidator = new Ajv({ allErrors: true }); 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | nodeSchemaValidator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 13 | 14 | const isASTNode = nodeSchemaValidator.compile(astNodeSchema); 15 | 16 | export function throwIfInvalidNode(node: unknown): asserts node is Node { 17 | if (!isASTNode(node)) { 18 | throw new Error(util.inspect(node) + ' is not a valid AST node.'); 19 | } 20 | } 21 | 22 | export function getNodeBounds(node: WithSrc): Bounds { 23 | const [start, length] = node.src.split(':', 2).map(val => parseInt(val)); 24 | return { start, length }; 25 | } 26 | 27 | export function getConstructor(node: ContractDefinition): FunctionDefinition | undefined { 28 | for (const fndef of findAll('FunctionDefinition', node)) { 29 | if (fndef.kind === 'constructor') { 30 | return fndef; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/solc/input-output.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | 3 | export interface SolcInput { 4 | sources: { 5 | [file in string]: { content: string } | { urls: string[] }; 6 | }; 7 | } 8 | 9 | export interface SolcOutput { 10 | contracts: { 11 | [file in string]: { 12 | [contract in string]: { 13 | storageLayout?: StorageLayout; 14 | }; 15 | }; 16 | }; 17 | sources: { 18 | [file in string]: { 19 | ast: SourceUnit; 20 | id: number; 21 | }; 22 | }; 23 | } 24 | 25 | export interface StorageLayout { 26 | storage: { 27 | astId: number; 28 | type: string; 29 | }[]; 30 | types: 31 | | null 32 | | { 33 | [t in string]?: { 34 | numberOfBytes: string; // ascii number 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/solc/layout-getter.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { SolcOutput, StorageLayout } from './input-output'; 4 | 5 | export type LayoutGetter = (c: ContractDefinition) => StorageLayout; 6 | 7 | export function layoutGetter(output: SolcOutput): LayoutGetter { 8 | const map = new Map(); 9 | 10 | for (const file in output.sources) { 11 | const { ast } = output.sources[file]; 12 | for (const contract of findAll('ContractDefinition', ast)) { 13 | map.set(contract.id, output.contracts[file][contract.name].storageLayout); 14 | } 15 | } 16 | 17 | return c => { 18 | const layout = map.get(c.id); 19 | if (layout === undefined) { 20 | throw new Error(`Storage layout for contract ${c.name} unavailable`); 21 | } 22 | return layout; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/solc/src-decoder.ts: -------------------------------------------------------------------------------- 1 | import { SolcOutput } from './input-output'; 2 | 3 | export interface SourceLocation { 4 | start: number; 5 | length: number; 6 | source: string; 7 | } 8 | 9 | export type SrcDecoder = (src: string) => SourceLocation; 10 | 11 | export function srcDecoder(output: SolcOutput): SrcDecoder { 12 | return src => { 13 | const [start, length, sourceId] = src.split(':').map(s => parseInt(s)); 14 | const source = Object.keys(output.sources).find(s => output.sources[s].id === sourceId); 15 | if (source === undefined) { 16 | throw new Error(`No source with id ${sourceId}`); 17 | } 18 | return { start, length, source }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/test-utils/get-build-info.ts: -------------------------------------------------------------------------------- 1 | import hre from 'hardhat'; 2 | import type { BuildInfo } from 'hardhat/types'; 3 | 4 | import { promises as fs } from 'fs'; 5 | import path from 'path'; 6 | 7 | export async function getBuildInfo(version: string): Promise { 8 | const buildInfoPath = path.join(hre.config.paths.artifacts, 'build-info'); 9 | const filenames = await fs.readdir(buildInfoPath); 10 | const buildInfos: BuildInfo[] = await Promise.all( 11 | filenames.map(async f => JSON.parse(await fs.readFile(path.join(buildInfoPath, f), 'utf8'))), 12 | ); 13 | 14 | const matching = buildInfos.filter(i => i.solcVersion.startsWith(version)); 15 | 16 | if (matching.length > 1) { 17 | throw new Error('More than 1 matching compilation found'); 18 | } else if (matching.length < 1) { 19 | throw new Error('Compilation not found'); 20 | } 21 | 22 | return matching[0]; 23 | } 24 | -------------------------------------------------------------------------------- /src/transform-0.8.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | 3 | import { getBuildInfo } from './test-utils/get-build-info'; 4 | 5 | import { SolcInput, SolcOutput } from './solc/input-output'; 6 | import { Transform } from './transform'; 7 | 8 | import { renameIdentifiers } from './transformations/rename-identifiers'; 9 | import { removeImmutable } from './transformations/remove-immutable'; 10 | import { removeStateVarInits } from './transformations/purge-var-inits'; 11 | import { 12 | removeLeftoverConstructorHead, 13 | transformConstructor, 14 | } from './transformations/transform-constructor'; 15 | import { renameInheritdoc } from './transformations/rename-inheritdoc'; 16 | import { addStorageGaps } from './transformations/add-storage-gaps'; 17 | import { renameContractDefinition } from './transformations/rename-contract-definition'; 18 | 19 | const test = _test as TestFn; 20 | 21 | interface Context { 22 | solcInput: SolcInput; 23 | solcOutput: SolcOutput; 24 | transform: Transform; 25 | } 26 | 27 | test.serial.before('compile', async t => { 28 | const buildInfo = await getBuildInfo('0.8'); 29 | 30 | t.context.solcInput = buildInfo.input; 31 | t.context.solcOutput = buildInfo.output as SolcOutput; 32 | }); 33 | 34 | test.beforeEach('transform', async t => { 35 | t.context.transform = new Transform(t.context.solcInput, t.context.solcOutput); 36 | }); 37 | 38 | test('rename parents in solidity 0.8', t => { 39 | const file = 'contracts/rename-0.8.sol'; 40 | t.context.transform.apply(renameIdentifiers); 41 | t.context.transform.apply(renameInheritdoc); 42 | t.snapshot(t.context.transform.results()[file]); 43 | }); 44 | 45 | test('correctly index when utf8 characters', t => { 46 | const file = 'contracts/TransformUtf8Chars.sol'; 47 | t.context.transform.apply(renameIdentifiers); 48 | t.context.transform.apply(renameContractDefinition); 49 | t.snapshot(t.context.transform.results()[file]); 50 | }); 51 | 52 | test('preserves immutable if allowed', t => { 53 | const file = 'contracts/TransformAllowedImmutable.sol'; 54 | t.context.transform.apply(transformConstructor()); 55 | t.context.transform.apply(removeLeftoverConstructorHead); 56 | t.context.transform.apply(removeStateVarInits); 57 | t.context.transform.apply(removeImmutable); 58 | t.snapshot(t.context.transform.results()[file]); 59 | }); 60 | 61 | test('custom contract size', t => { 62 | const file = 'contracts/TransformCustomSize.sol'; 63 | t.context.transform.apply(transformConstructor()); 64 | t.context.transform.apply(removeLeftoverConstructorHead); 65 | t.context.transform.apply(removeStateVarInits); 66 | t.context.transform.apply(removeImmutable); 67 | t.context.transform.apply(addStorageGaps); 68 | t.snapshot(t.context.transform.results()[file]); 69 | }); 70 | 71 | test('add storage gaps', t => { 72 | const file = 'contracts/TransformAddGap-0.8.sol'; 73 | t.context.transform.apply(addStorageGaps); 74 | t.snapshot(t.context.transform.results()[file]); 75 | }); 76 | -------------------------------------------------------------------------------- /src/transform-0.8.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/transform-0.8.test.ts` 2 | 3 | The actual snapshot is saved in `transform-0.8.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## rename parents in solidity 0.8 8 | 9 | > Snapshot 1 10 | 11 | `// SPDX-License-Identifier: UNLICENSED␊ 12 | pragma solidity ^0.8;␊ 13 | ␊ 14 | contract A {␊ 15 | function foo() external virtual {}␊ 16 | }␊ 17 | ␊ 18 | contract B is AUpgradeable {␊ 19 | /// @inheritdoc AUpgradeable␊ 20 | function foo() external override {}␊ 21 | }␊ 22 | ␊ 23 | library L {␊ 24 | struct S {␊ 25 | uint x;␊ 26 | }␊ 27 | }␊ 28 | ␊ 29 | contract U {␊ 30 | using LUpgradeable for LUpgradeable.S;␊ 31 | }␊ 32 | ` 33 | 34 | ## correctly index when utf8 characters 35 | 36 | > Snapshot 1 37 | 38 | `// SPDX-License-Identifier: MIT␊ 39 | ␊ 40 | pragma solidity ^0.8.0;␊ 41 | ␊ 42 | library StringsUpgradeable {␊ 43 | function toString(uint256) internal pure returns (string memory) {␊ 44 | return "";␊ 45 | }␊ 46 | }␊ 47 | ␊ 48 | library ECDSAUpgradeable {␊ 49 | enum RecoverError {␊ 50 | InvalidSignature␊ 51 | }␊ 52 | ␊ 53 | function tryRecover(bytes32, uint8, bytes32, bytes32) internal pure returns (address, RecoverError) {␊ 54 | // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}␊ 55 | return (address(0), RecoverError.InvalidSignature);␊ 56 | }␊ 57 | ␊ 58 | function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {␊ 59 | return keccak256(abi.encodePacked("\\x19Ethereum Signed Message:\\n", StringsUpgradeable.toString(s.length), s));␊ 60 | }␊ 61 | }␊ 62 | ␊ 63 | /**␊ 64 | * - https://docs.tokenbridge.net/eth-xdai-amb-bridge/about-the-eth-xdai-amb[ETH ⇌ xDai]␊ 65 | * - https://docs.tokenbridge.net/eth-qdai-bridge/about-the-eth-qdai-amb[ETH ⇌ qDai]␊ 66 | * - https://docs.tokenbridge.net/eth-etc-amb-bridge/about-the-eth-etc-amb[ETH ⇌ ETC]␊ 67 | */␊ 68 | contract CrossChainEnabledAMBUpgradeable {␊ 69 | }␊ 70 | ␊ 71 | ` 72 | 73 | ## preserves immutable if allowed 74 | 75 | > Snapshot 1 76 | 77 | `// SPDX-License-Identifier: MIT␊ 78 | pragma solidity ^0.8.2;␊ 79 | ␊ 80 | contract T1 {␊ 81 | function __T1_init() internal onlyInitializing {␊ 82 | __T1_init_unchained();␊ 83 | }␊ 84 | ␊ 85 | function __T1_init_unchained() internal onlyInitializing {␊ 86 | a = 1;␊ 87 | c = 3;␊ 88 | }␊ 89 | uint a;␊ 90 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 91 | uint immutable b = 4;␊ 92 | uint c;␊ 93 | }␊ 94 | ␊ 95 | contract T2 {␊ 96 | uint a = 1;␊ 97 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 98 | uint immutable b = 4;␊ 99 | uint c;␊ 100 | /// @custom:oz-upgrades-unsafe-allow constructor␊ 101 | constructor(uint _c) {␊ 102 | c = _c;␊ 103 | }␊ 104 | }␊ 105 | ␊ 106 | abstract contract T3 is T2 {␊ 107 | function __T3_init() internal onlyInitializing {␊ 108 | }␊ 109 | ␊ 110 | function __T3_init_unchained() internal onlyInitializing {␊ 111 | }␊ 112 | }␊ 113 | ` 114 | 115 | ## custom contract size 116 | 117 | > Snapshot 1 118 | 119 | `// SPDX-License-Identifier: UNLICENSED␊ 120 | pragma solidity ^0.8;␊ 121 | ␊ 122 | enum SomeEnum {␊ 123 | One,␊ 124 | Two,␊ 125 | Three␊ 126 | }␊ 127 | ␊ 128 | struct OneAndAHalfSlot {␊ 129 | uint256 x;␊ 130 | uint128 y;␊ 131 | }␊ 132 | ␊ 133 | contract SizeDefault {␊ 134 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 135 | uint immutable w1 = block.number;␊ 136 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 137 | uint immutable w2 = block.timestamp;␊ 138 | uint x; // slot 0 (after conversion to private)␊ 139 | uint constant y = 1;␊ 140 | uint224 z0; // slot 1␊ 141 | uint256 z1; // slot 2␊ 142 | uint32 z2; // slot 3␊ 143 | OneAndAHalfSlot s1; // slot 4&5␊ 144 | OneAndAHalfSlot s2; // slot 6&7␊ 145 | uint32 z3; // slot 8␊ 146 | uint32 z4; // slot 8␊ 147 | uint32 z5; // slot 8␊ 148 | uint64[5] a1; // slot 9&10␊ 149 | uint64[3] a2; // slot 11␊ 150 | SizeDefault c; // slot 12␊ 151 | SomeEnum e1; // slot 12␊ 152 | SomeEnum e2; // slot 12␊ 153 | SomeEnum e3; // slot 12␊ 154 | address payable blockhole; // slot 13␊ 155 | ␊ 156 | function __SizeDefault_init(uint _x) internal onlyInitializing {␊ 157 | __SizeDefault_init_unchained(_x);␊ 158 | }␊ 159 | ␊ 160 | function __SizeDefault_init_unchained(uint _x) internal onlyInitializing {␊ 161 | c = this;␊ 162 | e1 = SomeEnum.One;␊ 163 | e2 = SomeEnum.Two;␊ 164 | e3 = SomeEnum.Three;␊ 165 | blockhole = payable(0);␊ 166 | x = _x;␊ 167 | }␊ 168 | // gap should be 36 = 50 - 14␊ 169 | ␊ 170 | /**␊ 171 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 172 | * variables without shifting down storage in the inheritance chain.␊ 173 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 174 | */␊ 175 | uint256[36] private __gap;␊ 176 | }␊ 177 | ␊ 178 | /// @custom:storage-size 128␊ 179 | contract SizeOverride {␊ 180 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 181 | uint immutable w1 = block.number;␊ 182 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 183 | uint immutable w2 = block.timestamp;␊ 184 | uint x; // slot 0 (after conversion to private)␊ 185 | uint constant y = 1;␊ 186 | uint224 z0; // slot 1␊ 187 | uint256 z1; // slot 2␊ 188 | uint32 z2; // slot 3␊ 189 | OneAndAHalfSlot s1; // slot 4&5␊ 190 | OneAndAHalfSlot s2; // slot 6&7␊ 191 | uint32 z3; // slot 8␊ 192 | uint32 z4; // slot 8␊ 193 | uint32 z5; // slot 8␊ 194 | uint64[5] a1; // slot 9&10␊ 195 | uint64[3] a2 ; // slot 11␊ 196 | SizeOverride c; // slot 12␊ 197 | SomeEnum e1; // slot 12␊ 198 | SomeEnum e2; // slot 12␊ 199 | SomeEnum e3; // slot 12␊ 200 | address payable blockhole; // slot 13␊ 201 | ␊ 202 | function __SizeOverride_init(uint _x) internal onlyInitializing {␊ 203 | __SizeOverride_init_unchained(_x);␊ 204 | }␊ 205 | ␊ 206 | function __SizeOverride_init_unchained(uint _x) internal onlyInitializing {␊ 207 | c = this;␊ 208 | e1 = SomeEnum.One;␊ 209 | e2 = SomeEnum.Two;␊ 210 | e3 = SomeEnum.Three;␊ 211 | blockhole = payable(0);␊ 212 | x = _x;␊ 213 | }␊ 214 | // gap should be 114 = 128 - 14␊ 215 | ␊ 216 | /**␊ 217 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 218 | * variables without shifting down storage in the inheritance chain.␊ 219 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 220 | */␊ 221 | uint256[114] private __gap;␊ 222 | }␊ 223 | ` 224 | 225 | ## add storage gaps 226 | 227 | > Snapshot 1 228 | 229 | `// SPDX-License-Identifier: UNLICENSED␊ 230 | pragma solidity ^0.8;␊ 231 | ␊ 232 | type ShortString is bytes32;␊ 233 | ␊ 234 | contract Foo {␊ 235 | ShortString immutable s = ShortString.wrap(0);␊ 236 | ␊ 237 | /**␊ 238 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 239 | * variables without shifting down storage in the inheritance chain.␊ 240 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 241 | */␊ 242 | uint256[49] private __gap;␊ 243 | }␊ 244 | ` 245 | -------------------------------------------------------------------------------- /src/transform-0.8.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/transform-0.8.test.ts.snap -------------------------------------------------------------------------------- /src/transform-namespaces.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | 3 | import { getBuildInfo } from './test-utils/get-build-info'; 4 | 5 | import { SolcInput, SolcOutput } from './solc/input-output'; 6 | import { Transform } from './transform'; 7 | 8 | import { removeStateVarInits } from './transformations/purge-var-inits'; 9 | import { addNamespaceStruct } from './transformations/add-namespace-struct'; 10 | import { 11 | removeLeftoverConstructorHead, 12 | transformConstructor, 13 | } from './transformations/transform-constructor'; 14 | 15 | const test = _test as TestFn; 16 | 17 | interface Context { 18 | solcInput: SolcInput; 19 | solcOutput: SolcOutput; 20 | transformFile: (file: string) => Transform; 21 | } 22 | 23 | test.serial.before('compile', async t => { 24 | const buildInfo = await getBuildInfo('0.8.20'); 25 | 26 | t.context.solcInput = buildInfo.input; 27 | t.context.solcOutput = buildInfo.output as SolcOutput; 28 | }); 29 | 30 | test.beforeEach('transform', async t => { 31 | t.context.transformFile = (file: string) => 32 | new Transform(t.context.solcInput, t.context.solcOutput, { 33 | exclude: source => source !== file, 34 | }); 35 | }); 36 | 37 | test('add namespace', t => { 38 | const file = 'contracts/namespaces.sol'; 39 | const transform = t.context.transformFile(file); 40 | transform.apply(transformConstructor(() => true)); 41 | transform.apply(removeLeftoverConstructorHead); 42 | transform.apply(removeStateVarInits); 43 | transform.apply(addNamespaceStruct(() => true)); 44 | t.snapshot(transform.results()[file]); 45 | }); 46 | 47 | test('error with @custom:storage-size', t => { 48 | const file = 'contracts/namespaces-error-storage-size.sol'; 49 | const transform = t.context.transformFile(file); 50 | t.throws(() => transform.apply(addNamespaceStruct(() => true)), { 51 | message: 52 | 'Cannot combine namespaces with @custom:storage-size annotations (contracts/namespaces-error-storage-size.sol:5)', 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/transform-namespaces.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/transform-namespaces.test.ts` 2 | 3 | The actual snapshot is saved in `transform-namespaces.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## add namespace 8 | 9 | > Snapshot 1 10 | 11 | `// SPDX-License-Identifier: UNLICENSED␊ 12 | pragma solidity ^0.8.20;␊ 13 | ␊ 14 | contract C1 {␊ 15 | function __C1_init() internal onlyInitializing {␊ 16 | }␊ 17 | ␊ 18 | function __C1_init_unchained() internal onlyInitializing {␊ 19 | }␊ 20 | }␊ 21 | ␊ 22 | contract C2 {␊ 23 | event B();␊ 24 | ␊ 25 | struct A { uint z; }␊ 26 | ␊ 27 | uint constant C = 3;␊ 28 | // b␊ 29 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable␊ 30 | uint immutable y = 2;␊ 31 | ␊ 32 | /// @custom:storage-location erc7201:openzeppelin.storage.C2␊ 33 | struct C2Storage {␊ 34 | // a␊ 35 | uint x;␊ 36 | uint z;␊ 37 | ␊ 38 | string s1;␊ 39 | }␊ 40 | ␊ 41 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C2")) - 1)) & ~bytes32(uint256(0xff))␊ 42 | bytes32 private constant C2StorageLocation = 0xf05a05e0e3d15983ea921cad031aaea3040e9d631039045748753d29c5d24800;␊ 43 | ␊ 44 | function _getC2Storage() private pure returns (C2Storage storage $) {␊ 45 | assembly {␊ 46 | $.slot := C2StorageLocation␊ 47 | }␊ 48 | }␊ 49 | ␊ 50 | function __C2_init() internal onlyInitializing {␊ 51 | __C2_init_unchained();␊ 52 | }␊ 53 | ␊ 54 | function __C2_init_unchained() internal onlyInitializing {␊ 55 | C2Storage storage $ = _getC2Storage();␊ 56 | $.s1 = "";␊ 57 | }␊ 58 | function f() public {␊ 59 | C2Storage storage $ = _getC2Storage();␊ 60 | $.z = 3;␊ 61 | }␊ 62 | ␊ 63 | function g() public {␊ 64 | }␊ 65 | }␊ 66 | ␊ 67 | contract C3 {␊ 68 | function __C3_init() internal onlyInitializing {␊ 69 | }␊ 70 | ␊ 71 | function __C3_init_unchained() internal onlyInitializing {␊ 72 | }␊ 73 | /// @custom:storage-location erc7201:openzeppelin.storage.C3␊ 74 | struct C3Storage {␊ 75 | address x;␊ 76 | }␊ 77 | ␊ 78 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C3")) - 1)) & ~bytes32(uint256(0xff))␊ 79 | bytes32 private constant C3StorageLocation = 0xaa7e0685867d809c517036b8f21e99d58bd04b1da2b202f167c355fdf82b4a00;␊ 80 | ␊ 81 | function _getC3Storage() private pure returns (C3Storage storage $) {␊ 82 | assembly {␊ 83 | $.slot := C3StorageLocation␊ 84 | }␊ 85 | }␊ 86 | }␊ 87 | ␊ 88 | contract C4 {␊ 89 | /// @custom:storage-location erc7201:openzeppelin.storage.C4␊ 90 | struct C4Storage {␊ 91 | address x;␊ 92 | }␊ 93 | ␊ 94 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C4")) - 1)) & ~bytes32(uint256(0xff))␊ 95 | bytes32 private constant C4StorageLocation = 0x536a56e760d844b098efcc16711808e0b18bad9e07c2e82c78312ab719318d00;␊ 96 | ␊ 97 | function _getC4Storage() private pure returns (C4Storage storage $) {␊ 98 | assembly {␊ 99 | $.slot := C4StorageLocation␊ 100 | }␊ 101 | }␊ 102 | function __C4_init() internal onlyInitializing {␊ 103 | __C4_init_unchained();␊ 104 | }␊ 105 | ␊ 106 | function __C4_init_unchained() internal onlyInitializing {␊ 107 | C4Storage storage $ = _getC4Storage();␊ 108 | $.x = msg.sender;␊ 109 | }␊ 110 | }␊ 111 | ␊ 112 | contract C5 {␊ 113 | function __C5_init() internal onlyInitializing {␊ 114 | __C5_init_unchained();␊ 115 | }␊ 116 | ␊ 117 | function __C5_init_unchained() internal onlyInitializing {␊ 118 | C5Storage storage $ = _getC5Storage();␊ 119 | $.x = msg.sender;␊ 120 | }␊ 121 | /// @custom:storage-location erc7201:openzeppelin.storage.C5␊ 122 | struct C5Storage {␊ 123 | address x;␊ 124 | }␊ 125 | ␊ 126 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C5")) - 1)) & ~bytes32(uint256(0xff))␊ 127 | bytes32 private constant C5StorageLocation = 0xd94dd1cf5c0ce3bfbbd2555b11ad43bf11eeff03081ca744441b0fb7c0a6ec00;␊ 128 | ␊ 129 | function _getC5Storage() private pure returns (C5Storage storage $) {␊ 130 | assembly {␊ 131 | $.slot := C5StorageLocation␊ 132 | }␊ 133 | }␊ 134 | }␊ 135 | ␊ 136 | contract C6 {␊ 137 | function __C6_init() internal onlyInitializing {␊ 138 | }␊ 139 | ␊ 140 | function __C6_init_unchained() internal onlyInitializing {␊ 141 | }␊ 142 | }␊ 143 | ␊ 144 | contract C7 {␊ 145 | function __C7_init() internal onlyInitializing {␊ 146 | }␊ 147 | ␊ 148 | function __C7_init_unchained() internal onlyInitializing {␊ 149 | }␊ 150 | /// @custom:storage-location erc7201:openzeppelin.storage.C7␊ 151 | struct C7Storage {␊ 152 | uint x; // a comment␊ 153 | ␊ 154 | uint y;␊ 155 | }␊ 156 | ␊ 157 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C7")) - 1)) & ~bytes32(uint256(0xff))␊ 158 | bytes32 private constant C7StorageLocation = 0x931371859ca5c5440d3850c7cf9c14adb9d1257b7ddd15562d561cd48871d300;␊ 159 | ␊ 160 | function _getC7Storage() private pure returns (C7Storage storage $) {␊ 161 | assembly {␊ 162 | $.slot := C7StorageLocation␊ 163 | }␊ 164 | }␊ 165 | // a separate comment␊ 166 | }␊ 167 | ␊ 168 | contract C8 {␊ 169 | /// @custom:storage-location erc7201:openzeppelin.storage.C8␊ 170 | struct C8Storage {␊ 171 | address x;␊ 172 | address y;␊ 173 | }␊ 174 | ␊ 175 | // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.C8")) - 1)) & ~bytes32(uint256(0xff))␊ 176 | bytes32 private constant C8StorageLocation = 0xb93ad5010b0e46eab6ce47e44cb42c85a263d4e8daf058e8a66e4f114144f200;␊ 177 | ␊ 178 | function _getC8Storage() private pure returns (C8Storage storage $) {␊ 179 | assembly {␊ 180 | $.slot := C8StorageLocation␊ 181 | }␊ 182 | }␊ 183 | ␊ 184 | function __C8_init() internal onlyInitializing {␊ 185 | __C8_init_unchained();␊ 186 | }␊ 187 | ␊ 188 | function __C8_init_unchained() internal onlyInitializing {␊ 189 | C8Storage storage $ = _getC8Storage();␊ 190 | $.y = address(this);␊ 191 | $.x = msg.sender;␊ 192 | }␊ 193 | }␊ 194 | ` 195 | -------------------------------------------------------------------------------- /src/transform-namespaces.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/transform-namespaces.test.ts.snap -------------------------------------------------------------------------------- /src/transform.test.ts: -------------------------------------------------------------------------------- 1 | import _test, { TestFn } from 'ava'; 2 | import { getBuildInfo } from './test-utils/get-build-info'; 3 | 4 | import { findAll } from 'solidity-ast/utils'; 5 | import { getNodeBounds } from './solc/ast-utils'; 6 | import { SolcInput, SolcOutput } from './solc/input-output'; 7 | import { Transform } from './transform'; 8 | 9 | import { renameIdentifiers } from './transformations/rename-identifiers'; 10 | import { prependInitializableBase } from './transformations/prepend-initializable-base'; 11 | import { removeStateVarInits } from './transformations/purge-var-inits'; 12 | import { removeImmutable } from './transformations/remove-immutable'; 13 | import { removeInheritanceListArguments } from './transformations/remove-inheritance-list-args'; 14 | import { renameContractDefinition } from './transformations/rename-contract-definition'; 15 | import { fixImportDirectives } from './transformations/fix-import-directives'; 16 | import { fixNewStatement } from './transformations/fix-new-statement'; 17 | import { addRequiredPublicInitializer } from './transformations/add-required-public-initializers'; 18 | import { appendInitializableImport } from './transformations/append-initializable-import'; 19 | import { addStorageGaps } from './transformations/add-storage-gaps'; 20 | import { 21 | transformConstructor, 22 | removeLeftoverConstructorHead, 23 | } from './transformations/transform-constructor'; 24 | 25 | const test = _test as TestFn; 26 | 27 | interface Context { 28 | solcInput: SolcInput; 29 | solcOutput: SolcOutput; 30 | transform: Transform; 31 | transformFile: (file: string) => Transform; 32 | } 33 | 34 | test.serial.before('compile', async t => { 35 | const buildInfo = await getBuildInfo('0.6'); 36 | 37 | t.context.solcInput = buildInfo.input; 38 | t.context.solcOutput = buildInfo.output as SolcOutput; 39 | 40 | t.context.transformFile = (file: string) => 41 | new Transform(t.context.solcInput, t.context.solcOutput, { 42 | exclude: source => source !== file, 43 | }); 44 | }); 45 | 46 | test.beforeEach('transform', async t => { 47 | t.context.transform = new Transform(t.context.solcInput, t.context.solcOutput, { 48 | exclude: source => source.startsWith('contracts/invalid/'), 49 | }); 50 | }); 51 | 52 | test('read', t => { 53 | const text = t.context.transform.read({ src: '0:6:0' }); 54 | t.deepEqual('pragma', text); 55 | }); 56 | 57 | test('apply + read', t => { 58 | t.context.transform.apply(function* () { 59 | yield { kind: 'a', start: 1, length: 0, text: '~' }; 60 | }); 61 | const text = t.context.transform.read({ src: '0:6:0' }); 62 | t.deepEqual('p~ragma', text); 63 | }); 64 | 65 | test('apply + read invalid', t => { 66 | t.context.transform.apply(function* () { 67 | yield { kind: 'a', start: 1, length: 2, text: '~~' }; 68 | }); 69 | 70 | t.throws(() => t.context.transform.read({ src: '2:2:0' })); 71 | }); 72 | 73 | test('remove functions', t => { 74 | const file = 'contracts/TransformRemove.sol'; 75 | t.context.transform.apply(function* (sourceUnit) { 76 | if (sourceUnit.absolutePath === file) { 77 | for (const node of findAll('FunctionDefinition', sourceUnit)) { 78 | yield { ...getNodeBounds(node), kind: 'remove', text: '' }; 79 | } 80 | } 81 | }); 82 | 83 | t.snapshot(t.context.transform.results()[file]); 84 | }); 85 | 86 | test('rename identifiers', t => { 87 | const file = 'contracts/solc-0.6/Rename.sol'; 88 | t.context.transform.apply(renameIdentifiers); 89 | t.snapshot(t.context.transform.results()[file]); 90 | }); 91 | 92 | test('prepend Initializable base', t => { 93 | const file = 'contracts/solc-0.6/Rename.sol'; 94 | t.context.transform.apply(prependInitializableBase); 95 | t.snapshot(t.context.transform.results()[file]); 96 | }); 97 | 98 | test('purge var inits', t => { 99 | const file = 'contracts/solc-0.6/ElementaryTypes.sol'; 100 | t.context.transform.apply(removeStateVarInits); 101 | t.snapshot(t.context.transform.results()[file]); 102 | }); 103 | 104 | test('remove inheritance args', t => { 105 | const file = 'contracts/TransformInheritanceArgs.sol'; 106 | t.context.transform.apply(removeInheritanceListArguments); 107 | t.snapshot(t.context.transform.results()[file]); 108 | }); 109 | 110 | test('transform contract name', t => { 111 | const file = 'contracts/solc-0.6/Rename.sol'; 112 | t.context.transform.apply(renameContractDefinition); 113 | t.snapshot(t.context.transform.results()[file]); 114 | }); 115 | 116 | test('skip contract rename when Upgradeable suffix', t => { 117 | const file = 'contracts/solc-0.6/AlreadyUpgradeable.sol'; 118 | t.context.transform.apply(renameContractDefinition); 119 | t.snapshot(t.context.transform.results()[file]); 120 | }); 121 | 122 | test('fix import directives', t => { 123 | const file = 'contracts/solc-0.6/Local.sol'; 124 | t.context.transform.apply(fixImportDirectives(false)); 125 | t.snapshot(t.context.transform.results()[file]); 126 | }); 127 | 128 | test('fix import directives complex', t => { 129 | const file = 'contracts/TransformImport2.sol'; 130 | t.context.transform.apply(renameIdentifiers); 131 | t.context.transform.apply(fixImportDirectives(false)); 132 | t.snapshot(t.context.transform.results()[file]); 133 | }); 134 | 135 | test('append initializable import', t => { 136 | const file = 'contracts/solc-0.6/Local.sol'; 137 | t.context.transform.apply(appendInitializableImport('contracts/solc-0.6/Initializable.sol')); 138 | t.snapshot(t.context.transform.results()[file]); 139 | }); 140 | 141 | test('append initializable import custom', t => { 142 | const file = 'contracts/solc-0.6/Local.sol'; 143 | t.context.transform.apply(appendInitializableImport('contracts/solc-0.6/Initializable2.sol')); 144 | t.snapshot(t.context.transform.results()[file]); 145 | }); 146 | 147 | test('transform constructor', t => { 148 | const file = 'contracts/TransformConstructor.sol'; 149 | t.context.transform.apply(transformConstructor()); 150 | t.context.transform.apply(removeLeftoverConstructorHead); 151 | t.snapshot(t.context.transform.results()[file]); 152 | }); 153 | 154 | test('invalid constructors', t => { 155 | const tVarSubexpr = t.context.transformFile( 156 | 'contracts/invalid/TransformConstructorVarSubexpr.sol', 157 | ); 158 | t.throws(() => tVarSubexpr.apply(transformConstructor()), { 159 | message: `Can't transpile non-trivial expression in parent constructor argument (y + 1)`, 160 | }); 161 | 162 | const tVarSubexprVar = t.context.transformFile( 163 | 'contracts/invalid/TransformConstructorVarSubexprVar.sol', 164 | ); 165 | t.throws(() => tVarSubexprVar.apply(transformConstructor()), { 166 | message: `Can't transpile non-trivial expression in parent constructor argument (y + 1)`, 167 | }); 168 | 169 | const tDupExpr = t.context.transformFile('contracts/invalid/TransformConstructorDupExpr.sol'); 170 | t.throws(() => tDupExpr.apply(transformConstructor()), { 171 | message: `Can't transpile non-trivial expression in parent constructor argument (t.mint())`, 172 | }); 173 | }); 174 | 175 | test('fix new statement', t => { 176 | const file = 'contracts/TransformNew.sol'; 177 | t.context.transform.apply(fixNewStatement); 178 | t.context.transform.apply(addRequiredPublicInitializer([])); 179 | t.snapshot(t.context.transform.results()[file]); 180 | }); 181 | 182 | test('fix new statement in var init', t => { 183 | const file = 'contracts/TransformNewVarInit.sol'; 184 | t.context.transform.apply(transformConstructor()); 185 | t.context.transform.apply(removeStateVarInits); 186 | t.context.transform.apply(removeLeftoverConstructorHead); 187 | t.context.transform.apply(addRequiredPublicInitializer([])); 188 | t.snapshot(t.context.transform.results()[file]); 189 | }); 190 | 191 | test('exclude', t => { 192 | const file = 'contracts/TransformInitializable.sol'; 193 | const transform = new Transform(t.context.solcInput, t.context.solcOutput, { 194 | exclude: s => s === file, 195 | }); 196 | // eslint-disable-next-line require-yield 197 | transform.apply(function* (s) { 198 | t.not(s.absolutePath, file); 199 | }); 200 | t.false(file in transform.results()); 201 | }); 202 | 203 | test('add storage gaps', t => { 204 | const file = 'contracts/TransformAddGap.sol'; 205 | t.context.transform.apply(addStorageGaps); 206 | t.snapshot(t.context.transform.results()[file]); 207 | }); 208 | 209 | test('add requested public initializer', t => { 210 | const file = 'contracts/TransformConstructorWithArgs.sol'; 211 | t.context.transform.apply(addRequiredPublicInitializer([file])); 212 | t.snapshot(t.context.transform.results()[file]); 213 | }); 214 | 215 | test('remove immutable', t => { 216 | const file = 'contracts/TransformImmutable.sol'; 217 | t.context.transform.apply(removeImmutable); 218 | t.snapshot(t.context.transform.results()[file]); 219 | }); 220 | -------------------------------------------------------------------------------- /src/transform.test.ts.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/transform.test.ts` 2 | 3 | The actual snapshot is saved in `transform.test.ts.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## remove functions 8 | 9 | > Snapshot 1 10 | 11 | `pragma solidity ^0.6;␊ 12 | ␊ 13 | contract RemoveFunctions {␊ 14 | ␊ 15 | ␊ 16 | }␊ 17 | ` 18 | 19 | ## rename identifiers 20 | 21 | > Snapshot 1 22 | 23 | `pragma solidity ^0.6.0;␊ 24 | ␊ 25 | library RenameLibrary {␊ 26 | function test() external {␊ 27 | }␊ 28 | }␊ 29 | ␊ 30 | contract RenameContract {␊ 31 | }␊ 32 | ␊ 33 | contract RenameDeployer {␊ 34 | RenameContractUpgradeable rc = RenameContractUpgradeable(0);␊ 35 | ␊ 36 | constructor() public {␊ 37 | new RenameContractUpgradeable();␊ 38 | }␊ 39 | ␊ 40 | function deploy() external returns (RenameContractUpgradeable) {␊ 41 | return new RenameContractUpgradeable();␊ 42 | }␊ 43 | ␊ 44 | function test() external {␊ 45 | RenameLibraryUpgradeable.test();␊ 46 | }␊ 47 | }␊ 48 | ` 49 | 50 | ## prepend Initializable base 51 | 52 | > Snapshot 1 53 | 54 | `pragma solidity ^0.6.0;␊ 55 | ␊ 56 | library RenameLibrary {␊ 57 | function test() external {␊ 58 | }␊ 59 | }␊ 60 | ␊ 61 | contract RenameContract is Initializable {␊ 62 | }␊ 63 | ␊ 64 | contract RenameDeployer is Initializable {␊ 65 | RenameContract rc = RenameContract(0);␊ 66 | ␊ 67 | constructor() public {␊ 68 | new RenameContract();␊ 69 | }␊ 70 | ␊ 71 | function deploy() external returns (RenameContract) {␊ 72 | return new RenameContract();␊ 73 | }␊ 74 | ␊ 75 | function test() external {␊ 76 | RenameLibrary.test();␊ 77 | }␊ 78 | }␊ 79 | ` 80 | 81 | ## purge var inits 82 | 83 | > Snapshot 1 84 | 85 | `pragma solidity ^0.6.0;␊ 86 | ␊ 87 | contract ElementaryTypes {␊ 88 | address public owner;␊ 89 | bool active;␊ 90 | string hello;␊ 91 | int count;␊ 92 | uint ucount;␊ 93 | bytes32 samevar;␊ 94 | uint x;␊ 95 | uint y;␊ 96 | }␊ 97 | ` 98 | 99 | ## remove inheritance args 100 | 101 | > Snapshot 1 102 | 103 | `pragma solidity ^0.6;␊ 104 | ␊ 105 | contract A {␊ 106 | constructor(uint) public {}␊ 107 | }␊ 108 | ␊ 109 | contract B is A {␊ 110 | ␊ 111 | modifier hasModifier (){␊ 112 | _;␊ 113 | }␊ 114 | ␊ 115 | modifier hasModifierArgument(uint b) {␊ 116 | _;␊ 117 | }␊ 118 | ␊ 119 | constructor (uint b) public hasModifier hasModifierArgument(b) {}␊ 120 | }␊ 121 | ` 122 | 123 | ## transform contract name 124 | 125 | > Snapshot 1 126 | 127 | `pragma solidity ^0.6.0;␊ 128 | ␊ 129 | library RenameLibraryUpgradeable {␊ 130 | function test() external {␊ 131 | }␊ 132 | }␊ 133 | ␊ 134 | contract RenameContractUpgradeable {␊ 135 | }␊ 136 | ␊ 137 | contract RenameDeployerUpgradeable {␊ 138 | RenameContract rc = RenameContract(0);␊ 139 | ␊ 140 | constructor() public {␊ 141 | new RenameContract();␊ 142 | }␊ 143 | ␊ 144 | function deploy() external returns (RenameContract) {␊ 145 | return new RenameContract();␊ 146 | }␊ 147 | ␊ 148 | function test() external {␊ 149 | RenameLibrary.test();␊ 150 | }␊ 151 | }␊ 152 | ` 153 | 154 | ## skip contract rename when Upgradeable suffix 155 | 156 | > Snapshot 1 157 | 158 | `pragma solidity ^0.6.0;␊ 159 | ␊ 160 | contract AlreadyUpgradeable {␊ 161 | }␊ 162 | ` 163 | 164 | ## fix import directives 165 | 166 | > Snapshot 1 167 | 168 | `pragma solidity ^0.6.0;␊ 169 | ␊ 170 | import "./ImportedUpgradeable.sol";␊ 171 | ␊ 172 | contract Local is Imported2 {␊ 173 | constructor(uint x, uint y) Imported2(x, y) public { }␊ 174 | }␊ 175 | ` 176 | 177 | ## fix import directives complex 178 | 179 | > Snapshot 1 180 | 181 | `pragma solidity ^0.6.0;␊ 182 | ␊ 183 | import {Imp1Upgradeable} from "./TransformImport2-ImportedUpgradeable.sol";␊ 184 | import {Imp2Upgradeable as ImpX} from "./TransformImport2-ImportedUpgradeable.sol";␊ 185 | ␊ 186 | contract Foo {␊ 187 | using Imp1Upgradeable for Imp1Upgradeable.S;␊ 188 | using ImpX for ImpX.S;␊ 189 | }␊ 190 | ` 191 | 192 | ## append initializable import 193 | 194 | > Snapshot 1 195 | 196 | `pragma solidity ^0.6.0;␊ 197 | ␊ 198 | import './Imported.sol';␊ 199 | import {Initializable} from "./Initializable.sol";␊ 200 | ␊ 201 | contract Local is Imported2 {␊ 202 | constructor(uint x, uint y) Imported2(x, y) public { }␊ 203 | }␊ 204 | ` 205 | 206 | ## append initializable import custom 207 | 208 | > Snapshot 1 209 | 210 | `pragma solidity ^0.6.0;␊ 211 | ␊ 212 | import './Imported.sol';␊ 213 | import {Initializable} from "./Initializable2.sol";␊ 214 | ␊ 215 | contract Local is Imported2 {␊ 216 | constructor(uint x, uint y) Imported2(x, y) public { }␊ 217 | }␊ 218 | ` 219 | 220 | ## transform constructor 221 | 222 | > Snapshot 1 223 | 224 | `pragma solidity ^0.6;␊ 225 | ␊ 226 | contract Foo1 {␊ 227 | function __Foo1_init() internal onlyInitializing {␊ 228 | }␊ 229 | ␊ 230 | function __Foo1_init_unchained() internal onlyInitializing {␊ 231 | }␊ 232 | }␊ 233 | ␊ 234 | contract Foo2 {␊ 235 | event Ev();␊ 236 | function __Foo2_init() internal onlyInitializing {␊ 237 | __Foo2_init_unchained();␊ 238 | }␊ 239 | ␊ 240 | function __Foo2_init_unchained() internal onlyInitializing {␊ 241 | emit Ev();␊ 242 | }␊ 243 | }␊ 244 | ␊ 245 | contract Foo3 {␊ 246 | function __Foo3_init() internal onlyInitializing {␊ 247 | __Foo3_init_unchained();␊ 248 | }␊ 249 | ␊ 250 | function __Foo3_init_unchained() internal onlyInitializing {␊ 251 | x = 1;␊ 252 | }␊ 253 | uint x = 1;␊ 254 | }␊ 255 | ␊ 256 | contract Foo4 {␊ 257 | uint x = 1;␊ 258 | event Ev();␊ 259 | function __Foo4_init() internal onlyInitializing {␊ 260 | __Foo4_init_unchained();␊ 261 | }␊ 262 | ␊ 263 | function __Foo4_init_unchained() internal onlyInitializing {␊ 264 | x = 1;␊ 265 | emit Ev();␊ 266 | }␊ 267 | }␊ 268 | ␊ 269 | contract Bar1 {␊ 270 | function __Bar1_init() internal onlyInitializing {␊ 271 | }␊ 272 | ␊ 273 | function __Bar1_init_unchained() internal onlyInitializing {␊ 274 | }␊ 275 | ␊ 276 | }␊ 277 | ␊ 278 | contract Bar2 is Bar1 {␊ 279 | function __Bar2_init() internal onlyInitializing {␊ 280 | }␊ 281 | ␊ 282 | function __Bar2_init_unchained() internal onlyInitializing {␊ 283 | }␊ 284 | ␊ 285 | }␊ 286 | ␊ 287 | contract Bar3 is Bar2 {␊ 288 | function __Bar3_init() internal onlyInitializing {␊ 289 | }␊ 290 | ␊ 291 | function __Bar3_init_unchained() internal onlyInitializing {␊ 292 | }␊ 293 | ␊ 294 | }␊ 295 | ␊ 296 | contract Foo5 {␊ 297 | function __Foo5_init(function () external f) internal onlyInitializing {␊ 298 | __Foo5_init_unchained(f);␊ 299 | }␊ 300 | ␊ 301 | function __Foo5_init_unchained(function () external f) internal onlyInitializing {␊ 302 | f();␊ 303 | }␊ 304 | }␊ 305 | ␊ 306 | contract Foo6 {␊ 307 | function __Foo6_init(␊ 308 | uint a,␊ 309 | uint b␊ 310 | ) internal onlyInitializing {␊ 311 | __Foo6_init_unchained(a, b);␊ 312 | }␊ 313 | ␊ 314 | function __Foo6_init_unchained(␊ 315 | uint a,␊ 316 | uint b␊ 317 | ) internal onlyInitializing {␊ 318 | a = a + b;␊ 319 | }␊ 320 | }␊ 321 | ␊ 322 | contract Foo7 {␊ 323 | uint a;␊ 324 | function __Foo7_init(␊ 325 | uint _a␊ 326 | ) internal onlyInitializing {␊ 327 | __Foo7_init_unchained(_a);␊ 328 | }␊ 329 | ␊ 330 | function __Foo7_init_unchained(␊ 331 | uint _a␊ 332 | ) internal onlyInitializing {␊ 333 | a = _a;␊ 334 | }␊ 335 | }␊ 336 | ␊ 337 | contract Foo8 {␊ 338 | ␊ 339 | modifier hasModifier() {␊ 340 | _;␊ 341 | }␊ 342 | ␊ 343 | function __Foo8_init() internal onlyInitializing {␊ 344 | __Foo8_init_unchained();␊ 345 | }␊ 346 | ␊ 347 | function __Foo8_init_unchained() internal onlyInitializing hasModifier {␊ 348 | }␊ 349 | }␊ 350 | ␊ 351 | contract Foo9 {␊ 352 | function __Foo9_init(␊ 353 | uint a,␊ 354 | uint b␊ 355 | ) internal onlyInitializing {␊ 356 | __Foo9_init_unchained(a, b);␊ 357 | }␊ 358 | ␊ 359 | function __Foo9_init_unchained(␊ 360 | uint a,␊ 361 | uint b␊ 362 | ) internal onlyInitializing {␊ 363 | a = 0;␊ 364 | b = 0;␊ 365 | }␊ 366 | }␊ 367 | ␊ 368 | contract Foo10 is Foo7(123) {␊ 369 | function __Foo10_init() internal onlyInitializing {␊ 370 | __Foo7_init_unchained(123);␊ 371 | __Foo10_init_unchained();␊ 372 | }␊ 373 | ␊ 374 | function __Foo10_init_unchained() internal onlyInitializing {␊ 375 | bar = 1;␊ 376 | }␊ 377 | ␊ 378 | uint bar = 1;␊ 379 | }␊ 380 | ␊ 381 | contract Foo11 is Foo7 {␊ 382 | ␊ 383 | modifier hasModifier() {␊ 384 | _;␊ 385 | }␊ 386 | ␊ 387 | function __Foo11_init(uint a) internal onlyInitializing {␊ 388 | __Foo7_init_unchained(a);␊ 389 | __Foo11_init_unchained(a);␊ 390 | }␊ 391 | ␊ 392 | function __Foo11_init_unchained(uint) internal onlyInitializing hasModifier {␊ 393 | }␊ 394 | }␊ 395 | ␊ 396 | contract Foo12 is Foo7 {␊ 397 | ␊ 398 | function __Foo12_init(uint a) internal onlyInitializing {␊ 399 | __Foo7_init_unchained(a);␊ 400 | }␊ 401 | ␊ 402 | function __Foo12_init_unchained(uint) internal onlyInitializing {␊ 403 | }␊ 404 | }␊ 405 | ␊ 406 | contract Foo13 is Foo4 {␊ 407 | function __Foo13_init() internal onlyInitializing {␊ 408 | __Foo4_init_unchained();␊ 409 | }␊ 410 | ␊ 411 | function __Foo13_init_unchained() internal onlyInitializing { }␊ 412 | }␊ 413 | ␊ 414 | contract Foo14 is Foo7 {␊ 415 | ␊ 416 | modifier hasModifier(uint b) {␊ 417 | _;␊ 418 | }␊ 419 | ␊ 420 | function __Foo14_init(uint a, uint b) internal onlyInitializing {␊ 421 | __Foo7_init_unchained(a);␊ 422 | __Foo14_init_unchained(a, b);␊ 423 | }␊ 424 | ␊ 425 | function __Foo14_init_unchained(uint, uint b) internal onlyInitializing hasModifier(b) {␊ 426 | }␊ 427 | }␊ 428 | ␊ 429 | contract Foo15 is Foo7 {␊ 430 | uint x;␊ 431 | function __Foo15_init(uint _x) internal onlyInitializing {␊ 432 | __Foo7_init_unchained(_x);␊ 433 | __Foo15_init_unchained(_x);␊ 434 | }␊ 435 | ␊ 436 | function __Foo15_init_unchained(uint _x) internal onlyInitializing {␊ 437 | x = _x;␊ 438 | }␊ 439 | }␊ 440 | ␊ 441 | abstract contract Foo16 is Foo15 {␊ 442 | function __Foo16_init() internal onlyInitializing {␊ 443 | }␊ 444 | ␊ 445 | function __Foo16_init_unchained() internal onlyInitializing {␊ 446 | }␊ 447 | ␊ 448 | }␊ 449 | ␊ 450 | contract Foo17 is Foo4 {␊ 451 | function __Foo17_init() internal onlyInitializing {␊ 452 | __Foo4_init_unchained();␊ 453 | }␊ 454 | ␊ 455 | function __Foo17_init_unchained() internal onlyInitializing {}␊ 456 | }␊ 457 | ␊ 458 | abstract contract Foo18 is Foo17 {␊ 459 | function __Foo18_init() internal onlyInitializing {␊ 460 | __Foo4_init_unchained();␊ 461 | }␊ 462 | ␊ 463 | function __Foo18_init_unchained() internal onlyInitializing {␊ 464 | }␊ 465 | ␊ 466 | }␊ 467 | ␊ 468 | contract Foo19 {␊ 469 | function __Foo19_init(uint x) internal onlyInitializing {␊ 470 | }␊ 471 | ␊ 472 | function __Foo19_init_unchained(uint) internal onlyInitializing {}␊ 473 | }␊ 474 | ␊ 475 | contract Foo20 is Foo19 {␊ 476 | function __Foo20_init(uint x) internal onlyInitializing {␊ 477 | __Foo19_init_unchained(x);␊ 478 | }␊ 479 | ␊ 480 | function __Foo20_init_unchained(uint) internal onlyInitializing {}␊ 481 | }␊ 482 | ␊ 483 | contract Foo21 is Foo20 {␊ 484 | function __Foo21_init() internal onlyInitializing {␊ 485 | __Foo19_init_unchained(4);␊ 486 | __Foo20_init_unchained(4);␊ 487 | }␊ 488 | ␊ 489 | function __Foo21_init_unchained() internal onlyInitializing {}␊ 490 | }␊ 491 | ␊ 492 | contract Foo22 is Foo19 {␊ 493 | function __Foo22_init(uint y) internal onlyInitializing {␊ 494 | __Foo19_init_unchained(y + 1);␊ 495 | }␊ 496 | ␊ 497 | function __Foo22_init_unchained(uint) internal onlyInitializing {}␊ 498 | }␊ 499 | ␊ 500 | contract Foo23 is Foo9, Foo20 {␊ 501 | function __Foo23_init(uint x, uint y) internal onlyInitializing {␊ 502 | __Foo9_init_unchained(x, y);␊ 503 | __Foo19_init_unchained(y);␊ 504 | __Foo20_init_unchained(y);␊ 505 | }␊ 506 | ␊ 507 | function __Foo23_init_unchained(uint, uint) internal onlyInitializing {}␊ 508 | }␊ 509 | ␊ 510 | contract Foo24 is Foo23 {␊ 511 | function __Foo24_init() internal onlyInitializing {␊ 512 | __Foo9_init_unchained(1, 2);␊ 513 | __Foo19_init_unchained(2);␊ 514 | __Foo20_init_unchained(2);␊ 515 | __Foo23_init_unchained(1, 2);␊ 516 | }␊ 517 | ␊ 518 | function __Foo24_init_unchained() internal onlyInitializing {}␊ 519 | }␊ 520 | ␊ 521 | contract Foo25 is Foo19 {␊ 522 | function __Foo25_init() internal onlyInitializing {␊ 523 | __Foo19_init_unchained(1+2);␊ 524 | }␊ 525 | ␊ 526 | function __Foo25_init_unchained() internal onlyInitializing {}␊ 527 | }␊ 528 | ␊ 529 | interface IFoo {␊ 530 | function mint() external returns (uint);␊ 531 | }␊ 532 | ␊ 533 | contract Foo26 is Foo19 {␊ 534 | function __Foo26_init(IFoo t) internal onlyInitializing {␊ 535 | __Foo19_init_unchained(t.mint());␊ 536 | }␊ 537 | ␊ 538 | function __Foo26_init_unchained(IFoo) internal onlyInitializing {}␊ 539 | }␊ 540 | ␊ 541 | contract Foo27 is Foo26 {␊ 542 | function __Foo27_init(IFoo t) internal onlyInitializing {␊ 543 | __Foo19_init_unchained(t.mint());␊ 544 | __Foo26_init_unchained(t);␊ 545 | }␊ 546 | ␊ 547 | function __Foo27_init_unchained(IFoo) internal onlyInitializing {}␊ 548 | }␊ 549 | ␊ 550 | contract Foo28 {␊ 551 | function __Foo28_init(uint x) internal onlyInitializing {␊ 552 | }␊ 553 | ␊ 554 | function __Foo28_init_unchained(uint) internal onlyInitializing {}␊ 555 | }␊ 556 | ␊ 557 | abstract contract Foo29 is Foo28 {␊ 558 | function __Foo29_init(uint y) internal onlyInitializing {␊ 559 | }␊ 560 | ␊ 561 | function __Foo29_init_unchained(uint) internal onlyInitializing {}␊ 562 | }␊ 563 | ␊ 564 | abstract contract Foo30 is Foo28, Foo29 {␊ 565 | function __Foo30_init(uint z) internal onlyInitializing {␊ 566 | __Foo28_init_unchained(z);␊ 567 | }␊ 568 | ␊ 569 | function __Foo30_init_unchained(uint) internal onlyInitializing {}␊ 570 | }␊ 571 | ␊ 572 | abstract contract Foo31 is Foo19 {␊ 573 | function __Foo31_init(uint b) internal onlyInitializing {␊ 574 | }␊ 575 | ␊ 576 | function __Foo31_init_unchained(uint) internal onlyInitializing {}␊ 577 | }␊ 578 | ␊ 579 | abstract contract Foo32 is Foo20, Foo31 {␊ 580 | function __Foo32_init(uint b) internal onlyInitializing {␊ 581 | __Foo19_init_unchained(b);␊ 582 | __Foo20_init_unchained(b);␊ 583 | }␊ 584 | ␊ 585 | function __Foo32_init_unchained(uint) internal onlyInitializing {}␊ 586 | }` 587 | 588 | ## fix new statement 589 | 590 | > Snapshot 1 591 | 592 | `pragma solidity ^0.6;␊ 593 | ␊ 594 | contract Foo {␊ 595 | function initialize(uint x) public virtual initializer {␊ 596 | __Foo_init(x);␊ 597 | }␊ 598 | constructor(uint x) public {}␊ 599 | }␊ 600 | ␊ 601 | contract Bar {␊ 602 | function initialize() public virtual initializer {␊ 603 | __Bar_init();␊ 604 | }␊ 605 | }␊ 606 | ␊ 607 | contract TransformNew {␊ 608 | function test1() external {␊ 609 | Foo foo;␊ 610 | foo = new Foo();␊ 611 | foo.initialize(1);␊ 612 | }␊ 613 | ␊ 614 | function test2() external {␊ 615 | Bar bar;␊ 616 | bar = new Bar();␊ 617 | bar.initialize();␊ 618 | }␊ 619 | ␊ 620 | function test3() external {␊ 621 | address bar;␊ 622 | bar = address(new Bar());␊ 623 | Bar(bar).initialize();␊ 624 | }␊ 625 | }␊ 626 | ` 627 | 628 | ## fix new statement in var init 629 | 630 | > Snapshot 1 631 | 632 | `pragma solidity ^0.6;␊ 633 | ␊ 634 | contract Foo {␊ 635 | function initialize(uint x) public virtual initializer {␊ 636 | __Foo_init(x);␊ 637 | }␊ 638 | function __Foo_init(uint x) internal onlyInitializing {␊ 639 | }␊ 640 | ␊ 641 | function __Foo_init_unchained(uint) internal onlyInitializing {}␊ 642 | }␊ 643 | ␊ 644 | contract Bar {␊ 645 | function __Bar_init() internal onlyInitializing {␊ 646 | }␊ 647 | ␊ 648 | function __Bar_init_unchained() internal onlyInitializing {␊ 649 | }␊ 650 | function initialize() public virtual initializer {␊ 651 | __Bar_init();␊ 652 | }␊ 653 | }␊ 654 | ␊ 655 | contract TransformNew {␊ 656 | function __TransformNew_init() internal onlyInitializing {␊ 657 | __TransformNew_init_unchained();␊ 658 | }␊ 659 | ␊ 660 | function __TransformNew_init_unchained() internal onlyInitializing {␊ 661 | foo = new Foo();␊ 662 | foo.initialize(1);␊ 663 | bar = new Bar();␊ 664 | bar.initialize();␊ 665 | baz = address(new Bar());␊ 666 | Bar(baz).initialize();␊ 667 | }␊ 668 | Foo foo;␊ 669 | Bar bar;␊ 670 | address baz;␊ 671 | }␊ 672 | ` 673 | 674 | ## add storage gaps 675 | 676 | > Snapshot 1 677 | 678 | `pragma solidity ^0.6;␊ 679 | ␊ 680 | contract Foo0 {␊ 681 | ␊ 682 | /**␊ 683 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 684 | * variables without shifting down storage in the inheritance chain.␊ 685 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 686 | */␊ 687 | uint256[50] private __gap;␊ 688 | }␊ 689 | ␊ 690 | contract Foo1 {␊ 691 | uint x;␊ 692 | ␊ 693 | /**␊ 694 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 695 | * variables without shifting down storage in the inheritance chain.␊ 696 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 697 | */␊ 698 | uint256[49] private __gap;␊ 699 | }␊ 700 | ␊ 701 | contract Foo2 {␊ 702 | uint x;␊ 703 | bool b1;␊ 704 | bool b2;␊ 705 | ␊ 706 | /**␊ 707 | * @dev This empty reserved space is put in place to allow future versions to add new␊ 708 | * variables without shifting down storage in the inheritance chain.␊ 709 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps␊ 710 | */␊ 711 | uint256[48] private __gap;␊ 712 | }␊ 713 | ` 714 | 715 | ## add requested public initializer 716 | 717 | > Snapshot 1 718 | 719 | `pragma solidity ^0.6;␊ 720 | ␊ 721 | contract FooWithArgs {␊ 722 | function initialize(uint x, uint y) public virtual initializer {␊ 723 | __FooWithArgs_init(x, y);␊ 724 | }␊ 725 | constructor(uint x, uint y) public {}␊ 726 | }␊ 727 | ␊ 728 | contract FooWithArgs2 {␊ 729 | modifier hasModifierArguments(uint x) {␊ 730 | _;␊ 731 | }␊ 732 | function initialize(uint x, uint y) public virtual initializer {␊ 733 | __FooWithArgs2_init(x, y);␊ 734 | }␊ 735 | constructor(uint x, uint y) public hasModifierArguments(x) {}␊ 736 | }␊ 737 | ` 738 | 739 | ## remove immutable 740 | 741 | > Snapshot 1 742 | 743 | `pragma solidity ^0.6.5;␊ 744 | ␊ 745 | contract TransformImmutable {␊ 746 | uint x;␊ 747 | constructor() public {␊ 748 | x = 3;␊ 749 | }␊ 750 | }␊ 751 | ` 752 | -------------------------------------------------------------------------------- /src/transform.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-transpiler/9b4cc793dfa49fa6c92f354f1075b2d8a5106d0f/src/transform.test.ts.snap -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import { mapValues } from 'lodash'; 2 | 3 | import { SourceUnit } from 'solidity-ast'; 4 | import { Node } from 'solidity-ast/node'; 5 | import { SolcInput, SolcOutput } from './solc/input-output'; 6 | import { srcDecoder, SrcDecoder } from './solc/src-decoder'; 7 | import { layoutGetter, LayoutGetter } from './solc/layout-getter'; 8 | 9 | import { Shift, shiftBounds } from './shifts'; 10 | import { applyTransformation } from './transformations/apply'; 11 | import { compareTransformations, compareContainment } from './transformations/compare'; 12 | import { Bounds, Transformation, WithSrc } from './transformations/type'; 13 | import { ASTResolver } from './ast-resolver'; 14 | import { ByteMatch, matchBufferAt } from './utils/match'; 15 | 16 | type Transformer = (sourceUnit: SourceUnit, tools: TransformerTools) => Generator; 17 | 18 | interface ReadOriginal { 19 | (node: Node, type?: 'string'): string; 20 | (node: Node, type: 'buffer'): Buffer; 21 | } 22 | 23 | export interface TransformerTools { 24 | originalSource: string; 25 | originalSourceBuf: Buffer; 26 | readOriginal: ReadOriginal; 27 | matchOriginalAfter: (node: Node, re: RegExp) => ByteMatch | undefined; 28 | resolver: ASTResolver; 29 | getData: (node: Node) => Partial; 30 | getLayout: LayoutGetter; 31 | error: (node: Node, msg: string) => Error; 32 | } 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 35 | export interface TransformData {} 36 | 37 | interface TransformState { 38 | id: number; 39 | ast: SourceUnit; 40 | transformations: Transformation[]; 41 | shifts: Shift[]; 42 | content: Buffer; 43 | original: string; 44 | originalBuf: Buffer; 45 | } 46 | 47 | interface TransformOptions { 48 | exclude?: (source: string) => boolean; 49 | } 50 | 51 | export class Transform { 52 | private state: { 53 | [file in string]: TransformState; 54 | } = {}; 55 | 56 | private data = new WeakMap>(); 57 | 58 | readonly decodeSrc: SrcDecoder; 59 | readonly getLayout: LayoutGetter; 60 | readonly resolver: ASTResolver; 61 | 62 | constructor(input: SolcInput, output: SolcOutput, options: TransformOptions = {}) { 63 | this.decodeSrc = srcDecoder(output); 64 | this.getLayout = layoutGetter(output); 65 | this.resolver = new ASTResolver(output, options.exclude); 66 | 67 | for (const source in input.sources) { 68 | if (options.exclude?.(source)) { 69 | continue; 70 | } 71 | 72 | const s = input.sources[source]; 73 | if (!('content' in s)) { 74 | throw new Error(`Missing content for ${source}`); 75 | } 76 | 77 | const contentBuf = Buffer.from(s.content); 78 | this.state[source] = { 79 | id: output.sources[source].id, 80 | ast: output.sources[source].ast, 81 | original: s.content, 82 | originalBuf: contentBuf, 83 | content: contentBuf, 84 | transformations: [], 85 | shifts: [], 86 | }; 87 | } 88 | } 89 | 90 | apply(transform: Transformer): void { 91 | for (const source in this.state) { 92 | const { original: originalSource, originalBuf: originalSourceBuf, ast } = this.state[source]; 93 | const { resolver, getLayout } = this; 94 | const readOriginal = this.readOriginal.bind(this); 95 | const getData = this.getData.bind(this); 96 | const error = this.error.bind(this); 97 | const matchOriginalAfter = this.matchOriginalAfter.bind(this); 98 | const tools: TransformerTools = { 99 | originalSource, 100 | originalSourceBuf, 101 | resolver, 102 | readOriginal, 103 | getData, 104 | getLayout, 105 | error, 106 | matchOriginalAfter, 107 | }; 108 | 109 | for (const t of transform(ast, tools)) { 110 | const { id, content, shifts, transformations } = this.state[source]; 111 | const error = (byteIndex: number, msg: string) => 112 | this.error({ src: `${byteIndex}:0:${id}` }, msg); 113 | insertSortedAndValidate(transformations, t, error); 114 | 115 | const { result, shift } = applyTransformation(t, content, shifts, this); 116 | 117 | shifts.push(shift); 118 | 119 | this.state[source].content = result; 120 | } 121 | } 122 | } 123 | 124 | getData(node: Node): Partial { 125 | let data = this.data.get(node); 126 | if (data === undefined) { 127 | data = {}; 128 | this.data.set(node, data); 129 | } 130 | return data; 131 | } 132 | 133 | readOriginal(node: WithSrc, type?: 'string'): string; 134 | readOriginal(node: WithSrc, type: 'buffer'): Buffer; 135 | readOriginal(node: WithSrc, type: 'string' | 'buffer' = 'string'): string | Buffer { 136 | const { source, start, length } = this.decodeSrc(node.src); 137 | const { originalBuf } = this.state[source]; 138 | const buf = originalBuf.slice(start, start + length); 139 | if (type === 'buffer') { 140 | return buf; 141 | } else { 142 | return buf.toString('utf8'); 143 | } 144 | } 145 | 146 | read(node: WithSrc): string { 147 | const { source } = this.decodeSrc(node.src); 148 | const { content } = this.state[source]; 149 | const sb = this.getShiftedBounds(node); 150 | return content.slice(sb.start, sb.start + sb.length).toString(); 151 | } 152 | 153 | getShiftedBounds(node: WithSrc): Bounds { 154 | const { source, ...bounds } = this.decodeSrc(node.src); 155 | const { shifts, transformations } = this.state[source]; 156 | 157 | const incompatible = (t: Transformation) => { 158 | const c = compareContainment(t, bounds); 159 | return c === 'partial overlap' || (typeof c === 'number' && c > 0); 160 | }; 161 | if (transformations.some(incompatible)) { 162 | throw new Error(`Can't read from segment that has been partially transformed`); 163 | } 164 | 165 | return shiftBounds(shifts, bounds); 166 | } 167 | 168 | error(node: WithSrc, msg: string): Error { 169 | const { source, start } = this.decodeSrc(node.src); 170 | const line = byteToLineNumber(this.state[source].originalBuf, start); 171 | const error = new Error(`${msg} (${source}:${line})`); 172 | Error.captureStackTrace(error, this.error); // capture stack trace without this function 173 | return error; 174 | } 175 | 176 | matchOriginalAfter(node: Node, re: RegExp): ByteMatch | undefined { 177 | const { source, start, length } = this.decodeSrc(node.src); 178 | const buf = this.state[source].originalBuf; 179 | return matchBufferAt(buf, re, start + length); 180 | } 181 | 182 | results(): { [file in string]: string } { 183 | return mapValues(this.state, s => s.content.toString()); 184 | } 185 | 186 | asts(): SourceUnit[] { 187 | return Object.values(this.state).map(s => s.ast); 188 | } 189 | 190 | exclude(source: string) { 191 | delete this.state[source]; 192 | } 193 | } 194 | 195 | function insertSortedAndValidate( 196 | transformations: Transformation[], 197 | t: Transformation, 198 | error: (byteIndex: number, msg: string) => Error, 199 | ): void { 200 | transformations.push(t); 201 | transformations.sort(compareTransformations); // checks for overlaps 202 | for (let i = transformations.indexOf(t) + 1; i < transformations.length; i += 1) { 203 | const s = transformations[i]; 204 | const c = compareContainment(t, s); 205 | if (typeof c === 'number' && c < 0) { 206 | throw error(s.start, `A bigger area has already been transformed (${s.kind} > ${t.kind})`); 207 | } 208 | } 209 | } 210 | function byteToLineNumber(buf: Buffer, byteIndex: number): number { 211 | return buf.slice(0, byteIndex).toString('utf8').split('\n').length; 212 | } 213 | -------------------------------------------------------------------------------- /src/transformations/add-namespace-struct.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit, VariableDeclaration } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { getNodeBounds } from '../solc/ast-utils'; 4 | import { TransformerTools } from '../transform'; 5 | import { Transformation } from './type'; 6 | import { formatLines } from './utils/format-lines'; 7 | import { isStorageVariable } from './utils/is-storage-variable'; 8 | import { erc7201Location } from '../utils/erc7201'; 9 | import { contractStartPosition } from './utils/contract-start-position'; 10 | import { Node } from 'solidity-ast/node'; 11 | import { extractContractStorageSize } from '../utils/natspec'; 12 | 13 | export function getNamespaceStructName(contractName: string): string { 14 | return contractName + 'Storage'; 15 | } 16 | 17 | export function addNamespaceStruct(include?: (source: string) => boolean) { 18 | return function* (sourceUnit: SourceUnit, tools: TransformerTools): Generator { 19 | if (!include?.(sourceUnit.absolutePath)) { 20 | return; 21 | } 22 | 23 | const { error, resolver } = tools; 24 | 25 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 26 | const specifiesStorageSize = extractContractStorageSize(contract) !== undefined; 27 | 28 | if (specifiesStorageSize) { 29 | throw tools.error( 30 | contract, 31 | 'Cannot combine namespaces with @custom:storage-size annotations', 32 | ); 33 | } 34 | 35 | let start = contractStartPosition(contract, tools); 36 | 37 | let finished = false; 38 | 39 | const nonStorageVars: [number, VariableDeclaration][] = []; 40 | const storageVars: VariableDeclaration[] = []; 41 | 42 | // We look for the start of the source code block in the contract 43 | // where variables are written 44 | for (const n of contract.nodes) { 45 | if ( 46 | n.nodeType === 'VariableDeclaration' && 47 | (storageVars.length > 0 || isStorageVariable(n, resolver)) 48 | ) { 49 | if (finished) { 50 | throw error(n, 'All variables in the contract must be contiguous'); 51 | } 52 | 53 | if (!isStorageVariable(n, resolver)) { 54 | const varStart = getRealEndIndex(storageVars.at(-1)!, tools) + 1; 55 | nonStorageVars.push([varStart, n]); 56 | } else { 57 | storageVars.push(n); 58 | } 59 | } else if (storageVars.length > 0) { 60 | // We've seen storage variables before and the current node is not a 61 | // variable, so we consider the block to have finished 62 | finished = true; 63 | } else { 64 | // We haven't found storage variables yet. We assume the block of 65 | // variables will start after the current node 66 | start = getRealEndIndex(n, tools) + 1; 67 | } 68 | } 69 | 70 | if (storageVars.length > 0) { 71 | // We first move non-storage variables from their location to the beginning of 72 | // the block, so they are excluded from the namespace struct 73 | for (const [s, v] of nonStorageVars) { 74 | const bounds = { start: s, length: getRealEndIndex(v, tools) + 1 - s }; 75 | let removed = ''; 76 | 77 | yield { 78 | kind: 'relocate-nonstorage-var-remove', 79 | ...bounds, 80 | transform: source => { 81 | removed = source; 82 | return ''; 83 | }, 84 | }; 85 | 86 | yield { 87 | kind: 'relocate-nonstorage-var-reinsert', 88 | start, 89 | length: 0, 90 | text: removed, 91 | }; 92 | } 93 | 94 | if (nonStorageVars.length > 0) { 95 | yield { 96 | kind: 'relocate-nonstorage-var-newline', 97 | start, 98 | length: 0, 99 | text: '\n', 100 | }; 101 | } 102 | 103 | for (const v of storageVars) { 104 | const { start, length } = getNodeBounds(v); 105 | yield { 106 | kind: 'remove-var-modifier', 107 | start, 108 | length, 109 | transform: source => source.replace(/\s*\bprivate\b/g, ''), 110 | }; 111 | } 112 | 113 | const namespace = getNamespaceStructName(contract.name); 114 | const id = 'openzeppelin.storage.' + contract.name; 115 | 116 | const end = getRealEndIndex(storageVars.at(-1)!, tools) + 1; 117 | 118 | yield { 119 | kind: 'add-namespace-struct', 120 | start, 121 | length: end - start, 122 | transform: source => { 123 | // We extract the newlines at the beginning of the block so we can leave 124 | // them outside of the struct definition 125 | const [, leadingNewlines, rest] = source.match(/^((?:[ \t\v\f]*[\n\r])*)(.*)$/s)!; 126 | return ( 127 | leadingNewlines + 128 | formatLines(1, [ 129 | `/// @custom:storage-location erc7201:${id}`, 130 | `struct ${namespace} {`, 131 | ...rest.split('\n'), 132 | `}`, 133 | ``, 134 | `// keccak256(abi.encode(uint256(keccak256("${id}")) - 1)) & ~bytes32(uint256(0xff))`, 135 | `bytes32 private constant ${namespace}Location = ${erc7201Location(id)};`, 136 | ``, 137 | `function _get${namespace}() private pure returns (${namespace} storage $) {`, 138 | [`assembly {`, [`$.slot := ${namespace}Location`], `}`], 139 | `}`, 140 | ]).trimEnd() 141 | ); 142 | }, 143 | }; 144 | 145 | for (const fnDef of findAll('FunctionDefinition', contract)) { 146 | for (const ref of fnDef.modifiers.flatMap(m => [...findAll('Identifier', m)])) { 147 | const varDecl = resolver.tryResolveNode( 148 | 'VariableDeclaration', 149 | ref.referencedDeclaration!, 150 | ); 151 | if (varDecl && isStorageVariable(varDecl, resolver)) { 152 | throw error(ref, 'Unsupported storage variable found in modifier'); 153 | } 154 | } 155 | 156 | let foundReferences = false; 157 | if (fnDef.body) { 158 | for (const ref of findAll('Identifier', fnDef.body)) { 159 | const varDecl = resolver.tryResolveNode( 160 | 'VariableDeclaration', 161 | ref.referencedDeclaration!, 162 | ); 163 | if (varDecl && isStorageVariable(varDecl, resolver)) { 164 | if (varDecl.scope !== contract.id) { 165 | throw error(varDecl, 'Namespaces assume all variables are private'); 166 | } 167 | foundReferences = true; 168 | const { start } = getNodeBounds(ref); 169 | yield { kind: 'add-namespace-ref', start, length: 0, text: '$.' }; 170 | } 171 | } 172 | 173 | if (fnDef.kind !== 'constructor' && foundReferences) { 174 | // The constructor is handled in transformConstructor 175 | const { start: fnBodyStart } = getNodeBounds(fnDef.body); 176 | yield { 177 | kind: 'add-namespace-base-ref', 178 | start: fnBodyStart + 1, 179 | length: 0, 180 | text: `\n ${namespace} storage $ = _get${namespace}();`, 181 | }; 182 | } 183 | } 184 | } 185 | } 186 | } 187 | }; 188 | } 189 | 190 | function getRealEndIndex(node: Node, tools: TransformerTools): number { 191 | // VariableDeclaration node bounds don't include the semicolon, so we look for it, 192 | // and include a comment if there is one after the node. 193 | // This regex always matches at least the empty string. 194 | const { start, length } = tools.matchOriginalAfter(node, /(\s*;)?([ \t]*\/\/[^\n\r]*)?/)!; 195 | return start + length - 1; 196 | } 197 | -------------------------------------------------------------------------------- /src/transformations/add-required-public-initializers.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { minimatch } from 'minimatch'; 4 | 5 | import { newFunctionPosition } from './utils/new-function-position'; 6 | import { buildPublicInitialize } from './utils/build-pulic-initialize'; 7 | import { Transformation } from './type'; 8 | import { TransformerTools } from '../transform'; 9 | 10 | export function addRequiredPublicInitializer(publicInitializers: string[] | undefined) { 11 | return function* (sourceUnit: SourceUnit, tools: TransformerTools): Generator { 12 | const { getData } = tools; 13 | 14 | const requested = publicInitializers?.some(p => minimatch(sourceUnit.absolutePath, p)) ?? false; 15 | 16 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 17 | if ( 18 | getData(contract).isUsedInNewStatement || 19 | (requested && contract.contractKind === 'contract') 20 | ) { 21 | const start = newFunctionPosition(contract, tools); 22 | 23 | yield { 24 | start, 25 | length: 0, 26 | kind: 'add-external-initializer', 27 | text: buildPublicInitialize(contract, tools), 28 | }; 29 | } 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/transformations/add-storage-gaps.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit, ContractDefinition } from 'solidity-ast'; 2 | import { findAll, isNodeType } from 'solidity-ast/utils'; 3 | 4 | import { formatLines } from './utils/format-lines'; 5 | import { getNodeBounds } from '../solc/ast-utils'; 6 | import { StorageLayout } from '../solc/input-output'; 7 | import { Transformation } from './type'; 8 | import { TransformerTools } from '../transform'; 9 | import { extractContractStorageSize } from '../utils/natspec'; 10 | import { decodeTypeIdentifier } from '../utils/type-id'; 11 | import { parseTypeId } from '../utils/parse-type-id'; 12 | import { ASTResolver } from '../ast-resolver'; 13 | import { isStorageVariable } from './utils/is-storage-variable'; 14 | 15 | // By default, make the contract a total of 50 slots (storage + gap) 16 | const DEFAULT_SLOT_COUNT = 50; 17 | 18 | export function* addStorageGaps( 19 | sourceUnit: SourceUnit, 20 | { getLayout, resolver }: TransformerTools, 21 | ): Generator { 22 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 23 | if (contract.contractKind === 'contract') { 24 | const targetSlots = extractContractStorageSize(contract) ?? DEFAULT_SLOT_COUNT; 25 | 26 | const gapSize = targetSlots - getContractSlotCount(contract, getLayout(contract), resolver); 27 | 28 | if (gapSize <= 0) { 29 | throw new Error( 30 | `Contract ${contract.name} uses more than the ${targetSlots} reserved slots.`, 31 | ); 32 | } 33 | 34 | const contractBounds = getNodeBounds(contract); 35 | const start = contractBounds.start + contractBounds.length - 1; 36 | 37 | const text = formatLines(0, [ 38 | ``, 39 | [ 40 | `/**`, 41 | ` * @dev This empty reserved space is put in place to allow future versions to add new`, 42 | ` * variables without shifting down storage in the inheritance chain.`, 43 | ` * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps`, 44 | ` */`, 45 | `uint256[${gapSize}] private __gap;`, 46 | ], 47 | ]); 48 | 49 | yield { 50 | kind: 'add-storage-gaps', 51 | start, 52 | length: 0, 53 | text, 54 | }; 55 | } 56 | } 57 | } 58 | 59 | function getNumberOfBytesOfValueType(typeId: string, resolver: ASTResolver): number { 60 | const { head, tail } = parseTypeId(typeId); 61 | const details = head.match(/^t_(?[a-zA-Z]+)(?\d+)?/); 62 | switch (details?.groups?.base) { 63 | case 'bool': 64 | case 'byte': 65 | case 'enum': 66 | return 1; 67 | case 'address': 68 | case 'contract': 69 | return 20; 70 | case 'bytes': 71 | return parseInt(details.groups.size, 10); 72 | case 'int': 73 | case 'uint': 74 | return parseInt(details.groups.size, 10) / 8; 75 | case 'userDefinedValueType': { 76 | const definition = resolver.resolveNode('UserDefinedValueTypeDefinition', Number(tail)); 77 | const underlying = definition.underlyingType.typeDescriptions.typeIdentifier; 78 | if (underlying) { 79 | return getNumberOfBytesOfValueType(underlying, resolver); 80 | } else { 81 | throw new Error(`Unsupported value type: ${typeId}`); 82 | } 83 | } 84 | default: 85 | throw new Error(`Unsupported value type: ${typeId}`); 86 | } 87 | } 88 | 89 | function getContractSlotCount( 90 | contractNode: ContractDefinition, 91 | layout: StorageLayout, 92 | resolver: ASTResolver, 93 | ): number { 94 | // This tracks both slot and offset: 95 | // - slot = Math.floor(contractSizeInBytes / 32) 96 | // - offset = contractSizeInBytes % 32 97 | let contractSizeInBytes = 0; 98 | 99 | // don't use `findAll` here, we don't want to go recursive 100 | for (const varDecl of contractNode.nodes.filter(isNodeType('VariableDeclaration'))) { 101 | if (isStorageVariable(varDecl, resolver)) { 102 | // try get type details 103 | const typeIdentifier = decodeTypeIdentifier(varDecl.typeDescriptions.typeIdentifier ?? ''); 104 | 105 | // size of current object from type details, or try to reconstruct it if 106 | // they're not available try to reconstruct it, which can happen for 107 | // immutable variables 108 | const size = 109 | layout.types && layout.types[typeIdentifier] 110 | ? parseInt(layout.types[typeIdentifier]?.numberOfBytes ?? '') 111 | : getNumberOfBytesOfValueType(typeIdentifier, resolver); 112 | 113 | // used space in the current slot 114 | const offset = contractSizeInBytes % 32; 115 | // remaining space in the current slot (only if slot is dirty) 116 | const remaining = (32 - offset) % 32; 117 | // if the remaining space is not enough to fit the current object, then consume the free space to start at next slot 118 | contractSizeInBytes += (size > remaining ? remaining : 0) + size; 119 | } 120 | } 121 | 122 | return Math.ceil(contractSizeInBytes / 32); 123 | } 124 | -------------------------------------------------------------------------------- /src/transformations/append-initializable-import.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { Node } from 'solidity-ast/node'; 3 | import { findAll } from 'solidity-ast/utils'; 4 | 5 | import path from 'path'; 6 | import { relativePath } from '../utils/relative-path'; 7 | 8 | import { getNodeBounds } from '../solc/ast-utils'; 9 | import { Transformation } from './type'; 10 | 11 | export function appendInitializableImport(initializablePath: string) { 12 | return function* (sourceUnit: SourceUnit): Generator { 13 | const contracts = [...findAll('ContractDefinition', sourceUnit)]; 14 | if (!contracts.some(c => c.contractKind === 'contract')) { 15 | return; 16 | } 17 | 18 | let last: Node | undefined; 19 | for (const node of findAll('PragmaDirective', sourceUnit)) { 20 | last = node; 21 | } 22 | for (const node of findAll('ImportDirective', sourceUnit)) { 23 | last = node; 24 | } 25 | 26 | const relativeImportPath = relativePath( 27 | path.dirname(sourceUnit.absolutePath), 28 | initializablePath, 29 | ); 30 | 31 | const after = last ? getNodeBounds(last) : { start: 0, length: 0 }; 32 | const start = after.start + after.length; 33 | 34 | yield { 35 | start, 36 | length: 0, 37 | kind: 'append-initializable-import', 38 | text: `\nimport {Initializable} from "${relativeImportPath}";`, 39 | }; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/transformations/apply.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { applyTransformation } from './apply'; 4 | import { Transformation, TransformHelper } from './type'; 5 | import { Shift } from '../shifts'; 6 | 7 | const defaultHelper: TransformHelper = { 8 | read() { 9 | throw new Error('unimplemented'); 10 | }, 11 | }; 12 | 13 | function applyAll(content: string, ts: Transformation[], helper = defaultHelper): string { 14 | const shifts: Shift[] = []; 15 | return ts 16 | .reduce((content, t) => { 17 | const { result, shift } = applyTransformation(t, content, shifts, helper); 18 | shifts.push(shift); 19 | return result; 20 | }, Buffer.from(content)) 21 | .toString(); 22 | } 23 | 24 | test('apply non overlapping length preserved', t => { 25 | const source = '01234567'; 26 | const a = { kind: 'a', start: 0, length: 2, text: '00' }; 27 | const b = { kind: 'b', start: 2, length: 2, text: '00' }; 28 | t.is('00004567', applyAll(source, [a, b])); 29 | }); 30 | 31 | test('apply contained length preserved', t => { 32 | const source = '01234567'; 33 | const a = { kind: 'a', start: 2, length: 2, text: '00' }; 34 | const b = { kind: 'b', start: 0, length: 6, text: '000000' }; 35 | t.is('00000067', applyAll(source, [a, b])); 36 | }); 37 | 38 | test('apply non overlapping contracted', t => { 39 | const source = '01234567'; 40 | const a = { kind: 'a', start: 0, length: 2, text: 'a' }; 41 | const b = { kind: 'b', start: 2, length: 2, text: 'b' }; 42 | const c = { kind: 'c', start: 4, length: 2, text: 'c' }; 43 | t.is('abc67', applyAll(source, [a, b, c])); 44 | }); 45 | 46 | test('apply non overlapping expanded', t => { 47 | const source = '01234567'; 48 | const a = { kind: 'a', start: 0, length: 2, text: 'aaa' }; 49 | const b = { kind: 'b', start: 2, length: 2, text: 'bbb' }; 50 | const c = { kind: 'c', start: 4, length: 2, text: 'ccc' }; 51 | t.is('aaabbbccc67', applyAll(source, [a, b, c])); 52 | }); 53 | 54 | test('apply realistic transformations', t => { 55 | const source = 'a x = b(0);'; 56 | const a = { kind: 'a', start: 0, length: 1, text: 'aaaaaaa' }; 57 | const b = { kind: 'b', start: 6, length: 1, text: 'bbbbbbb' }; 58 | const c = { kind: 'c', start: 3, length: 7, text: '' }; 59 | t.is('aaaaaaa x;', applyAll(source, [a, b, c])); 60 | }); 61 | 62 | test('apply overlapping transformations', t => { 63 | const source = 'a x = b(0); a x = b(0);'; 64 | const a = { kind: 'a', start: 0, length: 1, text: 'aaaa' }; 65 | const b = { kind: 'b', start: 6, length: 1, text: 'bbbb' }; 66 | const c = { kind: 'c', start: 3, length: 7, text: '' }; 67 | const d = { kind: 'd', start: 11, length: 0, text: ' d;' }; 68 | t.is('aaaa x; d; a x = b(0);', applyAll(source, [a, b, c, d])); 69 | }); 70 | 71 | test('apply non overlapping with function transformation', t => { 72 | const source = '01234567'; 73 | const a = { 74 | kind: 'a', 75 | start: 0, 76 | length: 2, 77 | transform: (s: string) => s + '.', 78 | }; 79 | t.is('01.234567', applyAll(source, [a])); 80 | }); 81 | 82 | test('apply contained with function transformation', t => { 83 | const source = 'abcdef'; 84 | const a = { kind: 'a', start: 2, length: 2, text: 'xyz' }; 85 | const b = { 86 | kind: 'b', 87 | start: 1, 88 | length: 4, 89 | transform: (s: string) => s.toUpperCase(), 90 | }; 91 | t.is('aBXYZEf', applyAll(source, [a, b])); 92 | }); 93 | 94 | test('apply two stacked transformations of length zero', t => { 95 | const source = 'xxyy'; 96 | const a = { kind: 'a', start: 2, length: 0, text: 'aa' }; 97 | const b = { kind: 'b', start: 2, length: 0, text: 'bb' }; 98 | t.is('xxaabbyy', applyAll(source, [a, b])); 99 | }); 100 | -------------------------------------------------------------------------------- /src/transformations/apply.ts: -------------------------------------------------------------------------------- 1 | import { Transformation, TransformHelper } from './type'; 2 | import { Shift, shiftBounds } from '../shifts'; 3 | 4 | import { compareTransformations } from './compare'; 5 | 6 | interface ApplyResult { 7 | result: Buffer; 8 | shift: Shift; 9 | } 10 | 11 | export function applyTransformation( 12 | t: Transformation, 13 | content: Buffer, 14 | shifts: Shift[], 15 | helper: TransformHelper, 16 | ): ApplyResult { 17 | const sb = shiftBounds(shifts, t); 18 | const [pre, mid, post] = split(content, sb.start, sb.length); 19 | const text = Buffer.from('text' in t ? t.text : t.transform(mid.toString(), helper)); 20 | 21 | const shift = { 22 | amount: text.length - sb.length, 23 | location: t.start + t.length, 24 | lengthZero: t.length === 0, 25 | }; 26 | 27 | const result = Buffer.concat([pre, text, post]); 28 | 29 | return { result, shift }; 30 | } 31 | 32 | export function split(source: Buffer, start: number, length: number): [Buffer, Buffer, Buffer] { 33 | const pre = source.slice(0, start); 34 | const mid = source.slice(start, start + length); 35 | const post = source.slice(start + length); 36 | return [pre, mid, post]; 37 | } 38 | 39 | export function sortTransformations( 40 | transformations: Transformation[], 41 | sourcePath: string, 42 | ): Transformation[] { 43 | for (const t of transformations) { 44 | if (t.length < 0) { 45 | throw new Error(`${sourcePath}: transformation ${t.kind} has negative length`); 46 | } 47 | } 48 | 49 | return Array.from(transformations).sort(compareTransformations); 50 | } 51 | -------------------------------------------------------------------------------- /src/transformations/compare.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { TransformationText } from './type'; 4 | import { compareTransformations } from './compare'; 5 | 6 | function sortTransformations(ts: TransformationText[]): TransformationText[] { 7 | return Array.from(ts).sort(compareTransformations); 8 | } 9 | 10 | test('sort non overlapping', t => { 11 | const b = { kind: 'b', start: 2, length: 1, text: '' }; 12 | const a = { kind: 'a', start: 0, length: 1, text: '' }; 13 | t.deepEqual([a, b], sortTransformations([a, b])); 14 | t.deepEqual([a, b], sortTransformations([b, a])); 15 | }); 16 | 17 | test('sort contained', t => { 18 | const a = { kind: 'a', start: 1, length: 1, text: '' }; 19 | const b = { kind: 'b', start: 0, length: 4, text: '' }; 20 | t.deepEqual([a, b], sortTransformations([b, a])); 21 | t.deepEqual([a, b], sortTransformations([a, b])); 22 | }); 23 | 24 | test('reject partial overlap', t => { 25 | const a = { kind: 'a', start: 1, length: 2, text: '' }; 26 | const b = { kind: 'b', start: 0, length: 2, text: '' }; 27 | t.throws(() => sortTransformations([b, a])); 28 | }); 29 | 30 | test('sort contained with shared end', t => { 31 | const a = { kind: 'a', start: 1, length: 1, text: '' }; 32 | const b = { kind: 'b', start: 0, length: 2, text: '' }; 33 | t.deepEqual([a, b], sortTransformations([b, a])); 34 | t.deepEqual([a, b], sortTransformations([a, b])); 35 | }); 36 | 37 | test('sort contained with shared start', t => { 38 | const a = { kind: 'a', start: 0, length: 1, text: '' }; 39 | const b = { kind: 'b', start: 0, length: 2, text: '' }; 40 | t.deepEqual([a, b], sortTransformations([b, a])); 41 | t.deepEqual([a, b], sortTransformations([a, b])); 42 | }); 43 | 44 | test('sort equal lengths', t => { 45 | const a = { kind: 'a', start: 0, length: 1, text: '' }; 46 | const b = { kind: 'b', start: 0, length: 1, text: '' }; 47 | sortTransformations([b, a]); 48 | t.pass(); 49 | }); 50 | 51 | test('sort length zero as not overlapping', t => { 52 | const a = { kind: 'a', start: 0, length: 1, text: '' }; 53 | const b = { kind: 'b', start: 1, length: 0, text: '' }; 54 | const c = { kind: 'c', start: 1, length: 1, text: '' }; 55 | t.deepEqual([a, b, c], sortTransformations([a, b, c])); 56 | t.deepEqual([a, b, c], sortTransformations([b, a, c])); 57 | t.deepEqual([a, b, c], sortTransformations([c, a, b])); 58 | }); 59 | 60 | test('sort complex', t => { 61 | const tf = (kind: string, start: number, length: number) => ({ 62 | kind, 63 | start, 64 | length, 65 | text: '', 66 | }); 67 | const a = tf('a', 0, 1); // x 68 | const b = tf('b', 4, 1); // x 69 | const c = tf('c', 6, 1); // x 70 | const d = tf('d', 3, 5); // xxxxx 71 | const e = tf('e', 9, 1); // x 72 | const f = tf('f', 2, 9); // xxxxxxxxx 73 | // 0123456789A 74 | t.deepEqual([a, b, c, d, e, f], sortTransformations([a, b, c, d, e, f])); 75 | t.deepEqual([a, b, c, d, e, f], sortTransformations([b, f, a, c, e, d])); 76 | t.deepEqual([a, b, c, d, e, f], sortTransformations([a, f, b, d, c, e])); 77 | t.deepEqual([a, b, c, d, e, f], sortTransformations([e, b, a, d, f, c])); 78 | }); 79 | -------------------------------------------------------------------------------- /src/transformations/compare.ts: -------------------------------------------------------------------------------- 1 | import { Transformation, Bounds } from './type'; 2 | 3 | export function compareTransformations(a: Transformation, b: Transformation): number { 4 | const c = compareContainment(a, b); 5 | 6 | if (c === 'partial overlap') { 7 | throw new Error(`Transformations ${a.kind} and ${b.kind} overlap`); 8 | } else if (c === 'disjoint') { 9 | // sort by midpoint 10 | return a.start + a.length / 2 - (b.start + b.length / 2); 11 | } else { 12 | return c; 13 | } 14 | } 15 | 16 | export function compareContainment(a: Bounds, b: Bounds): number | 'disjoint' | 'partial overlap' { 17 | const a_end = a.start + a.length; 18 | const b_end = b.start + b.length; 19 | const x = (a.start - b.start) * (a_end - b_end); 20 | 21 | if (x > 0 || (x === 0 && a.length * b.length === 0)) { 22 | if (a.start === b.start && a.length === b.length) { 23 | return 0; 24 | } else if (a.start + a.length <= b.start || b.start + b.length <= a.start) { 25 | return 'disjoint'; 26 | } else { 27 | return 'partial overlap'; 28 | } 29 | } else { 30 | return a.length - b.length; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/transformations/fix-import-directives.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { getNodeBounds } from '../solc/ast-utils'; 4 | import { Transformation } from './type'; 5 | import { renameContract, renamePath } from '../rename'; 6 | import { TransformerTools } from '../transform'; 7 | 8 | declare module '../transform' { 9 | interface TransformData { 10 | importFromPeer: string; 11 | } 12 | } 13 | 14 | export function fixImportDirectives(withPeerProject?: boolean) { 15 | return function* ( 16 | ast: SourceUnit, 17 | { resolver, getData }: TransformerTools, 18 | ): Generator { 19 | for (const imp of findAll('ImportDirective', ast)) { 20 | const referencedSourceUnit = resolver.resolveNode('SourceUnit', imp.sourceUnit); 21 | 22 | if (withPeerProject && imp.symbolAliases.length == 0) { 23 | throw new Error( 24 | `Transpile with peer doesn't support import without aliases in ${imp.absolutePath}`, 25 | ); 26 | } 27 | 28 | const imports: Record = {}; 29 | 30 | for (const a of imp.symbolAliases) { 31 | const id = referencedSourceUnit.exportedSymbols[a.foreign.name]?.[0]; 32 | if (id === undefined) { 33 | throw new Error(`Can't find symbol imported in ${ast.absolutePath}`); 34 | } 35 | 36 | const node = resolver.resolveNode('*', id); 37 | const importFromPeer = getData(node).importFromPeer; 38 | const importPath = importFromPeer ?? renamePath(imp.file); 39 | 40 | imports[importPath] ||= []; 41 | imports[importPath].push( 42 | [ 43 | importFromPeer === undefined ? renameContract(a.foreign.name) : a.foreign.name, 44 | [null, undefined, a.foreign.name].includes(a.local) ? '' : ` as ${a.local}`, 45 | ].join(''), 46 | ); 47 | } 48 | 49 | const statement = []; 50 | for (const [path, aliases] of Object.entries(imports)) { 51 | statement.push(`import {${aliases.join(', ')}} from "${path}";`); 52 | } 53 | if (imp.symbolAliases.length == 0) { 54 | statement.push(`import "${renamePath(imp.file)}";`); 55 | } 56 | 57 | yield { 58 | kind: 'fix-import-directives', 59 | text: statement.join('\n'), 60 | ...getNodeBounds(imp), 61 | }; 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/transformations/fix-new-statement.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | 4 | import { getNodeBounds } from '../solc/ast-utils'; 5 | import { Transformation } from './type'; 6 | import { TransformerTools } from '../transform'; 7 | import { parseNewExpression } from '../utils/new-expression'; 8 | 9 | declare module '../transform' { 10 | interface TransformData { 11 | isUsedInNewStatement: boolean; 12 | } 13 | } 14 | 15 | // Finds statements of the form: 16 | // - x = new Foo(...); 17 | // - x = address(new Foo(...)); 18 | // and transforms them to use initializers: 19 | // x = new Foo(); 20 | // x.initialize(...); 21 | // Note that these are variable assignments. 22 | // Variable declarations are not supported. 23 | export function* fixNewStatement( 24 | sourceUnit: SourceUnit, 25 | tools: TransformerTools, 26 | ): Generator { 27 | const { resolver, getData } = tools; 28 | 29 | for (const statement of findAll('ExpressionStatement', sourceUnit)) { 30 | const { expression } = statement; 31 | 32 | if (expression.nodeType === 'Assignment') { 33 | const newExpr = parseNewExpression(expression.rightHandSide); 34 | 35 | if (newExpr) { 36 | const { typeName, args, initializeCall } = newExpr; 37 | const contract = resolver.resolveContract(typeName.referencedDeclaration); 38 | 39 | if (contract) { 40 | getData(contract).isUsedInNewStatement = true; 41 | 42 | const stBounds = getNodeBounds(statement); 43 | const afterStatement = stBounds.start + stBounds.length; 44 | 45 | yield { 46 | start: afterStatement, 47 | length: 0, 48 | kind: 'fix-new-statement', 49 | transform: (_, helper) => 50 | [ 51 | ';\n', 52 | ' '.repeat(4 * 2), 53 | initializeCall(helper.read(expression.leftHandSide), helper), 54 | ].join(''), 55 | }; 56 | 57 | if (args.length > 0) { 58 | const { start } = getNodeBounds(args[0]); 59 | const [lastArg] = args.slice(-1); 60 | const lastArgBounds = getNodeBounds(lastArg); 61 | const length = lastArgBounds.start + lastArgBounds.length - start; 62 | 63 | yield { 64 | start, 65 | length, 66 | kind: 'fix-new-statement-remove-args', 67 | text: '', 68 | }; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/transformations/peer-import.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { getNodeBounds } from '../solc/ast-utils'; 3 | import { Transformation } from './type'; 4 | import { TransformerTools } from '../transform'; 5 | import assert from 'assert'; 6 | 7 | export function* peerImport( 8 | ast: SourceUnit, 9 | { getData }: TransformerTools, 10 | ): Generator { 11 | for (const node of ast.nodes) { 12 | const { importFromPeer } = getData(node); 13 | if (importFromPeer !== undefined) { 14 | assert('name' in node); 15 | yield { 16 | ...getNodeBounds(node), 17 | kind: 'replace-declaration-with-peer-import', 18 | text: `import { ${node.name} } from "${importFromPeer}";`, 19 | }; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/transformations/prepend-initializable-base.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { getNodeBounds } from '../solc/ast-utils'; 4 | import { Transformation } from './type'; 5 | import { TransformerTools } from '../transform'; 6 | import { matchBufferAt } from '../utils/match'; 7 | 8 | export function* prependInitializableBase( 9 | sourceUnit: SourceUnit, 10 | { originalSourceBuf }: TransformerTools, 11 | ): Generator { 12 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 13 | if (contract.contractKind !== 'contract') { 14 | continue; 15 | } 16 | 17 | if (contract.baseContracts.length > 0) { 18 | const { start } = getNodeBounds(contract.baseContracts[0]); 19 | yield { 20 | kind: 'prepend-initializable-base', 21 | start, 22 | length: 0, 23 | text: `Initializable, `, 24 | }; 25 | } else { 26 | const bounds = getNodeBounds(contract); 27 | const re = /(?:abstract\s+)?contract\s+([a-zA-Z0-9$_]+)/; 28 | const match = matchBufferAt(originalSourceBuf, re, bounds.start); 29 | 30 | if (!match) { 31 | throw new Error(`Can't find ${contract.name} in ${sourceUnit.absolutePath}`); 32 | } 33 | 34 | const start = match.start + match.length; 35 | 36 | yield { 37 | start, 38 | length: 0, 39 | kind: 'prepend-initializable-base', 40 | text: ' is Initializable', 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/transformations/purge-var-inits.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { getNodeBounds } from '../solc/ast-utils'; 4 | import { TransformerTools } from '../transform'; 5 | import { hasConstructorOverride, hasOverride } from '../utils/upgrades-overrides'; 6 | 7 | import { Transformation } from './type'; 8 | import { isStorageVariable } from './utils/is-storage-variable'; 9 | 10 | export function* removeStateVarInits( 11 | sourceUnit: SourceUnit, 12 | { resolver }: TransformerTools, 13 | ): Generator { 14 | for (const contractNode of findAll('ContractDefinition', sourceUnit)) { 15 | if (hasConstructorOverride(contractNode)) { 16 | continue; 17 | } 18 | 19 | for (const varDecl of findAll('VariableDeclaration', contractNode)) { 20 | if (varDecl.value && isStorageVariable(varDecl, resolver)) { 21 | if (hasOverride(varDecl, 'state-variable-assignment', resolver)) { 22 | continue; 23 | } 24 | 25 | yield { 26 | ...getNodeBounds(varDecl), 27 | kind: 'purge-var-inits', 28 | transform: source => source.replace(/\s*=.*/s, ''), 29 | }; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/transformations/remove-immutable.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { getNodeBounds } from '../solc/ast-utils'; 4 | import { TransformerTools } from '../transform'; 5 | import { hasOverride } from '../utils/upgrades-overrides'; 6 | 7 | import { Transformation } from './type'; 8 | 9 | export function* removeImmutable( 10 | sourceUnit: SourceUnit, 11 | { resolver }: TransformerTools, 12 | ): Generator { 13 | for (const varDecl of findAll('VariableDeclaration', sourceUnit)) { 14 | if (varDecl.mutability === 'immutable') { 15 | if (hasOverride(varDecl, 'state-variable-immutable', resolver)) { 16 | continue; 17 | } 18 | 19 | yield { 20 | ...getNodeBounds(varDecl), 21 | kind: 'remove-immutable', 22 | transform: source => source.replace(/\s+\bimmutable\b/, ''), 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/transformations/remove-inheritance-list-args.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | 4 | import { getNodeBounds } from '../solc/ast-utils'; 5 | import { Transformation } from './type'; 6 | 7 | // Removes inheritance arguments from contracts inheritance list, for example 8 | // This: contract B is A(4) { 9 | // Becomes this: contract B is A { 10 | export function* removeInheritanceListArguments(sourceUnit: SourceUnit): Generator { 11 | for (const base of findAll('InheritanceSpecifier', sourceUnit)) { 12 | yield { 13 | ...getNodeBounds(base), 14 | kind: 'remove-inheritance-arguments', 15 | transform: source => source.replace(/\(.*\)/, ''), 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/transformations/rename-contract-definition.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | 4 | import { renameContract } from '../rename'; 5 | import { getNodeBounds } from '../solc/ast-utils'; 6 | import { Transformation } from './type'; 7 | import { TransformerTools } from '../transform'; 8 | import { matchBufferAt } from '../utils/match'; 9 | 10 | export function* renameContractDefinition( 11 | sourceUnit: SourceUnit, 12 | { originalSourceBuf }: TransformerTools, 13 | ): Generator { 14 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 15 | const bounds = getNodeBounds(contract); 16 | const re = /(?:abstract\s+)?(?:contract|library|interface)\s+([a-zA-Z0-9$_]+)/; 17 | const match = matchBufferAt(originalSourceBuf, re, bounds.start); 18 | 19 | if (!match) { 20 | throw new Error(`Can't find ${contract.name} in ${sourceUnit.absolutePath}`); 21 | } 22 | 23 | yield { 24 | ...match, 25 | kind: 'rename-contract-definition', 26 | transform: source => source.replace(/[a-zA-Z0-9$_]+$/, renameContract), 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/transformations/rename-identifiers.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { Node } from 'solidity-ast/node'; 3 | import { findAll } from 'solidity-ast/utils'; 4 | import { getNodeBounds } from '../solc/ast-utils'; 5 | import { Transformation } from './type'; 6 | import { renameContract } from '../rename'; 7 | import { ASTResolver } from '../ast-resolver'; 8 | import { TransformerTools } from '../transform'; 9 | 10 | function* findAllIdentifiers(node: Node) { 11 | const seen = new Set(); 12 | for (const id of findAll(['UserDefinedTypeName', 'IdentifierPath', 'Identifier'], node)) { 13 | if ('pathNode' in id && id.pathNode !== undefined) { 14 | seen.add(id.pathNode); 15 | } 16 | if (!seen.has(id)) { 17 | yield id; 18 | } 19 | } 20 | } 21 | 22 | export function* renameIdentifiers( 23 | sourceUnit: SourceUnit, 24 | { resolver, getData }: TransformerTools, 25 | ): Generator { 26 | const candidates = getTransitiveRenameCandidates(sourceUnit, resolver); 27 | const rename = new Set( 28 | Object.keys(candidates).filter(name => { 29 | const contract = resolver.resolveContract(candidates[name]); 30 | return contract !== undefined && getData(contract).importFromPeer === undefined; 31 | }), 32 | ); 33 | 34 | for (const ident of findAllIdentifiers(sourceUnit)) { 35 | const ref = ident.referencedDeclaration; 36 | const contract = ref != null ? resolver.resolveContract(ref) : undefined; 37 | 38 | const identName = 'pathNode' in ident ? ident.pathNode?.name : ident.name; 39 | 40 | if (identName === undefined) { 41 | throw new Error('Unrecognized AST'); 42 | } 43 | 44 | if (contract && rename.has(contract.name)) { 45 | yield { 46 | kind: 'rename-identifiers', 47 | text: renameContract(identName), 48 | ...getNodeBounds(ident), 49 | }; 50 | } else if (identName.includes('.')) { 51 | const [ns] = identName.split('.', 1); 52 | if (rename.has(ns)) { 53 | yield { 54 | kind: 'rename-identifiers', 55 | transform: s => s.replace(ns, renameContract(ns)), 56 | ...getNodeBounds(ident), 57 | }; 58 | } 59 | } 60 | } 61 | } 62 | 63 | function getTransitiveRenameCandidates( 64 | sourceUnit: SourceUnit, 65 | resolver: ASTResolver, 66 | ): Record { 67 | const ex: Record = {}; 68 | 69 | for (const sym in sourceUnit.exportedSymbols) { 70 | const ids = sourceUnit.exportedSymbols[sym]; 71 | if (ids) { 72 | ex[sym] = ids[0]; 73 | } 74 | } 75 | 76 | for (const imp of findAll('ImportDirective', sourceUnit)) { 77 | const referencedSourceUnit = resolver.resolveNode('SourceUnit', imp.sourceUnit); 78 | const subexports = getTransitiveRenameCandidates(referencedSourceUnit, resolver); 79 | if (imp.symbolAliases.length === 0) { 80 | Object.assign(ex, subexports); 81 | } else { 82 | for (const { foreign, local } of imp.symbolAliases) { 83 | if (local == null || foreign.name === local) { 84 | ex[foreign.name] = subexports[foreign.name]; 85 | } 86 | } 87 | } 88 | } 89 | 90 | return ex; 91 | } 92 | -------------------------------------------------------------------------------- /src/transformations/rename-inheritdoc.ts: -------------------------------------------------------------------------------- 1 | import { SourceUnit } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | 4 | import { renameContract } from '../rename'; 5 | import { getNodeBounds } from '../solc/ast-utils'; 6 | import { Transformation } from './type'; 7 | import { TransformerTools } from '../transform'; 8 | import { matchBuffer } from '../utils/match'; 9 | 10 | export function* renameInheritdoc( 11 | sourceUnit: SourceUnit, 12 | { readOriginal, getData, resolver }: TransformerTools, 13 | ): Generator { 14 | for (const contract of findAll('ContractDefinition', sourceUnit)) { 15 | const baseContracts = contract.linearizedBaseContracts.map(i => resolver.resolveContract(i)); 16 | 17 | for (const doc of findAll('StructuredDocumentation', contract)) { 18 | const bounds = getNodeBounds(doc); 19 | const re = /(@inheritdoc\s+)([a-zA-Z0-9$_]+)/; 20 | const match = matchBuffer(readOriginal(doc, 'buffer'), re); 21 | 22 | if (match) { 23 | yield { 24 | start: bounds.start + match.start + match.captureLengths[0], 25 | length: match.captureLengths[1], 26 | kind: 'rename-inheritdoc', 27 | transform: source => { 28 | const ref = baseContracts.find(c => c?.canonicalName == source); 29 | return ref !== undefined && getData(ref).importFromPeer !== undefined 30 | ? source 31 | : source.replace(/[a-zA-Z0-9$_]+$/, renameContract); 32 | }, 33 | }; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/transformations/transform-constructor.ts: -------------------------------------------------------------------------------- 1 | import { ModifierInvocation, SourceUnit } from 'solidity-ast'; 2 | 3 | import { getConstructor, getNodeBounds } from '../solc/ast-utils'; 4 | import { Transformation, TransformHelper } from './type'; 5 | import { buildSuperCallsForChain } from './utils/build-super-calls-for-chain'; 6 | import { findAll } from 'solidity-ast/utils'; 7 | import { FunctionDefinition, Identifier } from 'solidity-ast'; 8 | import { TransformerTools } from '../transform'; 9 | import { newFunctionPosition } from './utils/new-function-position'; 10 | import { formatLines } from './utils/format-lines'; 11 | import { hasConstructorOverride } from '../utils/upgrades-overrides'; 12 | import { getInitializerItems } from './utils/get-initializer-items'; 13 | import { parseNewExpression } from '../utils/new-expression'; 14 | import { getNamespaceStructName } from './add-namespace-struct'; 15 | import { ASTResolver } from '../ast-resolver'; 16 | import { isStorageVariable } from './utils/is-storage-variable'; 17 | 18 | function getArgsList(constructor: FunctionDefinition, helper: TransformHelper): string { 19 | return helper.read(constructor.parameters).replace(/^\((.*)\)$/s, '$1'); 20 | } 21 | 22 | // Removes parameters unused by the constructor's body 23 | function getUnchainedArguments( 24 | constructor: FunctionDefinition, 25 | helper: TransformHelper, 26 | modifiers: ModifierInvocation[], 27 | ): string { 28 | // Get declared parameters information 29 | const parameters = constructor.parameters.parameters; 30 | // Gets all arguments arrays and concat them into one array 31 | const usedOnModifiers = modifiers.flatMap((m: ModifierInvocation) => [ 32 | ...findAll('Identifier', m), 33 | ]); 34 | 35 | if (!parameters?.length) { 36 | return ''; 37 | } else { 38 | let result: string = getArgsList(constructor, helper); 39 | const usedIds = new Set( 40 | [...findAll('Identifier', constructor.body!)].map(i => i.referencedDeclaration), 41 | ); 42 | 43 | for (const p of parameters) { 44 | // Check if parameter is used on the body or the modifiers 45 | if ( 46 | !usedIds.has(p.id) && 47 | !usedOnModifiers.some((m: Identifier) => m!.referencedDeclaration! === p.id) 48 | ) { 49 | // Remove unused parameter 50 | result = result.replace(/\s+[a-z0-9$_]+/gi, m => (m.trim() === p.name ? '' : m)); 51 | } 52 | } 53 | return result; 54 | } 55 | } 56 | 57 | // Runs after transformConstructor to remove the constructor keyword and parameters until the first `{`. For example 58 | // This: constructor(uint a) /* modifiers */ public { function __Name_init(uint a) /* modifiers */ 59 | // Results in: function __Name_init(uint a) /* modifiers */ 60 | export function* removeLeftoverConstructorHead(sourceUnit: SourceUnit): Generator { 61 | for (const contractNode of findAll('ContractDefinition', sourceUnit)) { 62 | if (hasConstructorOverride(contractNode)) { 63 | continue; 64 | } 65 | 66 | const constructorNode = getConstructor(contractNode); 67 | if (constructorNode) { 68 | const { start: ctorStart } = getNodeBounds(constructorNode); 69 | const { start: bodyStart } = getNodeBounds(constructorNode.body!); 70 | yield { 71 | start: ctorStart, 72 | length: bodyStart + 1 - ctorStart, 73 | kind: 'remove-leftover-constructor', 74 | text: '', 75 | }; 76 | } 77 | } 78 | } 79 | 80 | // Inserts the init and unchained function declarations before the constructor first`{`, 81 | // and must run removeLeftoverConstructorHead after. For example 82 | // This: constructor(uint a) /* modifiers */ public 83 | // Results in: constructor(uint a) /* modifiers */ public { function __Name_init(uint a) /* modifiers */ 84 | export function transformConstructor(isNamespaced?: (source: string) => boolean) { 85 | return function* (sourceUnit: SourceUnit, tools: TransformerTools): Generator { 86 | const { resolver, getData } = tools; 87 | 88 | const useNamespaces = isNamespaced?.(sourceUnit.absolutePath) ?? false; 89 | 90 | for (const contractNode of findAll('ContractDefinition', sourceUnit)) { 91 | if (contractNode.contractKind !== 'contract' || hasConstructorOverride(contractNode)) { 92 | continue; 93 | } 94 | 95 | const { name } = contractNode; 96 | const { 97 | constructorNode, 98 | varInitNodes, 99 | modifiers, 100 | emptyUnchained: emptyConstructor, 101 | } = getInitializerItems(contractNode, resolver); 102 | 103 | const namespace = getNamespaceStructName(name); 104 | const constructorUsesStorage = 105 | constructorNode !== undefined && usesStorageVariables(constructorNode, resolver); 106 | 107 | const initializer = ( 108 | helper: TransformHelper, 109 | argsList = '', 110 | unchainedArgsList = '', 111 | argNames: string[] = [], 112 | ) => [ 113 | `function __${name}_init(${argsList}) internal onlyInitializing {`, 114 | buildSuperCallsForChain(contractNode, tools, helper), 115 | emptyConstructor ? [] : [`__${name}_init_unchained(${argNames.join(', ')});`], 116 | `}`, 117 | ``, 118 | [ 119 | `function`, 120 | `__${name}_init_unchained(${unchainedArgsList})`, 121 | `internal onlyInitializing`, 122 | ...modifiers.map(m => helper.read(m)), 123 | `{`, 124 | ].join(' '), 125 | 126 | // To correctly place the namespace variable before variable initilaization 127 | // nodes, for the constructor it must be emitted here rather than in addNamespaceStruct 128 | useNamespaces && (varInitNodes.length > 0 || constructorUsesStorage) 129 | ? [`${namespace} storage $ = _get${namespace}();`] 130 | : [], 131 | 132 | varInitNodes.flatMap(v => { 133 | const prefix = useNamespaces ? '$.' : ''; 134 | const newExpr = parseNewExpression(v.value!); 135 | if (!newExpr) { 136 | return [`${prefix}${v.name} = ${helper.read(v.value!)};`]; 137 | } else { 138 | const { typeName, newCall, initializeCall } = newExpr; 139 | const createdContract = resolver.resolveContract(typeName.referencedDeclaration); 140 | if (createdContract) { 141 | getData(createdContract).isUsedInNewStatement = true; 142 | } 143 | return [ 144 | `${prefix}${v.name} = ${newCall(helper)};`, 145 | `${initializeCall(v.name, helper)};`, 146 | ]; 147 | } 148 | }), 149 | `}`, 150 | ]; 151 | 152 | if (constructorNode) { 153 | const { start: bodyStart } = getNodeBounds(constructorNode.body!); 154 | const argNames = constructorNode.parameters.parameters.map(p => p.name); 155 | 156 | yield { 157 | start: bodyStart + 1, 158 | length: 0, 159 | kind: 'transform-constructor', 160 | transform: (_, helper) => { 161 | const argsList = getArgsList(constructorNode, helper); 162 | const unchainedArgsList = getUnchainedArguments(constructorNode, helper, modifiers); 163 | 164 | return formatLines( 165 | 1, 166 | initializer(helper, argsList, unchainedArgsList, argNames).slice(0, -1), 167 | ).trim(); 168 | }, 169 | }; 170 | } else { 171 | const start = newFunctionPosition(contractNode, tools); 172 | 173 | yield { 174 | start, 175 | length: 0, 176 | kind: 'transform-constructor', 177 | transform: (source, helper) => formatLines(1, initializer(helper)), 178 | }; 179 | } 180 | } 181 | }; 182 | } 183 | 184 | function usesStorageVariables(fnDef: FunctionDefinition, resolver: ASTResolver): boolean { 185 | if (fnDef.body) { 186 | for (const ref of findAll('Identifier', fnDef.body)) { 187 | const varDecl = resolver.tryResolveNode('VariableDeclaration', ref.referencedDeclaration!); 188 | if (varDecl && isStorageVariable(varDecl, resolver)) { 189 | return true; 190 | } 191 | } 192 | } 193 | return false; 194 | } 195 | -------------------------------------------------------------------------------- /src/transformations/type.ts: -------------------------------------------------------------------------------- 1 | export interface TransformHelper { 2 | read(node: WithSrc): string; 3 | } 4 | 5 | export interface WithSrc { 6 | src: string; 7 | } 8 | 9 | export interface Bounds { 10 | start: number; 11 | length: number; 12 | } 13 | 14 | export interface TransformationText extends Bounds { 15 | kind: string; 16 | text: string; 17 | } 18 | 19 | export interface TransformationFunction extends Bounds { 20 | kind: string; 21 | transform: (source: string, helper: TransformHelper) => string; 22 | } 23 | 24 | export type Transformation = TransformationText | TransformationFunction; 25 | -------------------------------------------------------------------------------- /src/transformations/utils/build-pulic-initialize.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | import { TransformerTools } from '../../transform'; 3 | import { formatLines } from './format-lines'; 4 | import { getConstructor } from '../../solc/ast-utils'; 5 | 6 | export function buildPublicInitialize( 7 | contract: ContractDefinition, 8 | tools: TransformerTools, 9 | ): string { 10 | const ctor = getConstructor(contract); 11 | 12 | let args = '()'; 13 | let argNames = ''; 14 | if (ctor) { 15 | args = tools.readOriginal(ctor.parameters); 16 | argNames = ctor.parameters.parameters.map(p => p.name).join(', '); 17 | } 18 | 19 | return formatLines(1, [ 20 | `function initialize${args} public virtual initializer {`, 21 | [`__${contract.name}_init(${argNames});`], 22 | '}', 23 | ]); 24 | } 25 | -------------------------------------------------------------------------------- /src/transformations/utils/build-super-calls-for-chain.ts: -------------------------------------------------------------------------------- 1 | import { flatten, keyBy } from 'lodash'; 2 | 3 | import { getConstructor } from '../../solc/ast-utils'; 4 | import { ContractDefinition, Expression, VariableDeclaration } from 'solidity-ast'; 5 | import { Node } from 'solidity-ast/node'; 6 | import { TransformHelper } from '../type'; 7 | import { TransformerTools } from '../../transform'; 8 | import { hasConstructorOverride } from '../../utils/upgrades-overrides'; 9 | import { getInitializerItems } from './get-initializer-items'; 10 | import { findAll } from 'solidity-ast/utils'; 11 | 12 | // builds an __init call with given arguments, for example 13 | // ERC20DetailedUpgradeable.__init(false, "Gold", "GLD", 18) 14 | function buildSuperCall(args: Node[], name: string, helper: TransformHelper): string { 15 | let superCall = `__${name}_init_unchained(`; 16 | if (args && args.length) { 17 | superCall += args.map(arg => helper.read(arg)).join(', '); 18 | } 19 | return superCall + ');'; 20 | } 21 | 22 | // builds all the __init calls a given contract, for example 23 | // ContextUpgradeable.__init(false); 24 | // ERC20DetailedUpgradeable.__init(false, 'Gold', 'GLD', 18); 25 | export function buildSuperCallsForChain( 26 | contractNode: ContractDefinition, 27 | { resolver }: TransformerTools, 28 | helper: TransformHelper, 29 | ): string[] { 30 | // first we get the linearized inheritance chain of contracts, excluding the 31 | // contract we're currently looking at 32 | const chain = contractNode.linearizedBaseContracts.map(baseId => { 33 | const base = resolver.resolveContract(baseId); 34 | if (base === undefined) { 35 | throw new Error(`Could not resolve ast id ${baseId}`); 36 | } 37 | return base; 38 | }); 39 | 40 | // we will need their ast ids for quick lookup 41 | const chainIds = new Set(chain.map(c => c.id)); 42 | 43 | // now we gather all constructor calls taken from the two possible sources 44 | // 1) "modifiers" on parent constructors, and 45 | // 2) arguments in the inheritance list (contract X is Y, Z(arg1, arg2) ...) 46 | // since the contract was compiled successfully, we are guaranteed that each base contract 47 | // will show up in at most one of these two places across all contracts in the chain (can also be zero) 48 | const ctorCalls = keyBy( 49 | flatten( 50 | chain.map(parentNode => { 51 | const res = []; 52 | const constructorNode = getConstructor(parentNode); 53 | if (constructorNode) { 54 | for (const call of constructorNode.modifiers) { 55 | // we only care about modifiers that reference base contracts 56 | const { referencedDeclaration } = call.modifierName; 57 | if ( 58 | referencedDeclaration != null && 59 | chainIds.has(referencedDeclaration) && 60 | call.arguments != null && 61 | call.arguments.length > 0 62 | ) { 63 | res.push({ call }); 64 | } 65 | } 66 | } 67 | for (const call of parentNode.baseContracts) { 68 | if (call.arguments != null && call.arguments.length > 0) { 69 | res.push({ call }); 70 | } 71 | } 72 | return res; 73 | }), 74 | ), 75 | mod => { 76 | if (mod.call.nodeType === 'ModifierInvocation') { 77 | if (mod.call.modifierName.referencedDeclaration == null) { 78 | throw new Error('Missing referencedDeclaration field'); 79 | } 80 | return mod.call.modifierName.referencedDeclaration; 81 | } else { 82 | return mod.call.baseName.referencedDeclaration; 83 | } 84 | }, 85 | ); 86 | 87 | const invalidReference = new Set(); 88 | const notInitializable = new Set(); 89 | const markNotInitializable = (parentNode: ContractDefinition) => { 90 | const parameters = getConstructor(parentNode)?.parameters?.parameters ?? []; 91 | for (const { id } of parameters) { 92 | invalidReference.add(id); 93 | } 94 | notInitializable.add(parentNode.id); 95 | }; 96 | 97 | const argsValues = new Map(); 98 | const parentArgsValues = new Map(); 99 | 100 | for (const parentNode of chain) { 101 | if (parentNode === contractNode) { 102 | continue; 103 | } 104 | 105 | const ctorCallArgs = ctorCalls[parentNode.id]?.call?.arguments; 106 | 107 | if (!ctorCallArgs) { 108 | // We don't have arguments for this parent, but it may be implicitly constructed (has zero args) 109 | if (isImplicitlyConstructed(parentNode)) { 110 | parentArgsValues.set(parentNode, []); 111 | } else { 112 | // If a parent is not initializable, we assume its parents might be initializable either, 113 | // because we may not have their constructor arguments. So we save the arguments in case 114 | // other parent reference it. 115 | markNotInitializable(parentNode); 116 | } 117 | } else { 118 | // We have arguments for this parent constructor, but they may include references to the constructor parameters of 119 | // "intermediate parents". We check all of these arguments for such references, and make sure they work with the 120 | // variables in scope. 121 | 122 | const parameters = getConstructor(parentNode)!.parameters.parameters; 123 | 124 | const parentArgs = ctorCallArgs.map((arg, index) => { 125 | const param = parameters[index]; 126 | 127 | if (arg.nodeType === 'Identifier') { 128 | // We have something like `constructor(uint x) Parent(x)`. 129 | // We have to get the value associated to this "source param" `uint x`, if any. 130 | const sourceParam = resolver.resolveNode( 131 | 'VariableDeclaration', 132 | arg.referencedDeclaration!, 133 | ); 134 | const sourceValue = argsValues.get(sourceParam); 135 | 136 | if (invalidReference.has(arg.referencedDeclaration!)) { 137 | // This parentNode is the parent of an uninitializable contract and uses a parameter that won't be in the context. 138 | markNotInitializable(parentNode); 139 | } else if (sourceValue) { 140 | if (sourceValue.nodeType === 'Literal' || sourceValue.nodeType === 'Identifier') { 141 | // If the source value is a literal or another identifier, we use it as the argument. 142 | arg = argsValues.get(sourceParam)!; 143 | } else { 144 | // If the source value is some other expression, this would be the second time it's used and we 145 | // reject this as it may have side effects. 146 | throw new Error( 147 | `Can't transpile non-trivial expression in parent constructor argument (${helper.read( 148 | sourceValue, 149 | )})`, 150 | ); 151 | } 152 | } 153 | } else { 154 | // We have something like `constructor(...) Parent()` where the expression is not a simple identifier. 155 | // We will only allow this expression if it is correct in the new context without any changes. 156 | const identifiers = [...findAll('Identifier', arg)]; 157 | for (const id of identifiers) { 158 | const sourceParam = resolver.tryResolveNode( 159 | 'VariableDeclaration', 160 | id.referencedDeclaration!, 161 | ); 162 | const sourceValue = sourceParam && argsValues.get(sourceParam); 163 | 164 | if (invalidReference.has(id.referencedDeclaration!)) { 165 | // This parentNode is the parent of an uninitializable contract and uses a parameter that won't be in the context. 166 | markNotInitializable(parentNode); 167 | } else if ( 168 | sourceValue && 169 | (sourceValue.nodeType !== 'Identifier' || sourceValue.name !== id.name) 170 | ) { 171 | // The variable gets its value from a child constructor, and it's not another variable with the same name. 172 | throw new Error( 173 | `Can't transpile non-trivial expression in parent constructor argument (${helper.read( 174 | arg, 175 | )})`, 176 | ); 177 | } 178 | } 179 | } 180 | argsValues.set(param, arg); 181 | return arg; 182 | }); 183 | 184 | parentArgsValues.set(parentNode, parentArgs); 185 | } 186 | } 187 | 188 | // once we have gathered all constructor calls for each parent, we linearize 189 | // them according to chain. 190 | const linearizedCtorCalls: string[] = []; 191 | 192 | chain.reverse(); 193 | 194 | for (const parentNode of chain) { 195 | if ( 196 | parentNode === contractNode || 197 | hasConstructorOverride(parentNode) || 198 | parentNode.contractKind === 'interface' || 199 | notInitializable.has(parentNode.id) 200 | ) { 201 | continue; 202 | } 203 | 204 | const args = parentArgsValues.get(parentNode) ?? []; 205 | 206 | if (args.length || !getInitializerItems(parentNode, resolver).emptyUnchained) { 207 | // TODO: we have to use the name in the lexical context and not necessarily 208 | // the original contract name 209 | linearizedCtorCalls.push(buildSuperCall(args, parentNode.name, helper)); 210 | } 211 | } 212 | 213 | return linearizedCtorCalls; 214 | } 215 | 216 | function isImplicitlyConstructed(contract: ContractDefinition): boolean { 217 | const ctor = getConstructor(contract); 218 | 219 | return ( 220 | contract.contractKind === 'contract' && 221 | (ctor == undefined || ctor.parameters.parameters.length === 0) 222 | ); 223 | } 224 | -------------------------------------------------------------------------------- /src/transformations/utils/contract-start-position.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | 3 | import { TransformerTools } from '../../transform'; 4 | import { getNodeBounds } from '../../solc/ast-utils'; 5 | import { matchBufferFrom } from '../../utils/match'; 6 | 7 | export function contractStartPosition( 8 | contract: ContractDefinition, 9 | { readOriginal }: TransformerTools, 10 | ): number { 11 | const offset = getNodeBounds(contract).start; 12 | let searchStart = 0; 13 | 14 | if (contract.baseContracts.length > 0) { 15 | const [lastParent] = contract.baseContracts.slice(-1); 16 | const pb = getNodeBounds(lastParent); 17 | searchStart = pb.start + pb.length - offset; 18 | } 19 | 20 | const brace = matchBufferFrom(readOriginal(contract, 'buffer'), /\{\n?/, searchStart); 21 | 22 | if (!brace) { 23 | throw new Error(`Can't find start of contract ${contract.name}`); 24 | } 25 | 26 | return offset + brace.start + brace.length; 27 | } 28 | -------------------------------------------------------------------------------- /src/transformations/utils/format-lines.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash'; 2 | 3 | export type Line = string | Line[]; 4 | 5 | export function formatLines(indent: number, lines: Line[]): string { 6 | function indentEach(indent: number, lines: Line[]): Line[] { 7 | return lines.map(line => 8 | Array.isArray(line) ? indentEach(indent + 1, line) : line && ' '.repeat(indent) + line, 9 | ); 10 | } 11 | return flatten(indentEach(indent, lines)).join('\n') + '\n'; 12 | } 13 | -------------------------------------------------------------------------------- /src/transformations/utils/get-initializer-items.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | import { findAll } from 'solidity-ast/utils'; 3 | import { ASTResolver } from '../../ast-resolver'; 4 | import { getConstructor } from '../../solc/ast-utils'; 5 | import { hasOverride } from '../../utils/upgrades-overrides'; 6 | import { isStorageVariable } from './is-storage-variable'; 7 | 8 | export function getInitializerItems(contract: ContractDefinition, resolver: ASTResolver) { 9 | const constructorNode = getConstructor(contract); 10 | 11 | const varInitNodes = [...findAll('VariableDeclaration', contract)].filter( 12 | v => 13 | v.value && 14 | isStorageVariable(v, resolver) && 15 | !hasOverride(v, 'state-variable-assignment', resolver), 16 | ); 17 | 18 | const modifiers = 19 | constructorNode?.modifiers.filter( 20 | call => !contract.linearizedBaseContracts?.includes(call.modifierName.referencedDeclaration!), 21 | ) ?? []; 22 | 23 | const emptyUnchained = 24 | !constructorNode?.body?.statements?.length && varInitNodes.length == 0 && modifiers.length == 0; 25 | 26 | return { 27 | constructorNode, 28 | varInitNodes, 29 | modifiers, 30 | emptyUnchained, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/transformations/utils/is-storage-variable.ts: -------------------------------------------------------------------------------- 1 | import { VariableDeclaration } from 'solidity-ast'; 2 | import { ASTResolver } from '../../ast-resolver'; 3 | import { hasOverride } from '../../utils/upgrades-overrides'; 4 | 5 | export function isStorageVariable(varDecl: VariableDeclaration, resolver: ASTResolver): boolean { 6 | if (!varDecl.stateVariable || varDecl.constant) { 7 | return false; 8 | } else { 9 | switch (varDecl.mutability) { 10 | case 'constant': 11 | // It's unclear if `varDecl.constant` and `varDecl.mutability === 'constant'` are equivalent so we use both just in case. 12 | return false; 13 | case 'immutable': 14 | return !hasOverride(varDecl, 'state-variable-immutable', resolver); 15 | default: 16 | return true; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/transformations/utils/new-function-position.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | 3 | import { TransformerTools } from '../../transform'; 4 | import { isNodeType } from 'solidity-ast/utils'; 5 | import { contractStartPosition } from './contract-start-position'; 6 | 7 | export function newFunctionPosition(contract: ContractDefinition, tools: TransformerTools): number { 8 | const firstFunctionIndex = contract.nodes.findIndex(isNodeType('FunctionDefinition')); 9 | 10 | if (firstFunctionIndex <= 0) { 11 | return contractStartPosition(contract, tools); 12 | } else { 13 | const prevNode = contract.nodes[firstFunctionIndex - 1]; 14 | // VariableDeclaration node bounds don't include the semicolon, so we look for it 15 | // in case prevNode is that type of node 16 | const m = tools.matchOriginalAfter(prevNode, /(\s*;)?([ \t\v\f]*[\n\r])*/)!; 17 | return m.start + m.length; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/erc7201.ts: -------------------------------------------------------------------------------- 1 | import { keccak256 } from 'ethereum-cryptography/keccak.js'; 2 | import { utf8ToBytes, hexToBytes, bytesToHex } from 'ethereum-cryptography/utils.js'; 3 | 4 | export function erc7201Location(id: string): string { 5 | const a = keccak256(utf8ToBytes(id)); 6 | const b = BigInt('0x' + bytesToHex(a)) - 1n; 7 | const c = hexToBytes(b.toString(16).padStart(64, '0')); 8 | const d = keccak256(c); 9 | d[31] = 0; 10 | return '0x' + bytesToHex(d); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/execall.ts: -------------------------------------------------------------------------------- 1 | export function* execall(re: RegExp, text: string) { 2 | re = new RegExp(re, re.flags + (re.sticky ? '' : 'y')); 3 | while (true) { 4 | const match = re.exec(text); 5 | if (match && match[0] !== '') { 6 | yield match; 7 | } else { 8 | break; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/match.ts: -------------------------------------------------------------------------------- 1 | export interface ByteMatch { 2 | start: number; 3 | length: number; 4 | captureLengths: number[]; 5 | } 6 | 7 | function withFlags(re: RegExp, add: string, remove = '') { 8 | const flags = new Set(...re.flags); 9 | for (const f of add) { 10 | flags.add(f); 11 | } 12 | for (const f of remove) { 13 | flags.delete(f); 14 | } 15 | return new RegExp(re, [...flags].join('')); 16 | } 17 | 18 | function matchWithFlags( 19 | buf: Buffer, 20 | re: RegExp, 21 | index: number, 22 | flags: string, 23 | ): ByteMatch | undefined { 24 | const str = buf.slice(index).toString('utf8'); 25 | const m = withFlags(re, flags).exec(str); 26 | if (m) { 27 | const start = index + Buffer.from(str.slice(0, m.index), 'utf8').length; 28 | const length = Buffer.from(str.slice(m.index, m.index + m[0].length), 'utf8').length; 29 | const captureLengths = m 30 | .slice(1) 31 | .map(c => (c === undefined ? c : Buffer.from(c, 'utf8').length)); 32 | return { start, length, captureLengths }; 33 | } 34 | } 35 | 36 | export function matchBuffer(buf: Buffer, re: RegExp): ByteMatch | undefined { 37 | return matchWithFlags(buf, re, 0, ''); 38 | } 39 | 40 | export function matchBufferFrom(buf: Buffer, re: RegExp, index: number): ByteMatch | undefined { 41 | return matchWithFlags(buf, re, index, 'g'); 42 | } 43 | 44 | export function matchBufferAt(buf: Buffer, re: RegExp, index: number): ByteMatch | undefined { 45 | return matchWithFlags(buf, re, index, 'y'); 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/matcher.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { matcher } from './matcher'; 4 | 5 | test('no patterns', t => { 6 | t.is(matcher([])('a'), undefined); 7 | }); 8 | 9 | test('no negative patterns', t => { 10 | t.true(matcher(['a*'])('a')); 11 | t.is(matcher(['b*'])('a'), undefined); 12 | }); 13 | 14 | test('excluded by a negative pattern', t => { 15 | t.false(matcher(['a*', '!a'])('a')); 16 | }); 17 | 18 | test('no positive patterns', t => { 19 | t.false(matcher(['!a'])('a')); 20 | t.is(matcher(['!b'])('a'), undefined); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/matcher.ts: -------------------------------------------------------------------------------- 1 | import { Minimatch } from 'minimatch'; 2 | 3 | export function matcher(patterns: string[]): (path: string) => boolean | undefined { 4 | const positivePatterns: Minimatch[] = []; 5 | const negativePatterns: Minimatch[] = []; 6 | 7 | for (const pat of patterns) { 8 | const m = new Minimatch(pat); 9 | if (m.negate) { 10 | negativePatterns.push(m); 11 | } else { 12 | positivePatterns.push(m); 13 | } 14 | } 15 | 16 | return path => { 17 | if (negativePatterns.some(m => !m.match(path))) { 18 | return false; 19 | } else if (positivePatterns.some(m => m.match(path))) { 20 | return true; 21 | } else { 22 | return undefined; 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/natspec.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition, StructuredDocumentation } from 'solidity-ast'; 2 | import { execall } from './execall'; 3 | 4 | interface NatspecTag { 5 | title: string; 6 | tag: string; 7 | args: string; 8 | } 9 | 10 | export function* extractNatspec(node: { 11 | documentation?: string | StructuredDocumentation | null; 12 | }): Generator { 13 | const doc = 14 | typeof node.documentation === 'string' ? node.documentation : node.documentation?.text ?? ''; 15 | 16 | for (const { groups } of execall( 17 | /^\s*(?:@(?\w+)(?::(?<tag>[a-z][a-z-]*))?)?(?: (?<args>(?:(?!^\s@\w+)[^])*))?/m, 18 | doc, 19 | )) { 20 | if (groups) { 21 | yield { 22 | title: groups.title ?? '', 23 | tag: groups.tag ?? '', 24 | args: groups.args ?? '', 25 | }; 26 | } 27 | } 28 | } 29 | 30 | export function extractContractStorageSize(contract: ContractDefinition): number | undefined { 31 | let targetSlots; 32 | for (const entry of extractNatspec(contract)) { 33 | if (entry.title === 'custom' && entry.tag === 'storage-size') { 34 | targetSlots = parseInt(entry.args); 35 | } 36 | } 37 | return targetSlots; 38 | } 39 | 40 | export function extractContractStateless(contract: ContractDefinition): boolean { 41 | for (const entry of extractNatspec(contract)) { 42 | if (entry.title === 'custom' && entry.tag === 'stateless') { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/new-expression.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from 'solidity-ast'; 2 | import { TransformHelper } from '../transformations/type'; 3 | 4 | export function parseNewExpression(expr: Expression) { 5 | let needsCast = false; 6 | 7 | if (expr.nodeType === 'FunctionCall' && expr.kind === 'typeConversion') { 8 | if ( 9 | expr.expression.nodeType === 'ElementaryTypeNameExpression' && 10 | expr.expression.typeName.name === 'address' 11 | ) { 12 | expr = expr.arguments[0]; 13 | needsCast = true; 14 | } 15 | } 16 | 17 | if (expr.nodeType === 'FunctionCall' && expr.expression.nodeType === 'NewExpression') { 18 | const functionCall = expr; 19 | const { arguments: args } = expr; 20 | const { typeName } = expr.expression; 21 | 22 | if (typeName.nodeType !== 'UserDefinedTypeName') { 23 | return undefined; 24 | } 25 | 26 | const initializeCall = (varName: string, helper: TransformHelper) => 27 | [ 28 | ...(needsCast ? [helper.read(typeName), '(', varName, ')'] : varName), 29 | '.initialize', 30 | '(', 31 | functionCall.arguments.map(a => helper.read(a)).join(', '), 32 | ')', 33 | ].join(''); 34 | 35 | const newCall = (helper: TransformHelper) => { 36 | const n = `new ${helper.read(typeName)}()`; 37 | return needsCast ? `address(${n})` : n; 38 | }; 39 | 40 | return { typeName, args, initializeCall, newCall }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/parse-type-id.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | // This module parses type identifiers. 4 | 5 | export interface ParsedTypeId { 6 | id: string; 7 | head: string; 8 | args?: ParsedTypeId[]; 9 | tail?: string; 10 | rets?: ParsedTypeId[]; 11 | } 12 | 13 | // The examples below illustrate the meaning of these properties. 14 | 15 | // 1) id = t_struct(MyStruct)storage 16 | // └──────┘ └──────┘ └─────┘ 17 | // head args[0] tail 18 | // rets = undefined 19 | 20 | // 2) id = t_function_internal_nonpayable(t_uint256,t_uint256)returns(t_address) 21 | // └────────────────────────────┘ └───────┘ └───────┘ └───────┘ 22 | // head args[0] args[1] rets[0] 23 | // tail = undefined 24 | 25 | // 3) id = t_uint256 26 | // └───────┘ 27 | // head 28 | // args = undefined 29 | // tail = undefined 30 | // rets = undefined 31 | 32 | export function parseTypeId(id: string): ParsedTypeId { 33 | const matcher = new StatefulGlobalMatcher(id); 34 | 35 | function parseList(): ParsedTypeId[] { 36 | const args = []; 37 | 38 | let i = matcher.index; 39 | let delim; 40 | 41 | while ((delim = matcher.match(/[(),]/))) { 42 | if (delim[0] === '(') { 43 | let depth = 1; 44 | while (depth > 0) { 45 | const paren = matcher.match(/[()]/)?.[0]; 46 | if (paren === '(') { 47 | depth += 1; 48 | } else if (paren === ')') { 49 | depth -= 1; 50 | } else { 51 | throw new Error(`Malformed type id ${id}`); 52 | } 53 | } 54 | } else { 55 | const subtype = id.slice(i, delim.index); 56 | if (subtype) { 57 | args.push(parseTypeId(subtype)); 58 | } 59 | i = delim.index + 1; 60 | if (delim[0] === ')') { 61 | break; 62 | } 63 | } 64 | } 65 | 66 | return args; 67 | } 68 | 69 | const openArgs = matcher.match(/\(/); 70 | const head = id.slice(0, openArgs?.index); 71 | let args, tail, rets; 72 | 73 | if (openArgs) { 74 | args = parseList(); 75 | 76 | const startTail = matcher.index; 77 | const openRet = matcher.match(/\(/); 78 | 79 | if (!openRet) { 80 | tail = id.slice(startTail) || undefined; 81 | } else { 82 | assert(id.slice(startTail, openRet.index) === 'returns', `Malformed type id ${id}`); 83 | rets = parseList(); 84 | } 85 | } 86 | 87 | return { id, head, args, tail, rets }; 88 | } 89 | 90 | class StatefulGlobalMatcher { 91 | index = 0; 92 | 93 | constructor(private readonly text: string) {} 94 | 95 | match(re: RegExp | string): RegExpExecArray | null { 96 | const gre = new RegExp(re, 'g'); 97 | gre.lastIndex = this.index; 98 | const match = gre.exec(this.text); 99 | if (match !== null) { 100 | this.index = gre.lastIndex; 101 | } 102 | return match; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/relative-path.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | // does the same as path.relative but the result always begins with . 4 | export function relativePath(from: string, to: string): string { 5 | const rel = path.relative(from, to); 6 | if (rel.startsWith('.')) { 7 | return rel; 8 | } else { 9 | return './' + rel; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/type-id.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | // Type Identifiers in the AST are for some reason encoded so that they don't 4 | // contain parentheses or commas, which have been substituted as follows: 5 | // ( -> $_ 6 | // ) -> _$ 7 | // , -> _$_ 8 | // This is particularly hard to decode because it is not a prefix-free code. 9 | // Thus, the following regex has to perform a lookahead to make sure it gets 10 | // the substitution right. 11 | export function decodeTypeIdentifier(typeIdentifier: string): string { 12 | return typeIdentifier.replace(/(\$_|_\$_|_\$)(?=(\$_|_\$_|_\$)*([^_$]|$))/g, m => { 13 | switch (m) { 14 | case '$_': 15 | return '('; 16 | case '_$': 17 | return ')'; 18 | case '_$_': 19 | return ','; 20 | default: 21 | throw new Error('Unreachable'); 22 | } 23 | }); 24 | } 25 | 26 | // Some Type Identifiers contain a _storage_ptr suffix, but the _ptr part 27 | // appears in some places and not others. We remove it to get consistent type 28 | // ids from the different places in the AST. 29 | export function normalizeTypeIdentifier(typeIdentifier: string): string { 30 | return decodeTypeIdentifier(typeIdentifier).replace(/_storage_ptr\b/g, '_storage'); 31 | } 32 | 33 | // Type Identifiers contain AST id numbers, which makes them sensitive to 34 | // unrelated changes in the source code. This function stabilizes a type 35 | // identifier by removing all AST ids. 36 | export function stabilizeTypeIdentifier(typeIdentifier: string): string { 37 | let decoded = decodeTypeIdentifier(typeIdentifier); 38 | const re = /(t_struct|t_enum|t_contract)\(/g; 39 | let match; 40 | while ((match = re.exec(decoded))) { 41 | let i; 42 | let d = 1; 43 | for (i = match.index + match[0].length; d !== 0; i++) { 44 | assert(i < decoded.length, 'index out of bounds'); 45 | const c = decoded[i]; 46 | if (c === '(') { 47 | d += 1; 48 | } else if (c === ')') { 49 | d -= 1; 50 | } 51 | } 52 | const re2 = /\d+_?/y; 53 | re2.lastIndex = i; 54 | decoded = decoded.replace(re2, ''); 55 | } 56 | return decoded; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/upgrades-overrides.ts: -------------------------------------------------------------------------------- 1 | import { ContractDefinition } from 'solidity-ast'; 2 | import { Node } from 'solidity-ast/node'; 3 | import { ASTResolver } from '../ast-resolver'; 4 | import { getConstructor } from '../solc/ast-utils'; 5 | import { execall } from '../utils/execall'; 6 | 7 | const errorKinds = [ 8 | 'state-variable-assignment', 9 | 'state-variable-immutable', 10 | 'external-library-linking', 11 | 'struct-definition', 12 | 'enum-definition', 13 | 'constructor', 14 | 'delegatecall', 15 | 'selfdestruct', 16 | 'missing-public-upgradeto', 17 | ] as const; 18 | 19 | type ValidationErrorKind = (typeof errorKinds)[number]; 20 | 21 | export function hasOverride( 22 | node: Node, 23 | override: ValidationErrorKind, 24 | resolver: ASTResolver, 25 | ): boolean { 26 | const overrides = getOverrides(node, resolver); 27 | return ( 28 | overrides.includes(override) || 29 | (override === 'state-variable-assignment' && 30 | node.nodeType === 'VariableDeclaration' && 31 | node.mutability === 'immutable' && 32 | overrides.includes('state-variable-immutable')) 33 | ); 34 | } 35 | 36 | export function getOverrides(node: Node, resolver: ASTResolver): ValidationErrorKind[] { 37 | const overrides = getOwnOverrides(node); 38 | if ('scope' in node) { 39 | const contract = resolver.resolveContract(node.scope); 40 | if (contract) { 41 | overrides.push(...getOwnOverrides(contract)); 42 | } 43 | } 44 | return overrides; 45 | } 46 | 47 | function getOwnOverrides(node: Node): ValidationErrorKind[] { 48 | if ('documentation' in node) { 49 | const doc = 50 | typeof node.documentation === 'string' ? node.documentation : node.documentation?.text ?? ''; 51 | 52 | const result: string[] = []; 53 | for (const { groups } of execall( 54 | /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s@\w+)[^])*)/m, 55 | doc, 56 | )) { 57 | if (groups && groups.title === 'custom' && groups.tag === 'oz-upgrades-unsafe-allow') { 58 | result.push(...groups.args.split(/\s+/)); 59 | } 60 | } 61 | 62 | result.forEach(arg => { 63 | if (!(errorKinds as readonly string[]).includes(arg)) { 64 | throw new Error(`NatSpec: oz-upgrades-unsafe-allow argument not recognized: ${arg}`); 65 | } 66 | }); 67 | 68 | return result as ValidationErrorKind[]; 69 | } else { 70 | return []; 71 | } 72 | } 73 | 74 | export function hasConstructorOverride(contract: ContractDefinition): boolean { 75 | const ctor = getConstructor(contract); 76 | return ctor 77 | ? [...getOwnOverrides(ctor), ...getOwnOverrides(contract)].includes('constructor') 78 | : false; 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "pretty": true, 5 | "disableSizeLimit": true, 6 | "module": "commonjs", 7 | "moduleResolution": "Node", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "target": "es2022", 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"], 18 | "ts-node": { 19 | "transpileOnly": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------