solidity 中有函数选择器(selector)的概念.
什么是 selector
在 solidity 中,所有 public (或 external) 函数有一个特殊的成员selector, 它对应一个ABI 函数选择器。
evm 函数选择器是一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序) (译注:这里的 高位在左的大端序
,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
简单来说,就是函数原型的 sha-3 hash值。
selector 有什么用途
在 以太坊Ethereum 生态系统中, 应用二进制接口 Application Binary Interface(ABI)
是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。
我们假定合约函数的接口都是强类型的,且在编译时是可知的和静态的;不提供自我检查机制。我们假定在编译时,所有合约要调用的其他合约接口定义都是可用的。
这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 以太坊Ethereum 生态系统中其他更合适的基础设施来处理它们。
官方说明: https://docs.soliditylang.org/en/v0.8.7/abi-spec.html
如何计算 selector
selector 可以通过两种方式获取,一种是查询 function.selector
,另一种就是自己计算。
写个简单的测试合约,就能秒懂。
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;
interface IABI {
function initialize(address a, address b, address c, address d) external;
}
contract ABI {
function initiliaze(address a, address b, address c, address d) public {
}
function getCodeByKeccak() public pure returns (bytes4) {
return bytes4(keccak256(bytes("initialize(address,address,address,address)")));
}
function getCodeBySelector() public view returns (bytes4) {
IABI addr = IABI(address(this));
return addr.initialize.selector;
}
}
当计算函数选择器(selector)时,计算传入bytes数组有两点需要注意:
- 函数参数只有类型,没有名称,例如上例中的
initialize(address,address,address,address)
- 参数中间没有空格!参数中间没有空格!参数中间没有空格!
如果要在链下计算,可以通过 ethers
提供的工具函数 id
,代码如下:
import { id } from 'ethers/lib/utils';
const hash = id('initialize(address,address,address,address)')
, selector = hash.slice(0, 10)
selector 重复怎么办?
如果一个合约中有重复的 selector
,编译器会报错。
如果函数原型的参数是自定义的结构体, 怎么办?
如果是结构体, 在计算 selector 时, 将结构体展开为 tuple 来计算。例如:
struct Reward {
address ctoken;
address underlying;
uint256 amount;
uint256 debt;
uint256 pending;
address benefit;
address owner;
address[] path;
},
function calcReward(Reword r, address to) returns (uint256);
在计算函数 calcReward
的selector时, 实际上是计算
calcReward((address,address,uint256,uint256,uint256,address,address,address[]),address)
这个字符串的 id
对于比较复杂的情况,我建议使用 ethers
库的 Interface
来计算函数的 selector, 链接: https://docs.ethers.io/v5/api/utils/abi/interface/
根据ABI创建 Interface
使用
Interface.getSighash()
获取函数的 selector使用
Interface.getFunction('xxx').format('sighash')
获取计算 selector 的函数签名
完整的示例如下:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("ABI selector", function() {
it('selector', async () => {
const iface = new ethers.utils.Interface([
// Constructor
"constructor(string symbol, string name)",
// State mutating method
"function transferFrom(address from, address to, uint amount)",
// State mutating method, which is payable
"function mint(uint amount) payable",
// Constant method (i.e. "view" or "pure")
"function balanceOf(address owner) view returns (uint)",
// An Event
"event Transfer(address indexed from, address indexed to, uint256 amount)",
// A Custom Solidity Error
"error AccountLocked(address owner, uint256 balance)",
// Examples with structured types
"function addUser(tuple(string name, address addr) user) returns (uint id)",
"function addUsers(tuple(string name, address addr)[] user) returns (uint[] id)",
"function getUser(uint id) view returns (tuple(string name, address addr) user)"
]);
console.log('addUsers getSighash: %s', iface.getSighash('addUsers'))
let addUserFormat = iface.getFunction('addUsers').format(ethers.utils.FormatTypes.getSighash)
console.log('addUsers format: %s', addUserFormat)
console.log('addUsers selector:', ethers.utils.id(addUserFormat).slice(0, 10))
})
})