Solidity 中的低级调用与高级调用

本文详细介绍了Solidity中两种调用合约的方法:通过合约接口的高级调用和使用call方法的低级调用,解释了为什么低级调用不会回滚而高级调用可能会回滚,并对比了这两种方法在调用空地址时的不同行为。

在 Solidity 中,一个合约可以通过两种方法调用其他合约:通过合约接口,这被认为是高级调用,或使用 call 方法,这是一种低级方法。

尽管这两种方法都使用 CALL 操作码,但 Solidity 对它们的处理方式不同。

在本文中,我们将比较这两者:为什么低级调用从不回滚,而高级调用可能会回滚;以及为什么对空地址的低级调用被视为成功,而对不存在的合约的高级调用会回滚。

为什么低级调用(或 delegatecall)从不回滚,而通过合约接口的调用可能回滚

在解释原因之前,让我引用一下 Solidity 文档中对此问题的说明。

当子调用发生异常时,它们会自动“冒泡”(即异常会重新抛出),除非在 try/catch 语句 中捕获。对此规则的例外是 send 和低级函数 call、delegatecallstaticcall:它们在发生异常时将返回 false 作为其第一个返回值,而不是“冒泡”。

下面我们展示一个高级调用和一个低级 call,以比较它们的行为。我将在下面的示例中使用 call 方法,但相同的原则可以扩展到 delegatecall

Caller 可以通过两种方式调用 Called 中的 ops()。请注意,ops() 总是会回滚:

pragma solidity ^0.8.0;

contract Caller {

    // 第一次调用 ops()
    function callByCall(address _address) public returns (bool success) {
        (success, ) = _address.call(abi.encodeWithSignature("ops()"));
    }

    // 第二次调用 ops()
    function callByInterface(address _address) public {
        Called called = Called(_address);
        called.ops();
    }
}
contract Called {

    // ops() 总是回滚
    function ops() public {
        revert();
    }
}

尽管这两种方法用来调用相同的函数,并且这两种方法都使用操作码 CALL但 Solidity 编译器生成的字节码 处理失败情况的方式不同。在 Caller 合约中执行这两个函数将揭示 Caller.callByInterface 会回滚,而 Caller.callByCall 不会。

在 EVM 级别,CALL 操作码返回一个布尔值,指示调用是否成功,并将此返回值放置在栈上。操作码本身不会触发回滚。

当通过合约接口进行调用时,Solidity 为我们处理这个返回值。它明确检查返回值是否为 false,并在调用不是在 try/catch 块中进行时发起回滚。

然而,在使用低级调用时,我们需要手动处理这个返回布尔值,并在需要时明确触发回滚。

contract Caller {

      //...
      function callByCall(address address) public returns (bool success) {
        (success, ) = address.call(abi.encodeWithSignature("ops()"));
        if (!success) {
            revert("发生错误");
        }
    }
    //...
}

下面的图示例说明了高级调用和低级调用之间在处理回滚时的区别。

低级调用处理回滚与高级调用处理回滚

调用空地址时 call 和通过接口调用的区别

Solidity 的低级 call 方法没有先检查被调用地址是否对应一个合约。合约可以使用 EXTCODESIZE 来检查地址是否为智能合约,这在幕后是 address.code.length 的操作码。如果大小为零,表示该地址没有部署合约。然而,call 方法没有包含这个检查;它直接执行 CALL 操作码。

使用接口时会检查目标的代码大小。换句话说,在为 callByInterface 函数生成的字节码中,在执行 CALL 操作码之前,会在指定地址执行 EXTCODESIZE 操作码。如果 EXTCODESIZE 返回的大小为零,表示该地址上没有合约,则该函数会在执行 CALL 操作码之前回滚。这就解释了为什么在使用不存在的合约地址执行时,callByInterface 函数会回滚,而 callByCall 不会。

下面的图示例说明了低级调用和高级调用如何与空合约交互的区别。

低级调用调用空合约与高级调用调用空合约

从根本上说,当执行时遇到 REVERT 操作码、耗尽Gas或尝试一些被禁止的操作(如除以零)时,执行可能会回滚。当对空地址进行调用时,以上条件均不会发生。

与 RareSkills 了解更多

如果你是 Solidity 新手,请查看我们的免费 Solidity 课程。如果你是经验丰富的 Solidity 程序员,请查看我们的高级 Solidity 训练营

作者

本文由 João Paulo Morais 与 RareSkills 合作撰写。

最初发布于 2024年5月1日

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

0 条评论

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