控制流
这一讲,我们将介绍Solidity中的控制流。
测试代码Control.sol,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Controlflow { // if else function ifElseTest(uint256 _number) public pure returns(bool){ if(_number == 0){ return(true); }else{ return(false); } } // for loop function forLoopTest() public pure returns(uint256){ uint sum = 0; for(uint i = 0; i < 10; i++){ sum += i; } return(sum); } // while function whileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; while(i < 10){ sum += i; i++; } return(sum); } // do-while function doWhileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; do{ sum += i; i++; }while(i < 10); return(sum); } // 三元运算符 ternary/conditional operator function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ // return the max of x and y return x >= y ? x: y; } }
注意⚠️ :Solidity中最常用的变量类型是uint,也就是正整数,取到负值的话,会报underflow错误。
控制流
Solidity的控制流与其他语言类似,主要包含以下几种:
if-else
1 2 3 4 5 6 7 function ifElseTest(uint256 _number) public pure returns(bool){ if(_number == 0){ return(true); }else{ return(false); } }
for循环
1 2 3 4 5 6 7 function forLoopTest() public pure returns(uint256){ uint sum = 0; for(uint i = 0; i < 10; i++){ sum += i; } return(sum); }
while循环
1 2 3 4 5 6 7 8 9 function whileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; while(i < 10){ sum += i; i++; } return(sum); }
do-while循环
1 2 3 4 5 6 7 8 9 function doWhileTest() public pure returns(uint256){ uint sum = 0; uint i = 0; do{ sum += i; i++; }while(i < 10); return(sum); }
三目运算
三元运算符是Solidity中唯一一个接受三个操作数的运算符,规则条件? 条件为真的表达式:条件为假的表达式。此运算符经常用作if语句的快捷方式。
1 2 3 4 5 // 三元运算符 ternary/conditional operator function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ // return the max of x and y return x >= y ? x: y; }
另外还有continue(立即进入下一个循环)和break(跳出当前循环)关键字可以使用。
构造函数和修饰器
这一讲,我们将用合约权限控制(Ownable)的例子介绍Solidity语言中构造函数(constructor)和独有的修饰器(modifier)。测试代码Owner.sol,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Owner { //定义owner变量 address public owner; //构造函数 constructor(address initialOwner){ //在部署合约时,将owner设置为initialOwner owner = initialOwner; } modifier onlyOwner{ //检查地址是否为owner require(msg.sender == owner); //如果是的话,继续运行函数主体,否则报错并revert交易 _; } // function changeOwner(address _newOwner) external onlyOwner{ //只有owner能运行这个函数 owner = _newOwner; } }
构造函数
构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址。
1 2 3 4 5 address owner; // 定义owner变量 // 构造函数 constructor(address initialOwner) { owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址 }
注意 :构造函数在不同的Solidity版本中的语法并不一致,在Solidity 0.4.22之前 ,构造函数不使用 constructor 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 Parents,构造函数名写成 parents),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 constructor 写法。
构造函数的旧写法代码示例:
1 2 3 4 5 6 pragma solidity =0.4.21; contract Parents { // 与合约名Parents同名的函数就是构造函数 function Parents () public { } }
修饰器
修饰器(modifier)是Solidity特有的语法,类似于面向对象编程中的装饰器(decorator),声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。
我们来定义一个叫做onlyOwner的modifier:
1 2 3 4 5 6 modifier onlyOwner{ //检查地址是否为owner require(msg.sender == owner); //如果是的话,继续运行函数主体,否则报错并revert交易 _; }
带有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子:
1 2 3 4 5 //定义一个带有onlyOwner修饰符的函数 function changeOwner(address _newOwner) external onlyOwner{ //只有owner能运行这个函数,并改变owner owner = _newOwner; }
我们定义了一个changeOwner函数,运行它可以改变合约的owner,但是由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。
运行测试
首先是部署代码,传递一个需要初始化的owner,初始化owner为0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
使用另一个用户0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,去调用合约,并尝试修改owner。owner修改失败!
尝试使用部署时输入的账户``0x5B38Da6a701c568545dCfcB03FcB875f56beddC4`。
去改变owner为0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,owner修改成功!
事件
这一讲,我们用转账ERC20代币为例来介绍Solidity中的事件(event)。
测试代码Events.sol,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract Events{ //定义_balances映射变量,记录每个地址的比特币数量 mapping(address => uint256) public _balances; //定义Transfer event,记录transfer交易的转账地址,接收地址和转账数量。 event Transfer(address indexed from,address indexed to,uint256 value); //定义_transfer函数,执行转账逻辑 function _transfer( address from, address to, uint256 amount ) external { //给转账地址一些初始代币 _balances[from] = 10000000; //from地址减去转账数量 _balances[from] -= amount; //to地址加上转账数量 _balances[to] += amount; //释放事件 emit Transfer(from, to, amount); } }
事件
Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点:
响应:应用程序(ethers.js )可以通过RPC接口订阅和监听这些事件,并在前端做响应。
经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。
声明事件
事件的声明由event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。
以ERC20代币合约的Transfer事件为例:
1 event Transfer(address indexed from, address indexed to, uint256 value);
我们可以看到,Transfer事件共记录了3个变量from,to和value,分别对应代币的转账地址,接收地址和转账数量,其中from和to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。
释放事件
我们可以在函数里释放事件。在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 定义_transfer函数,执行转账逻辑 function _transfer( address from, address to, uint256 amount ) external { _balances[from] = 10000000; // 给转账地址一些初始代币 _balances[from] -= amount; // from地址减去转账数量 _balances[to] += amount; // to地址加上转账数量 // 释放事件 emit Transfer(from, to, amount); }
EVM日志(Log)
以太坊虚拟机(EVM)用日志Log来存储Solidity事件,每条日志记录都包含主题topics和数据data两部分。
主题(Topics)
日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。
对于上面的Transfer事件,它的事件哈希就是:
1 2 3 keccak256("Transfer(address,address,uint256)") //0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
除了事件哈希,主题还可以包含至多3个indexed参数,也就是Transfer事件中的from和to。
indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
这里其实会引入一个新的问题,根据Solidity的官方文档 , 对于非值类型的参数(如arrays, bytes, strings), Solidity不会直接存储,而是会将Keccak-256哈希存储在主题中,从而导致数据信息的丢失。这对于某些依赖于链上事件的DAPP(跨链,用户注册等等)来说,可能会导致事件检索困难,需要解析哈希值。
数据(Data)
事件中不带 indexed的参数会被存储在 data 部分中,可以理解为事件的"值"。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。
Topics里面有三个元素,第一个是这个事件的哈希,第二和三是我们定义的两个indexed变量的信息,即转账的转出地址和接收地址。Data里面是剩下的不带indexed的变量,也就是转账数量。可以在上图看到Data部分为0x32,50的16进制。
继承
这一讲,我们介绍Solidity中的继承(inheritance),包括简单继承,多重继承,以及修饰器(Modifier)和构造函数(Constructor)的继承。
继承
继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,Solidity也是面向对象的编程,也支持继承。
规则
virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
override:子合约重写了父合约中的函数,需要加上override关键字 。
注意 :用override修饰public变量,会重写与变量同名的getter函数,例如:
1 mapping(address => uint256) public override balanceOf;
简单继承
我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 contract Yeye { event Log(string msg); // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); } function pop() public virtual{ emit Log("Yeye"); } function yeye() public virtual { emit Log("Yeye"); } }
我们再定义一个爸爸合约Baba,让他继承Yeye合约,语法就是contract Baba is Yeye,非常直观。在Baba合约里,我们重写一下hip()和pop()这两个函数,加上override关键字,并将他们的输出改为”Baba”;并且加一个新的函数baba,输出也是”Baba”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 contract Baba is Yeye{ // 继承两个function: hip()和pop(),输出改为Baba。 function hip() public virtual override{ emit Log("Baba"); } function pop() public virtual override{ emit Log("Baba"); } function baba() public virtual{ emit Log("Baba"); } }
完整的测试代码Yeye.sol,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Yeye{ event Log(string msg); // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); } function pop() public virtual{ emit Log("Yeye"); } function yeye() public virtual { emit Log("Yeye"); } } contract Baba is Yeye{ // 继承两个function: hip()和pop(),输出改为Baba。 function hip() public virtual override{ emit Log("Baba"); } function pop() public virtual override{ emit Log("Baba"); } function baba() public virtual{ emit Log("Baba"); } }
多重继承
Solidity的合约可以继承多个合约。规则:
继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。
如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。
重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。
例子:
1 2 3 4 5 6 7 8 9 10 contract Erzi is Yeye, Baba{ // 继承两个function: hip()和pop(),输出值为Erzi。 function hip() public virtual override(Yeye, Baba){ emit Log("Erzi"); } function pop() public virtual override(Yeye, Baba) { emit Log("Erzi"); } }
我们可以看到,Erzi合约里面重写了hip()和pop()两个函数,将输出改为”Erzi”,并且还分别从Yeye和Baba合约继承了yeye()和baba()两个函数。
完整的测试代码Yeye.sol,完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; contract Yeye{ event Log(string msg); // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); } function pop() public virtual{ emit Log("Yeye"); } function yeye() public virtual { emit Log("Yeye"); } } contract Baba is Yeye{ // 继承两个function: hip()和pop(),输出改为Baba。 function hip() public virtual override{ emit Log("Baba"); } function pop() public virtual override{ emit Log("Baba"); } function baba() public virtual{ emit Log("Baba"); } } contract Erzi is Yeye, Baba{ // 继承两个function: hip()和pop(),输出值为Erzi。 function hip() public virtual override(Yeye, Baba){ emit Log("Erzi"); } function pop() public virtual override(Yeye, Baba) { emit Log("Erzi"); } }
修饰器的继承
Solidity中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtual和override关键字即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 contract Base1 { modifier exactDividedBy2And3(uint _a) virtual { require(_a % 2 == 0 && _a % 3 == 0); _; } } contract Identifier is Base1 { //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数 function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { return getExactDividedBy2And3WithoutModifier(_dividend); } //计算一个数分别被2除和被3除的值 function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ uint div2 = _dividend / 2; uint div3 = _dividend / 3; return (div2, div3); } }
Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修饰器,也可以利用override关键字重写修饰器:
1 2 3 4 modifier exactDividedBy2And3(uint _a) override { _; require(_a % 2 == 0 && _a % 3 == 0); }
完整的测试代码Base1.sol,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract Base1 { modifier exactDividedBy2And3(uint _a) virtual { require(_a % 2 == 0 && _a % 3 == 0); _; } } contract Identifier is Base1 { //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数 function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { return getExactDividedBy2And3WithoutModifier(_dividend); } //计算一个数分别被2除和被3除的值 function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ uint div2 = _dividend / 2; uint div3 = _dividend / 3; return (div2, div3); } }
构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并由构造函数的参数来确定:
1 2 3 4 5 6 7 8 // 构造函数的继承 abstract contract A { uint public a; constructor(uint _a) { a = _a; } }
在继承时声明父构造函数的参数,例如:contract B is A(6)
在子合约的构造函数中声明构造函数的参数,例如:
1 2 3 contract C is A { constructor(uint _c) A(_c * _c) {} }
完整的测试代码A.sol如下:
1 2 3 4 5 6 7 8 9 10 11 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract A{ uint public a; constructor(uint _a) { a = _a; } } contract C is A { constructor(uint _c) A(_c * _c) {} }
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super关键字。
直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()
1 2 3 function callParent() public{ Yeye.pop(); }
super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop():
1 2 3 4 function callParentSuper() public{ // 将调用最近的父合约函数,Baba.pop() super.pop(); }
钻石继承
在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。
在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
我们先写一个合约God,再写Adam和Eve两个合约继承God合约,最后让创建合约people继承自Adam和Eve,每个合约都有foo和bar两个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /* 继承树: God / \ Adam Eve \ / people */ contract God { event Log(string message); function foo() public virtual { emit Log("God.foo called"); } function bar() public virtual { emit Log("God.bar called"); } } contract Adam is God { function foo() public virtual override { emit Log("Adam.foo called"); super.foo(); } function bar() public virtual override { emit Log("Adam.bar called"); super.bar(); } } contract Eve is God { function foo() public virtual override { emit Log("Eve.foo called"); super.foo(); } function bar() public virtual override { emit Log("Eve.bar called"); super.bar(); } } contract people is Adam, Eve { function foo() public override(Adam, Eve) { super.foo(); } function bar() public override(Adam, Eve) { super.bar(); } }
在这个例子中,调用合约people中的super.bar()会依次调用Eve、Adam,最后是God合约。
虽然Eve、Adam都是God的子合约,但整个过程中God合约只会被调用一次。原因是Solidity借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序。更多细节你可以查阅Solidity的官方文档 。
抽象合约和接口
这一讲,我们用ERC721的接口合约为例介绍Solidity中的抽象合约(abstract)和接口(interface),帮助大家更好的理解ERC721标准。
ERC-721是以太坊网络上用于实现非同质化代币(NFT)的标准。它定义了一组规则,允许创建具有唯一性的数字资产或代币。与ERC-20标准的同质化代币不同 ,ERC-721代币每一个都是独一无二的,可以用来表示艺术作品、收藏品、游戏道具、房地产等独特的资产。
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。如果我们还没想好具体怎么实现某个函数,那么可以把合约标为abstract,之后让别人补写上。
1 2 3 abstract contract InsertionSort{ function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); }
测试抽象合约的代码Abstract.sol,如下:
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; abstract contract Base{ string public name = "Base"; function getAlias() public pure virtual returns(string memory); } contract BaseImpl is Base{ function getAlias() public pure override returns(string memory){ return "BaseImpl"; } }
测试结果如下:
接口
接口类似于抽象合约,但它不实现任何功能。接口的规则:
不能包含状态变量
不能包含构造函数
不能继承除接口外的其他合约
所有函数都必须是external且不能有函数体
继承接口的非抽象合约必须实现接口定义的所有功能
虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们。
如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。
因为接口提供了两个重要的信息:
合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)。
接口id(更多信息见EIP165 )
另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具 ,也可以将ABI json文件转换为接口sol文件。
我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface IERC721 is IERC165 { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); function balanceOf(address owner) external view returns (uint256 balance); function ownerOf(uint256 tokenId) external view returns (address owner); function safeTransferFrom(address from, address to, uint256 tokenId) external; function transferFrom(address from, address to, uint256 tokenId) external; function approve(address to, uint256 tokenId) external; function getApproved(uint256 tokenId) external view returns (address operator); function setApprovalForAll(address operator, bool _approved) external; function isApprovedForAll(address owner, address operator) external view returns (bool); function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external; }
测试接口的代码Interface.sol,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; interface Base{ function getFirstName() external pure returns (string memory); function getLastName() external pure returns (string memory); } contract BaseImpl is Base{ function getFirstName() external pure override returns(string memory){ return "Amazing"; } function getLastName() external pure override returns(string memory){ return "Ang"; } }
IERC721事件
IERC721包含3个事件,其中Transfer和Approval事件在ERC20中也有。
Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenId。
Approval事件:在授权时被释放,记录授权地址owner,被授权地址approved和tokenId。
ApprovalForAll事件:在批量授权时被释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。
IERC721函数
balanceOf:返回某地址的NFT持有量balance。
ownerOf:返回某tokenId的主人owner。
transferFrom:普通转账,参数为转出地址from,接收地址to和tokenId。
safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。
approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。
getApproved:查询tokenId被批准给了哪个地址。
setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator。
isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
safeTransferFrom:安全转账的重载函数,参数里面包含了data。
什么时候使用接口?
如果我们知道一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。
无聊猿BAYC属于ERC721代币,实现了IERC721接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721接口就可以与它交互,比如用balanceOf()来查询某个地址的BAYC余额,用safeTransferFrom()来转账BAYC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 contract interactBAYC { // 利用BAYC地址创建接口合约变量(ETH主网) IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D); // 通过接口调用BAYC的balanceOf()查询持仓量 function balanceOfBAYC(address owner) external view returns (uint256 balance){ return BAYC.balanceOf(owner); } // 通过接口调用BAYC的safeTransferFrom()安全转账 function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{ BAYC.safeTransferFrom(from, to, tokenId); } }
异常
这一讲,我们介绍Solidity三种抛出异常的方法:error,require和assert,并比较三种方法的gas消耗。
异常
写智能合约经常会出bug,Solidity中的异常命令帮助我们debug。
测试代码Error.sol,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; // Gas cost在Remix中测试得到 使用0.8.26版本编译 // 参数使用 tokenId = 123, address = {any address} // 自定义error //error TransferNotOwner(); error TransferNotOwner(address sender); contract Errors { // 一组映射,记录每个TokenId的Owner mapping(uint256 => address) private _owners; // Error方法: gas cost 24095 // Error方法: gas cost 24113 function transferOwner1(uint256 tokenId, address newOwner) public { if (_owners[tokenId] != msg.sender) { //revert TransferNotOwner(); revert TransferNotOwner(msg.sender); } _owners[tokenId] = newOwner; } // require方法: gas cost 24211 function transferOwner2(uint256 tokenId, address newOwner) public { require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); _owners[tokenId] = newOwner; } // assert方法: gas cost 24109 function transferOwner3(uint256 tokenId, address newOwner) public { assert(_owners[tokenId] == msg.sender); _owners[tokenId] = newOwner; } }
Error
error是solidity 0.8.4版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在contract之外定义异常。下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:
1 error TransferNotOwner(); // 自定义error
我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址
1 error TransferNotOwner(address sender); // 自定义的带参数的error
在执行当中,error必须搭配revert(回退)命令使用。
1 2 3 4 5 6 7 function transferOwner1(uint256 tokenId, address newOwner) public { if(_owners[tokenId] != msg.sender){ revert TransferNotOwner(); // revert TransferNotOwner(msg.sender); } _owners[tokenId] = newOwner; }
我们定义了一个transferOwner1()函数,它会检查代币的owner是不是发起人,如果不是,就会抛出TransferNotOwner异常;如果是的话,就会转账。
不带参数的error,消耗gas量为24095
带参数的error,消耗gas量为24113
Require
require命令是solidity 0.8版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。
我们用require命令重写一下上面的transferOwner1函数:
1 2 3 4 function transferOwner2(uint256 tokenId, address newOwner) public { require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); _owners[tokenId] = newOwner; }
require消耗gas的量为24211
Assert
assert命令一般用于程序员写程序debug,因为它不能解释抛出异常的原因(比require少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。
我们用assert命令重写一下上面的transferOwner1函数:
1 2 3 4 function transferOwner3(uint256 tokenId, address newOwner) public { assert(_owners[tokenId] == msg.sender); _owners[tokenId] = newOwner; }
assert消耗gas的量为24109
三种方法的gas比较
我们比较一下三种抛出异常的gas消耗,查询gas的消耗 (使用0.8.26版本编译)
error方法gas消耗:24095 (加入参数后gas消耗:24113)
require方法gas消耗:24211
assert方法gas消耗:24109
我们可以看到,error方法gas最少,其次是assert,require方法消耗gas最多!因此,error既可以告知用户抛出异常的原因,又能省gas,大家要多用!(注意,由于部署测试时间的不同,每个函数的gas消耗会有所不同,但是比较结果会是一致的。)
备注: Solidity 0.8.0之前的版本,assert抛出的是一个 panic exception,会把剩余的 gas 全部消耗,不会返还。更多细节见官方文档 。
参考
https://www.wtf.academy/docs/solidity-101/HelloWeb3
https://github.com/AmazingAng/WTF-Solidity
https://learnblockchain.cn/docs/ethers.js/api.html