以太坊合约错误处理
Solidity
使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。当子调用中发生异常时,它们会自动“冒泡”(即异常被重新抛出),除非它们在try/catch
语句中被捕获。但是如果是在send
和低级别函数(call
, delegatecall
和staticcall
)调用里发生异常时,他们会返回 false(第一个返回值) 而不是冒泡异常。
注意:根据 EVM 的设计,如果被调用的地址不存在,低级别函数call
,delegatecall
和staticcall
的第一个返回值同样是 true。如果需要,请在调用之前检查账号的存在性。
异常可以包含传递给调用者的数据。该数据由一个 4 字节函数选择器和随后的 ABI 编码数据组成。异常可以以错误实例的形式返回错误数据给调用者。目前,Solidity
支持两种错误函数签名:Error(string) 和 Panic(uint256)。Error
用于“常规”错误条件,而Panic
用于无错误代码中意外出现的错误。
assert
检查异常 (Panic)
Assert
只能用于测试内部错误和检查不变量。正常运行的代码不应该造成Panic
,即使是在无效的外部输入上也是如此。如果发生这种情况,那么您的合同中有一个错误,您应该修复它。在以下情况下会生成Panic
异常,提供的错误码编号,用来指示 Panic 的类型。
- 0x00:用于通用编译器插入的
Panic
。 - 0x01:如果你调用
assert
的参数(表达式)结果为 false。 - 0x11:在
unchecked { … }
外,如果算术运算结果向上或向下溢出。 - 0x12:如果你用零当除数做除法或模运算(例如:
5 / 0
或23 % 0
) - 0x21:如果你将一个太大的数或负数值转换为一个枚举类型。
- 0x22:如果你访问一个没有正确编码的存储类型字节数组。
- 0x31:如果在空数组上
.pop()
。 - 0x32:如果你访问 bytesN 数组(或切片)的索引太大或为负数。
- 0x41:如果你分配了太多的内内存或创建了太大的数组。
- 0x51:如果你调用了零初始化内部函数类型变量。
可以提供消息字符串给 require(第二个参数)
require
检查错误 (Error)
require
函数会创建一个 Error(string) 类型的错误或者没有数据的错误,并且require
函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。下列情况将会产生一个Error(string)
(或没有数据)的错误:
- 如果你调用
require
的参数(表达式)最终结果为 false。 - 如果你在不包含代码的合约上执行外部函数调用。
- 如果您使用
revert()
或revert("description")
。 - 如果你通过合约接收以太币,而又没有
payable
修饰符的公有函数(包括构造函数和 fallback 函数)。 - 如果你的合约通过公有
getter
函数接收Ether
。
错误“冒泡”
Solidity
会转发来自子(外部)调用的错误(如果提供),这意味着它可能会导致Error
或Panic
(或给出的任何其他内容):
- 如果
.transfer()
失败。 - 如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),除非使用低级操作
call
、send
、delegatecall
、callcode
或staticcall
的函数调用。低级操作不会抛出异常,而是通过返回 false 来指示失败。 - 如果你使用
new
关键字创建合约,但合约创建没有正确结束。
revert
revert
函数是另一个可以在代码块中处理异常的方法, 可以用来标记错误并回退当前的调用。revert
调用中还可以包含有关错误信息的参数,这个信息会被返回给调用者,并且产生一个Error(string)
错误。下边的例子展示了错误字符串如何使用 revert (等价于 require) :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
contract VendingMachine {
function buy(uint amount) payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 下边是等价的方法来做同样的检查:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 执行购买操作
}
}
如果直接提供错误原因字符串,则这两个语法是等效的。
提供的字符串将经过 ABI 编码 如果它调用 Error(string) 函数。在上边的例子里,会产生如下的十六进制错误返回值:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)
try/catch
外部调用的失败,可以通过try/catch
语句来捕获,如下:
pragma solidity ^0.6.0;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果错误超过 10 次,永久关闭这个机制
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used。
errorCount++;
return (0, false);
}
}
}
try 关键字后面必须跟一个表示外部函数调用或合约创建的表达式。表达式内部的错误不会被捕获(例如,如果它是一个还涉及内部函数调用的复杂表达式),只会在外部调用本身内部发生还原。后面的部分(可选)声明了与外部调用返回的类型匹配的返回变量。在没有错误的情况下,这些变量被分配并且合约的执行在第一个成功块内继续。如果到达成功块的末尾,则在块之后继续执行。
Solidity
根据错误类型支持不同类型的catch
块:
catch Error(string memory reason) { ... }
:如果错误是由revert("reasonString")
(或导致此类异常的内部错误)引起的,则执行此 catch 子句。require(false, "reasonString")
catch Panic(uint errorCode) { ... }
:如果错误是由panic
引起的,即失败assert
、被零除、无效数组访问、算术溢出等,则将运行此catch
子句。catch (bytes memory lowLevelData) { ... }
:如果错误签名与任何其他子句不匹配,如果在解码错误消息时出现错误,或者如果没有提供错误数据和异常,则执行此子句。在这种情况下,声明的变量提供对低级错误数据的访问。catch { ... }
:如果你对错误数据不感兴趣,你可以只使用(甚至作为唯一的catch
子句)而不是前面的子句。catch { ... }
调用合约失败背后的原因可能是多方面的。不要假设错误消息直接来自被调用的合约:错误可能发生在调用链的更深处,而被调用的合约只是转发了它。此外,这可能是由于气体不足的情况,而不是故意的错误情况:调用者始终在调用中保留至少 1 /64 的气体,因此即使被调用的合约耗尽气体,调用者还剩一些气。