智能合约Solidity语言错误处理函数(requirerevertassert)使用详解

Posted StevenX5

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了智能合约Solidity语言错误处理函数(requirerevertassert)使用详解相关的知识,希望对你有一定的参考价值。

Solidity语言中定义了以下三种错误处理方式:

  • require:用于在执行前验证输入和条件;
  • revert:用于直接触发回退,可自定义异常处理;
  • assert:用于检查不应该为假的代码,失败的断言可能意味着代码层面存在错误。

异常处理将撤消当前调用对状态所做的所有更改,并且还可以向调用者抛出错误。

错误处理函数

  • Require()

    require(condition, description)
    

    require 首先检查 condition,如果条件为真则继续执行,否则提供一个消息字符串 description 用于标记错误(可选)。

  • revert()

    if (!condition) revert(); 
    if (!condition) revert(description);
    if (!condition) revert CustomError(arg1, arg2, ...);
    

    revert 可以直接触发回退,也可以抛出一个消息字符串用于标记错误,也可以自定义错误处理。

  • assert()

    assert(condition);
    

    assert 用于检查 condition 是否为真,检查失败时抛出异常。

函数共同点

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

if (msg.sender != owner)  revert(); 
assert(msg.sender == owner);
require(msg.sender == owner);

这三个语句都用于检查当前调用者是否为合约的所有者,如果检查结果不为真则抛出异常。

函数差异化

Gas开销

assert() 将消耗所有剩余的Gas,并恢复所有的操作。

require()revert() 将退还所有剩余的Gas,同时可以返回一个值(自定义的报错信息)。

适用场景

require() 的适用场景

  • 验证用户输入,如:

    require(input > 10);
    
  • 验证外部合约响应(返回值),如:

    require(external.send(amount));
    
  • 执行合约前验证状态条件,如:

    require(block.number > SOME_BLOCK_NUMBER);	// 或者
    require(balance[msg.sender] >= amount);
    

合约中应该尽量使用 require 来处理错误,且放在函数最开始的地方使用。

revert() 的适用场景

revert 函数与 require 函数类似,但是适用更复杂处理逻辑的场景。如果代码中需要复杂的 if/else 逻辑流,那么应该考虑适用 revert 函数而不是 require 函数。

assert() 的适用场景

  • 检查整数溢出(overflow/underflow),如:

    c = a + b;
    assert(c > b);
    
  • 检查不变量(invariants),如:

    assert(this.balance >= totalSupply);
    
  • 验证改变后的状态,如:

    assert(state);
    

合约中应该尽量少用 assert 调用,如果要适用 assert 应该在函数结尾处使用。

assertrequire 函数均被用来检查条件并在条件不满足时抛出异常,它们的主要区别是 require 应该被用于函数中检查条件,assert 用于预防不应该发生的情况,即不应该使条件错误。

合约例子

例子1:下面是一个合约例子,用来演示错误处理函数的用法。

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

// 错误处理及异常
contract Errors 
   // Require测试
    function testRequire(uint i) public pure 
        require(i > 10, "Input must be greater than 10");
    

    // Revert测试
    function testRevert(uint i) public pure 
        if (i <= 10) 
            revert("Input must be greater than 10");
        
    

    // Assert测试
    uint public num;
    function testAssert() public view 
        assert(num == 0);
    

    // 自定义错误
    error InsufficientBalance(uint balance, uint amount);
    function testCustomError(uint _amount) public view 
        uint bal = address(this).balance;
        if (bal < _amount) 
            revert InsufficientBalance(balance: bal, amount: _amount);
        
    

例子2:另一个合约例子,比上一个例子更复杂一些。

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

// 帐号存取款异常处理
contract ErrorsAccount 
    uint public balance;
    uint public constant MAX_UINT = 2**256 - 1;

    // 存入以太币到合约
    function deposit(uint _amount) public payable 
        uint oldBalance = balance;
        uint newBalance = balance + _amount;

        // 检查溢出
        require(newBalance >= oldBalance, "Overflow");

        balance = newBalance;
        assert(balance >= oldBalance);
    

    // 从合约提取以太币
    function withdraw(uint _amount) public payable 
        uint oldBalance = balance;

        // 检查溢出
        require(balance >= _amount, "Underflow");

        if (balance < _amount) 
            revert("Underflow");
        

        balance -= _amount;
        assert(balance <= oldBalance);
    

