• Blog
  • Talks

Upgrading multiple proxy contracts in Solidity in one call

2021-07-01This post is over 2 years old and may now be out of date

(2 minute read)

In a previous post I outlined how the Diamond Standard can be used to facilitate upgradeable smart contracts of virtually infinite size.

If one recalls, the structure looks like the following:

Diamond Standard

As I wrote in my previous post:

Each implementation contract is called a facet, like the facets of a diamond. In the proxy's fallback function it looks up the facet which implements the incoming function call from the mapping table that's in storage and then delegates the call to it.

In our architecture we plan to deploy hundreds and possibly thousands of contracts following the Diamond Standard, wherein each proxy contract points to the same set of facet contracts. When we wish to upgrade the facet contracts we would need to individually upgrade each proxy. Clearly this would cost a lot of gas (O(n) complexity, where n is the number of deployed proxies).

To avoid this problem we have decided to introduce what we (perhaps confusingly!) call a Delegate contract. The Delegate contract is a standard Diamond proxy contract which points to the facet contracts. Our actual business-case proxy contracts are then setup to utilise this singleton Delegate contract. The idea being that when we wish to upgrade our proxy contracts we would just need to make a single upgrade call to the Delegate contract instead:

Delegate contract

As shown above, in our implementation we have Entity contracts which are connected to the same singleton EntityDelegate contract instance. This contract contains the Diamond standard mapping table which maps methods to facet contracts.

Note: This pattern of using an intermediate "Delegate" would actually work for any type of proxy pattern one is using, not just the Diamond standard.

Implementation details

Let's go into the details of the code to see how we accomplish this.

The DiamondProxy contract repesents the proxy contract in the Diamond standard. We split up the fallback function such that facet resolution is its own function:

contract DiamondProxy {
    ...
    
  function resolveFacet (bytes4 _sig) public view virtual returns (address) {
    DiamondStorage storage ds = diamondStorage();
    return address(bytes20(ds.facets[_sig]));
  }
    
    ...

  // Finds facet for function that is called and executes the
  // function if it is found and returns any value.
  fallback() external payable {
    address facet = resolveFacet(msg.sig);
    require(facet != address(0), "Facet not found");

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

  receive() external payable {
  }
}

The resolveFacet() call is both public and overrideable in derived contracts. The Proxy contract makes use of this to allow for facet resolution to be performed by another contract:

contract Proxy is DiamondProxy {
  function _setDelegateAddress(address _addr) internal {
    dataAddress["delegate"] = _addr;
  }

  function getDelegateAddress() external view returns (address) {
    return dataAddress["delegate"];
  }

  function resolveFacet (bytes4 _sig) public view override returns (address) {
      // This works because DiamondProxy::resolveFacet() is public
    return DiamondProxy(payable(dataAddress["delegate"])).resolveFacet(_sig);
  }
}

Now it's simple. Our EntityDelegate singleton inherits from DiamondProxy and the Entity contract (of which there will be many instances) inherits from Proxy:

contract EntityDelegate is DiamondProxy {
  constructor () DiamondProxy() public {
    _registerFacets(...);
  }
}

contract Entity is Proxy {
  constructor () Proxy() public {
    _setDelegateAddress(.../* address of EntityDelegate */);
  }
}

When a call comes into the Entity its fallack function calls through to Proxy::resolveFacet(). This internally calls makes a contract call to DiamondProxy::resolveFacet() on the Delegate contract, which is located at dataAddress["delegate"].

Thus, the EntityDelegate simply acts as global storage layer for the Diamond standard mapping table, whilst the actual method delegation is done directly from the Entity contract instances. To upgrade the facets we only have to perform the upgrade on the EntityDelegate.

  • Home
  • Blog
  • Talks
  • Github
  • Linked-in
  • Email
  • RSS
© Hiddentao Ltd