Retrieve revert reason of contract invocation with Web3j
January 19, 2019

Solidity uses functions revert, require and assert as state-reverting exceptions to handle errors. revert is the generic one, assert should only be used to test for internal errors and invariants, and require should be used to ensure valid conditions, such as inputs or contract state variables or return values from external contracts calls.

Since version 0.4.22 of Solidity it is possible to provide an optional reason argument with the revert and require function. That means that functions such as

function myFunction(uint256 input) public view returns (uint256) {
    require(input >= 5);
    return input * input - 25;
}

can now be enriched like

function myFunction(uint256 input) public view returns (uint256) {
    require(input >= 5, "myFunction only accepts arguments which are greater than or equal to 5");
    return input * input - 25;
}

This looks like a nice enhancement, but how can I use Web3j to actually check if an eth_call to myFunction was reverted and what the accompanying reason was?

As explained in the pull request that added the Revert with reason functionality, the reason string is encoded as a function call to Error(string), including the function selector. That means that when the above function reverts because of the require function, the result will be:

0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000476d7946756e6374696f6e206f6e6c79206163636570747320617267756d656e747320776869636820617265206772656174686572207468616e206f7220657175616c20746f203500000000000000000000000000000000000000000000000000

A little formatting gives us:

0x08c379a0                                                       // Function selector
0000000000000000000000000000000000000000000000000000000000000020 // Offset of string return value
0000000000000000000000000000000000000000000000000000000000000047 // Length of string return value (the revert reason)
6d7946756e6374696f6e206f6e6c79206163636570747320617267756d656e74 // first 32 bytes of the revert reason
7320776869636820617265206772656174686572207468616e206f7220657175 // next 32 bytes of the revert reason
616c20746f203500000000000000000000000000000000000000000000000000 // last 7 bytes of the revert reason

If we use Web3j's EthCall when calling the function myFunction, this revert reason could be extracted like:

public Optional<String> getRevertReason(EthCall ethCall) {
    String errorMethodId = "0x08c379a0"; // Numeric.toHexString(Hash.sha3("Error(string)".getBytes())).substring(0, 10)
    List<TypeReference<Type>> revertReasonTypes = Collections.singletonList(TypeReference.create((Class<Type>) AbiTypes.getType("string")));

    if (!ethCall.hasError() && ethCall.getValue() != null && ethCall.getValue().startsWith(errorMethodId)) {
        String encodedRevertReason = ethCall.getValue().substring(errorMethodId.length());
        List<Type> decoded = FunctionReturnDecoder.decode(encodedRevertReason, revertReasonTypes);
        Utf8String decodedRevertReason = (Utf8String) decoded.get(0);
        return Optional.of(decodedRevertReason.getValue());
    }
    return Optional.empty();
}

Which will return an Optional of the string "myFunction only accepts arguments which are greater than or equal to 5".