合约执行

我们在Remix中编译、部署和运行这个合约例子。

例子1:部署完成后的界面如下图:

我们分别执行 testRequiretestReverttestAsserttestCustomError 四个测试函数,观察输出结果是否和我们期望的一样。

例子2:部署完成后的界面如下图:

我们分别执行 depositwithdraw 两个测试函数,观察输出结果是否和我们期望的一样。

智能合约语言 Solidity 教程系列9 - 错误处理

这是Solidity教程系列文章第9篇介绍Solidity 错误处理。
Solidity系列完整的文章列表请查看分类-Solidity

写在前面

Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,
如果你还不了解,建议你先看以太坊是什么

欢迎订阅区块链技术专栏阅读更全面的分析文章。

什么是错误处理

错误处理是指在程序发生错误时的处理方式,Solidity处理错误和我们常见的语言不一样,Solidity是通过回退状态的方式来处理错误。发生异常时会撤消当前调用(及其所有子调用)所改变的状态,同时给调用者返回一个错误标识。注意捕捉异常是不可能的,因此没有try ... catch...。

为什么Solidity处理错误要这样设计呢?
我们可以把区块链理解为是全球共享的分布式事务性数据库。全球共享意味着参与这个网络的每一个人都可以读写其中的记录。如果想修改这个数据库中的内容,就必须创建一个事务,事务意味着要做的修改(假如我们想同时修改两个值)只能被完全的应用或者一点都没有进行。
学习过数据库的同学,应该理解事务的含义,如果你对事务一词不是很理解,建议你搜索一下“数据库事务“。
Solidity错误处理就是要保证每次调用都是事务性的。

如何处理

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

除了可以两个函数assert和require来进行条件检查,另外还有两种方式来触发异常:

  1. revert函数可以用来标记错误并回退当前调用
  2. 使用throw关键字抛出异常(从0.4.13版本,throw关键字已被弃用,将来会被淘汰。)

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

注意:在一个不存在的地址上调用底层的函数call,delegatecall,callcode 也会返回成功,所以我们在进行调用时,应该总是优先进行函数存在性检查。

在下面通过一个示例来说明如何使用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;
    }
}

我们实际运行下,看看异常是如何发生的:

  1. 首先打开Remix,贴入代码,点击创建合约。如下图:
    技术分享图片

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

技术分享图片

  1. 运行测试2:附加2wei 去调用sendHalf,运行正常。
  2. 运行测试3:附加2wei以及sendHalf参数为当前合约本身,在转账是发生异常,因为合约无法接收转账,错误提示上图类似。

assert类型异常

在下述场景中自动产生assert类型的异常:

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

require类型异常

在下述场景中自动产生require类型的异常:

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

当发生require类型的异常时,Solidity会执行一个回退操作(指令0xfd)。
当发生assert类型的异常时,Solidity会执行一个无效操作(指令0xfe)。
在上述的两种情况下,EVM都会撤回所有的状态改变。是因为期望的结果没有发生,就没法继续安全执行。必须保证交易的原子性(一致性,要么全部执行,要么一点改变都没有,不能只改变一部分),所以需要撤销所有操作,让整个交易没有任何影响。

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

参考文献

欢迎来我的知识星球深入浅出区块链讨论区块链技术,同时我也会为大家提供区块链技术解答,作为星友福利,星友可加入区块链技术付费交流群。
深入浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。











以上是关于智能合约Solidity语言错误处理函数(requirerevertassert)使用详解的主要内容,如果未能解决你的问题,请参考以下文章

智能合约语言 Solidity 教程系列9 - 错误处理

智能合约语言 Solidity 教程系列9 - 错误处理

智能合约语言 Solidity - 错误处理

智能合约语言 Solidity 教程系列8 - Solidity API(特殊的变量及函数)

智能合约语言 Solidity 教程系列8 - Solidity API(特殊的变量及函数)

智能合约语言 Solidity 教程系列3 - 函数类型