29. 选择器
WTF Solidity极简入门: 29. 函数选择器Selector
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
函数选择器
当我们调用智能合约时,本质上是向目标合约发送了一段calldata
,在remix中发送一次交易后,可以在详细信息中看见input
即为此次交易的calldata
发送的calldata
中前4个字节是selector
(函数选择器)。这一讲,我们将介绍selector
是什么,以及如何使用。
msg.data
msg.data
是Solidity
中的一个全局变量,值为完整的calldata
(调用函数时传入的数据)。
在下面的代码中,我们可以通过Log
事件来输出调用mint
函数的calldata
:
1 | // event 返回msg.data |
当参数为0x2c44b726ADF1963cA47Af88B284C06f30380fC78
时,输出的calldata
为
1 | 0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 |
这段很乱的字节码可以分成两部分:
1 | 前4个字节为函数选择器selector: |
其实calldata
就是告诉智能合约,我要调用哪个函数,以及参数是什么。
method id、selector和函数签名
method id
定义为函数签名
的Keccak
哈希后的前4个字节,当selector
与method id
相匹配时,即表示调用该函数,那么函数签名
是什么?
其实在第21讲中,我们简单介绍了函数签名,为"函数名(逗号分隔的参数类型)"
。举个例子,上面代码中mint
的函数签名为"mint(address)"
。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。
注意,在函数签名中,uint
和int
要写为uint256
和int256
。
我们写一个函数,来验证mint
函数的method id
是否为0x6a627842
。大家可以运行下面的函数,看看结果。
1 | function mintSelector() external pure returns(bytes4 mSelector){ |
结果正是0x6a627842
:
由于计算method id
时,需要通过函数名和函数的参数类型来计算。在Solidity
中,函数的参数类型主要分为:基础类型参数,固定长度类型参数,可变长度类型参数和映射类型参数。
基础类型参数
solidity
中,基础类型的参数有:uint256
(uint8
, … , uint256
)、bool
, address
等。在计算method id
时,只需要计算bytes4(keccak256("函数名(参数类型1,参数类型2,...)"))
。例如,如下函数,函数名为elementaryParamSelector
,参数类型分别为uint256
和bool
。所以,只需要计算bytes4(keccak256("elementaryParamSelector(uint256,bool)"))
便可得到此函数的method id
。
1 | // elementary(基础)类型参数selector |
固定长度类型参数
固定长度的参数类型通常为固定长度的数组,例如:uint256[5]
等。例如,如下函数fixedSizeParamSelector
的参数为uint256[3]
。因此,在计算该函数的method id
时,只需要通过bytes4(keccak256("fixedSizeParamSelector(uint256[3])"))
即可。
1 | // fixed size(固定长度)类型参数selector |
可变长度类型参数
可变长度参数类型通常为可变长的数组,例如:address[]
、uint8[]
、string
等。例如,如下函数nonFixedSizeParamSelector
的参数为uint256[]
和string
。因此,在计算该函数的method id
时,只需要通过bytes4(keccak256("nonFixedSizeParamSelector(uint256[],string)"))
即可。
1 | // non-fixed size(可变长度)类型参数selector |
映射类型参数
映射类型参数通常有:contract
、enum
、struct
等。在计算method id
时,需要将该类型转化成为ABI
类型。
例如,如下函数mappingParamSelector
中DemoContract
需要转化为address
,结构体User
需要转化为tuple
类型(uint256,bytes)
,枚举类型School
需要转化为uint8
。因此,计算该函数的method id
的代码为bytes4(keccak256("mappingParamSelector(address,(uint256,bytes),uint256[],uint8)"))
。
1 | contract DemoContract { |
使用selector
我们可以利用selector
来调用目标函数。例如我想调用elementaryParamSelector
函数,我只需要利用abi.encodeWithSelector
将elementaryParamSelector
函数的method id
作为selector
和参数打包编码,传给call
函数:
1 | // 使用selector来调用函数 |
在日志中,我们可以看到elementaryParamSelector
函数被成功调用,并输出Log
事件。
总结
这一讲,我们介绍了什么是函数选择器
(selector
),它和msg.data
、函数签名
的关系,以及如何使用它调用目标函数。