Skip to content

Instantly share code, notes, and snippets.

@spalladino
Last active July 25, 2018 19:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spalladino/3f2fae42acc283cf564a9254e321b342 to your computer and use it in GitHub Desktop.
Save spalladino/3f2fae42acc283cf564a9254e321b342 to your computer and use it in GitHub Desktop.
Summary of experiments on initializer contracts

Proxy contract initialization

This gist contains a summary of the experiments around initialization of proxy contracts, comparing the current approach with new ones we have been working on. We used a sample ERC20 and an ERC721 contracts for testing. Most of these tests can be found in the labs repo. Credit for most approaches listed here goes to @frangio.

Initializer functions

Initializer functions is the current approach used for zOS. All contracts must be modified to have an initializer function, which is invoked right after creating the proxy. It must also be decorated with an isInitializer modifier to ensure it can be called only once.

Note that this modification, while is currently done manually, could be automated by using a tool.

contract MyToken is StandardToken, Migratable {
  function initializer(string _name, string _symbol, uint8 _decimals) isInitializer("MyToken", "1.0") public {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;
  }
}

Initializer contracts

This approach removes the need for the isInitializer decorator by moving the constructor to a separate contract altogether, that is delegatecalled by the proxy at the time of initialization. The code for the proxy then becomes:

constructor(address _constructor, address _implementation, bytes _args) public {
  require(_constructor.delegatecall(_args));
  _setImplementation(_implementation);
}

However, this approach requires splitting the original contract into an implementation contract, which contains everything the original does but the constructor and a constructor contract, which contains only the storage definition and constructor logic (though it may contain other leftovers from the original).

We explored three different options for automatically generating these contracts, described below.

Constructor contract redeployment

This is the safest approach in that it does not require modifying the code of the original contract. It generates the constructor contract by actually deploying the contract constructor code as a contract itself. However, since the constructor reads the arguments from the code (and not the calldata) this approach also requires redeploying the constructor contract with the arguments every time a proxy is created, generating very high gas deployment costs.

This requires just prepending the bytecode sequence 600d80380380916000396000f3 to the contract constructor code and deploying it. Every time the proxy is created, the constructor contract is redeployed, with the appended the arguments. The code for the proxy constructor becomes:

constructor(address _constructor, address _implementation, bytes _args) public {
  address _constructorWithArgs;
  uint256 args_size = _args.length;

  // Redeploy the contract constructor with the args appended
  assembly {
    let args := add(_args, 0x20)
    let ctor_size := extcodesize(_constructor)
    let size := add(ctor_size, args_size)
    let ctor := mload(0x40)
    mstore(0x40, add(ctor, size))

    extcodecopy(_constructor, ctor, 0, ctor_size)

    let ctor_args := add(ctor, ctor_size)
    for { let i := 0 } lt(i, args_size) { i := add(i, 0x20) } {
      mstore(add(ctor_args, i), mload(add(args, i)))
    }

    _constructorWithArgs := create(0, ctor, size)
  }

  // Delegatecall into it
  require(_constructorWithArgs.delegatecall());

  // Set implementation contract
  _setImplementation(_implementation);
}

Modifying the constructor bytecode

To avoid redeploying the contract for every proxy to be created, the contract constructor code can be modified so it reads the arguments from calldata instead from the code itself. This is done on an easily identifiable part of the assembly generated by Solidity.

This requires replacing the assembly code 60405161[CODESIZE]38038061[CODESIZE]8339 by 6040516100003603806100008337. The assembly code varies slightly depending on the length of the contract (larger-args PUSH operations may be used), and on whether the initializer arguments contain dynamically-sized args. The optimiser does not seem to affect the generated source here.

Furthermore, this approach can be optimised by dropping the actual runtime implementation from the constructor contract. This can be done by removing all bytecode after the sequence 6000396000f300.

This allows having a simpler proxy constructor, as described initially:

constructor(address _constructor, address _implementation, bytes _args) public {
  require(_constructor.delegatecall(_args));
  _setImplementation(_implementation);
}

Modifying the source code

This approach is equivalente to the previous one, but instead of modifying the bytecode, it modifies the source code using information from the AST. It generates two Solidity contracts: one of the implementation and another for the initializer. This approach is more complex to automate due to the need of modifying all ancestors contracts in the inheritance chain.

Same as the previous one, this approach can be optimised by removing the runtime logic from the constructor contract, but this requires evaluating the function call graph to detect which internal functions are called from the constructor, as these cannot be removed.

Given the following original contract:

// Original contract
contract MyToken is StandardToken {
  constructor(string _name, string _symbol, uint8 _decimals) public {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;
  }
}

These two contracts are generated:

// Implementation contract, contains everything but the ctor
contract MyToken_implementation is StandardToken {
}

// Note that this contract does not inherit from StandardToken
// It only contains the storage definition and the ctor
contract MyToken_initializer_optimised {
  mapping(address => uint256) balances;
  uint256 totalSupply_;
  string public name;
  string public symbol;
  uint8 public decimals;

  function initializer(string _name, string _symbol, uint8 _decimals) public {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;
  }
}

Gas usage comparison

This table compares the gas cost for all approaches, including the cost of deploying the contract instance without any modifications or zOS at all. All zOS based approaches have an initial cost, where the implementation (and constructor) contracts are deployed, and a per-instance cost, where only a proxy is created.

All numbers are in KGas. A spreadsheet with charts with this data can be found here.

ERC20 Initial ERC20 Per instance ERC721 Initial ERC721 Per instance
No zOS 0 1485 0 2315
Initializer functions 1800 690 2351 762
Constructor redeployment 2900 1997 4665 2838
Bytecode modified 2897 830 4661 965
Bytecode optimised 1602 829 2733 963
Source modified 2862 834 4200 907
Source optimised 1782 834 ? ?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment