From d537307e17c9622e43c87ff2e77d11e6b0402d0f Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 7 Jun 2021 15:46:09 -0300 Subject: [PATCH] generate a constructor that invokes all uninitialized parents --- CHANGELOG.md | 1 + README.md | 28 ++++++++++++++++- contracts/Test.sol | 20 ++++++++++++ src/core.test.ts.md | 52 +++++++++++++++++++++++++++++++ src/core.test.ts.snap | Bin 413 -> 539 bytes src/core.ts | 71 ++++++++++++++++++++++++++++++++++++++---- 6 files changed, 165 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6106b..c37ff76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add support for libraries. - Add support for abstract contracts and interfaces. +- Generate a constructor that invokes all uninitialized parents. ## 0.1.0 diff --git a/README.md b/README.md index df9d2dc..4f72aca 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Note: After setting up for the first time, you may need to recompile with `hardh The plugin will create "exposed" versions of your contracts that will be prefixed with an `X`, and its internal functions will be exposed as external functions with an `x` prefix. +These exposed contracts will be created in a `contracts-exposed` directory. We strongly suggest adding this directory to `.gitignore`. + If you have a contract called `Foo`, with an internal function called `_get`: ```javascript @@ -32,4 +34,28 @@ const foo = Foo.deploy(); await foo.x_get(); ``` -These exposed contracts will be created in a `contracts-exposed` directory. We strongly suggest adding this directory to `.gitignore`. +The plugin will also generate a constructor to initialize your abstract contracts. + +For example, with this set of contracts: + +```solidity +contract A { + constructor(uint a) {} +} +contract B { + constructor(uint b) {} +} +contract C is A, B { + constructor(uint c) A(0) {} +} +``` + +The plugin generates the following exposed version of `C`. Notice how a parameter for `B` was added. + +```solidity +contract XC is C { + constructor(uint256 c, uint256 b) C(c) B(b) {} +} +``` + +Note that if a contract is abstract because it's missing an implementation for a virtual function, the exposed contract will remain abstract too. diff --git a/contracts/Test.sol b/contracts/Test.sol index 7f26d4b..6432cd1 100644 --- a/contracts/Test.sol +++ b/contracts/Test.sol @@ -48,3 +48,23 @@ interface Iface { abstract contract Abs { function _abstract() internal pure virtual returns (uint); } + +contract Concrete is Abs { + function _abstract() internal pure override returns (uint) { + return 42; + } +} + +abstract contract Parent1 { + constructor(uint x) {} + + function _testParent1() internal {} +} + +abstract contract Parent2 { + constructor(uint y) {} +} + +abstract contract Child1 is Parent1 {} +abstract contract Child2 is Parent1, Parent2 {} +abstract contract Child3 is Parent1, Parent2, Child2 {} diff --git a/src/core.test.ts.md b/src/core.test.ts.md index f15fe6a..675a5d1 100644 --- a/src/core.test.ts.md +++ b/src/core.test.ts.md @@ -15,6 +15,8 @@ Generated by [AVA](https://avajs.dev). import "../contracts/Test.sol";␊ ␊ contract XFoo is Foo {␊ + constructor() {}␊ + ␊ function x_testFoo() external pure returns (uint256) {␊ return super._testFoo();␊ }␊ @@ -33,6 +35,8 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ contract XBar is Bar {␊ + constructor() {}␊ + ␊ function x_testBar() external pure returns (uint256) {␊ return super._testBar();␊ }␊ @@ -55,14 +59,62 @@ Generated by [AVA](https://avajs.dev). }␊ ␊ contract XLib {␊ + constructor() {}␊ + ␊ function x_testLib() external pure returns (uint256) {␊ return Lib._testLib();␊ }␊ }␊ ␊ abstract contract XIface is Iface {␊ + constructor() {}␊ }␊ ␊ abstract contract XAbs is Abs {␊ + constructor() {}␊ + }␊ + ␊ + contract XConcrete is Concrete {␊ + constructor() {}␊ + ␊ + function x_abstract() external pure returns (uint256) {␊ + return super._abstract();␊ + }␊ + }␊ + ␊ + contract XParent1 is Parent1 {␊ + constructor(uint256 x) Parent1(x) {}␊ + ␊ + function x_testParent1() external {␊ + return super._testParent1();␊ + }␊ + }␊ + ␊ + contract XParent2 is Parent2 {␊ + constructor(uint256 y) Parent2(y) {}␊ + }␊ + ␊ + contract XChild1 is Child1 {␊ + constructor(uint256 x) Parent1(x) {}␊ + ␊ + function x_testParent1() external {␊ + return super._testParent1();␊ + }␊ + }␊ + ␊ + contract XChild2 is Child2 {␊ + constructor(uint256 y, uint256 x) Parent2(y) Parent1(x) {}␊ + ␊ + function x_testParent1() external {␊ + return super._testParent1();␊ + }␊ + }␊ + ␊ + contract XChild3 is Child3 {␊ + constructor(uint256 y, uint256 x) Parent2(y) Parent1(x) {}␊ + ␊ + function x_testParent1() external {␊ + return super._testParent1();␊ + }␊ }␊ ` diff --git a/src/core.test.ts.snap b/src/core.test.ts.snap index b48ba7094843f7fd633f1ffc68e911810541ffa7..2799d10f4371a7a6ad36fd6383a511cceeb85c0e 100644 GIT binary patch literal 539 zcmV+$0_6QcRzV{yusj;4fQ(RP#* z)jmRx(Gz$BQMZ48FyYSDNjuQ&pA7YSv zK)Y0p;Pm9st+|JF$DzZJ3k5~jtp=PafdeJ0S41jTi;G&2e1)5H&VfoW+nob?=_{#3 z6euoAWw;wV_G>RmQ%2Htno4Qd+)Azjc_K#?OJ^ zj+}s`M6DhUh?(}uAp>r4?2C>xk!H(|OXUsD0!?V$X6F@OBXQqi{L>T*TXTtp{*yd2 zaLMr@+tdW&aw00lncYhibI5s9G?VAxc`2TkgEva>ywYevAKubI*A_ZG?NXf<$(zCD dR{0bjaN2_Z2J+Di`Dj;>e*rN4B35b%000t21PK5D literal 413 zcmV;O0b>3^RzVl+DPIT;SRacB00000000B+ zlFv#)K@`TvLV~;V4Gyk!;V3~al7dPDK`gih?&5kKWxzXgnK@HR_)fh;^a4ej77=aQ z_XN#c6D|A`whB6%GtBwU`3>_OAcUmIZT8_ge}8lL`jUBid>g%|hzmbW$R^oJ&Z#uX zY*f?5BKW7(R-wT|CNwM5J4_(&a;A3SyxFK9)tY{->UpsWdQkvc_IZcn5cc;a z2>K8k#XvDMN@&O#E^vFNod0ZfF|L6&F;jFpp^^wj-aLelii_T2icabfu}G@na@J(j z3Y-aXG0wi1`ah*5lUCDwl-4YEVlXye(3hNDt&jdTcJhwVtigwYat6CJ*I--trNPOB zWq { const isLibrary = c.contractKind === 'library'; - const isAbstract = c.abstract || !c.fullyImplemented; const contractHeader = [`contract X${c.name}`]; - if (isAbstract) { + if (!areFunctionsFullyImplemented(c, contractMap)) { contractHeader.unshift('abstract'); } if (!isLibrary) { @@ -79,6 +79,7 @@ function getExposedContent(ast: SourceUnit, inputPath: string, contractMap: Cont return [ contractHeader.join(' '), spaceBetween( + makeConstructor(c, contractMap), ...getInternalFunctions(c, contractMap).filter(isExternalizable).map(fn => { const args = getFunctionArguments(fn); const header = [ @@ -107,13 +108,71 @@ function getExposedContent(ast: SourceUnit, inputPath: string, contractMap: Cont ) } -interface Argument { - type: string; - name: string; +// Note this is not the same as contract.fullyImplemented, because this does +// not consider that missing constructor calls. +function areFunctionsFullyImplemented(contract: ContractDefinition, contractMap: ContractMap): boolean { + const parents = contract.linearizedBaseContracts.map(id => mustGet(contractMap, id)); + const abstractFunctionIds = new Set(parents.flatMap(p => [...findAll('FunctionDefinition', p)].filter(f => !f.implemented).map(f => f.id))); + for (const p of parents) { + for (const f of findAll('FunctionDefinition', p)) { + for (const b of f.baseFunctions ?? []) { + abstractFunctionIds.delete(b); + } + } + } + return abstractFunctionIds.size === 0; +} + +function makeConstructor(contract: ContractDefinition, contractMap: ContractMap): string[] { + const parents = contract.linearizedBaseContracts.map(id => mustGet(contractMap, id)); + const parentsWithConstructor = parents.filter(c => getConstructor(c)?.parameters.parameters.length); + const initializedParentIds = new Set(parents.flatMap(p => [ + ...p.baseContracts.filter(c => c.arguments?.length).map(c => c.id), + ...getConstructor(p)?.modifiers.map(m => m.modifierName.referencedDeclaration).filter(notNull) ?? [], + ])); + const uninitializedParents = parentsWithConstructor.filter(c => !initializedParentIds.has(c.id)); + + const missingArguments = new Map(); // name -> type + const parentArguments = new Map(); + + for (const c of uninitializedParents) { + const args = []; + for (const a of getConstructor(c)!.parameters.parameters) { + const name = missingArguments.has(a.name) ? `${c.name}_${a.name}` : a.name; + const type = getType(a, 'calldata'); + missingArguments.set(name, type); + args.push(name); + } + parentArguments.set(c.name, args); + } + return [ + [ + `constructor(${[...missingArguments].map(([name, type]) => `${type} ${name}`).join(', ')})`, + ...uninitializedParents.map(p => `${p.name}(${mustGet(parentArguments, p.name).join(', ')})`), + '{}' + ].join(' '), + ]; +} + +function getConstructor(contract: ContractDefinition): FunctionDefinition | undefined { + for (const fnDef of findAll('FunctionDefinition', contract)) { + if (fnDef.kind === 'constructor') { + return fnDef; + } + } +} + +function notNull(value: T): value is NonNullable { + return value != undefined; } function isExternalizable(fnDef: FunctionDefinition): boolean { - return fnDef.implemented && fnDef.parameters.parameters.every(p => p.storageLocation !== 'storage'); + return fnDef.kind !== 'constructor' && fnDef.implemented && fnDef.parameters.parameters.every(p => p.storageLocation !== 'storage'); +} + +interface Argument { + type: string; + name: string; } function getFunctionArguments(fnDef: FunctionDefinition): Argument[] {