Smart Contract Upgradeability using Eternal Storage

As mentioned in our Development Roadmap Part Two, the Kernel is the foundational layer of zeppelin_os, and we will soon be launching a prototype of it, starting with the functionality currently found in the OpenZeppelin framework.

We’re happy to announce another step towards launching the zeppelin_os Kernel. We’re now investigating different upgradeability mechanisms. As you may have read, there are several approaches to implementing contract upgradeability, like proxy libraries or eternal storage, among others. We’ve been working on a solution that combines some of these patterns.

The approach consists in having a Proxy that delegates calls to specific implementations which can be upgraded, assuming that the storage structure won’t ever change. This scenario is referred to as “Eternal Storage”. Let’s see how the Proxy contract looks like:

contract Proxy {
  function implementation() public view returns (address);
 
  function () payable public {
    address _impl = implementation();
    require(_impl != address(0));
    bytes memory data = msg.data;

    assembly {
      let result := delegatecall(gas, _impl, add(data, 0x20), mload(data), 0, 0)
      let size := returndatasize
      let ptr := mload(0x40)
      returndatacopy(ptr, 0, size)
      switch result
      case 0 { revert(ptr, size) }
      default { return(ptr, size) }
    }
  }
}

As you can see, given the proxy uses delegatecall to resolve the requested behaviors, the upgradeable contract’s state will be stored in the proxy contract itself.

Let’s analyze how this would work by building an upgradeable ERC20 token contract. To do this, we will need to manage two really different kinds of data, one related to the upgradeability mechanism and another strictly related to the token contract domain. Naming plays a really important role here if we want to express correctly what’s going on. This is the proposed model:

Upgradable token model using proxies and eternal storage

Proxy, UpgradeabilityProxy and UpgradeabilityStorage are generic contracts that can be used to implement upgradeability through proxies. In this example we use them to implement an upgradeable ERC20 token:

contract UpgradeabilityProxy is Proxy, UpgradeabilityStorage {
  event Upgraded(string version, address indexed implementation);

  function upgradeTo(string version, address implementation) public {
    require(_implementation != implementation);
    _version = version;
    _implementation = implementation;
    Upgraded(version, implementation);
  }
}

contract UpgradeabilityStorage {
  string internal _version;
  address internal _implementation;

  function version() public view returns (string) {
    return _version;
  }

  function implementation() public view returns (address) {
    return _implementation;
  }
}

The UpgradeabilityStorage contract holds data needed for upgradeability, while the TokenStorage contract holds token-related information. It defines the token-specific storage:

contract TokenStorage {
  uint256 internal _totalSupply;
  mapping (address => uint256) internal _balances;
  mapping (address => mapping (address => uint256)) internal _allowances;
}

On the other hand, TokenProxy extends from UpgradeabilityProxy (which in turn extends from UpgradeabilityStorage), and then from the TokenStorage contract:

contract TokenProxy is UpgradeabilityProxy, TokenStorage {}

TokenProxy is the contract that will delegate calls to specific implementations of the ERC20 token behavior. These behaviors are the code that can be upgraded by the token developer, for example Token_V0 and Token_V1:

contract Token_V0 is UpgradeableTokenStorage {
  using SafeMath for uint256;
  event Transfer(address indexed from, address indexed to, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);

  function totalSupply() public view returns (uint256) {...}
  function balanceOf(address owner) public view returns (uint256) {...}
  function transfer(address to, uint256 value) public {...}
  function transferFrom(address from, address to, uint256 value) public {...}
  function approve(address spender, uint256 value) public {...}
}

contract Token_V1 is UpgradeableTokenStorage {
  using SafeMath for uint256;
  event Transfer(address indexed from, address indexed to, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);

  function mint(address to, uint256 value) public {...}
  function burn(uint256 value) public {...}

  function totalSupply() public view returns (uint256) {...}
  function balanceOf(address owner) public view returns (uint256) {...}
  function transfer(address to, uint256 value) public {...}
  function transferFrom(address from, address to, uint256 value) public {...}
  function approve(address spender, uint256 value) public {...}
}

Example of a token behavior upgrade to provide mint and burn functionalities

As you can see, token behavior implementations extends from UpgradeableTokenStorage, which derives from UpgradeableTokenStorage and TokenStorage. This is what every implementation of the upgradeable behavior needs:

contract UpgradeableTokenStorage is UpgradeabilityStorage, TokenStorage {}

Notice that inheritance order in TokenProxy needs to be the same as the one in UpgradeableTokenStorage, to respect storage structure (given we are using delegatecall). Moreover, we are not defining any new state variables in the token behavior implementation contracts. This is a requirement of the proposed approach to ensure the proxy storage is not messed up.

Thanks to the Winding Tree and Aragon teams for their collaboration on this research and development.

Join Our Community:

+ Chat with the team and community on our Slack channel
+ Follow Zeppelin on Twitter
+ Read the whitepaper