在以太坊的世界里,合约函数调用不是“直接调用函数”,而是发送一段经过 ABI 编码的二进制数据。这些数据不仅包含了调用哪个函数的信息,还包括函数参数的序列化内容。理解 ABI 编码与函数选择器,可以帮助我们:
data 解读调用意图)call / delegatecall)函数选择器是函数调用数据的前 4 个字节,用来标识调用的是哪一个函数。
它的生成方式是:
selector = keccak256("函数名(参数类型列表)") 前 4 个字节例如:
function transfer(address to, uint256 amount) public;计算:
keccak256("transfer(address,uint256)")
= 0xa9059cbb...
selector = 0xa9059cbb作用:当 EVM 收到一笔交易时,会查看 msg.data 的前 4 个字节,通过函数选择器找到匹配的函数,然后解码后续数据作为参数。
ABI(Application Binary Interface)规定了参数如何序列化。以 transfer(address,uint256) 调用为例:
address 类型是 20 字节,要补齐到 32 字节(左填充 0)uint256 类型是 32 字节(大端序,左填充 0)我们可以通过 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;
}
}我们用 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 编码结果。
有时我们需要使用 call 来调用其他合约的函数:
(bool success, bytes memory data) = target.call(
abi.encodeWithSelector(
bytes4(keccak256("transfer(address,uint256)")),
to,
amount
)
);abi.encodeWithSelector:手动指定函数选择器abi.encodeWithSignature:直接用签名字符串计算选择器call 时不会检查参数类型是否匹配,需谨慎验证输入。abi.decode 时必须确保数据格式正确,否则会 revert。练习建议:
abi.encodeWithSelector 与手工拼接结果一致