OpenZeppelin – How to Access Values Returned via `return(0, returndatasize())`

assemblyopenzeppelinopenzeppelin-contractsreturndata

I was looking at the _delegate function in OpenZeppelin's Proxy.sol:

/**
 * @dev Delegates the current call to `implementation`.
 *
 * This function does not return to its internall call site, it will return directly to the external caller.
 */
function _delegate(address implementation) internal virtual {
    assembly {
        // Copy msg.data. We take full control of memory in this inline assembly
        // block because it will not return to Solidity code. We overwrite the
        // Solidity scratch pad at memory position 0.
        calldatacopy(0, 0, calldatasize())

        // Call the implementation.
        // out and outsize are 0 because we don't know the size yet.
        let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

        // Copy the returned data.
        returndatacopy(0, 0, returndatasize())

        switch result
        // delegatecall returns 0 on error.
        case 0 {
            revert(0, returndatasize())
        }
        default {
            return(0, returndatasize())
        }
    }
}

And I noticed that although the function declaration lacks a returns (type), the code returns a value via inline assembly. It seems odds to me that Solidity allows this to be compiled in the first place, but anyways.

I am trying to understand where can this be accessed. Clearly a high-level call from Solidity could not access the return data. Is it meant to be used by other low-level assembly code, which uses the returndatacopy instruction? (the "external caller" mentioned in the comments)

Best Answer

The solidity compiler (solc) will translate the return statements into return(ptr, size) opcodes.

The caller in order to access the callee returned data, in early EVM versions, had to pass a memory pointer and a memory size. For example call's last two parameters are the out and outSize. Other opcodes delegatecall, staticcall, callcode have the same parameters.

call(gas, address, value, in, insize, out, outsize)

A disadvantage of this mechanism is that the caller needs to know outSize in advance to reserve memory.

With the introduction of delegatecall in the Homestead fork writing proxies was possible, but there was the problem that a proxy wasn't able to allocate memory beforehand.

Finally in the Byzantium fork a couple of new opcodes allowed proxies to not need to allocate output memory before making the call: returndatasize() and returndatacopy(to, from, size).

After a call returndatasize() contains the data returned by the application, and you can use returndatacopy(to, from, size) to copy data to the caller memory.

let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

In delegatecall the output parameters are both set to zero: out = 0, and outSize = 0.

The next statement copies all the returned data from position 0, to the proxy memory starting at 0.

returndatacopy(0, 0, returndatasize())

Notes:

  • returndatacopy and returndatasize can also be used to copy the revert reason if the function call has failed.
Related Topic