Solidity 如果遇到异常错误,是通过回退状态的方式来进行处理。发生异常时,会撤消当前调用和所有子调用改变的状态变量,同时给调用者返回一个错误标识。
调用者调用某个函数方法,要么成功修改了所有状态变量,要么遇到异常不修改任何状态变量,不存在成功修改部分变量的情况,
Solidity 提供了 require 、assert 和 revert 来处理异常。同时可以使用 error
关键字来实现错误。
跟用错误字符串相比, error 更便宜并且允许你编码额外的数据,还可以用 NatSpec
为用户去描述错误。
Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。
如果异常在子调用发生,那么异常会自动冒泡到顶层(例如:异常会重新抛出),除非他们在 try/catch
语句中捕获了错误。 但是如果是在 send
和 低级 call
, delegatecall
和 staticcall
的调用里发生异常时, 他们会返回 false
(第一个返回值) 而不是冒泡异常。
警告注意:根据 EVM 的设计,如果被调用的地址不存在,低级别函数 call
, delegatecall
和 staticcall
第一个返回值同样是 true
。 如果需要,请在调用之前检查账号的存在性。
异常可以包含错误数据,以 error 示例 的形式传回给调用者。 内置的错误 Error(string)
和 Panic(uint256)
被作为特殊函数使用,下面将解释。 Error
用于 “常规” 错误条件,而 Panic
用于在(无 bug)代码中不应该出现的错误。
函数 assert 和 require 可用于检查条件并在条件不满足时抛出异常。
require 用来严查某些条件,如果不满足这些雕件,就会回退所有状态的变化。
require(condition[, 'Something bad happened'])
如果条件不满足则撤销状态更改 ,用于检查由输入或者外部组件引起的错误。可以同时提供一个错误消息。
- require 函数常常用来检查输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。
- require 可以有返回值,例如:
require(condition, 'Something bad happened');
。 - require 的返回值不宜过长,因为返回信息需要消耗 gas。
- 备注:在例子 2 测试中,并没有证明长度越长,消耗的 gas 越多。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo {
uint256 public amount = 0;
function test(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
require(_x > 20); // _x <= 10 时候会报错
}
}
注解 require
是一个像其他函数一样可被执行的函数。意味着,所有的参数在函数被执行之前就都会被执行。 尤其,在
require(condition, f())
里,函数 f
会被执行,即便 condition
为 True .
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo1 {
// 21611 gas
function test1(uint256 _x) external pure {
require(
_x < 10,
"My error info 1 balalaba balalaba balalaba balalaba balalaba "
);
}
}
contract Demo2 {
// 21611 gas
function test2(uint256 _x) external pure {
require(_x < 10, "Error");
}
}
contract Demo3 {
// 21611 gas
function test3(uint256 _x) external pure {
require(_x < 10, "error");
}
}
- 验证用户输入,例如:
require(input_var>100)
- 验证外部合约的调用结果,例如:
require(external.send(amount))
- 在执行状态更改操作之前验证状态条件,例如:
require(block.number > 49999)
或require(balance[msg.sender]>=amount)
- require() 语句的失败报错,应该被看作一个正常的判断语句流程不能通过的事件。
一般来说,使用 require()
的频率更多,通常应用于函数的开头和函数修改器内。
一句话: require() 函数用于检测输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。
assert(bool condition)
如果不满足条件,则会导致 Panic 错误,则撤销状态更改 - 用于检查内部错误。
assert()
与 require()
语句都需要满足括号中的条件,才能进行后续操作,若不满足则抛出错误。
- assert:断言,不能包括报错信息的。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo {
uint256 public amount = 0;
function test1(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
assert(amount == _x); // 必须等于_x,否则抛出错误
}
function test2(uint256 _x) external {
require(_x < 10, "My error info 1"); // _x >= 10 时候会报错
amount = _x;
assert(amount == 8); // 必须等于8,否则抛出错误
}
}
- 检查溢出
- 检查不变量
- 更改后验证状态
- 预防永远不会发生的情况
- assert()语句的失败报错,意味着发生了代码层面的错误事件,很大可能是合约中有一个 bug 需要修复。
- 也可以智能合约写测试。
一般来说,使用 assert()
的频率较少,通常用于函数的结尾。基本上,require()
应该用于检查条件,而 assert()
只是为了防止发生任何非常糟糕的事情。
assert 函数会创建一个 Panic(uint256)
类型的错误。同样的错误在以下列出的特定情形会被编译器创建。
assert 函数应该只用于测试内部错误,检查不变量,正常的函数代码永远不会产生 Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 assert 条件和函数调用。
下列情况将会产生一个 Panic 异常: 错误数据会提供的错误码编号,用来指示 Panic 的类型:
- 0x00: 用于常规编译器插入的 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: 如果你调用了零初始化内部函数类型变量。
语法: revert([string memory reason])
- 使用 revert:抛出错误,它使用圆括号接受一个字符串:语句将一个自定义的错误作为直接参数,没有括号:
revert(); revert("description");
- 使用
revert()
会触发一个没有任何错误数据的回退,而revert("description")
会产生一个Error(string)
错误。 - 使用 revert:触发自定义错误 ·
revert CustomError(arg1, arg2);
- 可以接收参数,方便判断。比如可以传入
msg.sender
/ 函数参数 等
- 可以接收参数,方便判断。比如可以传入
终止运行并撤销状态更改。可以同时提供一个解释性的字符串。
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ErrorDemo {
function testRevert(uint256 _x) external pure {
if (_x > 10) {
revert("_x > 10");
}
}
// 自定义错误
error MyError(address call, uint256 _i);
function testCustomError(uint256 _x) external view {
if (_x > 10) {
revert MyError(msg.sender, _x);
}
}
}
只要参数没有额外的附加效果,使用 if (!condition) revert(...);
和 require(condition, ...);
是等价的,例如当参数是字符串的情况。
require(false)
编译为0xfd
,这是revert()
的操作码,所以会退还所有剩余的 gas,同时可以返回一个自定义的报错信息。assert(false)
编译为0xfe
,这是一个无效的操作码,所以会消耗掉所有剩余的 gas,并恢复所有的操作。require
的 gas 消耗要小于assert
,而且可以有返回值,使用更为灵活。
错误信息:
require
函数可以创建无错误提示的错误,也可以创建一个 Error(string)
类型的错误。 require
函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。
当前不可以使用混合使用 require 和自定义错误,而是需要使用 if (!condition) revert CustomError();
。
下列情况将会产生一个 Error(string)
(或无错误提示)的错误:
- 如果你调用
require(x)
,而x
结果为false
。 - 如果你使用
revert()
或者revert("description")
。 - 如果你在不包含代码的合约上执行外部函数调用。
- 如果你通过合约接收以太币,而又没有
payable
修饰符的公有函数(包括构造函数和 fallback 函数)。 - 如果你的合约通过公有 getter 函数接收 Ether 。
在下面的情况下,来自外部调用的错误数据(如果提供的话)被转发,这意味可能Error
或 Panic
都有可能触发。
- 如果
.transfer()
失败。 - 如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了
gas,没有匹配函数,或者本身抛出一个异常),不包括使用低级别
call
,send
,delegatecall
,callcode
或staticcall
的函数调用。低级操作不会抛出异常,而通过返回false
来指示失败。 - 如果你使用
new
关键字创建合约,但合约创建没有正确结束。
你可以选择给 require
提供一个消息字符串,但 assert
不行。
如果你没有为 require
提供一个字符串参数,它会用空错误数据进行 revert, 甚至不包括错误选择器。
在下例中,你可以看到如何轻松使用 require
检查输入条件以及如何使用
assert
检查内部错误.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// 由于转账函数在失败时抛出异常并且不会调用到以下代码,因此我们应该没有办法检查仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
在内部, Solidity 对异常执行回退操作(指令 0xfd
),从而让 EVM 回退对状态所做的所有更改。回退的原因是无法安全地继续执行,因为无法达到预期的结果。 因为我们想要保持交易的原子性,最安全的动作是回退所有的更改,并让整个交易(或至少调用)没有任何新影响。
在这两种情况下,调用者都可以使用 try
/ catch
来应对此类失败,但是被调用函数的更改将始终被还原。
请注意, 在 0.8.0 之前,Panic 异常使用 invalid
指令,其会消耗了所有可用的 gas。 使用 require
的异常,在 Metropolis 版本之前会消耗所有的 gas。
以下三个语句的功能完全相同:
// revert
if(msg.sender != owner) {
revert();
}
// require
require(msg.sender == owner);
// assert
assert(msg.sender == owner);
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ErrorDemo {
function testRequire(uint256 _x) external pure {
require(_x > 10, "_x > 10");
}
function testRevert(uint256 _x) external pure {
if (_x > 10) {
revert("_x > 10");
}
}
function testAssert(uint256 _x) external pure {
assert(_x == 10);
}
error MyError(address call, uint256 _i);
function testCustomError(uint256 _x) external view {
if (_x > 10) {
revert MyError(msg.sender, _x);
}
}
}
Solidity 中的错误(关键字 error)提供了一种方便且省 gas 的方式来向用户解释为什么一个操作会失败。它们可以被定义在合约(包括接口和库)内部和外部。
error
只能通过revert
触发- 使用自定义 error 抛出错误,向调用者描述错误信息。
- 开发者可以在任何时候,任何条件下触发 自定义 Error
- error 花费更少的 gas。
error
可以定义在 contract 之外。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// 自定义错误
error MyError1(address call, uint256 _i);
contract ErrorDemo {
// 自定义错误
error MyError2(address call, uint256 _i);
function testCustom1(uint256 _x) external view {
if (_x > 10) {
revert MyError1(msg.sender, _x);
}
}
function testCustom2(uint256 _x) external view {
if (_x > 10) {
revert MyError2(msg.sender, _x);
}
}
}
错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// 转账时,没有足够的余额。
/// @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])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
错误不能被重写或覆盖,但是可以继承。只要作用域不同,同一个错误可以在多个地方定义。只能使用 revert
语句创建错误实例。
错误产生的数据,会通过 revert 操作传递给调用者,可以交由链外组件处理或在 try/catch 语句 中捕获它。注意,只有外部调用的错误才能被捕获。发生在内部调用或同一函数内的 revert 不能被捕获。
如果是调用 Error(string)
函数,这里提供的字符串将经过 ABI 编码。revert("Not enough Ether provided.");
会产生如下的十六进制错误返回值:
// Error(string) 的函数选择器
0x08c379a0
// 数据的偏移量(32)
0x0000000000000000000000000000000000000000000000000000000000000020
// 字符串长度(26)
0x000000000000000000000000000000000000000000000000000000000000001a
// 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000
提示信息可以通过 try/catch
(下面介绍)来获取到。
revert()
之前有一个同样用法的throw
,它在 0.4.13 版本弃用,在 0.5.0 移除。
使用一个自定义的错误实例通常会比字符串描述便宜得多。因为你可以使用错误名来描述它,它只被编码为四个字节。更长的描述可以通过 NatSpec 提供,这不会产生任何费用。
通过三个斜杠 ///
定义的错误,它比require
更省 gas。推荐代替 require 使用。
如果错误没有任何参数,错误只需要四个字节的数据,你可以使用 NatSpec,来进一步解释错误背后的原因,NatSpec 不会存储在链上。这个方式使得它同时也是一个非常便宜和方便的错误报告功能。
更具体地说,一个错误实例的 ABI 编码方式与调用相同名称和类型的函数的方式相同,它作为revert
操作码的返回数据使用。 这意味着错误数据由一个 4 字节的选择器和 ABI-encoded 数据组成。选择器是错误的签名的 keccak256 哈希的前四个字节组成。
代码结构如下
/// this is netspec error info
error MyError1();
例子如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ErrorDemo {
// netspec error
/// this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info
error MyError1();
/// 这是一个错误!老铁,你的输入参数错啦,必须要大于10的数字才可以通过!
error MyError2();
// 21647 gas
function test1(uint256 _x) external pure {
if (_x < 10) {
revert MyError1();
}
}
// 21691 gas
function test2(uint256 _x) external pure {
if (_x < 10) {
revert MyError2();
}
}
// 22036 gas
function test3(uint256 _x) external pure {
require(
_x > 10,
"this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info,this is netspec error info"
);
}
// 21974 gas
function test4(uint256 _x) external pure {
require(
_x > 10,
unicode"这是一个错误!老铁,你的输入参数错啦,必须要大于10的数字才可以通过!"
);
}
}
在当前合约发起对外部合约的调用,如果外部合约调用执行失败被 revert,外部合约状态会被回滚,当前合约状态也会被回滚。这是正常的逻辑。
但有时候我们并不想这样,要是能够捕获外部合约调用异常,然后根据情况做自己的处理会更好吗!所以,这种场景下适应于使用 try...catch
语句。
try catch
仅用于 外部函数调用 和合约创建调用。
- 外部函数调用
- 合约创建调用
try this.count() {
// 成功逻辑
return "success";
} catch Error(string memory reason) {
// 失败的逻辑: require / revert
// 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败
return reason;
} catch (bytes memory) {
// 失败逻辑
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
上面的逻辑也可以简写如下
try this.count() {
// 成功逻辑
return "success";
} catch (bytes memory) {
// 失败逻辑: require / revert / assert
return "assert error";
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Manager {
// function count() public pure returns (int256) {
// require(1 == 2, "require error");
// return 2;
// }
function count() public pure returns (int256) {
assert(1 == 2);
return 2;
}
function test() public view returns (string memory) {
// this 代表当前函数
try this.count() {
return "success";
} catch Error(string memory reason) {
// reason 是出错原因
// 调用 count() 失败时执行,通常是不满足 require 语句条件
// 或触发 revert 语句时所引起的调用失败
return reason;
} catch (bytes memory) {
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
}
}
也可以去掉catch Error(string memory reason)
,只使用 catch (bytes memory)
;
如下的测试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Manager {
function count() public pure returns (int256) {
require(1 == 2, "require error");
return 2;
}
function test() public view returns (string memory) {
// this 代表当前函数
try this.count() {
return "success";
} catch (bytes memory) {
// 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
return "assert error";
}
}
}
外部调用的失败,可以通过 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*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// This is executed in case of a panic,
// i.e. a serious error like division by zero
// or overflow. The error code can be used
// to determine the kind of error.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used。
errorCount++;
return (0, false);
}
}
}
try
关键词后面必须有一个表达式,代表外部函数调用或合约创建(new ContractName()
)。
以下内容摘自文档:
在表达式上的错误不会被捕获(例如,如果它是一个复杂的表达式,还涉及内部函数调用),只有外部调用本身发生的 revert 可以捕获。 接下来的 returns
部分(是可选的)声明了与外部调用返回的类型相匹配的返回变量。在没有错误的情况下,这些变量被赋值,合约将继续执行第一个成功块内代码。如果到达成功块的末尾,则在 catch
块之后继续执行。
Solidity 根据错误的类型,支持不同种类的捕获代码块:
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 子句) 而不是前面几个 catch 子句。
有计划在未来支持其他类型的错误数据。 Error
和 Panic
字符串目前是按原样解析的,不作为标识符处理。
为了捕捉所有的错误情况,你至少要有子句 catch { ... }
或 catch (bytes memory lowLevelData) { ... }
.
在 returns
和 catch
子句中声明的变量只在后面的块的范围内有效。
注解: 如果在 try/catch 语句内部返回的数据解码过程中发生错误,这将导致当前执行的合约出现异常,如此,它不会在 catch 子句中被捕获到。如果在 catch Error(string memory reason)
的解码过程中出现错误,并且有一个低级的 catch 子句,那么这个错误就会在低级 catch 子句被捕获。
注解: 如果执行到一个 catch 子句,那么外部调用的状态改变已经被回退了。如果执行到了成功块,那么外部调用的状态改变是有效的。如果状态改变已经被回退,那么要么在 catch 块中继续执行,要么是 try/catch 语句的执行本身被回退(例如由于上面提到的解码失败或由于没有提供低级别的 catch 子句时)。
注解:调用失败背后的原因可能是多方面的。请不要认为错误信息是直接来自被调用的合约。错误可能发生在调用链的更深处,而被调用的合约只是转发了(冒泡)错误。 另外,这可能是由于 out-of-gas 情况,而不是一个逻辑错误状况:调用者总是在调用中保留至少 1/64 的 gas,这样即使被调合约 gas 用完,调用方仍有一些 gas 预留(处理剩余逻辑)。