{"content":{"title":"理解Solidity中的函数选择器","body":"函数选择器是一个 4 字节的 ID，Solidity 用于在后台识别函数。\r\n\r\n函数选择器是 Solidity 合约了解在交易中你要调用哪个函数的方式。\r\n\r\n你可以使用 `.selector` 方法查看 4 字节 ID：\r\n\r\n```solidity!\r\npragma solidity 0.8.25;\r\n\r\ncontract SelectorTest{\r\n\r\n    function foo() public {}\r\n\r\n    function getSelectorOfFoo() external pure returns (bytes4) {\r\n        return this.foo.selector; // 0xc2985578\r\n    }\r\n}\r\n\r\n```\r\n\r\n如果一个函数不接受任何参数，你可以通过将这 4 个字节（选择器）作为数据发送到合约，并使用低级 `call` 来调用该函数。\r\n\r\n在以下示例中，`CallFoo` 合约通过调用 `FooContract` 及适当的四字节函数选择器来调用 `FooContract` 中定义的 foo 函数。\r\n\r\n```solidity!\r\npragma solidity 0.8.25;\r\n\r\ncontract CallFoo {\r\n\r\n    function callFooLowLevel(address _contract) external {\r\n        bytes4 fooSelector = 0xc2985578;\r\n\r\n        (bool ok, ) = _contract.call(abi.encodePacked(fooSelector)); \r\n        require(ok, \"call failed\");\r\n    }\r\n\r\n}\r\n\r\ncontract FooContract {\r\n\r\n    uint256 public x;\r\n\r\n    function foo() public {\r\n        x = 1;\r\n    }\r\n}\r\n\r\n```\r\n\r\n如果 `FooContract` 和 `CallFoo` 被部署，并且执行 `callFooLowLevel()` 函数并传入 `FooContract` 的地址，那么 `FooContract` 中的 `x` 值会被设定为 `1`。这表明成功调用了 `foo` 函数。\r\n\r\n## 使用 msg.sig 识别 Solidity 的函数调用和选择器\r\n\r\n`msg.sig` 是一个全局变量，返回交易数据的前四个字节，这就是 Solidity 合约了解要调用哪个函数的方式。\r\n\r\n`msg.sig` 在整个交易中保持不变，它不会因当前活动的函数而改变。所以即使公共函数在调用期间调用了另一个公共函数，`msg.sig` 也将是最初调用的函数的选择器。\r\n\r\n在以下代码中，`foo` 调用 `bar` 来获取 `msg.sig`，但当调用 `foo` 时返回的选择器是 `foo` 的函数选择器，而不是 `bar`：\r\n\r\n你可以在 [Remix 中测试代码](https://remix.ethereum.org/#code=Ly9TUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogTUlUCnByYWdtYSBzb2xpZGl0eSAwLjguMjU7Cgpjb250cmFjdCBTZWxlY3RvclRlc3QgewoKICAgIC8vIHJldHVybnMgZnVuY3Rpb24gc2VsZWN0b3Igb2YgYGJhcigpYCAweGZlYmIwZjdlCiAgICBmdW5jdGlvbiBiYXIoKSBwdWJsaWMgcHVyZSByZXR1cm5zIChieXRlczQpIHsKICAgICAgICByZXR1cm4gbXNnLnNpZzsKICAgIH0KCiAgICAvLyByZXR1cm5zIGZ1bmN0aW9uIHNlbGVjdG9yIG9mIGBmb28oKWAgMHhjMjk4NTU3OAogICAgZnVuY3Rpb24gZm9vKCkgcHVibGljIHB1cmUgcmV0dXJucyAoYnl0ZXM0KSB7CiAgICAgICAgcmV0dXJuIGJhcigpOwogICAgfQogICAgCiAgICBmdW5jdGlvbiB0ZXN0U2VsZWN0b3JzKCkgZXh0ZXJuYWwgcHVyZSByZXR1cm5zIChib29sKSB7CgogICAgICAgIGFzc2VydCh0aGlzLmZvby5zZWxlY3RvciA9PSAweGMyOTg1NTc4KTsKICAgICAgICBhc3NlcnQodGhpcy5iYXIuc2VsZWN0b3IgPT0gMHhmZWJiMGY3ZSk7CgogICAgICAgIHJldHVybiB0cnVlOwogICAgfQp9&lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.25+commit.b61c2a91.js):\r\n\r\n```solidity~\r\n//SPDX-License-Identifier: MIT\r\npragma solidity 0.8.25;\r\n\r\ncontract SelectorTest {\r\n\r\n    // 返回函数选择器 `bar()` 0xfebb0f7e\r\n    function bar() public pure returns (bytes4) {\r\n        return msg.sig;\r\n    }\r\n\r\n    // 返回函数选择器 `foo()` 0xc2985578\r\n    function foo() public pure returns (bytes4) {\r\n        return bar();\r\n    }\r\n\r\n    function testSelectors() external pure returns (bool) {\r\n        \r\n        assert(this.foo.selector == 0xc2985578);\r\n        assert(this.bar.selector == 0xfebb0f7e);\r\n        \r\n        return true;\r\n    }\r\n}\r\n\r\n```\r\n\r\n## Solidity 函数签名\r\n\r\nSolidity 中的函数签名是一个字符串，包含合约名称后接其接受的参数类型。参数的变量名称从参数中移除。\r\n\r\n在以下代码片段中，函数位于左侧，函数签名位于右侧：\r\n\r\n```solidity!\r\nfunction setPoint(uint256 x, uint256 y) --> \"setPoint(uint256,uint256)\"\r\nfunction setName(string memory name) --> \"setName(string)\"\r\nfunction addValue(uint v) --> \"addValue(uint256)\"\r\n\r\n```\r\n\r\n函数选择器中没有空格。所有 uint 类型必须明确包括其大小（uint256、uint40、uint8 等）。不包括 `calldata` 和 `memory` 类型。例如，`getBalanceById(uint)` 是一个无效的 _signature_。\r\n\r\n## 如何从函数签名计算函数选择器\r\n\r\n函数选择器是函数签名的 keccak256 哈希的前四个字节。\r\n\r\n```solidity!\r\nfunction returnFunctionSelectorFromSignature(\r\n    string calldata functionName\r\n) public pure returns(bytes4) {\r\n    bytes4 functionSelector = bytes4(keccak256(abi.encodePacked(functionName)));\r\n    return(functionSelector);\r\n}\r\n\r\n```\r\n\r\n将 **“`foo()`”** 输入上面的函数将返回 `0xc2985578`。\r\n\r\n以下代码演示了给定函数签名计算函数选择器。\r\n\r\n```solidity!\r\n//SPDX-License-Identifier: MIT\r\npragma solidity 0.8.25;\r\n\r\ncontract FunctionSignatureTest {\r\n\r\n    function foo() external {}\r\n    \r\n    function point(uint256 x, uint256 y) external {}\r\n    \r\n    function setName(string memory name) external {}\r\n    \r\n    function testSignatures() external pure returns (bool) {\r\n    \r\n    // 注意：转换为 bytes4 只取前 4 字节\r\n    // 并移除其余部分\r\n\r\n    assert(bytes4(keccak256(\"foo()\")) == this.foo.selector);\r\n        assert(bytes4(keccak256(\"point(uint256,uint256)\")) == this.point.selector);\r\n        assert(bytes4(keccak256(\"setName(string)\")) == this.setName.selector);\r\n    \r\n    return true;\r\n\r\n    }\r\n}\r\n\r\n```\r\n\r\n接口作为地址编码。例如，`f(IERC20 token)` 被编码为 `f(address token)`。将地址设为可支付不会改变签名。\r\n\r\n## 内部函数没有函数选择器\r\n\r\n公共和外部函数有选择器，但内部和私有函数没有。以下代码无法编译：\r\n\r\n```solidity!\r\ncontract Foo {\r\n\r\n    function bar() internal {\r\n    }\r\n\r\n    // 无法编译\r\n    function barSelector() external pure returns (bytes4) {\r\n        return this.bar.selector;\r\n    }\r\n}\r\n\r\n```\r\n\r\n如果将 `bar` 更改为 `public` 或 `external`，代码将编译成功。\r\n\r\n内部函数不需要函数选择器，因为函数选择器用于外部合约。换句话说，函数选择器是外部用户指定他们要调用的公共或外部函数的方式。\r\n\r\n## 为什么使用函数选择器而不是函数名？\r\n\r\nSolidity 函数名可以任意长，如果函数名很长，交易的大小和成本都会增加。如果提供函数选择器，交易通常会更小。\r\n\r\n## 回退函数有函数选择器吗？\r\n\r\n回退函数没有函数选择器。\r\n\r\n如果在回退函数内部记录 `msg.sig`，它将简单地记录交易数据的前四个字节。如果没有交易数据，则 `msg.sig` 将返回四个零字节。这并不意味着回退函数的选择器全为零，而是 `msg.sig` 尝试读取不存在的数据。\r\n\r\n```solidity!\r\ncontract FunctionSignatureTest {\r\n\r\n    event LogSelector(bytes4);\r\n\r\n    fallback() external payable {\r\n        emit LogSelector(msg.sig);\r\n    }\r\n}\r\n\r\n```\r\n\r\n这里是 [**Remix 上的代码**](https://remix.ethereum.org/#code=Ly8gU1BEWC1MaWNlZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgMC44LjI1OwoKY29udHJhY3QgRnVuY3Rpb25TaWduYXR1cmVUZXN0IHsKCiAgICBldmVudCBMb2dTZWxlY3RvcihieXRlczQpOwoKICAgIGZhbGxiYWNrKCkgZXh0ZXJuYWwgcGF5YWJsZSB7CiAgICAgICAgZW1pdCBMb2dTZWxlY3Rvcihtc2cuc2lnKTsKICAgIH0KfQ&lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.25+commit.b61c2a91.js) 以测试上述合约。以下是如何在 Remix 上触发回退函数的视频：\r\n\r\nhttps://img.learnblockchain.cn/2025/02/26/file.mp4\r\n\r\n## 函数选择器碰撞的概率\r\n\r\n函数选择器最多可以持有 2**32 – 1（约 42 亿）个可能值。因此，两个函数具有相同选择器的机会小但实际存在。\r\n\r\n```solidity\r\ncontract WontCompile {\r\n\r\n    function collate_propagate_storage(bytes16 x) external {}\r\n\r\n    function burn(uint256 amount) external {}\r\n}\r\n\r\n```\r\n\r\n上述示例摘自 [该论坛帖子](https://forum.openzeppelin.com/t/beware-of-the-proxy-learn-how-to-exploit-function-clashing/1070)。这两个函数的函数选择器都为 `0x42966c68`。\r\n\r\n## 有用的函数选择器资源\r\n\r\n[函数选择器计算器](https://www.evm-function-selector.click/)。请务必添加关键字 `function`。计算器期望格式为 `function burn(uint256)`。\r\n\r\n[函数选择器数据库](https://www.4byte.directory/)。将 4 个字节放入搜索框会返回匹配的已知函数。\r\n\r\n## 函数选择器与 EVM - 以太坊虚拟机\r\n\r\n对于熟悉 EVM 的读者来说，有一个常见的混淆源。四字节函数选择器发生在 Solidity 应用层，而不是 EVM 层。以太坊规范中没有任何条款要求函数必须用 4 字节识别。这是一个强烈的约定，但并非必需。\r\n\r\n事实上，一个常见的 [针对二层应用的优化](https://learnblockchain.cn/article/11231) 是用 1 字节函数选择器来识别函数。也就是说，所有函数调用都会转到回退函数，然后回退函数查看交易中的第一个字节来确定要调用哪个函数。\r\n\r\n## 通过 RareSkills 了解更多\r\n\r\n请查看我们的 [Solidity 实训营](https://learnblockchain.cn/openspace/1)，了解有关智能合约开发的更多高级主题。\r\n\r\n_原始发布日期：2024年3月30日_\r\n\r\n>- 原文链接： [rareskills.io/post/funct...](https://www.rareskills.io/post/function-selector)\r\n>- 登链社区 AI 助手，为大家转译优秀英文文章，如有翻译不通的地方，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/20722","address":null},"history":null,"timestamp":1740546026,"version":1}