【Solidity智能合约系列】09--Solidity错误处理

前言

我想错误处理这个词,对于有过编程经验的人来说都不陌生,它是指程序运行过程中发生错误(Error)或者异常(Exception)的处理方式。在类似Java这样的语言中,我们是通过try...catch...捕捉异常来处理错误的,然而Solidity处理错误和我们常见的语言不一样,下面我们就一起来了解一下在Solidity中的错误处理是怎么样的。

Solidity是通过回退状态的方式,发生异常时会撤消当前调用(及其所有子调用)所改变的状态,同时给调用者返回一个错误标识。

我们可以把区块链理解为是全球共享的分布式事务性数据库。全球共享意味着参与这个网络的每一个人都可以读写其中的记录。如果想修改这个数据库中的内容,就必须创建一个事务,事务意味着要做的修改(假如我们想同时修改两个值)只能被完全的应用或者一点都没有进行。Solidity错误处理就是要保证每次调用都是事务性的。

错误处理

Solidity提供了两个函数assertrequire,用于条件检查,如果条件不满足则抛出异常。assert函数只能用于检查内部错误和不变量,require函数用于检查输入变量或合同状态变量是否满足条件以及验证调用外部合约返回值。如果使用assert合理的话,有一个Solidity分析工具就可以帮我们分析出智能合约中的错误,帮助我们发现合约中有逻辑错误的bug。

正确运行的代码应该永远不会达到失败的assert状态,如果发生了这种情况,你应该修复你合约中的bug。

还有2种方式可以触发异常,revert函数可以用来标识一个错误,并且回退当前调用。将来可能会在revert中包含有关错误的详细信息,throw关键字可以用作revert()的替代。

注意:
从0.4.13版本,throw关键字已被弃用,将来会被淘汰。

当子调用中发生异常时,异常会自动向上“冒泡”。 不过也有一些例外:send,以及底层的函数调用call, delegatecallcallcode,当发生异常时,这些函数返回false

警告:
作为EVM虚拟机设计的一部分,在一个不存在的账户(地址)上调用底层的函数calldelegatecallcallcode 也会返回成功,所以我们在进行调用时,应该总是优先检查地址是否存在。

注意:捕捉异常是不可能的,因为没有try...catch...

在下面的例子中,你可以了解到如何使用require来轻松检查输入条件,以及assert用于内部错误检查:

pragma solidity ^0.4.0;

contract Sharer {
    function sendHalf(address addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0); // 仅允许偶数
        uint balanceBeforeTransfer = this.balance;
        addr.transfer(msg.value / 2); 
         // 如果失败,会抛出异常,下面的代码就不执行
        assert(this.balance == balanceBeforeTransfer - msg.value / 2);
        return this.balance;
    }
}

在Remix中去执行上面的这段代码:

测试1:附加1wei (奇数)去调用sendHalf函数,这时会发生异常,如下图:


运行测试2:附加2wei 去调用sendHalf函数,运行正常。

assert类型异常

在以下情景中会产生assert类型的异常:

  1. 如果访问数组时发生了越界或者数组下标为负数(如i >= x.lengthi < 0时访问x[i])
  2. 如果序号越界,或负的序号值时访问一个定长的bytesN
  3. 被除数为0(如5/023 % 0)。
  4. 在移位运算中,对一个二进制移动一个负的值(如:5<; i为-1时)。
  5. 整数进行可以显式转换为枚举时,如果将过大值,负值转为枚举类型则抛出异常
  6. 如果调用未初始化的内部函数类型的变量。
  7. 如果调用assert,它的参数的计算结果为false

require类型异常

在以下场景中会产生require类型的异常:

1、调用throw
2、调用require,其参数的运算结果为false
3、如果你通过消息调用一个函数,但在调用的过程中,并没有正确结束(gas不足,没有匹配到对应的函数,或被调用的函数出现异常)。底层调用如call,send,delegatecallcallcode除外,这几个底层调用函数不会抛出异常,但它们会通过返回false来表示失败。
4、如果在使用new关键字创建一个新合约时,出现第3条的原因没有正常完成。
5、如果调用外部函数调用时,被调用的对象不包含代码。
6、如果你的合约是通过没有payable修饰符的public的函数来接收Ether(以太币)时(包括构造函数,和回退函数)。
7、如果合约通过一个publicgetter函数(public getter funciton)接收以太币。
8、如果.transfer()执行失败

  • 当发生require类型的异常时,Solidity会执行一个回退操作(指令0xfd)。
  • 当发生assert类型的异常时,Solidity会执行一个无效操作(指令0xfe)。

在上述的两种情况下,EVM都会撤回所有的状态改变。是因为期望的结果没有发生,就没法继续安全执行。必须保证交易的原子性(也就是一致性,要么全部执行,要么一点改变都没有,不能只改变一部分),所以最安全的做法就是撤销所有操作,让整个交易没有任何影响。

注意assert类型的异常会消耗掉用的所有的gas, 而require不会消耗任何gas,从Metropolis版本( 即目前主网所在的版本)起。

参考:
https://solidity.readthedocs.io/en/v0.4.21/control-structures.html#error-handling-assert-require-revert-and-exceptions

你可能感兴趣的:(【Solidity智能合约系列】09--Solidity错误处理)