从Solidity v0.8.4 开始,有一种便捷且节省 gas 的方法可以通过使用自定义错误来向用户解释操作失败的原因。到目前为止,您已经可以使用字符串来提供有关失败的更多信息(例如,revert("Insufficient funds.");),但它们相当昂贵,尤其是在部署成本方面,并且难以在其中使用动态信息。
自定义错误使用 error 语句定义,该语句可以在合约内部和外部使用(包括接口和库)。
示例
以下合约显示了错误的使用示例
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; error Unauthorized(); contract VendingMachine { address payable owner = payable(msg.sender); function withdraw() public { if (msg.sender != owner) revert Unauthorized(); owner.transfer(address(this).balance); } // ... }
错误的语法类似于事件。它们必须与 revert 语句 一起使用,该语句会导致当前调用中的所有更改被回滚并将错误数据传递回调用方。不支持将错误与 require 一起使用(见下文)。
带参数的错误
也可以使用带参数的错误。例如,
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; /// Insufficient balance for transfer. Needed `required` but only /// `available` available. /// @param available balance available. /// @param required requested amount to transfer. error InsufficientBalance(uint256 available, uint256 required); contract TestToken { mapping(address => uint) balance; function transfer(address to, uint256 amount) public { if (amount > balance[msg.sender]) // Error call using named parameters. Equivalent to // revert InsufficientBalance(balance[msg.sender], amount); revert InsufficientBalance({ available: balance[msg.sender], required: amount }); balance[msg.sender] -= amount; balance[to] += amount; } // ... }
错误数据将与函数调用的 ABI 编码相同,即 abi.encodeWithSignature("InsufficientBalance(uint256,uint256)", balance[msg.sender], amount)。
我们希望框架能够直接支持自定义错误。以下是如何使用当前版本的 ethers.js 解码错误数据的示例
import { ethers } from 'ethers' // As a workaround, we have a function with the // same name and parameters as the error in the abi. const abi = [ 'function InsufficientBalance(uint256 available, uint256 required)', ] const interface = new ethers.utils.Interface(abi) const error_data = '0xcf479181000000000000000000000000000000000000' + '0000000000000000000000000100000000000000000000' + '0000000000000000000000000000000000000100000000' const decoded = interface.decodeFunctionData( interface.functions['InsufficientBalance(uint256,uint256)'], error_data ) // Contents of decoded: // [ // BigNumber { _hex: '0x0100', _isBigNumber: true }, // BigNumber { _hex: '0x0100000000', _isBigNumber: true }, // available: BigNumber { _hex: '0x0100', _isBigNumber: true }, // required: BigNumber { _hex: '0x0100000000', _isBigNumber: true } // ] console.log( 'Insufficient balance for transfer. ' + `Needed ${decoded.required.toString()} but only ` + `${decoded.available.toString()} available.` ) // Insufficient balance for transfer. Needed 4294967296 but only 256 available.
编译器包含合约可以发出的所有错误,这些错误包含在合约的 ABI-JSON 中。请注意,这将不包括通过外部调用转发的错误。类似地,开发人员可以为错误提供 NatSpec 文档,这些文档将成为用户和开发人员文档的一部分,并且可以免费详细解释错误。
使用错误数据时请谨慎,因为其来源未被跟踪。错误数据默认情况下会通过外部调用的链向上冒泡,这意味着合约可能会转发在其直接调用的任何合约中均未定义的错误。此外,任何合约都可以通过返回与错误签名匹配的数据来伪造任何错误,即使该错误在任何地方都未定义。
目前,Solidity 中没有方便的方法来捕获错误,但这已在计划中,可以在问题 #11278 中跟踪进度。此外,require(condition, "error message") 应转换为 if (!condition) revert CustomError()。
深入了解错误
在 第一个示例 中,revert Unauthorized(); 等效于以下 Yul 代码
let free_mem_ptr := mload(64) mstore(free_mem_ptr, 0x82b4290000000000000000000000000000000000000000000000000000000000) revert(free_mem_ptr, 4)
这与名称为 Unauthorized() 的函数调用的 ABI 编码相同。这里 0x82b42900 是 Unauthorized() 的“选择器”。相比之下,使用 revert 字符串,即 revert("Unauthorized"); 会导致以下 Yul 代码
let free_mem_ptr := mload(64) mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000) mstore(add(free_mem_ptr, 4), 32) mstore(add(free_mem_ptr, 36), 12) mstore(add(free_mem_ptr, 68), "Unauthorized") revert(free_mem_ptr, 100)
这里 0x08c379a0 是 Error(string) 的“选择器”。在此比较中,可以看出自定义错误降低了部署和运行时 gas 成本。请注意,运行时 gas 成本仅在满足 revert 条件时才相关。