External Calls

STATICCALL

contract SolidityContract {
	// "9a884bde": "get21()" - function signature 
    function get21() external pure returns (uint256) {
        return 21;
    }
} 
  • In Yul, we have to load the function selector into memory, then point to the region that will be part of our abi call, i.e. bytes 28 to 32 which contain the function signature

function externalViewCallNoArgs(address _a)
        external
        view
        returns (uint256)
    {
        assembly {
            mstore(0x00, 0x9a884bde)
            // 000000000000000000000000000000000000000000000000000000009a884bde
            //                                                         |       |
            //                                                         28      32
            let success := staticcall(gas(), _a, 28, 32, 0x00, 0x20)
            if iszero(success) {
                revert(0, 0)
            }
            return(0x00, 0x20)
        }
    }
  • Opcodes for making external calls: staticcall, call, and delegatecall

  • If we are inside of a view function, we have to use staticcall, because static calls do not change state. if state is being changed, the call will revert

  • The compiler will not allow us to use call within a view function, since the function is not supposed to modify state

  • Breakdown of arguments in staticcall :

    staticcall(gas(), _a, 28, 32, 0x00, 0x20)
    • gas() - amount of remaining gas the contract has left. can be hardcoded to a smaller amount, can be used if we don’t trust the receiving contract, since it may attempt to consume all the gas in a DoS attack

    • _a - address of the contract we are calling

    • 28, 32 - intended [tx.data](<http://tx.data>) , which is what we have loaded into memory

    • 0x00, 0x20 - region in memory that we are going to copy the results back into. so when the function returns, it will override the function selector in 0x00, but it does not matter since we don’t need this info any more

Getting return values from a revert call in Yul

// function to call: "73712595": "revertWith999()",
    function revertWith999() external pure returns (uint256) {
        assembly {
            mstore(0x00, 999)
            revert(0x00, 0x20)
        }
    }

// Yul calling function 
function getViaRevert(address _a) external view returns (uint256) {
        assembly {
            mstore(0x00, 0x73712595)
            pop(staticcall(gas(), _a, 28, 32, 0x00, 0x20)) // discard the return boolean value since we know it will be 0 / false
            return(0x00, 0x20) // returns 999 
        }
    }
  • when a function reverts, it will still return the revert values to the same location that we assigned to it, in this case 0x00 - 0x20

Calling a function that includes arguments

  • since the arguments take up more than 64 bytes, we cant put it in scratch space

  • have to store the function arguments into locations in memory with mstore

// function to call:"196e6d84": "multiply(uint128,uint16)",
function multiply(uint128 _x, uint16 _y) external pure returns (uint256) {
        return _x * _y;
    }

// Yul calling function 
function callMultiply(address _a) external view returns (uint256 result) {
        assembly {
            let mptr := mload(0x40)
            let oldMptr := mptr
            mstore(mptr, 0x196e6d84)
            mstore(add(mptr, 0x20), 3) // _x 
            mstore(add(mptr, 0x40), 11) // _y 
            mstore(0x40, add(mptr, 0x60)) // advance the memory pointer 3 x 32 bytes
            //  00000000000000000000000000000000000000000000000000000000196e6d84
            //  0000000000000000000000000000000000000000000000000000000000000003
            //  000000000000000000000000000000000000000000000000000000000000000b
            let success := staticcall(
                gas(),
                _a,
                add(oldMptr, 28), // fast forward to get the function signature 
                mload(0x40), // advanced memory pointer after arguments have been stored in memory 
                0x00,
                0x20
            )
            if iszero(success) {
                revert(0, 0)
            }

            result := mload(0x00)
        }
    }

CALL

  • Difference from staticcall is the addition of a callvalue() variable, which is used to forward the ETH received as part of the transaction (msg.value )

  • If a function is not payable, callvalue() can just be set to 0

// function to call : "4018d9aa": "setX(uint256)"
function setX(uint256 _x) external {
        x = _x;
    }

// Yul calling function 
function externalStateChangingCall(address _a) external {
        assembly {
            mstore(0x00, 0x4018d9aa)
            mstore(0x20, 999)
            // memory now looks like this
            // 0x000000000000000000000000000000000000000000000000000000004018d9aa
            // 000000000000000000000000000000000000000000000000000000000000000999
            let success := call(
                gas(),
                _a,
                callvalue(),
                28,
                add(28, 32),
                0x00,
                0x00
            )
            if iszero(success) {
                revert(0, 0)
            }
        }
    }

Unknown return size

  • in some situations, we don’t know what the size of the return value will be

  • if the function returns some unknown data size, e.g. a string or an array, we use returndatasize() to obtain the return data size, and ignore the allocated positions in staticcall

function unknownReturnSize(
        address _a,
        uint256 amount
    ) external view returns (bytes memory) {
        assembly {
            mstore(0x00, 0x7c70b4db)
            mstore(0x20, amount)

            let success := staticcall(gas(), _a, 28, add(28, 32), 0x00, 0x00)
            if iszero(success) {
                revert(0, 0)
            }

            returndatacopy(0, 0, returndatasize())
            return(0, returndatasize())
        }
    }
  • returndatacopy will copy the return value into a location in memory: in this case, copy into slot 0, the data at 0, up to the total size of the return data

DELEGATECALL

  • Openzeppelin implementation of delegatecall

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.
						// msg.value: hardcoded to zero 
            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())
            }
        }
    }

Last updated