- 1. 什么是函数选择器(Function Selector)
- 2. ABI 编码流程
- 3. Solidity 中的函数选择器使用
- 4. 使用 Foundry 验证 ABI 编码
- 5. 低级调用与函数选择器
- 6. 安全注意事项
- 总结
在以太坊的世界里,合约函数调用不是“直接调用函数”,而是发送一段经过 ABI 编码的二进制数据。这些数据不仅包含了调用哪个函数的信息,还包括函数参数的序列化内容。理解 ABI 编码与函数选择器,可以帮助我们:
- 调试交易数据(从原始
data
解读调用意图) - 手写低级调用(
call
/delegatecall
) - 分析合约安全问题(例如函数签名冲突)
1. 什么是函数选择器(Function Selector)
函数选择器是函数调用数据的前 4 个字节,用来标识调用的是哪一个函数。
它的生成方式是:
selector = keccak256("函数名(参数类型列表)") 前 4 个字节
例如:
function transfer(address to, uint256 amount) public;
计算:
keccak256("transfer(address,uint256)")
= 0xa9059cbb...
selector = 0xa9059cbb
作用:当 EVM 收到一笔交易时,会查看 msg.data
的前 4 个字节,通过函数选择器找到匹配的函数,然后解码后续数据作为参数。
2. ABI 编码流程
ABI(Application Binary Interface)规定了参数如何序列化。以 transfer(address,uint256)
调用为例:
- 计算函数选择器:
0xa9059cbb
- 按 ABI 规则编码参数:
address
类型是 20 字节,要补齐到 32 字节(左填充 0)uint256
类型是 32 字节(大端序,左填充 0)
- 拼接:
data = 0xa9059cbb 000000000000000000000000<to地址20字节> 000000000000000000000000000000000000000000000000<amount>
3. Solidity 中的函数选择器使用
我们可以通过 Solidity 内置属性获取函数选择器:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SelectorExample {
function getSelector() public pure returns (bytes4) {
return this.transfer.selector;
}
function transfer(address to, uint256 amount) public pure returns (bool) {
return true;
}
}
4. 使用 Foundry 验证 ABI 编码
我们用 Foundry 写一个测试,验证 ABI 编码是否正确。
src/SelectorExample.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SelectorExample {
function transfer(address to, uint256 amount) public pure returns (bool) {
return true;
}
}
test/SelectorExample.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/SelectorExample.sol";
contract SelectorExampleTest is Test {
SelectorExample example;
function setUp() public {
example = new SelectorExample();
}
function testSelector() public {
bytes4 selector = example.transfer.selector;
assertEq(selector, bytes4(keccak256("transfer(address,uint256)")));
}
function testABIEncoding() public {
address to = address(0x123);
uint256 amount = 100;
bytes memory encoded = abi.encodeWithSelector(
example.transfer.selector,
to,
amount
);
emit log_bytes(encoded);
}
}
运行:
➜ counter git:(main) ✗ forge test --match-path test/SelectorExample.t.sol -vvv
[⠊] Compiling...
[⠆] Compiling 1 files with Solc 0.8.29
[⠰] Solc 0.8.29 finished in 1.26s
Compiler run successful with warnings:
Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning.
--> src/SelectorExample.sol:5:23:
|
5 | function transfer(address to, uint256 amount) public pure returns (bool) {
| ^^^^^^^^^^
Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning.
--> src/SelectorExample.sol:5:35:
|
5 | function transfer(address to, uint256 amount) public pure returns (bool) {
| ^^^^^^^^^^^^^^
Ran 2 tests for test/SelectorExample.t.sol:SelectorExampleTest
[PASS] testABIEncoding() (gas: 5278)
Logs:
0xa9059cbb00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000064
[PASS] testSelector() (gas: 5512)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.14ms (2.12ms CPU time)
Ran 1 test suite in 438.24ms (8.14ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
输出的 log_bytes
会显示完整的 ABI 编码结果。
5. 低级调用与函数选择器
有时我们需要使用 call
来调用其他合约的函数:
(bool success, bytes memory data) = target.call(
abi.encodeWithSelector(
bytes4(keccak256("transfer(address,uint256)")),
to,
amount
)
);
abi.encodeWithSelector
:手动指定函数选择器abi.encodeWithSignature
:直接用签名字符串计算选择器
6. 安全注意事项
- 函数签名冲突: 不同函数如果签名哈希的前 4 个字节相同(极低概率),可能导致调用混淆。
- 低级调用缺乏类型检查: 使用
call
时不会检查参数类型是否匹配,需谨慎验证输入。 - 解码风险: 用
abi.decode
时必须确保数据格式正确,否则会 revert。
总结
- 函数选择器是调用函数的“身份证”
- ABI 编码定义了参数如何序列化
- 熟悉函数选择器和 ABI 编码,可以进行底层调试、合约互操作以及安全分析
练习建议:
- 计算几个常见 ERC20 函数的选择器
- 用 Foundry 编写测试,验证
abi.encodeWithSelector
与手工拼接结果一致 - 尝试用低级调用向一个已部署的 ERC20 发送交易

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—