“与其感慨路难行,不如马上出发。”
异常处理

目前 Solidity 0.8.x 版本有三种方式处理异常:revert require assert

error 声明自定义错误

error 是 solidity 0.8.4 版本新加的内容,必须搭配 revert 命令使用。相比字符串错误信息,自定义 error 更加节省 gas,这是因为:

  • 不需要对多个错误信息进行字符串拼接和格式化;
  • 通过自定义错误名称描述错误,而自定义错误名称部署在区块上时,只占用4个字节的空间。
error CustomError();
error CustomErrorWithMessage(string message);

revert 处理异常

revert();
revert("Error message");
revert CustomError();
revert CustomErrorWithMessage("Error message");

require 处理异常

require(num > 0);
require(num > 0, "Error message");

assert 处理异常

assert 函数创建了一个 Panic(uint256) 类型的错误。正确运行的代码不应该创建一个 Panic 异常,甚至在无效的外部输入时也不应该。因此 assert 应该只用于测试内部错误,以及检查变量值。

assert(address == 0x1111111111111111111111111111111111111111);

Error 是业务层面的错误,通常也会作为业务的一部分,在编程的过程中进行分支判断和处理。

Panic 是程序层面的错误,在编程阶段可能无法预知,通常在编译阶段发现并得以修复。

try catch 捕获异常

当我们与外部合约进行交互,并且希望捕获外部合约抛出的异常时,可以使用 try catch 语句。但对于 send 和低级别函数 call delegatecall staticcall,它们的错误无法被捕获,因为在发生异常时,false 作为第一个参数被返回,而不是“冒泡”。

// 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异常的情况下执行,错误代码可以用来确定错误的种类。
            errorCount++;
            return (0, false);
        } catch (bytes memory /*lowLevelData*/) {
            // 在使用revert()的情况下,会执行这个命令。
            errorCount++;
            return (0, false);
        }
    }
}
  • returns (uint v) {...} 可选,正确调用外部函数时,程序会进入该分支,并返回外部函数的返回值
  • catch Error(string memory /*reason*/) {...} 如果错误由 Error 异常引起的,且带有错误信息,这个子句将被运行
  • catch Panic(uint /*errorCode*/) {...} 如果错误由 Panic 异常引起的,这个子句将被运行
  • catch (bytes memory /*lowLevelData*/) {...} 如果错误签名与其他子句不匹配, 或者在解码错误信息时出现了错误,或者没有与异常一起提供错误数据, 那么这个子句就会被执行。在这种情况下,声明的变量提供了对低级错误信息的访问
  • catch {...} 如果您对错误数据不感兴趣,您可以直接使用该语句(甚至作为唯一的catch子句)来代替前面的子句

为了捕捉所有的错误情况,您至少要有 catch {...}catch (bytes memory lowLevelData) {...} 子句。