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 常用于以下场景:

• 验证输入参数或外部合约调用结果。

• 检查调用方是否具有足够的权限。

• 验证输入数据的合法性。

Go 复制代码
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费用,因为它用于内部错误

Go 复制代码
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来存储错误信息。

Go 复制代码
// 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并提供更加具体的错误信息。

Go 复制代码
// 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 关键字声明的一个可选函数;
  • 构造函数只在合约部署时调用一次,并用于初始化合约的状态变量;
  • 如果没有显式定义的构造函数,则由编译器创建默认构造函数。

声明语法

构造函数声明语法如下:

Go 复制代码
constructor(<paramslist>) <Access Modifier> {
	// todo
} 

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

Go 复制代码
// 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;
    }
}

继承的构造函数

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

Go 复制代码
// 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();
    }
}

间接初始化

Go 复制代码
// 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被用于最多的是行为检查,**这样可以使得减少检查代码的复用以及让代码看起来更简介易懂。**比如,检查调用者是否有权限执行这个函数,传入的参数是否有错误等等。

Go 复制代码
// 定义了一个名为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 修改器:

Go 复制代码
function changeRecord(string memory _record) public isOwner {
    record = _record;
}

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

总结来说,_ 在函数修改器中是一个指示编译器继续执行函数主体的指令。

modifier对函数参数的操作

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

Go 复制代码
// 这个合约可以执行运算
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们的排列顺序

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

Go 复制代码
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。

相关推荐
加密新世界12 小时前
指南: 如何在 MEV 项目中使用 Yul
区块链
MavenTalk2 天前
solana链上智能合约开发案例一则
rust·区块链·智能合约·dapp·solana
kejijianwen3 天前
Algen的跨链互操作性:增强区块链连接性
运维·centos·区块链
Sui_Network3 天前
World Wide Walrus:下一代数据存储协议
大数据·人工智能·web3·去中心化·区块链
Huazzi.3 天前
区块链中的wasm合约是什么?
区块链·wasm
一水鉴天3 天前
智能工厂的设计软件 为了监管控一体化的全能Supervisor 的监督学习 之 序6 进化论及科学的信息技术创新:分布式账本/区块链/智能合约
开发语言·人工智能·学习·区块链·智能合约·分布式账本
电报号dapp1193 天前
TON商城与Telegram App:生态融合与去中心化未来的精彩碰撞
去中心化·区块链
加密新世界3 天前
掌控 Solidity:事件日志、继承和接口的深度解析
区块链
MavenTalk4 天前
两大新兴开发语言大比拼:Move PK Rust
开发语言·后端·rust·区块链
TMGM202410284 天前
加密市场动态:暴涨后的调整与未来趋势
区块链