错误处理

Solidity 中有两种错误:

  • Error:外部错误,属于可以预见或可修复的错误,错误归因是外部环境导致,非代码逻辑本身存在问题。例如用户传参错误
  • Panic:内部编程错误,属于不可预见或不可修复的错误,错误归因通常是内部代码逻辑存在问题。例如未检查导致除0错误

当抛出的错误没有被处理时,整个合约交易将被取消,并重置到交易执行前的状态。

revert 与 Error

我们可以使用 revert 来抛出 Error。使用方式如下:

Revert.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Revert {
    int public count;

    function revert1() public {
        count++;
        // 直接抛出错误,不符带任何数据
        revert();
    }

    function revert2() public {
        count++;
        // 抛出错误,带一个字符串揭示错误原因
        revert("Not owner");
    }

    // 一个自定义错误
    error NotOwner(address from);

    function revert3() public {
        count++;
        // 抛出自定义错误
        revert NotOwner(msg.sender);
    }
}

部署合约后触发 revert1 函数,将抛出错误,且没有返回任何错误信息。

触发 revert2 函数,将抛出错误,并附带有解释错误原因的字符串:Reason provided by the contract: "Not owner".

触发 revert3 函数,将抛出错误,并附有 NotOwner 错误的参数:

Error provided by the contract:
NotOwner
Parameters:
{
  "from": {
    "value": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
  }
}

以前三个函数抛出错误,合约将重置回调用前状态,使得 count 值将一直维持为 0 值。

require

为了使用简便,特别是在验证输入参数是否合法时,可以使用 require 来简化判断合法性以及在非法时抛出错误:

Require.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Require {
    address owner;

    function Destroy(address payable addr) public {
        // 要求 owner 等于 msg.sender,否则抛出错误 Error("Not owner")
        require(owner == msg.sender, "Not owner");
        selfdestruct(addr);
    }
}

require 接受两个参数,第一个参数为一个布尔值(通常使用一个布尔表达式),当布尔值为真是不抛出错误;当布尔值为假时,将抛出一个 ErrorError 可以附带一个错误原因字符串。该字符串由 require 第二个参数给出。

assert 与 Panic

对于 Panic,除了由EVM检测并触发,开发人员也可以通过 assert 触发:

Assert.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Assert {
    function add(uint a, uint b) public pure returns (uint c) {
        c = a + b;
        assert(c > a && c > b);
    }
}

assert 只接受一个布尔值作为参数,如果为假则 Panic

Panic 代码

一个 Panic 错误会携带一个 Panic 代码,用于指示 Panic 类型:

  • 0x00:编译器插入的 panic
  • 0x01:使用 assert 触发的 panic
  • 0x11:未使用 unchecked {} 包围的数字运算出现了溢出
  • 0x12:除0或求余0
  • 0x21:把一个负数或过大的数,转换为枚举
  • 0x22:访问一个错误编码的存储区字节数组
  • 0x31:对空数组调用pop()函数
  • 0x32:数组访问下标非法
  • 0x41:分配内存过大,或创建数组过大
  • 0x51:调用一个未被初始化的内部函数变量

try/catch

对于外部调用以及合约创建导致的异常(Error 或 Panic),可以通过 try/catch 语句捕获:

TryCatch.sol
运行
复制
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract Callee {
    function callee(string calldata name) external pure returns (string memory) {
        require(
            keccak256(bytes(name)) == keccak256(bytes("Mathew")),
            "Only Mathew can call this"
        );
        return "Hi, Mathew";
    }
}

contract TryCatch {
    function test(string calldata name) public returns (string memory) {
        Callee c = new Callee();
        // try 后面必须跟一个外部调用或者,或者合约创建语句
        try c.callee(name) returns (string memory result) {
            return result;
        } catch Error(string memory reason) {
            // catch Error(string memory) 用于捕获 Error 型错误
            // 直接 revert(reason) 再次抛出该错误
            revert(reason);
        } catch Panic(uint errorCode) {
            // catch Panic 用于捕获 Panic 型错误,errorCode 为 Panic 代码
            revert(unicode"调用 callee 导致 Panic");
        } catch (bytes memory lowLevelData) {
            // catch(bytes memory lowLevelData) 用于捕获所有异常
            // Panic 代码或者 Error 参数保存在 lowLevelData 中
            return string(lowLevelData);
        } catch {
            // 与 catch(bytes memory lowLevelData) 一样用于捕获所有异常
            // 但是并不关心异常附带的数据,因此没有参数
            // 与 catch(bytes memory lowLevelData) 只能二选一
            return "Something went wrong.";
        }
    }
}