4

I want to get the revert reason of a failed call and then use that revert reason in the same on-chain transaction. Is this possible?

5 Answers 5

15

I modified shanes answer:

function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {
    // If the _res length is less than 68, then the transaction failed silently (without a revert message)
    if (_returnData.length < 68) return 'Transaction reverted silently';

    assembly {
        // Slice the sighash.
        _returnData := add(_returnData, 0x04)
    }
    return abi.decode(_returnData, (string)); // All that remains is the revert string
}

It seems to work and that way we dont need the extra library

1
7

Yes, this is possible. In short, you can use the following code:

(bool success, bytes memory res) = _destination.call.gas(_gasLimit).value(_value)(_data);

// Get the revert message of the call and revert with it if the call failed
if (!success) {
    string memory _revertMsg = _getRevertMsg(res);
}
/// @dev Get the revert message from a call
/// @notice This is needed in order to get the human-readable revert message from a call
/// @param _res Response of the call
/// @return Revert message string
function _getRevertMsg(bytes memory _res) internal pure returns (string memory) {
    // If the _res length is less than 68, then the transaction failed silently (without a revert message)
    if (_res.length < 68) return 'Transaction reverted silently';
    bytes memory revertData = _res.slice(4, _res.length - 4); // Remove the selector which is the first 4 bytes
    return abi.decode(revertData, (string)); // All that remains is the revert string
}

Breaking that down a little, all unsuccessful calls will return an erroneous response with the signature Error(string) (0x08c379a). With that in mind, you can simply treat it as any other signature.

if (_res.length < 68) return 'Transaction reverted silently';

The above line will look at the response and return Transaction reverted silently if there was no revert reason.

bytes memory revertData = _res.slice(4, _res.length - 4);

The above line will derive the revert reason string from the response in the format bytes format. As a note, the slice method is from this BytesLib.sol file.

return abi.decode(revertData, (string));

The above line will decode the data into a string, which is the desired output.

Note: For full disclsure, I am a cofounder of Authereum and those code blocks are from the Authereum contracts that I helped write.

3
  • 2
    Thanks for the explination. Where does 68 come from? 64 bytes + 4 for the signature? Why at a minimum will a revert lead to more than 64 bytes of data? Aug 20, 2021 at 1:31
  • 1
    I would also like to know understand this.
    – Leeren
    Sep 30, 2021 at 3:55
  • That's because of the way dynamic data is encoded. ABI for solidity reverts is Error(string), so the first 4 bytes will be selector, next 32 bytes will be string offset, next 32 bytes will be string length and all the remaining bytes will be string data. See docs.soliditylang.org/en/latest/… Nov 1, 2021 at 4:59
6

While the answer from @Paddy looks correct, it screws up the length of _returnData before passing it to abi.decode. The screwed up length is much bigger than it should be. Hopefully abi.decode will just ignore extra bytes after the decoded string, but it would be better not to rely on this, so here is cleaner version:

function extractRevertReason (bytes memory revertData)
internal pure returns (string memory reason) {
    uint l = revertData.length;
    if (l < 68) return "";
    uint t;
    assembly {
        revertData := add (revertData, 4)
        t := mload (revertData) // Save the content of the length slot
        mstore (revertData, sub (l, 4)) // Set proper length
    }
    reason = abi.decode (revertData, (string));
    assembly {
        mstore (revertData, t) // Restore the content of the length slot
    }
}
3

If only interested in bubbling up the revert then could do:

(success, returnData) = _target.call{value: _value}(_data);
if (!success) {
    assembly {
        revert(add(returnData, 32), returnData)
    }
}

Assembly's revert takes the memory location of the beginning of the revert data and its length. returnData points to the beginning of bytes memory which starts with a length prefix 32 bytes long. After that prefix the actual revert data follows.

1

There are many cases of reverts:

A generic way to bubble up the error can be:

// Taken from https://github.com/superfluid-finance/protocol-monorepo/blob/dev/packages/ethereum-contracts/contracts/libs/CallUtils.sol

    /// @dev Bubble up the revert from the returnedData (supports Panic, Error & Custom Errors)
    /// @notice This is needed in order to provide some human-readable revert message from a call
    /// @param returnedData Response of the call
    function revertFromReturnedData(bytes memory returnedData) internal pure {
        if (returnedData.length < 4) {
            // case 1: catch all
            revert("CallUtils: target revert()");
        } else {
            bytes4 errorSelector;
            assembly {
                errorSelector := mload(add(returnedData, 0x20))
            }
            if (errorSelector == bytes4(0x4e487b71) /* `seth sig "Panic(uint256)"` */) {
                // case 2: Panic(uint256) (Defined since 0.8.0)
                // solhint-disable-next-line max-line-length
                // ref: https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require)
                string memory reason = "CallUtils: target panicked: 0x__";
                uint errorCode;
                assembly {
                    errorCode := mload(add(returnedData, 0x24))
                    let reasonWord := mload(add(reason, 0x20))
                    // [0..9] is converted to ['0'..'9']
                    // [0xa..0xf] is not correctly converted to ['a'..'f']
                    // but since panic code doesn't have those cases, we will ignore them for now!
                    let e1 := add(and(errorCode, 0xf), 0x30)
                    let e2 := shl(8, add(shr(4, and(errorCode, 0xf0)), 0x30))
                    reasonWord := or(
                        and(reasonWord, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000),
                        or(e2, e1))
                    mstore(add(reason, 0x20), reasonWord)
                }
                revert(reason);
            } else {
                // case 3: Error(string) (Defined at least since 0.7.0)
                // case 4: Custom errors (Defined since 0.8.0)
                uint len = returnedData.length;
                assembly {
                    revert(add(returnedData, 32), len)
                }
            }
        }
    }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.