-
Notifications
You must be signed in to change notification settings - Fork 7
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.
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.
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
}
}
For functions that do not need to modify any state we could also use STATICCALL.
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).
/// @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
}
}
}
}
}