• Blog
  • Talks
  • Investing
  • About

Upgradeable smart contracts with eternal storage

2019-10-03This post is over 2 years old and may now be out of date

(2 minute read)

For the Nayms project we've opted to build upgradeable smart contracts so that we can do permissioned upgrades of our smart contract logic whilst keeping our on-chain data unchanged.

In attempting to figure out how to design for upgradeable contracts I looked at the OpenZepellin project's approaches as well as other commentary on the idea:

Initially I thought about storing data separately in its own contract (like Rocket Pool does) but decided against this as this then introduces a new problem to solve - that of controlling access to the data.

Thus, the proxy pattern (using delegatecall) became the preferred option. In this pattern the proxy contract forwards all incoming calls to an logic contract which actually has the business logic. Because the forwarding is done using delegatecall, the logic contract gets run in the context of the proxy contract's memory space, meaning it operates on the data in the proxy contract rather than its own:

pragma solidity >=0.5.8;

contract Proxy {
  /**
   * The logic contract address.
   */
  address public implementation;

  /**
  * @dev This event will be emitted every time the implementation gets upgraded
  * @param implementation representing the address of the upgraded implementation
  */
  event Upgraded(address indexed implementation, string version);

  /**
   * Constructor.
   */
  constructor (address _implementation) public {
    require(_implementation != address(0), 'implementation must be valid');
    implementation = _implementation;
  }

  /**
   * @dev Set new logic contract address.
   */
  function setImplementation(address _implementation) public {
    require(_implementation != address(0), 'implementation must be valid');
    require(_implementation != implementation, 'already this implementation');

    implementation = _implementation;

    emit Upgraded(_implementation, version);
  }

  /**
  * @dev Fallback function allowing to perform a delegatecall to the given implementation.
  * This function will return whatever the implementation call returns.
  * (Credits: OpenZepellin)
  */
  function () payable external {
    address _impl = getImplementation();
    require(_impl != address(0), 'implementation not set');

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

Note: I've deliberately omitted authorisation code to keep things simple!.

When using a proxy pattern the approaches can be broken up into two types:

  • "Shared" storage structure - both the proxy and logic contracts have the same storage data structures. This ensures that the logic contract does not overwrite important proxy member variables when storing data:
pragma solidity >=0.5.8;

contract SharedStorage {
  address public implementation;
}

contract Proxy is SharedStorage {
  /* same as before */
}

contract LogicVersion1 is SharedStorage {
  // add member variables
  bool isAllowed;
  string personName;
}

contract LogicVersion2 is SharedStorage {
  /*
  Note that successive versions of the _logic_ contract must define all of their
  predecessor's member variables in the same order.
   */
  bool isAllowed;
  string personName;
  // add new member variables...
  uint256 total;
}
  • "Unshared" storage structure - the proxy contract reserves one or more specific storage slots for storing upgradeability information using inline assembly code. Thus the logic contract does not need to share such storage data and can simply define the useful member variables. However, successive logic contracts are still forced to define all of their predecessor's member variables in the same order:
pragma solidity >=0.5.8;

contract Proxy {
  bytes32 private constant IMPL_POS = keccak256("implementation.address");  

  function setImplementation(address _implementation) public {
    /* pre-condition checks (same as before) */

    bytes32 position = IMPL_POS;
    assembly {
      sstore(position, _implementation)
    }

    /* emit event (same as before) */
  }

}

contract LogicVersion1 {
  bool isAllowed;
  string personName;
}

contract LogicVersion2 {
  bool isAllowed;
  string personName;
  uint256 total;
}

After considering the options available, I felt that the "shared" storage structure approach would work just fine; but instead of inflexible member variables, an eternal storage approach made more sense:

pragma solidity >=0.5.8;

contract EternalStorage {
  // scalars
  mapping(string => address) dataAddress;
  mapping(string => string) dataString;
  mapping(string => bytes32) dataBytes32;
  mapping(string => int256) dataInt256;
  mapping(string => uint256) dataUint256;
  mapping(string => bool) dataBool;
  // arrays
  mapping(string => address[]) dataManyAddresses;
  mapping(string => bytes32[]) dataManyBytes32s;
  mapping(string => int256[]) dataManyInt256;
  mapping(string => uint256[]) dataManyUint256;
  mapping(string => bool[]) dataManyBool;
}

contract Proxy is EternalStorage {
  constructor (_implementation) {
    dataAddress["implementation"] = _implementation;
  }

  setImplementation(address _implementation) {
    /* pre-condition checks (same as before) */

    dataAddress["implementation"] = _implementation;

    /* emit event (same as before) */
  }
}

contract LogicVersion1 is EternalStorage {
  // we don't need to define anything here since mappings automatically
  // return default values for unset keys
}

contract LogicVersion2 is EternalStorage {
  // we don't need to define anything here since we didn't need to define
  // anything in LogicVersion1 :)
}

The beauty of this approach is that the logic contracts do not need to explicitly define any member variables since the mappings in the EternalStorage base class in effect define all possible storage slots already.

This, I believe, is the most flexible storage structure though code readability and ease-of-use is slightly sacrificed.

And this is how the Nayms contracts are built. We added some code to ensure upgrades are only possible when authorised:

pragma solidity >=0.5.8;

import "./ECDSA.sol";

interface IProxyImpl {
  function getImplementationVersion() pure external returns (string memory);
}

contract Proxy is EternalStorage {
   * @dev Point to a new implementation.
   * This is internal so that descendants can control access to this in custom ways.
   */
  function setImplementation(address _implementation) internal {
    require(_implementation != address(0), 'implementation must be valid');
    require(_implementation != dataAddress["implementation"], 'already this implementation');

    IProxyImpl impl = IProxyImpl(_implementation);
    string memory version = impl.getImplementationVersion();

    dataAddress["implementation"] = _implementation;

    emit Upgraded(_implementation, version);
  }

  /**
   * @dev Get the signer of the given signature which represents an authorization to upgrade
   * @return {address} address of signer
   */
  function getUpgradeSigner(address _implementation, bytes memory _signature) pure internal returns (address) {
    require(_implementation != address(0), 'implementation must be valid');

    // get implementation version
    IProxyImpl impl = IProxyImpl(_implementation);
    string memory version = impl.getImplementationVersion();
    // generate hash
    bytes32 h = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(version)));
    // get signer
    address signer = ECDSA.recover(h, _signature);
    // check is valid
    require(signer != address(0), 'valid signer not found');

    return signer;
  }
}

