Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7751: Wrapping of bubbled up reverts

Handling bubbled up reverts using custom errors with additional context

Authors Daniel Gretzke (@gretzke), Sara Reynolds (@snreynolds), Alice Henshaw (@hensha256), Marko Veniger <marko.veniger@tenderly.co>, Hadrien Croubois (@Amxx)
Created 2024-08-06
Discussion Link https://ethereum-magicians.org/t/erc-7751-wrapping-of-bubbled-up-reverts/20740

Abstract

This ERC proposes a standard for handling bubbled up reverts in Ethereum smart contracts using a dedicated custom error. This standard aims to improve the clarity and usability of revert reasons by allowing additional context to be passed alongside the raw bytes of the bubbled up revert. The WrappedError custom error should wrap reverts from called contracts and provide a consistent interface for parsing and handling reverts in tools like Etherscan or Tenderly.

Motivation

Currently, when a smart contract calls another and the called contract reverts, the revert reason is usually bubbled up and thrown as is. This can make it more difficult to tell which context the error came from. By standardizing the use of custom errors with additional context, more meaningful and informative revert reasons can be provided. This will improve the debugging experience and make it easier for developers and infrastructure providers like Etherscan to display accurate stack traces.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

In order to wrap a revert, a contract MUST revert with the following error that corresponds to the following signature 0x90bfb865:

error WrappedError(address target, bytes4 selector, bytes reason, bytes details);

Where:

  • target is the address of the called contract that reverted.
  • selector is the selector of the called function that reverted. If the call was an ETH transfer without any data, the selector MUST be bytes4(0)
  • reason is the raw bytes of the revert reason.
  • details is optional additional context about the revert. In cases where no additional context is needed, the details bytes can be empty. In cases with additional context, the details bytes MUST be an ABI encoded custom error declared on the contract that emits the WrappedError error.

Rationale

By including the called contract and function, raw revert bytes and additional context, developers can provide more detailed information about the failure. Additionally, by standardizing the way reverts are bubbled up, it also enables nested bubbled up reverts where multiple reverts thrown by different contracts can be followed recursively. The reverts can also be parsed and handled by tools like Etherscan and Foundry to further enhance the readability and debuggability of smart contract interactions, as well as facilitating better error handling practices in general.

Backwards Compatibility

This ERC does not introduce any backwards incompatibilities. Existing contracts can adopt this standard incrementally.

Test Cases

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

contract Token {
    mapping(address => uint256) public balanceOf;

    event  Transfer(address indexed sender, address indexed recipient, uint amount);

    function transfer(address to, uint256 amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
}

contract Vault {
    Token token;

    error WrappedError(address target, bytes4 selector, bytes reason, bytes details);
    error ERC20TransferFailed(address recipient);


    constructor(Token token_) {
        token = token_;
    }

    function withdraw(address to, uint256 amount) external {
        // logic
        try token.transfer(to, amount) {} catch (bytes memory error) {
            revert WrappedError(address(token), token.transfer.selector, error, abi.encodeWithSelector(ERC20TransferFailed.selector, to));
        }
    }
}

contract Router {
    Vault vault;

    error WrappedError(address target, bytes4 selector, bytes reason, bytes details);

    constructor(Vault vault_) {
        vault = vault_;
    }

    function withdraw(uint256 amount) external {
        // logic
        try vault.withdraw(msg.sender, amount) {} catch (bytes memory error) {
            revert WrappedError(address(vault), vault.withdraw.selector, error, "");
        }
    }
}

contract Test {
    function test_BubbledNestedReverts(uint256 amount) external {
        Token token = new Token();
        Vault vault = new Vault(token);
        Router router = new Router(vault);

        try router.withdraw(amount) {} catch (bytes memory thrownError) {
            bytes memory expectedError = abi.encodeWithSelector(
                Router.WrappedError.selector, address(vault), vault.withdraw.selector, abi.encodeWithSelector(
                    Vault.WrappedError.selector,
                    address(token),
                    token.transfer.selector,
                    abi.encodeWithSignature("Error(string)", "insufficient balance"),
                    abi.encodeWithSelector(Vault.ERC20TransferFailed.selector, address(this))
                ), ""
            );
            assert(keccak256(thrownError) == keccak256(expectedError));
        }
    }
}

Reference Implementation

When catching a revert from a called contract, the calling contract should revert with a custom error following the above conventions.

contract Foo {

    error WrappedError(address target, bytes4 selector, bytes reason, bytes details);
    error MyCustomError(uint256 x);

    function foo(address to, bytes memory data) external {
        // logic
        (bool success, bytes memory returnData) = to.call(data);
        if (!success) {
            revert WrappedError(to, bytes4(data), returnData, abi.encodeWithSelector(MyCustomError.selector, 42));
        }
    }
}

Security Considerations

Smart contracts could either drop or purposefully suppress the bubbled up reverts along the revert chain. Additionally, smart contracts may also lie or incorrectly report the wrapped reverts, so the information is not guaranteed to be accurate.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Daniel Gretzke (@gretzke), Sara Reynolds (@snreynolds), Alice Henshaw (@hensha256), Marko Veniger <marko.veniger@tenderly.co>, Hadrien Croubois (@Amxx), "ERC-7751: Wrapping of bubbled up reverts [DRAFT]," Ethereum Improvement Proposals, no. 7751, August 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7751.