Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。
如果异常在子调用发生,那么异常会自动 冒泡
到顶层(异常会重新抛出)。除非它们在 try/catch
语句中被捕获。 但是如果是在 send
和底层函数(low-level functions
)如:call
, delegatecall
和 staticcall
的调用里发生异常时, 他们会返回 false
(第一个返回值) 而不是 冒泡异常
。
注意:根据 EVM 的设计,如果被调用的地址不存在,底层函数
call
,delegatecall
和staticcall
也或第一个返回值同样是true
。 如果需要,请在调用之前检查账号的存在性。
外部调用的异常可以被 try/catch
捕获。
异常包含的错误数据,以错误实例
的形式传递回调用者。内置的 Error(string)
和 Panic(uint256)
被特殊函数使用,如下所述。Error
用于“常规”错误条件,而 Panic
用于不应该出现在无错误代码中的错误。
用 assert
检查异常(Panic
) 和 require
检查错误(Error
)
函数assert
和 require
可用于检查条件并在条件不满足时抛出异常。
该assert
函数创建一个类型的错误Panic(uint256)
。在某些情况下,编译器会创建相同的错误,如下所示。
assert
函数只能用于测试内部错误,检查不变量。
正常的函数代码永远不会产生 Panic
, 甚至是基于一个无效的外部输入时。
如果发生了,那就说明出现了一个需要你修复的 bug
。如果使用得当,语言分析工具可以识别出那些会导致 Panic
的 assert
条件和函数调用。
下列情况将会产生一个Panic异常: 提供的错误码编号,用来指示Panic的类型。
- 0x01: 如果你调用
assert
的参数(表达式)结果为false
。 - 0x11: 在
unchecked { … }
外,如果算术运算结果向上或向下溢出。 - 0x12; 如果你用零当除数做除法或模运算(例如 或 )。
5 / 0
23 % 0
- 0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。
- 0x22: 如果你访问一个没有正确编码的存储
byte
数组. - 0x31: 如果在空数组上
.pop()
。 - 0x32: 如果你访问
bytesN
数组(或切片)的索引太大或为负数。
(例如:x[i]
而 或 ).i >= x.length
i < 0
- 0x41: 如果你分配了内存过多或创建了的数组太大。
- 0x51: 如果调用内部函数类型的零初始化变量。
require
函数要么创建一个Error(string)
类型的错误,要么创建创建一个没有任何数据的错误,并且require
函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。
下列情况将会产生一个 Error(string)
(或没有数据)的错误:
- 如果你调用
require
的参数(表达式)最终结果为false
。 - 如果你使用
revert()
或revert("description")
- 如果你在不包含代码的合约上执行外部函数调用。
- 如果你通过合约接收以太币,而又没有
payable
修饰符的公有函数(包括构造函数和 fallback 函数)。 - 如果你的合约通过公有 getter 函数接收 Ether 。
对于以下情况,来自外部调用(如果提供)的错误数据将被转发。这意味着它可以引起 Error
或 Panic
(或任何其他给出的):
- 如果
.transfer()
失败。 - 如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),不包括使用底层操作
call
,send
,delegatecall
,callcode
或staticcall
的函数调用。底层操作不会抛出异常,而通过返回false
来指示失败。 - 如果你使用
new
关键字创建合约,但合约创建没有正确完成
。
如果您不向
require
提供字符串参数,它将返回空错误数据,甚至不包括错误选择器。
可以给 require
提供一个消息字符串,而 assert
不行。
在下例中,你可以看到如何轻松使用require
检查输入条件以及如何使用 assert
检查内部错误。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// 因为转账失败时抛出一个异常,并且不能回调到这里,所以应该没有办法让我们仍然有一半的钱。
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
在内部, Solidity 对异常执行回退操作(指令 0xfd
),从而让 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 我们想要保持交易的原子性,最安全的动作是回退所有的更改,并让整个交易(或至少调用)没有任何新影响。
在这两种情况下,调用者都可以使用 try/catch
来应对此类失败,但是调用者中的更改将始终被还原。
请注意: 在0.8.0 之前,
Panic
异常使用invalid
指令,其会消耗了所有可用的gas
。 使用require
的异常,在Metropolis
版本之前会消耗所有的gas
。
revert语句/函数
- 可以使用
revert语句
和revert函数
来触发直接还原。 -
revert语句
接受一个自定义错误
作为不带括号的直接参数:revert CustomError(arg1, arg2);
- 出于向后兼容的原因,还有
revert()函数
,它使用圆括号并接受一个字符串:revert();
revert(“description”);
- 错误数据将被传递回调用者,并可以在那里捕获。使用
revert()
导致不带任何错误数据的恢复,而revert("description")
将创建一个Error(string)
错误。 - 使用
自定义错误实例
通常比使用字符串描述
便宜得多,因为你可以使用错误名称来描述它,该名称仅用4个字节编码。更长的描述可以通过NatSpec
提供,而不会产生任何费用。
下边的例子展示了错误字符串如何使用revert
(等价于require
) :
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 另一种方法
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 执行购买操作...
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
如果直接提供错误原因字符串,则这两个语法是等效的,根据开发人员的偏好选择。
注意:这个
require
函数的求值方式与任何其他函数一样。这意味着在执行函数本身之前会计算所有参数。特别是,在require(condition, f())
该函数f
将被执行,即使在condition
是真的。
这里提供的字符串将经过 ABI 编码
如果它调用 Error(string)
函数。 在上边的例子里,revert("Not enough Ether provided.");
会产生如下的十六进制错误返回值:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)
调用者可以使用try
/catch
检索所提供的消息,如下所示。
revert()
之前有一个同样用法的throw
关键字,它在v0.4.13
版本弃用,在v0.5.0
移除。
try/catch
外部调用的失败,可以通过 try/catch
语句来捕获,如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
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*/) {
//如果在getData内部调用 revert,并提供了一个原因字符串,则执行此操作。
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// 这个是在Panic情况下执行,例如一个严重的错误,除以0或溢出。
//错误代码可以用来确定错误的类型。
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// 这是在使用revert()时执行的
errorCount++;
return (0, false);
}
}
}
try
关键字后面必须跟一个表示外部函数调用,或合约创建的表达式(new ContractName())
。
表达式内部的错误不会被捕获(例如,如果它是一个包含内部函数调用的复杂表达式),只会在外部调用本身内部发生还原。
这个 returns
后面的部分(可选)声明与外部调用返回的类型匹配的返回变量。
在没有错误的情况下,这些变量被赋值,并在第一个成功块内继续执行合约。如果到达成功块的末尾,则在 catch
块之后继续执行。
Solidity支持不同类型的 catch
块,具体取决于错误类型:
-
catch Error(string memory reason){…}
:如果错误是由revert("reasonString")
或require(false, "reasonString")
(或引起此类异常的内部错误)引起的,则执行该catch
子句。 -
catch Panic(uint errorCode){…}
:如果错误是由Panic
引起的,即错误的assert
、除0、无效的数组访问、算术溢出等,则将运行该catch
子句。 -
catch (bytes memory lowLevelData){…}
:如果错误签名与任何其他子句不匹配,如果在解码错误消息时出现错误,或者异常中没有提供错误数据,则执行该子句。在这种情况下,声明的变量提供了对底层错误数据的访问。 -
catch { ... }
:如果你对错误数据不感兴趣,你可以使用catch{…}
(即使是唯一的catch
子句)而不是前面的子句。
计划在未来支持其他类型的错误数据。字符串Error
和Panic
当前按原样解析,不作为标识符处理。
为了捕获所有的错误情况,你至少需要有catch{…}
或子句catch (bytes memory lowLevelData){…}
。
returns
和catch
子句中声明的变量仅在后面的块中的作用域中。
注意:如果在对
try/catch
语句中的返回数据进行解码期间发生错误,则会d导致当前执行的合约出现异常,因此,catch
子句中不会捕获该异常。如果在catch Error(string memory reason)
的解码过程中出现错误,并且存在底层catch
子句,则会在那里捕获该错误。
注意:如果执行到达
catch
代码块,则外部调用的状态更改效果已恢复。如果执行达到成功块,效果不会恢复。如果效果已恢复,则执行要么在catch
块中继续,要么try/catch
语句本身的执行恢复(例如,由于如上所述的解码失败,或者由于没有提供底层catch
子句)。
注意:失败调用的原因可能是多方面的。不要假设错误消息直接来自被调用的合约:错误可能发生在调用链的更深处,而被调用的合约只是转发了它。这可能是由于
gas
不足的情况,而不是故意的错误情况:调用者始终在调用中保留至少 1/64 的gas
,因此即使被调用的合约耗尽gas
,调用者还剩一些gas
。