Skip to content

Commit

Permalink
generate a constructor that invokes all uninitialized parents
Browse files Browse the repository at this point in the history
  • Loading branch information
frangio committed Jun 7, 2021
1 parent b6f5fdb commit d537307
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
20 changes: 20 additions & 0 deletions contracts/Test.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
52 changes: 52 additions & 0 deletions src/core.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();␊
}␊
Expand All @@ -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();␊
}␊
Expand All @@ -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();␊
}␊
}␊
`
Binary file modified src/core.test.ts.snap
Binary file not shown.
71 changes: 65 additions & 6 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hre from 'hardhat';
import path from 'path';
import 'array.prototype.flatmap/auto';

import { SourceUnit, ContractDefinition, FunctionDefinition, VariableDeclaration, StorageLocation } from 'solidity-ast';
import { findAll } from 'solidity-ast/utils';
Expand Down Expand Up @@ -67,9 +68,8 @@ function getExposedContent(ast: SourceUnit, inputPath: string, contractMap: Cont

...Array.from(findAll('ContractDefinition', ast), c => {
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) {
Expand All @@ -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 = [
Expand Down Expand Up @@ -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<string, string>(); // name -> type
const parentArguments = new Map<string, string[]>();

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<T>(value: T): value is NonNullable<T> {
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[] {
Expand Down

0 comments on commit d537307

Please sign in to comment.