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.