Solidity语言中定义了以下三种错误处理方式:
require
:用于在执行前验证输入和条件;revert
:用于直接触发回退,可自定义异常处理;assert
:用于检查不应该为假的代码,失败的断言可能意味着代码层面存在错误。
异常处理将撤消当前调用对状态所做的所有更改,并且还可以向调用者抛出错误。
错误处理函数
-
Require()
require(condition, description)
require
首先检查condition
,如果条件为真则继续执行,否则提供一个消息字符串description
用于标记错误(可选)。 -
revert()
if (!condition) revert(); if (!condition) revert(description); if (!condition) revert CustomError(arg1, arg2, ...);
revert
可以直接触发回退,也可以抛出一个消息字符串用于标记错误,也可以自定义错误处理。 -
assert()
assert(condition);
assert
用于检查condition
是否为真,检查失败时抛出异常。
函数共同点
以下三个语句的功能完全相同:
if (msg.sender != owner) { revert(); }
assert(msg.sender == owner);
require(msg.sender == owner);
这三个语句都用于检查当前调用者是否为合约的所有者,如果检查结果不为真则抛出异常。
函数差异化
Gas开销
assert()
将消耗所有剩余的Gas,并恢复所有的操作。
require()
和 revert()
将退还所有剩余的Gas,同时可以返回一个值(自定义的报错信息)。
适用场景
require()
的适用场景
-
验证用户输入,如:
require(input > 10);
-
验证外部合约响应(返回值),如:
require(external.send(amount));
-
执行合约前验证状态条件,如:
require(block.number > SOME_BLOCK_NUMBER); // 或者 require(balance[msg.sender] >= amount);
合约中应该尽量使用 require
来处理错误,且放在函数最开始的地方使用。
revert()
的适用场景
revert
函数与 require
函数类似,但是适用更复杂处理逻辑的场景。如果代码中需要复杂的 if/else
逻辑流,那么应该考虑适用 revert
函数而不是 require
函数。
assert()
的适用场景
-
检查整数溢出(overflow/underflow),如:
c = a + b; assert(c > b);
-
检查不变量(invariants),如:
assert(this.balance >= totalSupply);
-
验证改变后的状态,如:
assert(state);
合约中应该尽量少用 assert
调用,如果要适用 assert
应该在函数结尾处使用。
assert
与 require
函数均被用来检查条件并在条件不满足时抛出异常,它们的主要区别是 require
应该被用于函数中检查条件,assert
用于预防不应该发生的情况,即不应该使条件错误。
合约例子
例子1:下面是一个合约例子,用来演示错误处理函数的用法。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 错误处理及异常
contract Errors {
// Require测试
function testRequire(uint i) public pure {
require(i > 10, "Input must be greater than 10");
}
// Revert测试
function testRevert(uint i) public pure {
if (i <= 10) {
revert("Input must be greater than 10");
}
}
// Assert测试
uint public num;
function testAssert() public view {
assert(num == 0);
}
// 自定义错误
error InsufficientBalance(uint balance, uint amount);
function testCustomError(uint _amount) public view {
uint bal = address(this).balance;
if (bal < _amount) {
revert InsufficientBalance({balance: bal, amount: _amount});
}
}
}
例子2:另一个合约例子,比上一个例子更复杂一些。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 帐号存取款异常处理
contract ErrorsAccount {
uint public balance;
uint public constant MAX_UINT = 2**256 - 1;
// 存入以太币到合约
function deposit(uint _amount) public payable {
uint oldBalance = balance;
uint newBalance = balance + _amount;
// 检查溢出
require(newBalance >= oldBalance, "Overflow");
balance = newBalance;
assert(balance >= oldBalance);
}
// 从合约提取以太币
function withdraw(uint _amount) public payable {
uint oldBalance = balance;
// 检查溢出
require(balance >= _amount, "Underflow");
if (balance < _amount) {
revert("Underflow");
}
balance -= _amount;
assert(balance <= oldBalance);
}
}
合约执行
我们在Remix中编译、部署和运行这个合约例子。
例子1:部署完成后的界面如下图:
我们分别执行 testRequire
、testRevert
、testAssert
和 testCustomError
四个测试函数,观察输出结果是否和我们期望的一样。
例子2:部署完成后的界面如下图:
我们分别执行 deposit
和 withdraw
两个测试函数,观察输出结果是否和我们期望的一样。