1

I want to understand, what happens under the hoods in this (very unsafe, but educative) example.

contract Actual {
      function () external { }
}

interface Expected {
     function g() external returns (uint256);
}


contract MyContract {
    function g(address a) public returns (uint256 x) {
          // fails at run-time
          x = Expected(a).g();
    }
 }

Let us suppose I have create an instance of MyContract i_MC and an Instance of Actual i_A. Now I call function i_MC.g() with i_A's address as input. The call results in a revert.

The high-level explanation is very simple: i_MC expect to call the g() function of an instance of type Expected, but it actually invoked the fallback function of i_A, which does not return anything.

Is that the case, because the caller is looking for an unallocated portion of memory (after the \mu_i in the yellow paper), because the fallback function did not execute the RETURN opcode, which also expands the memory?

2 Answers 2

2

Yes, you are correct. Since byzatium fork we have two new opcodes RETURNDATASIZE and RETURNDATACOPY. They allow the caller contract to determine how much data was returned by the callee.

In new versions of the solidity compiler it is enforced that the returned data size matches of the function being called and revert if they don't match.

2
  • Nice, thank you for the answer, your explanation makes sense! (I saw the RETURNDATASIZE in the bytecode and found also an interesting def. in the yellow paper). If I am not wrong, however, the compiled programs check only if the return data is at least great as expected (through the LT command with the expected size). I could potentially return a bigger data structure without complains (e.g., two ints instead of one, or an array of more than two elements instead of a single int). Am I right?
    – Briomkez
    Commented Mar 31, 2019 at 9:16
  • 1
    Yes, You are correct, a contract can ignore any data exceeding the expected length.
    – Ismael
    Commented Mar 31, 2019 at 22:43
2

It might be important to make a distinction between the EVM (and what is possible) and the solc compiler (and what is enforced).

Have a look at this excerpt from a fallback function in a proxy contract. It offers no expectation about the return data size (no returns()). It could be anything the EVM can do.

Even so, it faithfully returns whatever it gets when it invokes a function in another contract.

    function () external payable {
        address implementationAddress = userImplementation(msg.sender);
        //solium-disable-next-line security/no-inline-assembly
        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize)
            let result := delegatecall(gas, implementationAddress, ptr, calldatasize, 0, 0)
            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
}

One might ask how this is useful or how to use it. The secret is using the implementation contract's ABI at the proxy contract's address. A fallback is used to catch all of the implementation contract's function signatures, which are then delegated to the implementation contract. The implementation responds per the ABI and the proxy parrots the response.

The advantage of this slight of hand is the proxy can periodically change the contract it uses for implementation. This setup preserves state data and contract address across implementation upgrades.

If that doesn't make sense out of context, the full contract is here: https://github.com/rob-Hitchens/TrustlessUpgrades

Hope it helps.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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