函数选择器(function selector)是一个 4 字节 ID,Solidity 在底层使用它来标识函数。
Solidity 合约正是通过函数选择器来知道你在交易中试图调用哪个函数的。
你可以使用 .selector 方法查看这个 4 字节 ID:
pragma solidity 0.8.25;
contract SelectorTest{
function foo() public {}
function getSelectorOfFoo() external pure returns (bytes4) {
return this.foo.selector; // 0xc2985578
}
}
如果一个函数不接受任何参数,你可以通过低级的 call 将这 4 个字节(选择器)作为数据发送给合约,以此来调用该函数。
在下面的示例中,CallFoo 合约通过向 FooContract 发起带有相应 4 字节函数选择器的调用,来调用在 FooContract 中定义的 foo 函数。
pragma solidity 0.8.25;
contract CallFoo {
function callFooLowLevel(address _contract) external {
bytes4 fooSelector = 0xc2985578;
(bool ok, ) = _contract.call(abi.encodePacked(fooSelector));
require(ok, "call failed");
}
}
contract FooContract {
uint256 public x;
function foo() public {
x = 1;
}
}
如果 FooContract 和 CallFoo 已部署,并且使用 FooContract 的地址执行了 callFooLowLevel() 函数,那么 FooContract 内的 x 的值将被设置为 1。这表明成功调用了 foo 函数。
在 Solidity 中使用 msg.sig 识别函数调用和选择器
msg.sig 是一个全局变量,它返回交易数据的前四个字节,Solidity 合约正是依靠它来知道要调用哪个函数的。
msg.sig 在整个交易过程中会被保留,它不会根据当前处于活动状态的函数而改变。因此,即使一个 public 函数在调用期间调用了另一个 public 函数,msg.sig 仍将是最初被调用的函数的选择器。
在下面的代码中,foo 调用 bar 来获取 msg.sig,但是当调用 foo 时返回的选择器是 foo 的函数选择器,而不是 bar 的:
你可以在 这里使用 Remix 测试代码:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract SelectorTest {
// returns function selector of `bar()` 0xfebb0f7e
function bar() public pure returns (bytes4) {
return msg.sig;
}
// returns function selector of `foo()` 0xc2985578
function foo() public pure returns (bytes4) {
return bar();
}
function testSelectors() external pure returns (bool) {
assert(this.foo.selector == 0xc2985578);
assert(this.bar.selector == 0xfebb0f7e);
return true;
}
}
Solidity 函数签名
Solidity 中的函数签名是一个字符串,包含函数名称以及随后的它所接收的参数类型。变量名会从参数中被移除。
在下面的代码片段中,左侧是函数,右侧是函数签名:
function setPoint(uint256 x, uint256 y) --> "setPoint(uint256,uint256)"
function setName(string memory name) --> "setName(string)"
function addValue(uint v) --> "addValue(uint256)"
函数选择器中没有空格。所有 uint 类型都必须明确包含其大小(uint256、uint40、uint8 等)。不包括 calldata 和 memory 类型。例如,getBalanceById(uint) 是一个无效的 签名。
如何从函数签名计算函数选择器
函数选择器是函数签名的 keccak256 哈希值的前四个字节。
function returnFunctionSelectorFromSignature(
string calldata functionName
) public pure returns(bytes4) {
bytes4 functionSelector = bytes4(keccak256(abi.encodePacked(functionName)));
return(functionSelector);
}
将 “foo()” 传入上面的函数将返回 0xc2985578。
下面的代码演示了在给定函数签名的情况下如何计算函数选择器。
//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
contract FunctionSignatureTest {
function foo() external {}
function point(uint256 x, uint256 y) external {}
function setName(string memory name) external {}
function testSignatures() external pure returns (bool) {
// NOTE: Casting to bytes4 takes the first 4 bytes
// and removes the rest
assert(bytes4(keccak256("foo()")) == this.foo.selector);
assert(bytes4(keccak256("point(uint256,uint256)")) == this.point.selector);
assert(bytes4(keccak256("setName(string)")) == this.setName.selector);
return true;
}
}
接口被编码为地址。例如,f(IERC20 token) 被编码为 f(address token)。将地址设为 payable 不会改变签名。
内部函数没有函数选择器
public 和 external 函数有选择器,但 internal 和 private 函数没有。下面的代码无法编译:
contract Foo {
function bar() internal {
}
// does not compile
function barSelector() external pure returns (bytes4) {
return this.bar.selector;
}
}
如果将 bar 更改为 public 或 external,代码将成功编译。
内部函数不需要函数选择器,因为函数选择器是供外部合约使用的。也就是说,外部用户是通过函数选择器来指定他们试图调用哪个 public 或 external 函数的。
为什么使用函数选择器而不是函数名称?
Solidity 函数名称可以任意长,如果函数名称很长,它将增加交易的大小和成本。如果提供函数选择器而不是名称,交易的体积通常会更小。
fallback 函数有函数选择器吗?
fallback 函数没有函数选择器。
如果在 fallback 函数内记录 msg.sig,它只会简单地记录交易数据的前四个字节。如果没有交易数据,那么 msg.sig 将返回四个字节的零。这并不意味着 fallback 的函数选择器全是零,这意味着 msg.sig 试图读取不存在的数据。
contract FunctionSignatureTest {
event LogSelector(bytes4);
fallback() external payable {
emit LogSelector(msg.sig);
}
}
这里有 Remix 上的代码 可以测试上述合约。下面是一段视频,展示了如何在 Remix 上触发 fallback 函数:
函数选择器碰撞的概率
一个函数选择器最多可以容纳 2**32 - 1(约 42 亿)个可能的值。因此,两个函数具有相同选择器的几率很小,但确实存在。
contract WontCompile {
function collate_propagate_storage(bytes16 x) external {}
function burn(uint256 amount) external {}
}
上面的例子取自这个 论坛帖子。这两个函数的函数选择器都是 0x42966c68。
有关函数选择器的实用资源
函数选择器计算器。请确保添加 function 关键字。该计算器预期的格式类似于 function burn(uint256)。
函数选择器数据库。将这 4 个字节输入搜索框,它就会找出与之匹配的已知函数。
函数选择器与 EVM — 以太坊虚拟机
对于熟悉 EVM 的读者来说,这通常也是一个容易引起混淆的地方。4 字节函数选择器发生在 Solidity 应用层,而不是 EVM 层。在以太坊的规范中,没有任何规定表明必须用 4 个字节来标识函数。这是一个强约定,但并不是强制要求的。
事实上,一种常见的 Layer 2 应用程序优化 是使用 1 字节的函数选择器来标识函数。也就是说,所有函数调用都会进入 fallback 函数,然后 fallback 函数查看交易中的第一个字节来决定调用哪个函数。
在 RareSkills 了解更多
请查看我们的 Solidity 训练营 以获取智能合约开发中更高级的主题。
首次发布于 2024 年 3 月 30 日