理解Solidity中的函数选择器

本文详细介绍了Solidity中的函数选择器(Function Selector),包括其定义、使用方法、计算方式以及相关注意事项。文章还探讨了函数选择器与EVM的关系,并提供了相关的代码示例和实用资源。

函数选择器是一个 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
    }
}

如果一个函数不接受任何参数,你可以通过将这 4 个字节(选择器)作为数据发送到合约,并使用低级 call 来调用该函数。

在以下示例中,CallFoo 合约通过调用 FooContract 及适当的四字节函数选择器来调用 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;
    }
}

如果 FooContractCallFoo 被部署,并且执行 callFooLowLevel() 函数并传入 FooContract 的地址,那么 FooContract 中的 x 值会被设定为 1。这表明成功调用了 foo 函数。

使用 msg.sig 识别 Solidity 的函数调用和选择器

msg.sig 是一个全局变量,返回交易数据的前四个字节,这就是 Solidity 合约了解要调用哪个函数的方式。

msg.sig 在整个交易中保持不变,它不会因当前活动的函数而改变。所以即使公共函数在调用期间调用了另一个公共函数,msg.sig 也将是最初调用的函数的选择器。

在以下代码中,foo 调用 bar 来获取 msg.sig,但当调用 foo 时返回的选择器是 foo 的函数选择器,而不是 bar

你可以在 Remix 中测试代码:

//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

contract SelectorTest {

    // 返回函数选择器 `bar()` 0xfebb0f7e
    function bar() public pure returns (bytes4) {
        return msg.sig;
    }

    // 返回函数选择器 `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 等)。不包括 calldatamemory 类型。例如,getBalanceById(uint) 是一个无效的 signature

如何从函数签名计算函数选择器

函数选择器是函数签名的 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) {

    // 注意:转换为 bytes4 只取前 4 字节
    // 并移除其余部分

    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)。将地址设为可支付不会改变签名。

内部函数没有函数选择器

公共和外部函数有选择器,但内部和私有函数没有。以下代码无法编译:

contract Foo {

    function bar() internal {
    }

    // 无法编译
    function barSelector() external pure returns (bytes4) {
        return this.bar.selector;
    }
}

如果将 bar 更改为 publicexternal,代码将编译成功。

内部函数不需要函数选择器,因为函数选择器用于外部合约。换句话说,函数选择器是外部用户指定他们要调用的公共或外部函数的方式。

为什么使用函数选择器而不是函数名?

Solidity 函数名可以任意长,如果函数名很长,交易的大小和成本都会增加。如果提供函数选择器,交易通常会更小。

回退函数有函数选择器吗?

回退函数没有函数选择器。

如果在回退函数内部记录 msg.sig,它将简单地记录交易数据的前四个字节。如果没有交易数据,则 msg.sig 将返回四个零字节。这并不意味着回退函数的选择器全为零,而是 msg.sig 尝试读取不存在的数据。

contract FunctionSignatureTest {

    event LogSelector(bytes4);

    fallback() external payable {
        emit LogSelector(msg.sig);
    }
}

这里是 Remix 上的代码 以测试上述合约。以下是如何在 Remix 上触发回退函数的视频:

https://img.learnblockchain.cn/2025/02/26/file.mp4

函数选择器碰撞的概率

函数选择器最多可以持有 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 的读者来说,有一个常见的混淆源。四字节函数选择器发生在 Solidity 应用层,而不是 EVM 层。以太坊规范中没有任何条款要求函数必须用 4 字节识别。这是一个强烈的约定,但并非必需。

事实上,一个常见的 针对二层应用的优化 是用 1 字节函数选择器来识别函数。也就是说,所有函数调用都会转到回退函数,然后回退函数查看交易中的第一个字节来确定要调用哪个函数。

通过 RareSkills 了解更多

请查看我们的 Solidity 实训营,了解有关智能合约开发的更多高级主题。

原始发布日期:2024年3月30日

  • 原文链接: rareskills.io/post/funct...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/