(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.