
WTF Solidity 进阶实战函数选择器 Selector 详解——从 calldata 到 method id 的底层原理与调用实践【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity函数选择器selector是 Solidity 中连接人类可读的函数签名与机器可执行的 calldata 字节码的桥梁理解它是掌握合约调用、ABI 编码与低级 call 交互的基石。本篇以 29_Selector/readme.md 为核心结合仓库中的完整源码 29_Selector/Selector.sol系统讲解 selector 的定义、method id 的计算规则、四类参数类型的签名写法以及如何用 selector 主动调用目标函数帮助你彻底吃透 EVM 函数分发的底层逻辑。从 calldata 说起一次合约调用的本质当我们调用一个智能合约函数时本质上是在向目标合约发送一段十六进制数据这段数据就是calldata调用数据。在 Remix 中发起一笔交易后可以在交易详情里看到input字段它即为本次交易的calldata发送的calldata中前 4 个字节就是函数选择器selector。剩余部分则是按照 ABI 规范编码的函数参数。也就是说calldata整体告诉 EVM 两件事我要调用哪个函数以及参数是什么。msg.data承载完整 calldata 的全局变量msg.data是 Solidity 中的一个全局变量其值就是完整的calldata调用函数时传入的数据。在 Selector.sol 中mint函数通过Log事件将msg.data原样输出// event 返回msg.data event Log(bytes data); function mint(address /*to*/) external{ emit Log(msg.data); }当调用参数为地址0x2c44b726ADF1963cA47Af88B284C06f30380fC78时输出得到的calldata为0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78这段字节码可以清晰拆分成两部分前4个字节为函数选择器selector 0x6a627842 后面32个字节为输入的参数 0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78前 4 个字节0x6a627842精确标识了被调用的函数而后面 32 字节一个 word则是address类型参数的标准 ABI 编码。这正是 EVM 函数分发机制的最直观体现。method id、selector 与函数签名三个概念的关系函数签名function signature形如函数名(逗号分隔的参数类型)的字符串例如上述mint函数的签名是mint(address)。在同一个合约中不同函数拥有不同的函数签名因此可通过签名唯一确定要调用哪个函数。关于函数签名的概念在第21讲调用其他合约中已有初步介绍。method id函数签名经Keccak哈希后取前 4 个字节即bytes4(keccak256(函数签名))。Keccak256 哈希函数本身的特性可参见第28讲Hash。selector函数选择器当selector与某个函数的method id相匹配时即表示 calldata 调用的是该函数。重要注意在函数签名中uint和int必须规范书写为uint256和int256否则计算出的 method id 将与真实 ABI 中的 selector 不一致导致调用失败。用代码验证 method idSelector.sol 中的mintSelector函数用代码验证了mint函数的 method id 是否为0x6a627842function mintSelector() external pure returns(bytes4 mSelector){ return bytes4(keccak256(mint(address))); }运行结果正如预期返回值为0x6a627842四类参数类型与 method id 的计算规则计算 method id 需要依赖函数名和参数类型。在 Solidity 中函数参数类型主要分为四类基础类型、固定长度类型、可变长度类型和映射类型即需要转换为 ABI 类型的复合类型。Selector.sol 为每一类都提供了对应函数并通过SelectorEvent事件输出this.函数名.selector这一内置语法获取的真实 selector与手工keccak256计算的结果相互印证。基础类型参数基础类型包括uint256以及uint8到uint256的系列、bool、address等。计算 method id 时直接拼接即可bytes4(keccak256(函数名(参数类型1,参数类型2,...)))。以 elementaryParamSelector 为例函数名为elementaryParamSelector参数类型分别为uint256和bool// elementary基础类型参数selector // 输入param1: 1param2: 0 // elementaryParamSelector(uint256,bool) : 0x3ec37834 function elementaryParamSelector(uint256 param1, bool param2) external returns(bytes4 selectorWithElementaryParam){ emit SelectorEvent(this.elementaryParamSelector.selector); return bytes4(keccak256(elementaryParamSelector(uint256,bool))); }其 method id 为0x3ec37834对应签名elementaryParamSelector(uint256,bool)。固定长度类型参数固定长度类型通常是定长数组如uint256[5]。签名中需带上数组长度。以 fixedSizeParamSelector 为例其参数为uint256[3]// fixed size固定长度类型参数selector // 输入 param1: [1,2,3] // fixedSizeParamSelector(uint256[3]) : 0xead6b8bd function fixedSizeParamSelector(uint256[3] memory param1) external returns(bytes4 selectorWithFixedSizeParam){ emit SelectorEvent(this.fixedSizeParamSelector.selector); return bytes4(keccak256(fixedSizeParamSelector(uint256[3]))); }method id 计算为bytes4(keccak256(fixedSizeParamSelector(uint256[3])))结果是0xead6b8bd。可变长度类型参数可变长度类型包括动态数组与字符串如address[]、uint8[]、string等。签名中数组不带长度。以 nonFixedSizeParamSelector 为例参数为uint256[]和string// non-fixed size可变长度类型参数selector // 输入 param1: [1,2,3] param2: abc // nonFixedSizeParamSelector(uint256[],string) : 0xf0ca01de function nonFixedSizeParamSelector(uint256[] memory param1,string memory param2) external returns(bytes4 selectorWithNonFixedSizeParam){ emit SelectorEvent(this.nonFixedSizeParamSelector.selector); return bytes4(keccak256(nonFixedSizeParamSelector(uint256[],string))); }method id 计算为bytes4(keccak256(nonFixedSizeParamSelector(uint256[],string)))结果是0xf0ca01de。复合类型参数contract、struct、enum这类参数在计算 method id 时必须转换为对应的 ABI 类型contract 类型转换为addressstruct 类型转换为tuple如User结构体转换为(uint256,bytes)enum 类型转换为uint8。Selector.sol 中为此定义了空合约DemoContract、结构体User包含uint256 uid与bytes name和枚举School包含SCHOOL1、SCHOOL2、SCHOOL3三个成员contract DemoContract { // empty contract } contract Selector{ // Struct User struct User { uint256 uid; bytes name; } // Enum School enum School { SCHOOL1, SCHOOL2, SCHOOL3 } ... // mapping映射类型参数selector // 输入demo: 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99 user: [1, 0xa0b1], count: [1,2,3], mySchool: 1 // mappingParamSelector(address,(uint256,bytes),uint256[],uint8) : 0xe355b0ce function mappingParamSelector(DemoContract demo, User memory user, uint256[] memory count, School mySchool) external returns(bytes4 selectorWithMappingParam){ emit SelectorEvent(this.mappingParamSelector.selector); return bytes4(keccak256(mappingParamSelector(address,(uint256,bytes),uint256[],uint8))); } ... }四个参数demo、user、count、mySchool分别转换为address、(uint256,bytes)、uint256[]、uint8最终签名为mappingParamSelector(address,(uint256,bytes),uint256[],uint8)method id 为0xe355b0ce。无参数函数除了上述四类还有一个特殊情况无参数函数。源码中的 nonParamSelector 展示了空参数签名的写法// 无参数selector // 输入 无 // nonParamSelector() 0x03817936 function nonParamSelector() external returns(bytes4 selectorWithNonParam){ emit SelectorEvent(this.nonParamSelector.selector); return bytes4(keccak256(nonParamSelector())); }空参数函数的签名就是函数名加一对空括号nonParamSelector()其 method id 为0x03817936。使用 selector 调用目标函数知道了 method id 的计算方法后就可以利用 selector 主动构造 calldata 并调用目标函数。核心手段是abi.encodeWithSelector将函数的 method id 作为 selector、与函数参数一起打包编码生成完整的 calldata再交给call低级函数执行。Selector.sol 中的callWithSignature函数展示了完整用法——它依次用五个 selector 调用本合约的五个目标函数// 使用selector来调用函数 function callWithSignature() external{ // 初始化uint256数组 uint256[] memory param1 new uint256[](3); param1[0] 1; param1[1] 2; param1[2] 3; // 初始化struct User memory user; user.uid 1; user.name 0xa0b1; // 利用abi.encodeWithSelector将函数的selector和参数打包编码 // 调用nonParamSelector函数 (bool success0, bytes memory data0) address(this).call(abi.encodeWithSelector(0x03817936)); // 调用elementaryParamSelector函数 (bool success1, bytes memory data1) address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0)); // 调用fixedSizeParamSelector函数 (bool success2, bytes memory data2) address(this).call(abi.encodeWithSelector(0xead6b8bd, [1,2,3])); // 调用nonFixedSizeParamSelector函数 (bool success3, bytes memory data3) address(this).call(abi.encodeWithSelector(0xf0ca01de, param1, abc)); // 调用mappingParamSelector函数 (bool success4, bytes memory data4) address(this).call(abi.encodeWithSelector(0xe355b0ce, 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99, user, param1, 1)); require(success0 success1 success2 success3 success4); }关键点解析selector 直接编码例如abi.encodeWithSelector(0x3ec37834, 1, 0)将0x3ec37834elementaryParamSelector(uint256,bool)的 method id与参数1、0打包编码结果天然以 selector 开头等价于手工拼接selector ABI 参数。返回值处理call返回(bool, bytes memory)二元组分别表示调用是否成功与目标函数的返回值。代码末尾用require断言五个调用全部成功防止静默失败。自调用场景这里用address(this).call(...)调用本合约的函数用于教学演示在实际跨合约场景中把address(this)替换为目标合约地址即可原理完全一致。在 Remix 中运行callWithSignature后日志区可以看到SelectorEvent事件被依次触发证明elementaryParamSelector等目标函数均被成功调用selector 与 ABI 编码、低级调用的深层联系与 abi.encodeWithSignature 的关系abi.encodeWithSelector与abi.encodeWithSignature功能几乎一致区别仅在第一个参数前者直接传入 4 字节 selector后者传入函数签名字符串。在第27讲ABI 编码的 ABIEncode.sol 中可以看到abi.encodeWithSignature(foo(uint256,address,string,uint256[2]), ...)与abi.encodeWithSelector(bytes4(keccak256(foo(uint256,address,string,uint256[2]))), ...)的编码结果完全相同——因为在编译与执行层面函数签名最终都会被归一化为 selector。selector 本质上就是函数签名的 Keccak 哈希前 4 字节。与 call 低级函数的配合call是address类型的低级成员函数用于与其他合约交互。正如第22讲Call所讲call的使用规则是目标合约地址.call(字节码)其中字节码通常由abi.encodeWithSignature或abi.encodeWithSelector生成并可附带{value: 发送数额, gas: gas数额}控制转账与 gas。当我们不知道对方合约的源代码或 ABI、无法生成合约变量时第21讲介绍的声明合约变量调用方案将失效call selector 是唯一的调用途径。selector 在复杂场景中的延伸应用从源码结构看selector 的适用场景远不止本讲演示的基础调用静态调用第27讲中展示了用abi.encodeWithSelector构造数据后配合staticcall进行只读调用的模式可安全读取链上数据而不产生状态变更。代理合约与升级在代理proxy模式下调用方发出的 calldata 中同样包含目标函数的 selector代理合约通过delegatecall原样转发 calldata 给逻辑合约执行selector 正是透明转发机制得以成立的前提。函数选择器冲突由于 selector 只有 4 字节理论上存在两个不同函数签名碰撞出相同 selector 的可能性这正是安全专题中选择器冲突攻击的根源理解 selector 的生成规则是防御此类问题的前提。总结这一讲围绕函数选择器selector展开了完整讲解首先从msg.data入手说明 calldata 由前 4 字节 selector 参数编码构成接着给出 method id 的准确定义——函数签名函数名(逗号分隔的参数类型)的 Keccak 哈希前 4 字节并强调uint/int必须写作uint256/int256随后逐一演示基础类型、固定长度、可变长度与复合类型contract 转 address、struct 转 tuple、enum 转 uint8四类参数的签名写法最后通过abi.encodeWithSelector与call的组合展示了用 selector 主动调用目标函数的完整流程。结合 Selector.sol 中每个函数手工 keccak256 计算 this.函数名.selector内置语法的双重印证本讲内容可以作为后续学习 ABI 编码第27讲、Call第22讲、代理合约与安全攻击等主题的坚实基础。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考