Note: the ECDSA library that is referred to above is from OpenZepellin

The logic contract (aka the implementation contract) must simply implement the IProxyImpl interface, which specifies a function to return the implementation version. This method is used as a way for an implementation to provide meta information about itself, as well as a way for us to ensure that the upgrade to a specific implementation is authorised via digital signatures (the getUpgradeSigner() method above).

Every upgrade in our system requires two signatures, one from each primary user of the proxy contract. In our case we have two entities - asset managers and client managers - who are the primary users.

Here is a simplified version of our actual upgradeable contract (i.e. which inherits from Proxy) which uses the two aforementioned signatures to enable an upgrade:

import "./Proxy.sol";

contract ConcreteProxy is Proxy {
  /**
   * @dev Constructor.
   * @param {address} _impl address of initial logic contract.
   * @param {address} _assetMgr address of "asset manager" upgrade authorizor.
   * @param {address} _clientMgr address of "client manager" upgrade authorizor.
   */
  constructor (
    address _impl,
    address _assetMgr,
    address _clientMgr
  ) Proxy(_impl) public {
    dataAddress["assetMgr"] = _assetMgr;
    dataAddress["clientMgr"] = _clientMgr;
  }

  /**
   * @dev Upgrade the logic/imlementation contrat
   * @param  {address} _implementation New implementation
   * @param  {bytes} _assetMgrSig Signature of asset manager
   * @param  {bytes} _clientMgr Signature of client manager
   */
  function upgrade (address _implementation, bytes memory _assetMgrSig, bytes memory _clientMgrSig) public {
    address assetMgr = getUpgradeSigner(_implementation, _assetMgrSig);
    address clientMgr = getUpgradeSigner(_implementation, _clientMgrSig);

    require(assetMgr == dataAddress["assetMgr"], 'must be approved by asset mgr');
    require(clientMgr = dataAddress["clientMgr"], 'must be approved by client mgr');

    setImplementation(_implementation);
  }
}
  • Home
  • Blog
  • Talks
  • Investing
  • About
  • Twitter
  • Github
  • Linked-in
  • Email
  • RSS
© Hiddentao Ltd