控制流
这一讲,我们将介绍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