8

Since Byzantium we can implement upgradable proxy contracts much easier with the use of returndatacopy and returndatasize assembly instructions. This means we no longer have to register return types and sizes like when using the EtherRouter.

The most reliable way we know how to structure a proxy contract is like the Zeppelin Proxy where the delegatecall is made in assembly. However, it also seems to work when doing the delegatecall as a high level Solidity call where the proxy contract fallback function looks like this instead:

function () public {

    bool callSuccess = upgradableContractAddress.delegatecall(msg.data);

    if (callSuccess) {
        assembly {
            returndatacopy(0x0, 0x0, returndatasize)
            return(0x0, returndatasize)
        }
    } else {
        revert();
    }
}

This approach (see the whole proxy here) is a bit more succinct and requires less knowledge of assembly to understand. My minimal tests for this approach seem to work.

So in what situations will this high level approach not work?

And if there aren't any, how likely is it that the compiled bytecode for the high-level delegatecall will change between versions of Solidity, breaking this approach for those versions?

3
2

One issue is that you copy your data into memory starting at address 0. This will work for return sizes less than 64 bytes, but will start overwriting other memory at that point.

Instead you should do something more like

let m := mload(0x40)
returndatacopy(m, 0, returndatasize)
return(0, returndatasize)
3
0

To mitigate the issue mentioned by @Tjaden Hess, you could do what OpenZeppelin does:

function functionDelegateCall(
    address target,
    bytes memory data,
    string memory errorMessage
) internal returns (bytes memory) {
    (bool success, bytes memory returndata) = target.delegatecall(data);
    if (success) {
        return returndata;
    } else {
        if (returndata.length > 0) {
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }

That is, if the call did not fail, you return the data normally via Solidity. Otherwise you revert via assembly to bubble up the revert reason.

Pro tip: see my implementation of this in PRBProxy.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.