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.