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 theout
andoutSize
. Other opcodesdelegatecall
,staticcall
,callcode
have the same parameters.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()
andreturndatacopy(to, from, size)
.After a call
returndatasize()
contains the data returned by the application, and you can usereturndatacopy(to, from, size)
to copy data to the caller memory.In
delegatecall
the output parameters are both set to zero:out = 0
, andoutSize = 0
.The next statement copies all the returned data from position 0, to the proxy memory starting at 0.
Notes:
returndatacopy
andreturndatasize
can also be used to copy the revert reason if the function call has failed.