Solidity 101
学习Solidity 101 记录
Solidity 是一种用于编写以太坊虚拟机(EVM)智能合约的编程语言。
开发工具
本教程中,我们将使用 Remix 运行 Solidity 合约。Remix 是以太坊官方推荐的智能合约集成开发环境(IDE),适合新手,可以在浏览器中快速开发和部署合约,无需在本地安装任何程序。网址:https://remix.ethereum.org
Hello world
pragma solidity ^0.8.21;// 表示源文件将不允许小于 0.8.21 版本或大于等于 0.9.0 的编译器编译(第二个条件由 ^ 提供)
contract HelloWeb3 { // 创建合约(contract),合约名为 HelloWeb3
string public _string = "hello web3";
}Remix 会使用 Remix 虚拟机(以前称为 JavaScript 虚拟机)来模拟以太坊链,运行智能合约,类似在浏览器里运行一条测试链。
Remix 还会为你分配一些测试账户,每个账户里有 100 ETH(测试代币),随意使用。
点击部署 Deploy 即可部署合约。
值类型
- 值类型(Value Type) :包括布尔型,整数型等等,这类变量赋值时候直接传递数值。
- 引用类型(Reference Type) :包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
- 映射类型(Mapping Type) : Solidity中存储键值对的数据结构,可以理解为哈希表
布尔
bool public _bool = true;整形
int public _int = -1;
uint public _uint = 1;
uint256 public _number = 123456;
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小
地址类型
有两类:
- 普通地址:存储一个 20 字节的值(以太坊地址的大小)。
- payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。
address public _address = 0x......;
address payable public _add1 = payable(_adderss) // 可以转账、查余额
uint256 public balance = _address.balance;定长字节数组
- 定长字节数组: 属于值类型,数组长度在声明之后不能改变。根据字节数组的长度分为 bytes1, bytes8, bytes32 等类型。定长字节数组最多存储 32 bytes 数据,即bytes32。
- 不定长字节数组: 属于引用类型(之后的章节介绍),数组长度在声明之后可以改变,包括 bytes 等。
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0]; // 0x4d
枚举
类似C预言中的枚举
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;函数
Solidity 中函数的形式:
// 方括号中的是可写可不 写的关键字
function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
[returns (<return types>)]
{
<function body>
}解释:
- function:声明函数时的固定用法。
<function name>:函数名。([parameter types[, ...]]):圆括号内写入函数的参数,即输入到函数的变量类型和名称。{internal|external|public|private}:函数可见性说明符,共有4种。- public:内部和外部均可见。
- private:只能从本合约内部访问,继承的合约也不能使用。
- external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
- internal: 只能从合约内部访问,继承的合约可以用。
- 注意
- 1:合约中定义的函数需要明确指定可见性,它们没有默认值。
- 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。
[pure|view|payable]:决定函数权限/功能的关键字。- payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。
- pure 和 view 的介绍见下一节。
[virtual|override]: 方法是否可以被重写,或者是否是重写方法。virtual用在父合约上,标识的方法可以被子合约重写。override用在自合约上,表明方法重写了父合约的方法。<modifiers>: 自定义的修饰器,可以有0个或多个修饰器。[returns ()]:函数返回的变量类型和名称。<function body>: 函数体。
solidity 引入pure|view这两个关键字主要是因为 以太坊交易需要支付gas。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas)。
- pure,中文意思是“纯”,这里可以理解为”纯打酱油的”。pure 函数既不能读取也不能写入链上的状态变量。
- view,“看”,这里可以理解为“看客”。view函数能读取但也不能写入状态变量。
- 非 pure 或 view 的函数既可以读取也可以写入状态变量。
在以太坊中,以下语句被视为修改链上状态:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用 selfdestruct.
- 通过调用发送以太币。
- 调用任何未标记 view 或 pure 的函数。
- 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
pure vs view
pragma solidity ^0.8.21;
contract FunctionTypes {
uint256 public number = 5;
function add() external {// 这个函数不能标记为pure,
number = number + 1;
}
// pure: 不能读写链上的状态变量
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}
// view: 可以读不可以写
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}
}internal vs external
由于 internal 函数只能由合约内部调用,我们必须再定义一个 external 的 minusCall() 函数,通过它间接调用内部的 minus() 函数。
pragma solidity ^0.8.21;
contract FunctionTypes {
uint256 public number = 5;
function minus() internal{ // 内部函数
number = number - 1;
}
// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}
}payable
如果在remix的value中设置了1eth,点击minusPayable后,这1eth会转入合约地址,合约地址的余额会增加:
{
"0": "uint256: balance 1000000000000000000"
}pragma solidity ^0.8.21;
contract FunctionTypes {
uint256 public number = 5;
function minus() internal{
number = number - 1;
}
function minusCall() external {
minus();
}
// 能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance){
minus();
balance = address(this).balance;
}
}函数输出
有两个:return 和 returns
- returns:跟在函数名后面,用于声明返回的变量类型及变量名。
- return:用于函数主体中,返回指定的变量。
function returnMultiple() public pure returns(uint256,bool,uint256[3] memory)
{
return(1,true,[uint256(1),2,5]);
}因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰。
命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}结构式复制
function readReturn() public pure{
// 读取所有返回值
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
// 读取部分
(, _bool2, ) = returnNamed();
}变量数据存储和作用域
引用类型包括数组和结构体,由于这类变量比较复杂,占用存储空间大,在使用时必须要声明数据存储的位置。
Solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。整体消耗gas从多到少依次为:storage > memory > calldata
与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)
赋值本质上是创建引用指向本体,因此修改本体或者是引用,变化可以被同步。
- storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。
uint[] x = [1,2,3]; // 状态变量:数组 x
function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}-
memory赋值给memory,会创建引用,改变新变量会影响原变量。
-
其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。这有时会涉及到开发中的问题,比如从storage中读取数据,赋值给memory,然后修改memory的数据,但如果没有将memory的数据赋值回storage,那么storage的数据是不会改变的。
变量的作用域
有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable)
- 状态变量存储在链上,所有合约内函数都可以访问,gas消耗高。状态变量在合约内、函数外声明。
contract Variables {
uint public x = 1;
uint public y;
string public z;
}-
局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量不上链,gas低。
-
全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用。
function global() external view returns(address, uint, bytes memory){
address sender = msg.sender; // 请求发起地址
uint blockNum = block.number;// 当前区块高度
bytes memory data = msg.data;// 请求的数据
return(sender, blockNum, data);
}常见的全局变量:
blockhash(uint blockNumber): (bytes32):给定区块的哈希值 – 只适用于最近的256个区块,不包含当前区块。block.coinbase: (address payable):当前区块矿工的地址。block.gaslimit: (uint):当前区块的gaslimit。block.number: (uint):当前区块的number。block.timestamp: (uint):当前区块的时间戳,为unix纪元以来的秒。gasleft(): (uint256):剩余 gas。msg.data: (bytes calldata):完整call data。msg.sender: (address payable):消息发送者 (当前caller)。msg.sig: (bytes4):calldata的前四个字节 (function identifier)。msg.value: (uint):当前交易发送的wei值。block.blobbasefee: (uint):当前区块的blob基础费用。这是Cancun升级新增的全局变量。blobhash(uint index): (bytes32):返回跟当前交易关联的第index个blob的版本化哈希(第一个字节为版本号,当前为0x01,后面接KZG承诺的SHA256哈希的最后31个字节)。若当前交易不包含blob,则返回空字节。这是Cancun升级新增的全局变量。
以太单位
Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失:
- wei: 1
- gwei: 1e9 = 1000000000
- ether: 1e18 = 1000000000000000000
时间单位
可以在合约中规定一个操作必须在一周内完成,或者某个事件在一个月后发生。这样就能让合约的执行可以更加精确,不会因为技术上的误差而影响合约的结果。
- seconds: 1
- minutes: 60 seconds = 60
- hours: 60 minutes = 3600
- days: 24 hours = 86400
- weeks: 7 days = 604800
引用类型
有两个,一个是数组array,一个是结构体struct
数组
有定长和变长两种
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
// bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytes或bytes1[]。
// bytes 比 bytes1[] 省gas。
对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);如果创建的是动态数组,需要一个一个元素的赋值。
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;成员:
length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素。pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。
结构体
有四种赋值的方法:
// 结构体
struct Student{
uint256 id;
uint256 score;
}
Student student; // 初始一个student结构体
// 给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}
// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}
// 方法3:构造函数式
function initStudent3() external {
student = Student(3, 90);
}
// 方法4:key value
function initStudent4() external {
student = Student({id: 4, score: 60});
}映射类型
声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType和_ValueType分别是Key和Value的变量类型。
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
映射的规则
-
映射的
_KeyType只能选择Solidity内置的值类型,比如uint,address等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。 -
映射的存储位置必须是
storage,因此可以用于合约的状态变量,不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。 -
如果映射声明为
public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。 -
给映射新增的键值对的语法为
_Var[_Key] = _Value,其中_Var是映射变量名,_Key和_Value对应新增的键值对。
function writeMap(uint _key, address _value) public {
idToAddress[_key] = _value;
}变量的初始值
-
值类型初始值:
- boolean:
false - string:
""(空字符串) - int/uint:
0 - enum: 枚举第一个元素(如
enum Action {Buy, Hold}初始为Buy) - address:
0x0000000000000000000000000000000000000000 - function(内部/外部): 空白函数
- boolean:
-
引用类型初始值:
- mapping: 所有元素为对应类型默认值
- struct: 所有成员为默认值(如
struct Student {uint id;}初始id=0) - 动态数组:
[](空数组) - 静态数组: 所有成员为默认值(如
uint[3]初始[0,0,0])
-
delete操作符:将变量重置为初始值(如
delete _bool将_bool设为false)
常数
constant(常量)和immutable(不变量)。
-
constant与immutable关键字:- 用于状态变量,初始化后值不可改,提升安全性且节省gas。
- 仅数值变量适用,
string和bytes可声明为constant,不可为immutable。
-
constant特性:- 必须声明时初始化,后续不可修改,否则编译报错。
- 示例:
uint256 constant MY_CONST = 10;
// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;immutable特性:- 可在声明时或构造函数中初始化(v0.8.21后未显式初始化用数值初始值)。
- 声明与构造函数均初始化时,以构造函数为准,支持全局变量/自定义函数初始化。
- 示例:
uint256 immutable IMMUTABLE_VAL; constructor() { IMMUTABLE_VAL = 20; }
// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
// 在`Solidity v8.0.21`以后,下列变量数值暂为初始值
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;控制流
也就是if else while for 之类的,和其他语言一样。
构造函数和修饰器
构造函数(constructor),修饰器(modifier)。
构造函数
每个合约可以定义一个,并在部署合约的时候自动运行一次。
address owner;
constructor(address initOwner){
owner = initOwner;
}修饰器 modifier
类似面向对象编程中的装饰器,声明函数拥有的特性,并减少代码冗余。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。
定义了一个changeOwner函数,运行它可以改变合约的owner,但是由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
// 带有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}事件 event
- 事件(event)定义:EVM日志的抽象,用于记录合约关键操作,支持前端监听响应,且存储成本低(约2,000 gas/次,远低于链上存储)。
声明事件
事件的声明由event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:
from和to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。
event Transfer(address indexed from, address indexed to, uint256 value);释放事件
在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。
// 定义_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 日志
以太坊虚拟机(EVM)用日志Log来存储Solidity事件,每条日志记录都包含主题topics和数据data两部分。

