Alert Source Discuss
Standards Track: ERC

ERC-7751: 对冒泡回滚进行包装

使用带有附加上下文的自定义错误处理冒泡回滚

Authors Daniel Gretzke (@gretzke), Sara Reynolds (@snreynolds), Alice Henshaw (@hensha256), Marko Veniger <marko.veniger@tenderly.co>, Hadrien Croubois (@Amxx)
Created 2024-08-06

摘要

本 ERC 提出了一种标准,用于使用专用的自定义错误处理以太坊智能合约中的冒泡回滚。该标准旨在通过允许传递伴随冒泡回滚的原始字节的附加上下文,来提高回滚原因的清晰度和可用性。WrappedError 自定义错误应该包装来自被调用合约的回滚,并为解析和处理诸如 Etherscan 或 Tenderly 之类的工具中的回滚提供一致的接口。

动机

目前,当一个智能合约调用另一个合约并且被调用的合约回滚时,回滚原因通常会冒泡并原样抛出。这会增加判断错误来自哪个上下文的难度。通过标准化使用带有附加上下文的自定义错误,可以提供更有意义和更具信息性的回滚原因。这将改善调试体验,并使开发者和诸如 Etherscan 之类的基础设施提供商更容易显示准确的堆栈跟踪。

规范

本文档中关键词“必须”、“禁止”、“需要”、“应当”、“不应当”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”按照 RFC 2119 和 RFC 8174 中的描述进行解释。

为了包装一个回滚,合约必须回滚并返回以下与签名 0x90bfb865 对应的错误:

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

其中:

  • target 是被调用并回滚的合约的地址。
  • selector 是被调用并回滚的函数的选择器。如果调用是没有任何数据的 ETH 转账,则选择器必须bytes4(0)
  • reason 是回滚原因的原始字节。
  • details 是关于回滚的可选附加上下文。在不需要额外上下文的情况下,details 字节可以是空的。 在具有额外上下文的情况下,details 字节必须是 ABI 编码的自定义错误,该错误在发出 WrappedError 错误的合约上声明。

原理

通过包含被调用的合约和函数、原始回滚字节以及附加上下文,开发者可以提供关于失败的更详细信息。 此外,通过标准化回滚的冒泡方式,还可以实现嵌套的冒泡回滚,其中可以递归地跟踪由不同合约抛出的多个回滚。 回滚也可以被诸如 Etherscan 和 Foundry 之类的工具解析和处理,以进一步增强智能合约交互的可读性和可调试性,并总体上促进更好的错误处理实践。

向后兼容性

此 ERC 不引入任何向后不兼容性。现有合约可以逐步采用此标准。

测试用例

// SPDX-License-Identifier: CC0-1.0
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));
        }
    }
}

参考实现

当捕获来自被调用合约的回滚时,调用合约应使用遵循上述约定的自定义错误进行回滚。

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));
        }
    }
}

安全注意事项

智能合约可能会丢弃或有目的地抑制沿回滚链冒泡的回滚。此外,智能合约也可能谎报或错误地报告包装的回滚,因此不能保证信息的准确性。

版权

版权和相关权利已通过 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: 对冒泡回滚进行包装," Ethereum Improvement Proposals, no. 7751, August 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7751.