sodility入门认识
入门本人是通过wtf这个网站里面学习的sodility语言,这个网站非常全面,链接:https://www.wtf.academy。非常适合小白食用(本人也是小白,最近刷完入门)。基本框架就按照wtf网站就行了,下面谈谈本人的一些理解和心得。如有不当,欢迎大佬指出。感谢wtf网站。
入门
本人是通过wtf这个网站里面学习的sodility语言,这个网站非常全面,链接:https://www.wtf.academy。非常适合小白食用(本人也是小白,最近刷完入门)。基本框架就按照wtf网站就行了,下面谈谈本人的一些理解和心得。如有不当,欢迎大佬指出。
1.开始写sodility程序
-
我们只需要打出pragma,“ // SPDX-License-Identifier: MIT ”便会自行出现
-
版本号的话,用最新版即可。新版本可以兼容旧版本。
-
关于contract,我理解为编程语言的主函数。整个程序的核心是主合约,它的分支是一些子合约。后面会提到子合约
-
语句结束以分号结尾
以上基本认识
2.认识一些变量与规则
-
uint 无符号变量,它自身类型多,你在remix打一下就知道了。
uint256表示一个无符号的 256 位整数,即可以表示范围在 0 到 2^256 - 1 之间的整数 -
uint256 public _number2 = 2**2; // 指数
-
比较运算符(返回布尔值):
<=,<,==,!=,>=,> -
&&和||运算符遵循短路规则,这意味着,假如存在f(x) || g(y)的表达式,如果f(x)是true,g(y)不会被计算 -
地址类型:address:地址类型用于存储 20 字节的值,这对应于以太坊网络上的地址大小。可以存储和操作地址类型的变量。
-
payable修饰符 地址用于接收以太币,并且提供了安全的以太币转账功能// payable: 递钱,能给合约支付eth的函数 function minusPayable() external payable returns(uint256 balance) { minus(); balance = address(this).balance; } -
失败处理:
- 在使用
transfer进行转账时,如果转账失败,将会抛出异常并中止当前合约的执行,所有对状态的更改将被还原,但是转账失败的情况下不会影响其他的合约。 - 在使用
send进行转账时,如果转账失败,send会返回 false,并且当前合约的执行不会中止,其他的操作可以继续执行。但是,开发者应该检查send的返回值来确定转账是否成功,并相应地处理失败的情况。 balance和transfer(),可以用来查询ETH余额以及安全转账(内置执行失败的处理)
- 在使用
-
定长(
byte,bytes8,bytes32)-
byte表示一个字节,大小为 8 位。byte类型可以存储的值的范围是从 0 到 255,这是因为一个字节可以表示 8 位二进制数,从全零(00000000)到全一(11111111),对应着十进制数的范围从 0 到 255。 -
bytes8表示 8 个字节,大小为 8 字节 * 8 位/字节 = 64 位。 -
bytes32表示 32 个字节,大小为 32 字节 * 8 位/字节 = 256 位
-
-
以
hex开头的字符串会被解析为固定长度的字节数组,其长度取决于字符串中包含的十六进制数字的数量。因此,
hex"0011223344556677"包含了 16 个十六进制数字,对应 8 个字节的数据。因为每个十六进制数字代表 4 位,而每个字节包含 8 位(1 字节 = 2 个十六进制数字),所以总共有 16 位 / 4 位 = 4 个字节,因此长度为 8 字节。 -
bytes32类型的长度是固定的,因此在赋值给_byte32变量时,Solidity 会在字符串后面填充足够的空字节使其长度达到 32 字节。 -
枚举 enum:集合数据类型,下面用wtf上的代码表现
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell enum ActionSet { Buy, Hold, Sell } // 创建enum变量 action ActionSet action = ActionSet.Buy;枚举中的每个常量都被自动赋予一个整数值,默认从
0开始,依次递增。因此,ActionSet.Buy对应于整数0,ActionSet.Hold对应于整数1,ActionSet.Sell对应于整数2。 -
状态变量,可以理解为主合约定义的变量
-
全局变量:不用申明,直接使用;
- block 相关变量:
block.coinbase: 当前区块的矿工地址。block.difficulty: 当前区块的难度。block.gaslimit: 当前区块的 gas 限制。block.number: 当前区块的编号。block.timestamp: 当前区块的时间戳。
- msg 相关变量:
msg.data: 完整的 calldata。msg.sender: 当前调用的发件人(地址)。msg.sig: 调用数据的前 4 个字节(函数标识符)。msg.value: 与消息一起发送的以太币数量(wei)。
- tx 相关变量:
tx.gasprice: 交易的 gas 价格。tx.origin: 交易的原始发送者。
- 全局变量:
address(this): 合约地址。blockhash(uint blockNumber) returns (bytes32): 返回指定区块的区块哈希。gasleft() returns (uint256): 返回当前剩余的 gas。
- block 相关变量:
-
显示转换:程序员明确地指定将一个数据类型转换为另一个数据类型
比如说数组: [uint(1),2,3],第一个数我们显示转换了,整个数组都是uint类型
-
节点概念;参与区块链网络运行的任何设备和计算机,可以是完整的区块链节点,也可以是执行部分功能的节点。节点负责维护区块链网络的完整副本,并参与网络的共识过程,验证和广播交易以及区块
-
区块:区块是区块链中存储数据的基本单位.每个区块包含一组交易和区块头信息,其中的交易记录了数据的变化。区块通过哈希链接在一起,形成了一个不断增长的链式结构,这就是所谓的区块链。每个区块通常包含前一个区块的哈希值,这样就确保了区块链的不可篡改性。
-
存储在链上在区块链上存储数据意味着将数据存储在区块链网络中的每个节点上。
-
链上状态通常指的是区块链上保存的数据状态。
3.函数类型
基本架构,与wtf提到的一样:
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
-
function是定义一个函数固定用法
-
后接函数名,括号里是传递的参数
-
internal:只能从合约内部访问,继承的合约可以用,包括该合约中的子合约
external:提供一个外部接口,,为内部函数提供使用渠道
public:透明化
private:类似internal,区别是继承的子合约不能使用
-
pure用于声明函数不会读取或修改合约的状态。啥搞不了就非常不耗gas,相对安全,要修改参数你只能传入参数并返回一个新的参数
-
view可以读取参数但不能修改参数,,不需要传入参数,可以直接使用合约里面定义的状态变量。最终也是需要以返回的形式,看修改的参数
-
returns 规定返回类型,也可直接指明变量名称为命名式返回,类型是硬性要求
-
return:用于函数主体,就是函数里面的内容。指定返回变量
// 命名式返回 function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ _number = 2; _bool = false; _array = [uint256(3),2,1]; }// 返回多个变量 function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ return(1, true, [uint256(1),2,5]); }8.解构赋值:是一种快速从数组或对象中提取数据并赋值给变量的方法。
pragma solidity ^0.8.0; contract DestructuringAssignment { function getData() external pure returns (uint256, uint256) { return (10, 20); } function process() external pure { uint256 a; uint256 b; (a, b) = getData(); // 使用解构赋值将返回的元组拆分为单独的变量 // 现在变量 a 的值为 10,变量 b 的值为 20 } }注意
1.如果设置函数没有声明可见性修饰符,则默认public
2.使用pure,修饰符表示该函数不会读取或修改合约状态,并且不会调用其他合约。它只能执行计算操作。参数类型对应
3.声明变量,数组类型 可用uint256;
4.memory:数据位置标识符,存储临时数据,不消耗gas;数据在函数执行完后清除
5.数组规定了几个就必须几个
6.类型要一样,1是属于uint8,也属于unit256,即使这样,你前面也必须声明uint256. 数组类型,,前面第一个元素uint(element),即之后整个数组都是uint256类型
7.
uint256[3]类型,这是一个包含三个元素的固定大小数组
4.数据位置,赋值
这里直接用wtf的原话更好
solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。大致用法:
-
storage:合约里的状态变量默认都是storage,存储在链上。 -
memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。 -
calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。例子:
在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:
-
storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。例子:uint[] x = [1,2,3]; // 状态变量:数组 x function fStorage() public{ //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x uint[] storage xStorage = x; xStorage[0] = 100; }
2.storage赋值给memory,会创建独立的副本,修改其中一个不会影响另一个;反之亦然。例子:
uint[] x = [1,2,3]; // 状态变量:数组 x
function fMemory() public view{
//声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
uint[] memory xMemory = x;
xMemory[0] = 100;
xMemory[1] = 200;
uint[] memory xMemory2 = x;
xMemory2[0] = 300;
}
用了wtf的例子。要注意memory使用是在函数中的。
变量作用域
-
局部变量(在函数内部声明的变量)默认存储在
memory中,gas高 -
状态变量(在合约中声明的变量)默认存储在
storage中,gas低 -
**全局变量:**全局变量是全局范围工作的变量,都是
solidity预留关键字 -
常用的全局变量
msg.sender: 当前调用的发件人地址。msg.value: 与消息一起发送的以太币数量(wei)。block.number: 当前区块的编号。block.timestamp: 当前区块的时间戳。tx.origin: 交易的原始发送者地址。address(this): 合约自身的地址。gasleft(): 返回当前剩余的 gas 数量。blockhash(uint blockNumber) returns (bytes32): 返回指定区块的区块哈希。
5.引用类型 array struct
数组分为固定长度数组和动态数组两种:
- 固定
// 固定长度
uint[8] array1;
bytes1[5] array2;
address[100] array3;
- 动态
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
-
注意:
bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytes或bytes1[]。在gas上,bytes比bytes1[]便宜。因为bytes1[]在memory中要增加31个字节进行填充,会产生额外的gas。但是在storage中,由于内存紧密打包,不存在字节填充。 -
解释这个增加31字节是为什么:字节对齐(Byte Alignment)是一种数据存储和访问的规范,它确保数据在内存中的地址是按照一定规则对齐的。在大多数计算机系统中,数据类型的地址通常需要满足一定的对齐要求,以便在访问内存时能够高效地操作数据。在 Solidity 中,动态大小的数据类型(例如动态大小的数组
bytes、string、动态大小的结构体等)在内存中需要按照字节对齐的方式进行存储。这意味着它们的起始地址必须是特定字节数的倍数。目前 Solidity 的字节对齐要求是 32 字节。 -
array4是一个动态数组,而array5是一个固定大小的数组,所以不能直接将array5赋值给array4。你可以通过以下方式解决这个问题:solidityCopy codefunction addpush() pure public returns (uint[] memory) { uint[2] memory array5 = [1, 2]; uint[] memory array4 = new uint[](array5.length); for (uint i = 0; i < array5.length; i++) { array4[i] = array5[i]; } array4.push(3); return array4; }数组成员(动态数组)
length
: 数组有一个包含元素数量的length成员,memory`数组的长度在创建后是固定的。push:数组最后添加元素
pop:数组最后移除元素
即使你在动态数组后面限制了大小,也视为动态数组,因为其可以改变的性质未变。 <!--StartFragment-->
结构体(Struct)是一种自定义的复合数据类型,用于存储一组相关的数据。结构体可以包含多个不同类型的成员变量,这些成员变量可以是基本数据类型、数组、映射、其他结构体等。
solidityCopy code
pragma solidity ^0.8.0;
contract MyContract {
struct Person {
string name;
uint age;
address wallet;
}
// 声明一个结构体类型的变量
Person public alice;
}
6.映射Mapping (键值对)
映射的格式为mapping(_KeyType => _ValueType)
mapping(address => uint256) public balances;
address 是键的类型,uint256 是值的类型。这个 mapping 可以用来存储地址与对应的 uint256 值,例如用户的余额信息。
作为 mapping 中值的变量类型可以是 Solidity 中任何合法的数据类型,包括但不限于以下类型:
- 基本数据类型(uint、int、address、bool、bytes、string)
- 枚举类型(enum)
- 结构体(struct)
- 数组(包括静态数组和动态数组)
- 合约类型(contract)
- 地址数组(address[])
- 字节数组数组(bytes[])
- 其他映射类型(mapping)
- 函数类型(function) 等等。
作为 mapping 中键(key)的变量类型,可以使用任何不可变的数据类型,包括但不限于:
- 整数类型(如 uint、int)
- 地址类型(如 address)
- 字符串类型(如 string)
- 枚举类型(如 enum)
- 字节数组类型(如 bytes1、bytes32)
- 合约类型(如合约地址)
- 固定大小的字节数组类型(如 bytes1[10])
要给映射变量 map 新增键值对:
-
直接赋值:使用
map[key] = value的方式将新的键值对添加到映射中。如果键key不存在于映射中,则会创建一个新的键值对;如果键key已经存在,则会更新对应的值为value。 -
使用函数:你可以编写一个函数,接受键和值作为参数,并将它们添加到映射中。这个函数可以是合约的一个公共方法,供外部调用。例如:
mapping(uint => string) public map;
function addKeyValuePair(uint key, string memory value) public {
map[key] = value;
}
这样,当调用 addKeyValuePair 函数时,你就可以向映射中添加新的键值对了。
这里用gpt生成的代码可以很好理解
映射账户余额
solidityCopy codepragma solidity ^0.8.0;
contract BalanceTracker {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
payable(msg.sender).transfer(_amount);
}
}
-
balances[msg.sender] += msg.value;:这行代码是在deposit函数中,它的作用是将当前交易发送的以太币数量(msg.value)加到msg.sender的余额中。balances[msg.sender]是一个映射,用于记录每个地址的余额,msg.sender表示当前交易的发送者(即调用deposit函数的用户)。+=运算符是加法赋值运算符,它会将右侧的值加到左侧的值上,并将结果存储在左侧的变量中。 -
function withdraw(uint256 _amount) public { ... }:这是一个withdraw函数,它用于从合约中提取以太币。在函数内部:-
require(balances[msg.sender] >= _amount, "Insufficient balance");:这行代码用于检查调用者的余额是否足够提取所需的金额_amount。如果余额不足,则会抛出异常,终止函数执行。 -
balances[msg.sender] -= _amount;:这行代码会从调用者的余额中减去提取的金额_amount。同样,balances[msg.sender]是一个映射,用于记录每个地址的余额。-=运算符是减法赋值运算符,它会将右侧的值从左侧的值中减去,并将结果存储在左侧的变量中。 -
payable(msg.sender).transfer(_amount);:最后一行代码是将提取的金额_amount转账给调用者。payable(msg.sender)将调用者地址转换为payable类型,从而可以调用transfer函数进行转账。transfer函数会将指定的金额转账给指定的地址(即调用者),并将剩余的 gas 退还给调用者。 -
对于 Solidity 中的映射类型,如果某个键还未被显式赋值过,那么其对应的值会被默认初始化为该数据类型的零值。对于整数类型(如 uint),其默认值为 0。
因此,如果
balances[msg.sender]这个键之前从未被赋过值,那么它的默认值就是 0。在这种情况下,balances[msg.sender] += msg.value就相当于balances[msg.sender] = balances[msg.sender] + msg.value,也就是将msg.value的值加到 0 上,然后再存回balances[msg.sender]中。
-
delete操作符
delete a会让变量a的值变为初始值。
7. 变量初始值
值类型初始值
-
boolean:false -
string:"" -
int:0 -
uint:0 -
enum: (wtf)// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell enum ActionSet { Buy, Hold, Sell } // 创建enum变量 action ActionSet action = ActionSet.Buy;-
bytes1 类型的初始值:0x00
-
address:0x0000000000000000000000000000000000000000(或address(0)) -
functioninternal: 空白方程external: 空白方程
引用类型初始值(wtf)
- 映射
mapping: 所有元素都为其默认值的mapping - 结构体
struct: 所有成员设为其默认值的结构体 - 数组
array- 动态数组:
[] - 静态数组(定长): 所有成员设为其默认值的静态数组
- 动态数组:
-
8.常数 constant和immutable
状态变量声明这个两个关键字之后,不能在合约后更改数值,节约gas
- 对于 constant 变量,你必须在声明时就对其进行初始化,且该值在编译时确定,并且它是一个编译时常量,无法在运行时更改。
- 对于 immutable 变量,你可以在声明时不初始化,但必须在构造函数中初始化,而且该值在部署合约时确定,并且在合约的生命周期内不可更改
string和bytes可以声明为constant,但不能为immutable。- 只有数值变量可以声明
constant和immutable
控制流
三元运算符三元运算符是solidity中唯一一个接受三个操作数的运算符,规则条件? 条件为真的表达式:条件为假的表达式。 此运算符经常用作 if 语句的快捷方式。
插入排序
wtf上面讲的一个插入排序代码,错误原因就是 uint j它是取不到负数的,所以会报错。
9.构造函数
构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。
-
比如定义 address owner
constructor() { owner = msg.sender; }自动用部署者的地址初始化
modifier:
- 可以用来声明函数某些特性。
- 主要使用场景是运行函数前的检查。
- 可以减少代码冗余。
- 修饰器的确有类似于面向对象编程中的装饰器(decorator)的概念,但这个描述略显不准确,因为修饰器并不像装饰器一样可以动态地修改函数的行为。
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
require(): 检查条件是否满足,如果不满足条件,则中止当前的 EVM 执行,并回滚状态变化。assert(): 在代码中编写错误时触发。如果assert()的条件不满足,则会触发异常,导致当前的 EVM 执行中止。throw: 在旧版本的 Solidity 中,用于抛出异常,但在 Solidity 0.4.0 版本之后被弃用,建议使用revert()。
10.事件
事件通常用于记录合约中重要的状态变化或行为。参数声明指定了事件所记录的信息,包括数据的类型和名称。
主题 topics
日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。对于上面的Transfer事件,它的事件哈希就是:
keccak256("Transfer(address,address,uint256)")
//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
除了事件哈希,主题还可以包含至多3个indexed参数,也就是Transfer事件中的from和to。
indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
数据 data
事件中不带 indexed的参数会被存储在 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。
直接引用wtf的代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
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; // 给转账地址一些初始代币
_balances[from] -= amount; // from地址减去转账数量
_balances[to] += amount; // to地址加上转账数量
// 释放事件
emit Transfer(from, to, amount);
}
}
在事件声明中,参数声明由参数的类型和名称组成,例如 address indexed from, address indexed to, uint256 value。在这个例子中,有三个参数:from、to 和 value。每个参数的类型指定了它所记录的数据的类型,而参数的名称则用于标识数据的含义。
使用 indexed 关键字来修饰参数。使用 indexed 关键字修饰的参数会被记录在事件的主题中,以便日后可以更高效地检索这些数据。在上面的例子中,from 和 to 参数被修饰为 indexed,表示它们的值会被记录在事件的主题中,而 value 参数没有被修饰,它的值会被记录在事件的数据部分。
使用 emit 来调用事件函数会触发该事件,并将事件数据记录在以太坊的日志中。这样做可以让应用程序监听和响应这些事件。
继承
规则
virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。override:子合约重写了父合约中的函数,需要加上override关键字。
引用wtf代码非常清晰
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract yeye{
event Log(string owner);
function hip() public virtual {
emit Log("yeye");
}
function pop() public virtual {
emit Log("yeye");
}
function man() public virtual {
emit Log("nidaiye");
}
}
contract baba is yeye{
function hip() public virtual override {
emit Log("baba");
}
}
contract erzi is yeye,baba{
function hip() public override (yeye,baba){
emit Log("erzi");
}
}
要子合约就必须在父合约的函数后面声明virtual,要改写就override。这里getter函数,你理解为 override相当于提供与父合约相同变量更改的渠道
修饰器和构造函数的继承跟这个一样。
钻石继承:举个例子,就是 a为 b c的父合约,,d为bc的子合约。
contract A {
function foo() public virtual {
// 一些逻辑
}
}
contract B is A {
function foo() public override(A) {
// 一些逻辑
super.foo();
}
}
contract C is A {
function foo() public override(A) {
// 一些逻辑
super.foo();
}
}
contract D is B, C {
function foo() public override(B, C) {
// 一些逻辑
super.foo();
}
}
super关键字就是引用上一个合约的函数,,这样传递,最终引用了a的函数foo()
11.抽象合约
就是你没写完的代码做个标记,没有函数主体,就是函数内容没写完
它的目的是让子合约来完成当前合约的一些具体功能,一般加上virtual,,我的理解是如果你要直接完善当前合约,不用子合约,也可以不用virtual。
接口
接口类似于抽象合约,但它不实现任何功能。接口的规则:
- 不能包含状态变量
- 不能包含构造函数
- 不能继承除接口外的其他合约
- 所有函数都必须是external且不能有函数体
- 继承接口的合约必须实现接口定义的所有功能
下面这个我能力有限,用wtf的内容加上一些解释
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。
在Solidity中,函数体指的是函数声明中的大括号内部的部分,它包含了函数的实际执行代码。在函数体中,你可以编写逻辑来实现函数的功能。例如,在以下的函数声明中,函数体指的是大括号内的部分:
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // 这里是函数体
}
在这个例子中,函数体包含了一个简单的加法运算,并返回结果。
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,它是 ERC721 非代币标准的接口。让我们逐个解释其中的内容:
-
继承自
IERC165接口: 这一行代码表明IERC721接口继承了IERC165接口,即该接口实现了 ERC165 标准,以便检查合约是否支持某些接口。 -
事件定义: 这里定义了三个事件:
Transfer、Approval、ApprovalForAll。事件用于通知外部观察者有关合约中状态变化的信息。每个事件都有三个参数:from(发送者地址)、to(接收者地址)、tokenId(代币ID),并且这些参数都被标记为indexed,以便在日志中进行高效的检索。 -
函数定义: 定义了一系列函数来管理 ERC721 代币的转移、授权和查询等操作。这些函数包括:
balanceOf: 返回指定地址拥有的 ERC721 代币的数量。ownerOf: 返回指定代币ID的所有者地址。safeTransferFrom: 安全地将代币从一个地址转移到另一个地址。如果接收地址是合约,它将检查接收者是否实现了 ERC721Receiver 接口,并调用其onERC721Received函数。transferFrom: 将代币从一个地址转移到另一个地址,与safeTransferFrom相似,但不进行接收地址的安全检查。approve: 为指定地址授权代币所有权。getApproved: 返回指定代币ID的已授权地址。setApprovalForAll: 批准指定地址代表所有者操作所有的 ERC721 代币。isApprovedForAll: 检查是否授权了指定地址操作所有者的所有 ERC721 代币。safeTransferFrom: 安全地将代币从一个地址转移到另一个地址,与前面的safeTransferFrom函数相比,多了一个data参数,用于传递额外的数据。
这些函数定义了 ERC721 非代币标准中的核心功能,任何合约如果要符合 ERC721 标准,都必须实现这些函数。
什么时候使用接口?
如果我们知道一个合约实现了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);
}
}
注意
-
如果函数
foo被标记为internal,则它只能被当前合约及其子合约内部的其他函数访问,而不能被外部合约或外部账户访问。由于internal可见性限制了函数的访问范围,使其仅在当前合约及其继承的合约内可见,因此无法被合约的子合约重写。子合约只能访问函数foo的实现,但不能重写它,因为在 Solidity 中,重写函数必须使用override关键字,并且函数的可见性必须匹配其原始定义。 -
interface是 Solidity 中的一个关键字,用于声明接口(interface)。接口类似于抽象合约,但不包含实现任何功能,只定义了函数签名。接口定义了一组函数和事件,其他合约可以通过实现这些函数和事件来满足接口的要求。 -
// 利用BAYC地址创建接口合约变量(ETH主网) IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);
-
被标记为抽象(abstract)的合约是不能被部署的,因为抽象合约中包含了至少一个未实现的函数,缺少函数主体(即{}中的内容)。因此,抽象合约必须被其他合约继承并实现其未实现的函数,然后才能被部署。
异常
写智能合约经常会出bug,solidity中的异常命令帮助我们debug。
在 Solidity 中,可以使用 revert()、require()、assert() 和 throw 来引发错误。这些函数的作用如下:
revert(): 中止当前的 EVM 执行并回滚所有的状态和以太币转账。require(): 检查条件是否满足,如果不满足条件,则中止当前的 EVM 执行,并回滚状态变化。assert(): 在代码中编写错误时触发。如果assert()的条件不满足,则会触发异常,导致当前的 EVM 执行中止。throw: 在旧版本的 Solidity 中,用于抛出异常,但在 Solidity 0.4.0 版本之后被弃用,建议使用revert()。