When a revert happens in solidity, all the state changes done in that transaction are rolled back. All the changes done in sub calls are also rolled back. If we called a contract A which in turn tried to do a token transfer in contract B but contract B reverts, all the changes done by contract A would also be rolled back. We might not always want this behavior. We might want to catch the exception generated by contract B and handle the scenario in the code itself.
Solidity does not directly support exception handling yet, but we can make use of the low-level functions call
, delegatecall
and callcode
as they return false
in case of an exception instead of triggering a revert in the contract that called them. The state changes made inside the low-level function will still be rolled back, but the original transaction will proceed normally and the state changes done in the original transaction will NOT be rolled back.
We can use this feature of Solidity to simulate a Try Catch like behavior. I’ll show how to do this with the help of an example. Imagine there is a non-ERC20 compliant ERC20 token (like many ICO ERC20 tokens :)) that reverts rather than returning false in case of a failing transaction and returns the true boolean as well as the transacted amount if a transaction succeed. Essentially, the transferFrom
function of that contract will look like:
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool, uint256)
The full sample code of this token is available on https://github.com/maxsam4/try-catch-solidity/blob/master/contracts/Token.sol, but it’s not the critical component of this example so let’s move forward. Just a note, reverting instead of resulting false does not make it non-compliant to ERC20 but returning the transacted amount along with the boolean does. I am returning the amount to showcase how to handle return data with low-level calls.
The low level call
, delegatecall
and callcode
functions return a Boolean and a Bytes variable. The Boolean shows if the transaction completed without reverting (raising an exception). Think of it as the “Success” boolean. It is false when the sub call reverts. The Bytes variable contains the data returned by the sub call. The sub call may return multiple variables, but they are combined together then returned as a bytes
variable to the user by the low-level calls. We can then decode this to get our actual return parameters.
To create a low-level call in Solidity, we need to pass in the function selector (this defines what function we need to call) and the parameters that we want to give. If we’re going to pass ether, we can append a .value()
like we do in any other external call made in Solidity. To get the function selector, we can do contractName.functionName.selector
or generate it manually by taking the first 4 bytes of the keccak256 hash of the function signature. We can use abi.encode
to combine multiple parameters that we want to pass to the call and then use abi.encodePacked
to combine these parameters to the function selector. Here’s how it actually looks:
// https://github.com/maxsam4/try-catch-solidity/blob/master/contracts/Example.sol
pragma solidity ^0.5.0; import "./Token.sol"; /** * @dev This contract showcases a simple Try-catch call in Solidity */ contract Example { Token public token; uint256 public lastAmount; constructor(Token _token) public { token = _token; } event TransferFromFailed(uint256 _amount); function tryTransferFrom(address _from, address _to, uint256 _amount) public returns(bool returnedBool, uint256 returnedAmount) { lastAmount = _amount; // We can query this after transferFrom reverts to confirm that the whole transaction did NOT revert // and the changes we made to the state are still present. (bool success, bytes memory returnData) = address(token).call( // This creates a low level call to the token abi.encodePacked( // This encodes the function to call and the parameters to pass to that function token.transferFrom.selector, // This is the function identifier of the function we want to call abi.encode(_from, _to, _amount) // This encodes the parameter we want to pass to the function ) ); if (success) { // transferFrom completed successfully (did not revert) (returnedBool, returnedAmount) = abi.decode(returnData, (bool, uint256)); } else { // transferFrom reverted. However, the complete tx did not revert and we can handle the case here. // I will emit an event here to show this emit TransferFromFailed(_amount); } } }
The else
block in the above example works as the catch block. It is run when an exception is triggered in the low-level call. The example calls a function of an external contract(token), but you can use the same trick to do calls to the same contract by calling address(self)
instead of address(externalContract)
. Just wrap the “dangerous” code inside a function and create an external low-level call to it. This self calling makes it behave more like ‘Try Catch’ in other languages. There is no proper way to do error handling or exception handling in Solidity but this is as close as it gets.
Now, go ahead and give it a shot! The sample code can be found in https://github.com/maxsam4/try-catch-solidity. If you have any questions, feel free to drop a comment or reach out to me via social media/email.