• Blog
  • Talks

Nested delegate call in Solidity

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

(1 minute read)

Are nested delegate calls possible within Solidity? of course. I had a situation whereby I was using the Proxy upgradeability pattern but my implementation had gotten too large to deploy. So I wanted to split it up into sub-implementations, which meant needing to delegate from within a delegate.

I couldn't find any examples online so I built my own and tested it out. Here is the code below.

I'll use a modified version of Eternal Storage as the base class for my contracts so that all delegates have the exact same storage layout:

contract EternalStorage {
  mapping(string => address) dataAddress;
  mapping(string => uint256) dataUint256;
}

Let's look at the first contract D1:

import "./EternalStorage.sol";

contract D1 is EternalStorage {
  function setValue (uint256 i) public {
    dataUint256["value"] = i;
  }
    
  function getValue () public view returns (uint256) {
    return dataUint256["value"];
  }
}

The contract allows one to get and set a value. Now let's look at D2:

import "./D1.sol";

contract D2 is EternalStorage {
  using Delegate for *;
    
  function setValue (uint256 i) public {
    (bool success, bytes memory returnedData) = dataAddress["d1"].delegatecall(abi.encodeWithSelector(
      bytes4(keccak256("setValue(uint256)")),
      i
    ));
    require(success, string(returnedData));
  }
    
  function getValue () public view returns (uint256) {
    return dataUint256["value"];
  }
}

In D2 we delegate the setValue() call to a D1 instance that's stored in dataAddress["d1"]. Finally, we have the D3 contract:

import "./EternalStorage.sol";

contract D3 is EternalStorage {
  constructor (address _d1, address _d2) public {
    dataAddress["d1"] = _d1;
    dataAddress["impl"] = _d2;
  }
    
  function () external payable {
    address impl = dataAddress["impl"];
    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) }
    }
  }
}

D3 implements the proxy pattern, whereby it doesn't expose an API of its own - instead it passes on all calls to the delegate stored in dataAddress["impl"], which we will set as an instance of D2.

Thus the delegation is as follows: D3 -> D2 -> D1.

When we call D3.setValue() the EVM should end up executing D1.setValue() within D3's memory context.

Here is how the test code looks (using Truffle):

contract('Delegate test', accounts => {
  let d1
  let d2
  let d3
    
  beforeEach(async () => {
    d1 = await D1.new()
    d2 = await D2.new()
    const d3Proxy = await D3.new(d1.address, d2.address)
    d3 = await D2.at(d3Proxy.address)
  })
    
  it('sets the value', async () => {
    await d1.setValue(3)
    await d1.getValue().should.eventually.eq(3)
    await d2.setValue(6)
    await d1.getValue().should.eventually.eq(3)
    await d2.getValue().should.eventually.eq(0)
    await d3.setValue(9)
    await d1.getValue().should.eventually.eq(3)
    await d2.getValue().should.eventually.eq(0)
    await d3.getValue().should.eventually.eq(9)
  })
})

The d3.setValue() call is the key one. It shows that d3's value that gets set, not d1's or d2's.

Note: d2.setvalue() has no effect since in d2's memory space dataAddress["d1"] is empty, which means that the delegate call fails.

  • Home
  • Blog
  • Talks
  • Github
  • Linked-in
  • Email
  • RSS
© Ramesh Nair