主题 topics
日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。对于上面的Transfer事件,它的事件哈希就是:
keccak256("Transfer(address,address,uint256)")
//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
数据 data
事件中不带 indexed 的参数会被存储在 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes {
mapping ( address => uint256 ) private _balances;
event Transfer(address indexed from, address indexed to, uint256 value);
// 定义_transfer函数,执行转账逻辑
function _transfer(
address from,
address to,
uint256 amount
) external {
_balances[from] = 10000000;
_balances[from] -= amount;
_balances[to] += amount;
// 释放事件
emit Transfer(from, to, amount);
}
}部署后可以看到日志:
其中from式产生日志的合约地址,topic是事件签名(哈希),args部分是事件参数
继承 inheritance
包括简单继承,多重继承,以及修饰器(Modifier)和构造函数(Constructor)的继承。
规则
virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。override:子合约重写了父合约中的函数,需要加上override关键字。
简单继承
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");
}
}在写一个继承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");
}
}多重继承
-
继承时要按辈分最高到最低的顺序排。比如写一个
Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。 -
如果某一个函数在多个继承的合约里都存在,比如例子中的
hip()和pop(),在子合约里必须重写,不然会报错。 -
重写在多个父合约中都重名的函数时,
override关键字后面要加上所有父合约名字,例如override(Yeye, 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关键字即可。
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,并由构造函数的参数来确定:
// 构造函数的继承
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}- 在继承时声明父构造函数的参数,例如:
contract B is A(1) - 在子合约的构造函数中声明构造函数的参数,例如:
contract C is A {
constructor(uint _c) A(_c * _c) {}
}调用父合约的函数
有两种:
- 直接调用:
父合约名.函数名()
function callParent() public{
Yeye.pop();
}- 使用super关键字:子合约可以利用
super.函数名()来调用最近的父合约函数。
Solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop():
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}菱形继承
在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
在下面的例子中,调用合约people中的super.bar()会依次调用Eve、Adam,最后是God合约
// 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();
}
}抽象合约和接口
这里使用ERC721的接口合约为例介绍Solidity中的抽象合约(abstract)和接口(interface)
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}接口
接口类似于抽象合约,但不实现任何功能。
- 接口的规则:
- 不能包含状态变量
- 不能包含构造函数
- 不能继承除接口外的其他合约
- 所有函数都必须是
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都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。
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;
}IERC721事件:
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。
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);
}
}异常
三种抛出异常的方法:error,require和assert
error方法gas最少,其次是assert,require方法消耗gas最多。
error 异常
error 异常方便且高效(省gas)。同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。
error TransferNotOwner(); // 自定义error
error TransferNotOwner(address sender); // 自定义的带参数的error
在执行当中,error必须搭配revert(回退)命令使用。
定义了一个transferOwner1()函数,它会检查代币的owner是不是发起人,如果不是,就会抛出TransferNotOwner异常;如果是的话,就会转账。
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
// revert TransferNotOwner(msg.sender);
}
_owners[tokenId] = newOwner;
}require 异常
唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。
使用require重写上面的:
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}assert 异常
assert命令一般用于程序员写程序debug,因为它不能解释抛出异常的原因(比require少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}