Solidity 异常处理

Solidity 是通过回退状态的方式来处理异常错误。

Solidity 发生异常时,会撤消当前调用和所有子调用改变的状态,同时给调用者返回一个错误标识。

Solidity 提供了require 、assert 和 revert 来处理异常。

1. require 示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SolidityTest {
    bool public flag;
    function setFlag() external {
        // 判断调用者地址 是否等于当前合约地址,成立则继续运行。
        require(msg.sender == address(this));
        flag = true;
    }
}

require 函数常常用来检查输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。

require 可以有返回值,例如:require(condition, 'Something bad happened');。

require 的返回值不宜过长,因为返回信息需要消耗 gas。

2. require、assert、revert 共同点

assert()与require()语句都需要满足括号中的条件,才能进行后续操作,若不满足则抛出错误。

以下三个语句的功能完全相同:

// revert
if(msg.sender != owner) { 
   revert();
 }
// require
require(msg.sender == owner);

// assert
assert(msg.sender == owner);

3. require、assert 不同点

assert(false) 编译为 0xfe,这是一个无效的操作码,所以会消耗掉所有剩余的 gas,并恢复所有的操作。

require(false) 编译为 0xfd,这是revert()的操作码,所以会退还所有剩余的 gas,同时可以返回一个自定义的报错信息。

require 的 gas 消耗要小于 assert,而且可以有返回值,使用更为灵活。

4. require、assert 使用场景

require() 函数用于检测输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。

require() 语句的失败报错,应该被看作一个正常的判断语句流程不能通过的事件。

assert()语句的失败报错,意味着发生了代码层面的错误事件,很大可能是合约中有一个bug需要修复。

使用 require() 的场景

  • 验证用户输入,例如:require(input_var>100)
  • 验证外部合约的调用结果,例如:require(external.send(amount))
  • 在执行状态更改操作之前验证状态条件,例如:require(block.number > 49999) 或 require(balance[msg.sender]>=amount)

一般来说,使用require()的频率更多,通常应用于函数的开头。

使用 assert() 的场景

  • 检查溢出
  • 检查不变量
  • 更改后验证状态
  • 预防永远不会发生的情况

一般来说,使用assert()的频率较少,通常用于函数的结尾。

基本上,require() 应该用于检查条件,而 assert() 只是为了防止发生任何非常糟糕的事情。

5. try...catch

我们在当前合约发起对外部合约调用的话,如果外部合约调用执行失败被 revert,外部合约状态会被回滚,当前合约状态也会被回滚。

但有时候我们并不想这样,要是能够捕获外部合约调用异常,然后根据情况做自己的处理不是更好吗?所以,这种场景下适应于使用 try...catch 语句。

require 示例如下:

pragma solidity ^0.8.0;
contract Manager {
    function count() public pure returns(int){
        require(1==2,"require error");
        return 2;
    }
    
    function test() public view returns(string memory) {
        try this.count()  {
            return "success";
        } catch Error(string memory reason/* 出错原因 */) {
            // 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败
            return reason;
        } catch (bytes memory) {
            // 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
            return "assert error";
        }
    }
}

以上代码将会触发 catch Error(string memory reason) ,最终输出 require error。

assert 示例如下:

pragma solidity ^0.8.0;
contract Manager {
    function count() public pure returns(int){
        assert(1==2);
        return 2;
    }
    
    function test() public view returns(string memory) {
        try this.count()  {
            return "success";
        } catch Error(string memory reason/* 出错原因 */) {
            // 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败
            return reason;
        } catch (bytes memory) {
            // 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行
            return "assert error";
        }
    }
}

以上代码将会触发 catch (bytes memory) ,最终输出 assert error。

下一章:Solidity 编程风格

良好统一的编程风格,有助于提高代码的可读性和可维护性。下面是有关Solidity编程风格的几条建议。 1. 代码布局缩进使用4个空格代替制表符作为缩进,避免空格与制表符混用。空2行规则2个合约定义之间空 ...