Cover photo

Solidity 课程 12: 异常

solidity一个错误将撤消在交易期间对状态所做的所有改变。有三种抛出异常的方法:errorrequireassert

1、Error、revert

Errorsolidity 0.8版本新加的内容,方便且高效(省gas)的向用户解释操作失败的原因。人们可以在contract之外定义异常。

下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

error TransferNotOwner(); // 自定义errorfunction

transferOwner1(uint256 tokenId, address newOwner) public {
        if(_owners[tokenId] != msg.sender){
            revert TransferNotOwner();
        }
        _owners[tokenId] = newOwner;
}

注意:在执行当中,error 必须搭配 revert 命令使用

// custom error
error InsufficientBalance(uint balance, uint withdrawAmount);

function testCustomError(uint _withdrawAmount) public view {
    uint bal = address(this).balance;
    if (bal < _withdrawAmount) {
        revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
    }
}

我们也可以直接使用revert

    function testRevert(uint _i) public pure {
        // Revert is useful when the condition to check is complex.
        // This code does the exact same thing as the example above
        if (_i <= 10) {
            revert("Input must be greater than 10");
        }
    }

2、Require

require命令是solidity 0.8 版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。

他很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,”异常的描述”),当检查条件不成立的时候,就会抛出异常。

我们用require命令重写一下上面的transferOwner函数:

function transferOwner2(uint256 tokenId, address newOwner) public {
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
        _owners[tokenId] = newOwner;
}
    function testRequire(uint _i) public pure {
        // Require should be used to validate conditions such as:
        // - inputs
        // - conditions before execution
        // - return values from calls to other functions
        require(_i > 10, "Input must be greater than 10");
    }

3、Assert

assert应该只用于调试,因为他不能解释抛出异常的原因(比require少个字符串)。他的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

我们用assert命令重写一下上面的transferOwner函数:

function transferOwner3(uint256 tokenId, address newOwner) public {
        assert(_owners[tokenId] == msg.sender);
        _owners[tokenId] = newOwner;
}

function testAssert() public view {
        // Assert should only be used to test for internal errors,
        // and to check invariants.

        // Here we assert that num is always equal to 0
        // since it is impossible to update the value of num
        assert(num == 0);
}

4、三种方法的gas比较

我们比较一下三种抛出异常的gas消耗,方法很简单,部署合约,分别运行写的transferOwner函数的三个版本。

  1. error方法gas消耗:24445

  2. require方法gas消耗:24743

  3. assert方法gas消耗:24446

我们可以看到,error方法gas cost最少,其次是assertrequire方法消耗gas最多。因此,error是最佳的,既可以告知用户抛出异常的原因,又能省gas。

5、统计产生异常的部分原因

下列情况将会产生一个 assert 式异常:

  1. 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.lengthi < 0)。

  2. 如果你访问固定长度 bytesN 的索引太大或为负数。

  3. 如果你用零当除数做除法或模运算(例如 5 / 023 % 0 )。

  4. 如果你移位负数位。

  5. 如果你将一个太大或负数值转换为一个枚举类型。

  6. 如果你调用内部函数类型的零初始化变量。

  7. 如果你调用 assert 的参数(表达式)最终结算为 false。

下列情况将会产生一个 require 式异常:

  1. 调用 throw

  2. 如果你调用 require 的参数(表达式)最终结算为 false

  3. 如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作 callsenddelegatecall 或者 callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。

  4. 如果你使用 new 关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。

  5. 如果你对不包含代码的合约执行外部函数调用。

  6. 如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。

  7. 如果你的合约通过公有 getter 函数接收 Ether 。

  8. 如果 .transfer() 失败。

总结

这节学习了处理异常的3种方法,revert(error)、require 和 assert,其中 error 最合适,既能省gas,又可以知道抛出的异常。

Code:

https://github.com/Luca-Hsu/SuperSolidity

Reference:

https://solidity-cn.readthedocs.io/zh/develop/control-structures.html#assert-require-revert-and-exceptions

https://mirror.xyz/ninjak.eth/XhhLu7PV1cAhOp9_m-dk9OoTj7offC7DkYYgsV3e31I

https://solidity-by-example.org/error