solidity中的Error和Modifier详解
异常
写智能合约经常会出bug,solidity中的异常命令帮助我们debug。
Error
error是solidity 0.8.4版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在contract之外定义异常。
在Solidity中,异常处理是确保智能合约安全性和正确性的关键步骤。Solidity提供了几种主要方法来处理异常,包括error、require和assert。以下是这些方法的详细讲解:
require
:用于检查条件是否为真,如果条件为假,则会抛出异常并回滚交易。assert
:用于检查不应该为假的条件,用于捕捉代码中的严重错误。revert
:用于在特定条件下回滚交易,可以提供错误消息。- 自定义错误:从 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。