Skip to content
This repository was archived by the owner on Mar 18, 2019. It is now read-only.

Safe external contract calls

Brecht Devos edited this page Aug 30, 2018 · 2 revisions

Safe external contract calls

Loopring protocol V2 allows external contracts to hook into the protocol code. We do not want these external contracts to be able to fail the transaction in any way.

Vanilla Solidity

Normally you would do something like this in solidity to call an external contract:

amount = IBrokerInterceptor(contractAddress).getAllowance(
    tokenOwner,
    broker,
    tokenAddress
);

This can fail the complete transaction when one of the following is true:

  • contractAddress is not a valid contract address
  • the function contains code with a failing require statement
  • the function produces a revert for any reason
  • the function uses all available gas (e.g. by calling assert)

Solidity will automatically propagate the error using the above code.

Solidity + Assembly

We can work around this by using call directly on the contract address to call the function:

bool success = contractAddress.call.gas(5000)(
    bytes4(keccak256("getAllowance(address,address,address)")),
    owner,
    broker,
    token
);

call does not automatically propagate the error: the return value of call is whether the function call succeeded and is not the return value of the function! This value will be false when one of the scenarios listed above happened.

Do note the optional gas() function that gets called on call. If we wouldn't do this the external contract function could use up all available gas which would still fail the complete transaction. For every function we should define a reasonable amount of gas that can be used.

The first argument is the first 4 bytes of the signature of the function we want to call. Make sure to create this signature correctly! If the signature doesn't match exactly with what is expected the call will fail. All parameter types need to be given with explicit size e.g. if there's a uint parameter this needs to be given as uint256 in the signature. The return value is not part of the function signature.

We can get to the return data by using the assembly function returndatacopy:

assembly {
    switch returndatasize()
    // We expect a single uint256 value
    case 32 {
        returndatacopy(0, 0, 32)
        allowance := mload(0)
    }
    // Unexpected return value
    default {
        allowance := 0
    }
}

STATICCALL

For functions that do not need to modify any state we could also use STATICCALL.

Small gas optimization

We can save some gas by calculating it manually because the solidity compiler isn't smart enough to do it for us. Use this tool to calculate the hash and strip off the first 8 characters (4 bytes).

Example: IBrokerInterceptor Proxy

/// @title A safe wrapper around the IBrokerInterceptor functions
/// @author Brecht Devos - <[email protected]>.
library BrokerInterceptorProxy {

    function getAllowanceSafe(
        address brokerInterceptor,
        address owner,
        address broker,
        address token
        )
        internal
        returns (uint allowance)
    {
        bool success = brokerInterceptor.call.gas(5000)(
            0xe7092b41, // bytes4(keccak256("getAllowance(address,address,address)"))
            owner,
            broker,
            token
        );
        // Just return an allowance of 0 when something goes wrong
        if (success) {
            assembly {
                switch returndatasize()
                // We expect a single uint256 value
                case 32 {
                    returndatacopy(0, 0, 32)
                    allowance := mload(0)
                }
                // Unexpected return value
                default {
                    allowance := 0
                }
            }
        } else {
            allowance = 0;
        }
    }

    function onTokenSpentSafe(
        address brokerInterceptor,
        address owner,
        address broker,
        address token,
        uint    amount
        )
        internal
        returns (bool ok)
    {
        ok = brokerInterceptor.call.gas(25000)(
            0x9e80e44d, // bytes4(keccak256("onTokenSpent(address,address,address,uint256)"))
            owner,
            broker,
            token,
            amount
        );
        if (ok) {
            assembly {
                switch returndatasize()
                // We expect a single bool value
                case 32 {
                    returndatacopy(0, 0, 32)
                    ok := mload(0)
                }
                // Unexpected return value
                default {
                    ok := 0
                }
            }
        }
    }
	
}