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 / 023 % 0 - 0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。
- 0x22: 如果你访问一个没有正确编码的存储
byte数组. - 0x31: 如果在空数组上
.pop()。 - 0x32: 如果你访问
bytesN数组(或切片)的索引太大或为负数。
(例如:x[i]而 或 ).i >= x.lengthi < 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。


