自学内容网 自学内容网

solidity中的Error和Modifier详解

异常

写智能合约经常会出bug,solidity中的异常命令帮助我们debug。

Error

error是solidity 0.8.4版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在contract之外定义异常。

在Solidity中,异常处理是确保智能合约安全性和正确性的关键步骤。Solidity提供了几种主要方法来处理异常,包括error、require和assert。以下是这些方法的详细讲解:

  1. require:用于检查条件是否为真,如果条件为假,则会抛出异常并回滚交易。
  2. assert:用于检查不应该为假的条件,用于捕捉代码中的严重错误。
  3. revert:用于在特定条件下回滚交易,可以提供错误消息。
  4. 自定义错误:从 Solidity 0.8.4 开始,引入了自定义错误类型,用于节省 Gas 并提供更加具体的错误信息。 

1. require 语句

require 语句用于在函数执行之前声明前提条件,即在执行代码之前必须满足的约束。它接受一个参数,并在评估后返回布尔值,还有一个可选的自定义字符串消息。如果为false,则会引发异常并终止执行。未使用的gas会返回给调用者,状态也会回滚到原始状态。require 常用于以下场景:

•  验证输入参数或外部合约调用结果。
•  检查调用方是否具有足够的权限。
•  验证输入数据的合法性。 

pragma solidity ^0.5.0;
contract requireStatement {
    function checkInput(uint _input) public view returns(string memory) {
        require(_input >= 0, "invalid uint8");
        require(_input <= 255, "invalid uint8");
        return "Input is Uint8";
    }
}

 2. assert 语句

assert 语句用于检查代码逻辑中的不变量,即程序在任何时候都应该满足的条件。如果assert失败,意味着代码中存在致命的错误。assert 通常用于捕捉代码中的严重错误,特别是不应该发生的逻辑错误。当它失败的时候会回滚交易,但是不会消耗太多的Gas费用,因为它用于内部错误

pragma solidity ^0.5.0;
contract assertExample {
    uint x = 0;
    function increment() public {
        x += 1;
        assert(x > 0); // 确保 x 永远大于 0
    }
}

3. revert语句

revert用于在特定条件下回滚交易,可以提供错误消息。它与require类似,但revert不消耗Gas来存储错误信息。

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

contract Example{
  function checkCoundition(uint a) public pure{
    require(a>10,"Error: a must be greater than 10");
    //如果a不大于10,交易将会被回滚,并且会显示错误信息
    if(a==20){
        revert("Error:a cannot be 20");
    }
    //如果a为20,交易将会被回滚,并且会显示错误信息
  }
}

4. 自定义错误

从Solidity 0.8.4开始,引入了自定义错误类型,用于节省Gas并提供更加具体的错误信息。

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

contract Example {
    error InvalidNumber(uint value);

    function checkNumber(uint a) public pure {
        if (a <= 10) {
            revert InvalidNumber(a);
        }
        // 如果a不大于10,将使用自定义错误类型回滚交易
    }
}

在这个例子中,我们定义了一个名为InvalidNumber的自定义错误类型,它接受一个uint参数。在checkNumber函数中,如果a不大于10,我们使用revert关键字和自定义错误类型来回滚交易,并提供具体的错误信息。
自定义错误类型的好处是,它们允许合约的用户更容易地识别和处理特定的错误情况,同时减少了合约的Gas消耗。 

这里不回将错误信息存储在交易日志当中,因此更节省Gas费用。

构造函数

  • 构造函数是使用 constructor 关键字声明的一个可选函数;
  • 构造函数只在合约部署时调用一次,并用于初始化合约的状态变量;
  • 如果没有显式定义的构造函数,则由编译器创建默认构造函数。

声明语法

构造函数声明语法如下:

constructor(<paramslist>) <Access Modifier> {
// todo
} 

例如,下面的合约声明了一个构造函数,用于对状态变量进行初始化。

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

// 构造函数
contract Simple {
    string str;
             
    // 声明构造函数,并初始化状态变量
    constructor() {                 
        str = "hello simple";
    }
    
    // 定义一个函数返回状态变量的值
    function getValue() public view returns(string memory) {
        return str;
    }
}

继承的构造函数

如果父合约没有定义构造函数,则调用默认构造函数,如果在父合约中定义了构造函数,并且有一些参数,则子合约需要提供所有参数。有两种方法来调用父合约的构造函数:

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

contract Base{
    uint data;

    //构造函数
    constructor(uint _data){
        data=_data;
    }
}

//继承合约(直接初始化)
contract Derived is Base(2){
    //构造函数
    constructor(){}

    //定义一个函数访问父合约的状态变量
    function getData() external view returns(uint){
        uint result =data**2;
        return result;
    }
}
//调用合约
contract Caller{
    //创建子合约对象
    Derived c =new Derived();

    //通过子合约对象访问父合约和子合约的函数
    function getResult() public view returns(uint){
        return c.getData();
    }
}

