1

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)

2

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.
2
  • When you said "The compiler will translate return statements into return(ptr, size) opcodes", did you refer to the Solidity compiler? That is, that Solidity translates the high-level return statement into the low-level RETURN opcode? Sep 1 at 6:18
  • @PaulRazvanBerg Yes, that is correct.
    – Ismael
    Sep 1 at 16:01
0

There is not an explicit return data because the proxy contract executes the code of the proxee , but in its own memory context: the proxy doesn't know where to save the result and what to save.

The expected result is to have the return value of the proxee function at the same memory position where the proxee contract would save it.

Let's say that the logic contract would save its result at memory position 0x80, then in order to work, the proxy contract need to have the result at 0x80.

This depends on the contract state at the moment of the execution, so it's not possible to know in advance where it will be saved(*)

if something fails, then THIS function returns 0 where THIS function should return a value

--

So in brief: since when a function returns, it "saves" the position in memory of the result, and the value of the result:

  • if it works => the position and value will be the one of the proxee logic
  • if it doesn't work => the position and value will be the one defined by this function (return 0, basically)
  • in both scenarios => what called the function will have a memory position and value to read

So this is not 100% true:

a high-level call from Solidity could not access the return data.

the proxy contract is blind to what the logic contract does, but every method of the proxee contract will work because the whole logic will be replicated and behave as it was called without the proxy(**)

--

(*) it is actually possible if you know the state of the contract, the source and low-level analyze it

(**) in order to avoid that the proxy conflicts with the proxee memory areas there are different solutions

Example to test with remix

Prepare logic contract and proxy contract

BOX / LOGIC

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract Box {
    uint256 private _value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 value);

    // Stores a new value in the contract
    function store(uint256 value) public {
        _value = value;
        emit ValueChanged(value);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return _value;
    }
    
    function pippo(uint256 asd) public pure returns (uint256) {
        return asd;
    }
}

PROXY

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract MyProxy is ERC1967Proxy {
   
    constructor(address _logic, bytes memory _data)  ERC1967Proxy(_logic, _data) {}
  
    // debug method to expose the address
    function getImplementationAddress() public view returns(address) {
        return ERC1967Upgrade._getImplementation();
    }       
}

now

  1. select BOX from left file panel and deploy logic contract (copy the address for the next step)

  2. select PROXY from left file panel and deploy the proxy by passing these parameters (copy the address for the next step) enter image description here

  3. select BOX again from the left file panel and use deploy AT ADDRESS enter image description here

now you can call the box method passing via the proxy

3
  • Thanks again for your help, @Stormsson. But I don't think that this answered my question. I was specifically asking how to access the returned value. I can do it by writing a contract that calls the proxy, can't I? Aug 30 at 17:11
  • I'll edit the answer to provide an example
    – Stormsson
    Aug 30 at 17:17
  • 1
    also yes, you can write a contract that call the proxy BUT using the logic ABI
    – Stormsson
    Aug 30 at 18:16

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.