Solidity
里的哈希表。Solidity
文档里把函数归到数值类型,但我觉得他跟其他类型差别很大,所以单独分一类。1.布尔型(bool):true 或者 false
2.整型(uint):uint、uint8、uint16、uint32、uint256
整型数值没有负数
3.地址类型(address):地址类型(address)存储一个 20 字节的值(以太坊地址的大小)
4.定长字节数组:字节数组bytes
分两种,一种定长(byte
, bytes8
, bytes32
),另一种不定长
5.枚举型(enum):枚举(enum
)是solidity
中用户定义的数据类型
1.数组(Array):数组(Array
)是solidity
常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种
数组成员:
length
: 数组有一个包含元素数量的length
成员,memory
数组的长度在创建后是固定的。push()
: 动态数组
和bytes
拥有push()
成员,可以在数组最后添加一个0
元素。push(x)
: 动态数组
和bytes
拥有push(x)
成员,可以在数组最后添加一个x
元素。pop()
: 动态数组
和bytes
拥有pop()
成员,可以移除数组最后一个元素。2.结构体(struct):Solidity
支持通过构造结构体的形式定义新的类型
// 结构体用法
struct Student{
uint256 id;
uint256 score;
}
Mapping:声明映射的格式为mapping(_KeyType => _ValueType)
映射的规则:
_KeyType
只能选择solidity
默认的类型,比如uint
,address
等,不能用自定义的结构体。规则2:映射的存储位置必须是storage
,因此可以用于合约的状态变量,函数中的storage
变量。不能用于public
函数的参数或返回结果中,因为mapping
记录的是一种关系 (key - value pair)。
规则3:如果映射声明为public
,那么solidity
会自动给你创建一个getter
函数,可以通过Key
来查询对应的Value
。
规则4:给映射新增的键值对的语法为_Var[_Key] = _Value
,其中_Var
是映射变量名,_Key
和_Value
对应新增的键值对
1.function
:声明函数时的固定用法,想写函数,就要以function关键字开头。
2.(
:圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。
3.{internal|external|public|private}
:函数可见性说明符,一共4种。没标明函数类型的,默认internal
。
public
: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成 getter
函数,用于查询数值).private
: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。external
: 只能从合约外部访问(但是可以用this.f()
来调用,f
是函数名)internal
: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)。4.[pure|view|payable]
:决定函数权限/功能的关键字。
pure和view都不需要付gas
5.[returns ()]
:函数返回的变量类型和名称。
constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。
immutable
变量可以在声明时或构造函数中初始化,因此更加灵活。
修饰器(modifier
)是solidity特有的语法,类似于面向对象编程中的decorator
,声明函数拥有的特性,并减少代码冗余。
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
构造函数(constructor
)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner
地址:
address owner; // 定义owner变量
// 构造函数
constructor() public {
owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址
}
Solidity
中的事件(event
)是EVM
上日志的抽象,它具有两个特点:
ether.js
)可以通过RPC
接口订阅和监听这些事件,并在前端做响应。EVM
上比较经济的存储数据的方式,每个大概消耗2,000-5,000 gas
不等。相比之下,存储一个新的变量至少需要20,000 gas
。事件的声明由event
关键字开头,然后跟事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20
代币合约的Transfer
事件为例:
event Transfer(address indexed from, address indexed to, uint256 value);
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。
override
:子合约重写了父合约中的函数,需要加上override
关键。
contract Baba is Yeye{}
contract Erzi is Yeye, Baba{}
修饰器的继承
用法同函数继承
contract B is A(1)
error:方便高效省gas
error TransferNotOwner(); // 自定义error,在执行当中,error必须搭配revert(回退)命令使用。
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}
gas
随着描述异常的字符串长度增加使用方法:require(检查条件,”异常的描述”)
,当检查条件
不成立的时候,就会抛出异常。
我们用require
命令重写一下上面的transferOwner
函数:
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}
assert命令一般用于程序员写程序debug,因为他不能解释抛出异常的原因(比require
少个字符串)。他的用法很简单,assert(检查条件)
,当检查条件
不成立的时候,就会抛出异常。
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}
SafeMath用来防止溢出,有四个方法 — add
, sub
, mul
, 以及 div。
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
solidity
支持利用import
关键字导入其他源代码中的合约,让开发更加模块化。
// 通过文件相对位置import
import './Yeye.sol';
Solidity
支持两种特殊的回调函数,receive()
和fallback()
,他们主要在两种情况下被使用:
ETH
proxy contract
)接收ETH函数 receive
receive()
只用于处理接收ETH
。一个合约最多有一个receive()
函数,声明方式与一般函数不一样,不需要function
关键字:receive() external payable { ... }
receive()
函数不能有任何的参数,不能返回任何值,必须包含external
和payable
。
当合约接收ETH的时候,receive()
会被触发。
fallback()
函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract
。fallback()
声明时不需要function
关键字,必须由external
修饰,一般也会用payable
修饰,用于接收ETH:fallback() external payable { ... }
。
receive
和fallback
都能够用于接收ETH
,他们触发的规则如下:
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()
transfer(发送ETH数额)
。transfer()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。transfer()
如果转账失败,会自动revert
(回滚交易)。send(发送ETH数额)
。send()
的gas
限制是2300
,足够用于转账,但对方合约的fallback()
或receive()
函数不能实现太复杂的逻辑。send()
如果转账失败,不会revert
。send()
的返回值是bool
,代表着转账成功或失败,需要额外代码处理一下。call{value: 发送ETH数额}("")
。call()
没有gas
限制,可以支持对方合约fallback()
或receive()
函数实现复杂逻辑。call()
如果转账失败,不会revert
。call()
的返回值是(bool, data)
,其中bool
代表着转账成功或失败,需要额外代码处理一下。call
是address
类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data)
,分别对应call
是否成功以及目标函数的返回值。
call
的使用规则call
的使用规则如下:
目标合约地址.call(二进制编码);
其中二进制编码
利用结构化编码函数abi.encodeWithSignature
获得:
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名
为"函数名(逗号分隔的参数类型)"
。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)
。
另外call
在调用合约时可以指定交易发送的ETH
数额和gas
:
目标合约地址.call{value:发送ETH数额, gas:gas数额}(二进制编码);
abi.encode
将给定参数利用ABI规则编码。ABI
被设计出来跟智能合约交互,他将每个参数转填充为32字节的数据,并拼接在一起。
abi.decode
abi.decode
用于解码abi.encode
生成的二进制编码,将它还原成原本的参数。
selector
定义为函数签名
的哈希的前4个字节
函数签名,为"函数名(逗号分隔的参数类型)"
。举个例子,mint
的函数签名为"mint(address)"
。在智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。
在solidity
中,try-catch
只能被用于external
函数或创建合约时constructor
(被视为external
函数)的调用。基本语法如下:
try externalContract.f() {
// call成功的情况下 运行一些代码
} catch {
// call失败的情况下 运行一些代码
}
其中externalContract.f()
时某个外部合约的函数调用,try
模块在调用成功的情况下运行,而catch
模块则在调用失败时运行。
IERC20
是ERC20
代币标准的接口合约,规定了ERC20
代币需要实现的函数和事件。
IERC20
定义了2
个事件:Transfer
事件和Approval
事件,分别在转账和授权时被释放
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
IERC20
定义了6
个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。
totalSupply()
返回代币总供给balanceOf()
返回账户余额transfer()
转账allowance()
返回授权额度approve()
授权transferFrom()
授权转账IERC721
包含3个事件,其中Transfer
和Approval
事件在ERC20
中也有。
Transfer
事件:在转账时被释放,记录代币的发出地址from
,接收地址to
和tokenid
。Approval
事件:在授权时释放,记录授权地址owner,被授权地址
approved和
tokenid`。ApprovalForAll
事件:在批量授权时释放,记录批量授权的发出地址owner
,被授权地址operator
和授权与否的approved
。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
。面是ERC1155
的元数据接口合约IERC1155MetadataURI
:
/**
* @dev ERC1155的可选接口,加入了uri()函数查询元数据
*/
interface IERC1155MetadataURI is IERC1155 {
/**
* @dev 返回第`id`种类代币的URI
*/
function uri(uint256 id) external view returns (string memory);
IERC1155
事件TransferSingle
事件:单类代币转账事件,在单币种转账时释放。TransferBatch
事件:批量代币转账事件,在多币种转账时释放。ApprovalForAll
事件:批量授权事件,在批量授权时释放。URI
事件:元数据地址变更事件,在uri
变化时释放。IERC1155
函数balanceOf()
:单币种余额查询,返回account
拥有的id
种类的代币的持仓量。balanceOfBatch()
:多币种余额查询,查询的地址accounts
数组和代币种类ids
数组的长度要相等。setApprovalForAll()
:批量授权,将调用者的代币授权给operator
地址。。isApprovedForAll()
:查询批量授权信息,如果授权地址operator
被account
授权,则返回true
。safeTransferFrom()
:安全单币转账,将amount
单位id
种类的代币从from
地址转账给to
地址。如果to
地址是合约,则会验证是否实现了onERC1155Received()
接收函数。safeBatchTransferFrom()
:安全多币转账,与单币转账类似,只不过转账数量amounts
和代币种类ids
变为数组,且长度相等。如果to
地址是合约,则会验证是否实现了onERC1155BatchReceived()
接收函数。