间接初始化

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

contract Base{
    string str;

    //构造函数
    constructor(string memory _str){
        str =_str;
    }
}
    //继承合约(间接初始化)
    contract Derived is Base{
        //构造函数
        constructor(string memory _info) Base(_info){}

        //定义一个函数访问父合约的状态变量
        function getStr() external view returns(string memory){
            return str;
        }
    }
    //调用合约
    contract Caller{
        Derived c =new Derived("Hello Constructor");

        //通过子合约对象访问父合约和子合约的函数
        function getResult() public view returns(string memory){
            return c.getStr();
        }
    }

 Modifier

modifier可以改变函数的行为。可以被继承和重写。

其实modifier被用于最多的是行为检查这样可以使得减少检查代码的复用以及让代码看起来更简介易懂。比如,检查调用者是否有权限执行这个函数,传入的参数是否有错误等等。

// 定义了一个名为NoteBook的智能合约
contract NoteBook {
    // 声明了一个公共的字符串变量record,用于存储NoteBook的内容
    // 由于是公共的,所以它可以在合约外部被读取
    string public record; 

    // 声明了一个address类型的变量owner,用于存储NoteBook的拥有者的地址
    address owner; 

    // 构造函数,它在合约部署时执行一次
    constructor() {
        // 在合约部署时,将msg.sender(部署者地址)赋值给owner变量
        owner = msg.sender;
    }
    
    // changeRecord函数用于修改NoteBook的内容
    // 参数_record是一个新的字符串,用于更新record变量
    function changeRecord(string memory _record) public isOwner {
        // 更新record变量为新的值
        record = _record;
    }
    
    // 定义了一个名为isOwner的modifier(函数修改器)
    // 这个修改器用于检查调用者是否是NoteBook的拥有者
    modifier isOwner {
        // require函数用于断言一个条件,如果条件为false,则触发异常
        // 这里检查msg.sender(当前调用者的地址)是否等于owner
        // 如果不是,则返回错误信息"You are not the owner of this NoteBook"
        require(msg.sender == owner, "You are not the owner of this NoteBook");
        
        // 如果检查通过,则执行后面的_;
        // _是modifier中的一个特殊符号,表示原函数的执行
        _;
    }
}

这里的 _ 表示在 require 语句执行并且条件满足后,控制流将跳转到被 isOwner 修改的函数的主体部分。换句话说,_ 是一个占位符,它告诉编译器在成功通过修改器的条件检查后,继续执行函数的剩余部分。

例如,如果在 changeRecord 函数中使用 isOwner 修改器:

function changeRecord(string memory _record) public isOwner {
    record = _record;
}

当 changeRecord 函数被调用时,首先会执行 isOwner 修改器中的代码。如果 msg.sender 不等于 owner,require 语句会触发一个异常,函数执行停止,并且状态回滚。如果 msg.sender 等于 owner,则执行 _ 之后的代码,即 record = _record;,这将更新 record 变量的值。
总结来说,_ 在函数修改器中是一个指示编译器继续执行函数主体的指令。

modifier对函数参数的操作

执行函数时有时候也会对函数的参数有所要求,为了让函数内的代码更简洁我们便可以写在modifier中。那如何对函数参数进行检查呢?这个和函数的操作一样,调用时传参便可。看如下例子:

// 这个合约可以执行运算
contract Operation{

// 除法运算
    function division(uint256 opt1, uint256 opt2) public checkZero(opt2) pure returns(uint256){
        return opt1 / opt2;
    }
    
    // 检查除数是否为0
    modifier checkZero(uint256 divisor) {
        require (divisor != 0, "divisor can't be 0");
        _;
    }
}

在以上代码中我们需要做的是检查除法运算中的除数是否为0,若是0则中止运行,并给予提示。代码简单就不啰嗦了。

当然modifier还可以对storage中的变量进行检查

modifier的执行顺序

一个函数可能需要做多个检查,那么我们可以写多个modifier,调用时只需将每个modifier以空格隔开。而检查顺序也就是modifier们的排列顺序

但还有一种可能会迷惑大家的写法:

contract modifierOder {
    address owner;
    uint256 a;
    
    constructor() {
        owner = msg.sender;
    }
    
    function test(uint num) public checkPara(num) returns(uint256) {
        a = 10;
        return a;
    }
    
    // 修改a 
    modifier checkPara(uint number) {
        a = 1;
        _;
        a = 100;
    }

}

如以上代码所示:在 _后又有一句代码a = 100 。函数执行完return后,后面的代码则不再执行,但是在modifier中,执行完函数体 _ 还会接着执行 a = 100 这条语句。所以尽管函数返回的a 的值为10,但是最后a的值变成了100。


原文地址:https://blog.csdn.net/2302_79993788/article/details/143138841

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!