Solidity入门学习(6)-错误处理

598 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

一、错误处理机制

区块链具有分布式、不可篡改的特性,像一个大型的分布式数据库,也正是由于这个特性,需要智能合约在处理错误时也保持事务性和原子性。参与共识的节点想要完成某一项状态改变,那么这项状态改变是需要放在事务中完成的,一旦某一环节失败,事务就要发生回滚。因此,Solidity采用了异常回退的方式来处理错误,发生异常时会撤消当前调用(及其所有子调用)所改变的状态,同时返回给caller一个错误标识。

需要注意的是,网上查找到的Solidity关于错误处理的资料,其实在应用时是和编译器版本有关系的,比如关于throw这种抛出异常的方式,就在0.5.0之后被弃用了,在学习和使用时应当注意编译器版本的差别。

Solidity内置的异常有两种——Error(string)和Panic(uint256),这两种异常都是作为函数来使用。Error用于预料之中的“错误”,Panic用于描述在本应无错误代码中出现的异常。

二、Error的用法

2.1 默认error

产生error的三种方式:assert,require和revert。 Require抛出异常的示例:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.1;

contract Demo{

     function sendValue(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0); 
        uint balanceBeforeTransfer = address(this).balance;
        addr.transfer(msg.value / 2);  
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
        return address(this).balance;
    }
}

这段代码需要调用者在完成转账操作时,附带的value必须是偶数,如果是奇数,会在第一处条件校验时失败“require(msg.value % 2 == 0); ”,如果我们在调用sendValue方法进行转账时,设置value值为1,则会出现如下的错误(Remix中直接调用sendValue方法):

require demo.png

可以看到此次调用方法被revert掉了,并且提示:The transaction has been reverted to the initial state.

看到教材里说“如果异常在子调用发生,那么异常会自动冒泡到顶层”,于是为刚才抛错的方法再包一层调用,如下面的代码所示,一样传入value为1的值触发错误,此时看到返回给caller的错误是顶层的Demo.callerA的error,debug跟进发现确实是把error从sendValue函数中冒泡到了调用函数callerA中,再返回给外部调用者。

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.1;

contract Demo{

     function sendValue(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0); 
        uint balanceBeforeTransfer = address(this).balance;
        addr.transfer(msg.value / 2);  
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
        return address(this).balance;
    }

    function callerA(address payable addr) public payable {
        
        uint balance=sendValue(addr);
        assert (balance<0);
    }

2.2 带string信息的Error

可用require和revert产生Error(string)错误,用法为:

revert(“description”);

require(condition, “description”);其中condition为false时,抛出带description描述信息的Error

2.3 自定义形式的Error

示例如下,自定义一个error,在revert语句中创建此error的实例,这样的自定义error只能用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;
    }
    // ...
}

为什么会有自定义类型的error呢?自定义error有哪些好处呢?

首先,我们在coding时,如果可以自定义error,那么代码会更具体系化,返回的错误信息更具语义化,代码结构化更强,可读性更好。其次,使用一个自定义的错误实例通常会比字符串描述便宜得多。因为可以使用编码为4个字节的错误名来描述它,如果需要更长的描述可以使用NatSpec,这不会产生任何费用。

三.关于Panic异常

Solidity的官方文档上有这样一段对于Panic的介绍(v0.8.17版本的文档):

assert 函数应该只用于测试内部错误,检查不变量,正常的函数代码永远不会产生Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 assert 条件和函数调用。 下列情况将会产生一个Panic异常: 错误数据会提供的错误码编号,用来指示Panic的类型:

  1. 0x00: 用于常规编译器插入的Panic。
  2. 0x01: 如果你调用 assert 的参数(表达式)结果为 false 。
  3. 0x11: 在 unchecked { ... } 外,如果算术运算结果向上或向下溢出。
  4. 0x12; 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )。
  5. 0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。
  6. 0x22: 如果你访问一个没有正确编码的存储byte数组.
  7. 0x31: 如果在空数组上 .pop() 。
  8. 0x32: 如果你访问 bytesN 数组(或切片)的索引太大或为负数。(例如: x[i] 而 i >= x.length 或 i < 0).
  9. 0x41: 如果你分配了太多的内内存或创建了太大的数组。
  10. 0x51: 如果你调用了零初始化内部函数类型变量。

根据上面这段官方文档的叙述,当我用assert(表达式)让表达式结果为false时,我会得到一个Panic,然而我在remix上尝试,得到的仍然是一个Error并且被revert了,那么我如何得到一个Panic呢? 经过查询,发现:

Note: Panic exceptions used to use the invalid opcode before Solidity 0.8.0, which consumed all gas available to the call. Exceptions that use require used to consume all gas until before the Metropolis release.

于是我把上面这段代码从0.8.1换成0.7.5,再次相同参数进行调用,得到了invalid opcode,而且果然用掉了所有的available gas,对比来看确实是0.8.0以后的版本对开发者更友好了。

下面第一张图是0.7.5版本的运行结果,得到了invalid opcode,第二张图是相同的代码相同的调用用0.8.1版本出现的结果:

invalid opcode.png

require demo.png

四、小结

  • Solidity以错误回退的方式处理异常
  • 0.8.0版本以后,由invalid opcode形式呈现的Panic已经不存在了
  • 可以通过assert,require和revert语句产生error,Error(string)类型的错误只能由require和revert方式产生,自定义结构的error只能由revert产生
  • try…catch可以捕获error,但只有外部调用的错误或者创建合约时才能用try…catch捕获。

参考资料:

Solidity官方文档learnblockchain.cn/docs/solidi…

Solidity Fundamentals: Error Handling:medium.com/coinmonks/s…