Delegatecall:详细与动画指南

本文详细介绍了EVM中的delegatecall操作码,解释了其工作原理、使用场景及潜在问题,并提供了多个代码示例帮助理解。

这篇文章详细解释了 delegatecall 的工作原理。以太坊虚拟机 (EVM) 提供了四个用于在合约之间调用的 opcodes

  • CALL (F1)
  • CALLCODE (F2)
  • STATICCALL (FA)
  • DELEGATECALL (F4)

值得注意的是,CALLCODE opcode 自 Solidity v5 起已被弃用,取而代之的是 DELEGATECALL。这些 opcodes 在 Solidity 中有直接的实现,并可以作为 address 类型变量的方法执行。

为了更好地理解 delegatecall 的工作原理,首先让我们回顾一下 CALL opcode 的功能。

CALL

为了演示调用,考虑以下合约:

contract Called {
    uint public number;

    function increment() public {
        number++;
    }
}

从另一个合约执行 increment() 函数的最直接方法是利用 Called 合约接口。在这种做法中,我们可以简单地使用语句 called.increment() 来执行该函数,其中 calledCalled 的地址。但调用 increment() 也可以通过低级调用来实现,如下合约所示:

contract Caller {

    address constant public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138; // Called 的地址

    function callIncrement() public {
        calledAddress.call(abi.encodeWithSignature("increment()"));
    }
}

每个 address 类型的变量,例如 calledAddress 变量,都有一个名为 call 的方法。此方法要求一个参数,该参数是要在交易中执行的输入数据,即 ABI 编码的 calldata。在上述情况下,输入数据必须对应于 increment() 函数的签名,具有 函数选择器 0xd09de08a。我们使用 abi.encodeWithSignature 方法从函数定义生成该签名。

如果你在 Caller 合约中执行 callIncrement 函数,你会看到 Called 中的状态变量 number 会增加 1。call 方法 不验证目标地址实际上是否对应于一个现有合约,也不验证其是否包含指定函数

调用交易的可视化在下面的视频中:

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

Call 返回一个元组

call 方法返回一个包含两个值的元组。第一个值是一个布尔值,指示交易的成功或失败。第二个值(类型为 bytes)保存了由 call 执行的函数的返回值,如果有的话,经过 ABI 编码。

为了检索 call 的返回值,我们可以修改 callIncrement 函数如下:

function callIncrement() public {
    (bool success, bytes memory data) = called.call(
        abi.encodeWithSignature("increment()")
    );    
}

call 方法从不回退。如果交易不 成功,则 success 将为 false,程序员需要相应地处理这一点。

处理调用失败

让我们修改上面的合约,添加另一个对不存在函数的调用,如下所示。

contract Caller {
    address public constant calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;

    function callIncrement() public {
        (bool success, bytes memory data) = called.call(
            abi.encodeWithSignature("increment()")
        );

        if (!success) {
            revert("Something went wrong");
        }
    }
        // 调用一个不存在的函数
    function callWrong() public {
        (bool success, bytes memory data) = called.call(
            abi.encodeWithSignature("thisFunctionDoesNotExist()")
        );

        if (!success) {
            revert("Something went wrong");
        }
    }
}

我故意创建了两个函数:一个具有正确的 increment 函数签名,另一个具有无效的签名。第一个函数将返回 truesuccess,而第二个将返回 false。返回布尔值被明确处理,如果 success 为 false,交易将回退。

我们必须小心跟踪调用是否成功,我们将很快重新审视这个问题。

EVM 在后台运行的内容

increment 函数的目的是增加名为 number 的状态变量。由于 EVM 对状态变量没有知识,但 在存储槽上操作 ,因此该函数实际上做的是增加槽 0 的值。这一操作发生在 Called 合约的存储中。

使用 `call` 函数调用的合约示例

回顾 call 方法的使用将有助于我们形成关于如何使用 delegatecall 的想法。

DELEGATECALL

发起 delegatecall 的合约在自己的环境中执行目标智能合约的逻辑。

一种心理模型是,它复制目标智能合约的代码并在自己内部运行该代码。目标智能合约通常被称为“实现合约”。

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

就像 call 一样,delegatecall 也具有要由目标合约执行的输入数据作为参数。

这是与上面动画对应的 Called 合约的代码,在 Caller 的环境中运行:

contract Called {
    uint public number;

    function increment() public {
        number++;
    }
}

Caller 的代码:

contract Caller {
    uint public number;

    function callIncrement(address _calledAddress) public {
        _calledAddress.delegatecall(
            abi.encodeWithSignature("increment()")
        );
    }
}

这个 delegatecall 将执行 increment 函数;然而,执行将发生在一个关键的区别上。Caller 合约的存储将被修改,而 不是 Called 的存储。仿佛 Caller 合约借用了 Called 的代码在其上下文中执行。

下图进一步说明了 delegatecall 如何修改 Caller 的存储而不是 Called 的存储。

使用 `call` 函数调用的合约示例

下图展示了使用 calldelegatecall 执行 increment 函数之间的区别。

使用 `call` 和 `delegatecall` 执行 `increment` 函数之间的区别

存储槽冲突

发起 delegatecall 的合约必须非常小心预测其存储槽将被修改的部分。前面的示例工作正常,因为 Caller 没有使用槽 0 中的状态变量。在使用 delegatecall 时,一个常见的错误是忘记这一点。让我们看一下这个例子的实现。

contract Called {
    uint public number;

    function increment() public {
        number++;
    }
}

contract Caller {
        // 此处有一个新的存储变量
    address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;

    uint public myNumber;

    function callIncrement() public {        
        called.delegatecall(
            abi.encodeWithSignature("increment()")
        );
    }

}

请注意,在上面的更新合约中,槽 0 的内容是 Called 合约的地址,而 myNumber 变量现在则存储在槽 1 中。

如果部署提供的合约并执行 callIncrement 函数,Caller 存储的槽 0 将被增加,但实际上是 calledAddress 变量在那,而不是 myNumber 变量。

下面的视频展示了这个错误:

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

让我们在下面说明发生了什么。

在使用 delegatecall 时,调用者的存储槽

因此,在使用 delegatecall 时务必谨慎,因为它可能会意外地破坏我们的合约。在上述示例中,程序员很可能并不想通过 callIncrement 函数修改 calledAddress 变量。

让我们对 Caller 进行一个小改动,将状态变量 myNumber 移至槽 0。

contract Caller {

    uint public myNumber;

    address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;

    function callIncrement() public {
        called.delegatecall(
            abi.encodeWithSignature("increment()")
        );
    }
}

现在,当执行 callIncrement 函数时,myNumber 变量将被增加,因为这正是 increment 函数的目的。我故意将 Caller 中的变量名称与 Called 中的不同命名,以展示 变量名称并不重要;重要的是它们处于哪个槽中。对两个合约的状态变量进行对齐对于 delegatecall 的正常运行至关重要。

将实现与数据解耦

delegatecall 最重要的用途之一是将数据存储的合约(如此例中的 Caller)与执行逻辑所在的合约(如 Called)解耦。因此,如果希望更改执行逻辑,只需用另一个合约替换 Called,并更新对实现合约的引用,而无需触及存储。Caller 不再受到它拥有哪些函数的限制,它可以从其他合约委托调用所需的函数。

如果需要更改执行逻辑,例如,减少 myNumber 的值而不是增加,可以创建一个新的实现合约,如下所示。

contract NewCalled {

    uint public number;

    function increment() public {
        number = number - 1;
    }
}

不幸的是,无法更改将被调用的函数名称,因为这样做会更改其签名。

创建完新实现合约 NewCalled 后,只需部署这个新合约并在 Caller 中更改 calledAddress 状态变量。否则,Caller 需要有一个机制来更改它所发起 delegateCall 的地址,这一点为了保持代码的简洁性而未包含。

我们成功地修改了 Caller 合约使用的业务逻辑。将数据与执行逻辑分离使我们能够在 Solidity 中创建可升级的智能合约。

delegatecall() 允许合约将数据与业务逻辑分离

在上图中,左侧的合约同时处理数据和逻辑。而右侧,顶部的合约持有数据,但更新数据的机制保存在逻辑合约中。要更新数据,将对逻辑合约执行一次 delegatecall

处理 delegatecall 返回值

就像 call 一样,delegatecall 也返回一个包含两个值的元组:一个布尔值,指示执行的成功与否,以及通过 delegatecall 执行的函数的返回值(以字节表示)。要查看如何处理这个返回值,让我们写一个新示例。

contract Called {
    function calculateDiscountPrice(
        uint256 amount, 
        uint256 discountRate
    ) public pure returns (uint) {
        return amount - (amount * _discountRate)/100;
    }
}

contract Caller {
    uint public price = 200;
    uint public discountRate = 10;
    address public called;

    function setCalled(address _called) public {
        called = _called;
    }

    function setDiscountPrice() public  {
        (bool success, bytes memory data) = called.delegatecall(
            abi.encodeWithSignature(
                "calculateDiscountPrice(uint256,uint256)", 
                price, 
            discountRate)
        );

        if (success) {
            uint newPrice = abi.decode(data, (uint256));
            price = newPrice;
        }
    }
}

Called 合约包含计算折扣价格的逻辑。我们通过执行 calculateDiscountPrice 函数利用该逻辑,采用 delegatecall。此函数返回一个值,我们必须使用 abi.decode 来对其进行解码。在根据此返回值做出任何决策之前,检查该函数是否成功执行至关重要,否则我们可能会尝试解析不存在的返回值或最终解析反转原因字符串。

何时调用和 delegatecall 返回 false

一个关键点是要理解何时成功值将为 truefalse基本上,它取决于被执行的函数是否会反转。 执行可以反转的三种途径:

  • 如果遇到 REVERT opcode,
  • 如果耗尽 gas,
  • 如果尝试进行某些禁止的操作,如除以零。

如果通过 delegatecall(或 call)执行的函数遇到这些情况之一,它将反转,返回值将为 false。

一个经常困惑开发者的问题是,为什么对不存在合约的 delegatecall 不会反转且仍报告执行成功。基于我们所说的,空地址永远不会遇到三种反转条件,因此它永远不会反转。

另一个存储变量问题示例

让我们稍微修改一下上面的代码,给出另一个与存储布局相关的错误示例。

Caller 合约仍然通过 delegatecall 调用实现合约,但现在 Called 合约从状态变量中读取一个值。这可能看起来是一个小改动,但实际上会导致灾难。你能找出为什么吗?

contract Called {
    uint public discountRate = 20;

    function calculateDiscountPrice(uint256 amount) public pure returns (uint) {
        return amount - (amount * discountRate)/100;
    }
}

contract Caller {
    uint public price = 200;
    address public called;
    function setCalled(address _called) public {
        called = _called;
    }
    function setDiscount() public  {
        (bool success, bytes memory data) =called.delegatecall(
            abi.encodeWithSignature(
                "calculateDiscountPrice(uint256)", 
                price
            )
        );

        if (success) {
            uint newPrice = abi.decode(data, (uint256));
            price = newPrice;
        }
    }
}

问题在于,calculateDiscountPrice 正在读取一个状态变量,具体来说是位于槽 0 的。请记住,在 delegatecall 中,函数在调用合约的存储中执行。换句话说,你可能认为正在使用 Called 合约的 discountRate 变量来计算新的 price,但实际上你是在使用 Caller 合约的 price 变量!存储变量 Called.discountRateCalled.price 占用槽 0。

将会获得 200% 的折扣,这个折扣相当可观(并将导致函数反转,因为新的计算价格将变为负数,这是 uint 类型变量所不允许的)。

不可变和常量变量在 delegatecall 中:一个错误故事

delegatecall 相关的另一个棘手问题是当涉及不可变或常量变量时。让我们考察一个许多经验丰富的 Solidity 程序员误解的示例:

contract Caller {
    uint256 private immutable a = 3;
    function getValueDelegate(address called) public pure returns (uint256) {
        (bool success, bytes memory data) = called.delegatecall(
            abi.encodeWithSignature("getValue()"));
        return abi.decode(data, (uint256)); // 这个是 3 还是 2?
    }
}

contract Called {
    uint256 private immutable a = 2;

    function getValue() public pure returns (uint256) {
        return a;
    }
}

问题是:当执行 getValueDelegate 时,将返回 2 还是 3?让我们来推理一下。

  • getValueDelegate 函数执行了 getValue 函数,理论上返回与状态变量相对应的值。
  • 由于是 delegatecall,我们应该检查调用合约中的槽,而不是被调用合约中的槽。
  • Caller 中变量 a 的值为 3,因此响应必须为 3。 nail it.

惊讶的是,正确的答案是 2。为什么呢?

不可变或常量状态变量并不是真正的状态变量:它们不占用槽。当我们声明不可变变量时,其值在合约字节码中硬编码,在 delegatecall 执行过程中被调用。因此,getValue 函数返回的是硬编码的值 2。

msg.sender、msg.value 和 address(this)

如果在 Called 合约中使用 msg.sendermsg.valueaddress(this),所有这些值将对应于 Caller 合约的 msg.sendermsg.valueaddress(this) 值。我们来回顾一下 delegatecall 是如何运作的:一切都在调用合约的上下文中进行。实现合约仅提供要执行的字节码,仅此而已。

delegatecall() 背后,caller contract 是如何利用 called contract 的字节码

让我们在一个示例中应用这个概念。考虑以下代码:

contract Called {
    function getInfo() public payable returns (address, uint, address) {
        return (msg.sender, msg.value, address(this));
    }
}

contract Caller {
    function getDelegatedInfo(
        address _called
    ) public payable returns (address, uint, address) {        
        (bool success, bytes memory data) = _called.delegatecall(
            abi.encodeWithSignature("getInfo()")
        );
        return abi.decode(data, (address, uint, address));      
    }
}

Called 合约中,我正在利用 msg.sendermsg.valueaddress(this),并在 getInfo 函数中返回这些值。下图是执行 getDelegateInfo 并显示返回值的情况。

  • msg.sender对应于执行交易的账户,具体来说是第一个 Remix 默认账户,即 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  • msg.value 显示为 1 以太,反映原始交易中的值。
  • address(this)Caller 合约的地址,正如左侧图上所示,而不是 Called 合约的地址。

delegatecall() 背后,caller contract 是如何利用 called contract 的字节码

在 Remix 中,我们展示了对 msg.sender (0)、msg.value (1) 和 address(this) (2) 的日志值。

msg.data 和在 delegatecall 中的输入数据

msg.data 属性返回正在执行的上下文的 calldata。当 msg.data 在通过 EOA 直接通过交易执行的函数中被调用时,msg.data 表示交易的输入数据。

当我们执行调用或 delegatecall 时,我们在调用实现合约的参数中指定将要执行的输入数据。因此,原始 calldata 与通过 delegatecall 创建的子上下文中的 calldata 不同,因此 msg.data 也会有所不同。

图像显示在 delegatecall() 中,调用者和被调用合约的 msg.data 不同

以下代码将用于演示这一点。

contract Called {
    function returnMsgData() public pure returns (bytes memory) {
        return msg.data;
    }
}

contract Caller {
    function delegateMsgData(
        address _called    
    ) public returns (bytes memory data) {
        (, data) = _called.delegatecall(
            abi.encodeWithSignature("returnMsgData()"));
    }
}

原始交易执行 delegateMsgData 函数,该函数要求一个地址类型的参数。因此,输入数据将包括函数签名以及一个 ABI 编码的地址。

delegateMsgData 函数随后代表调用 returnMsgData 函数。要完成此操作,传递给运行时的 calldata 必须包含 returnMsgData 的签名。因此,returnMsgData 中的 msg.data 的值是其自身的签名,给定为 0x0b1c837f

在下图中,我们可以看到 returnMsgData 的返回值是其自身的签名,经过 ABI 编码。

图像显示的 returnMsgData 的返回是其自身的签名,经过 ABI 编码。

解码的输出是 returnMsgData 函数的签名,经过 ABI 编码为字节。

代码大小的反例

我们提到可以将 delegatecall 概念化为借用实现合约的字节码并在调用合约中执行。然而,有一个例外,CODESIZE opcode。

假设一个智能合约在其字节码中具有 CODESIZECODESIZE 仅返回 合约的大小。在 delegatecall 时,codesize 不返回调用者的代码大小——它返回的是 delegatecalled 的代码的大小。

为演示这一属性,我们提供了以下代码。在 Solidity 中,可以通过 codesize() 函数在汇编中执行 CODESIZE。我们有两个实现合约 CalledACalledB,它们仅因局部变量(ContractB 中的 unused——在 ContractA 中缺距)不同而不同,这用于确保这两个合约的大小不同。通过 Caller 合约的 getSizes 函数对这些合约进行调用。

// codesize 1103
contract Caller {
    function getSizes(
        address _calledA,
        address _calledB
    ) public returns (uint sizeA, uint sizeB) {
        (, bytes memory dataA) = _calledA.delegatecall(
            abi.encodeWithSignature("getCodeSize()")
        );
        (, bytes memory dataB) = _calledB.delegatecall(
            abi.encodeWithSignature("getCodeSize()")
        );
        sizeA = abi.decode(dataA, (uint256));
        sizeB = abi.decode(dataB, (uint256));
    }
}

// codesize 174
contract CalledA {
    function getCodeSize() public pure returns (uint size) {
        assembly {
            size := codesize()
        }
    }
}

// codesize 180
contract CalledB {
    function getCodeSize() public pure returns (uint size) {
        uint unused = 100;
        assembly {
            size := codesize()
        }
    }
}

// 你可以使用此合约检查合约的大小
contract MeasureContractSize {
    function measureConctract(address c) external view returns (uint256 size){
        size = c.code.length;
    }
}

如果 codesize 函数返回调用者合约的大小,那么通过 delegate调用 ContractAContractB 的情况下,getSizes() 返回的值将会相同。也就是说,它们将是 Caller 的大小,即 1103。然而,正如下图所示,返回的值是不同的,明确表明它们是 CalledACalledB 的大小。

CalledA 和 CalledB 合约的代码大小输出

delegatecall 的 delegatecall

人们可能会好奇:如果一个合约发起一个 delegatecall 到第二个合约,该合约又发起一个 delegatecall 到第三个合约,会发生什么?在这种情况下,上下文将保持为发起第一个 delegatecall 的合约,而不是中间合约。

其工作原理如下:

  • Caller 合约对 CalledFirst 合约中的函数 logSender() 发起 delegatecall。
  • 此函数打算发出日记,记录 msg.sender
  • 此外,CalledFirst 合约除了创建此日志外,还 delegatecall 到 CalledLast 合约。
  • CalledLast 合约也发出事件,该事件同样记录 msg.sender

以下图表描述了此流程。

通过使用 delegatecall 和 logSender() 相互依赖的三个合约

请记住,所有 delegatecall 要做的只是借用被委托合约的字节码。以这种方式可视化,我们会发现 msg.sender始终是原始 msg.sender,因为一切都发生在 Caller 内。请查看下面的动画:

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

下面提供一些源代码以测试 delegatecall 到 delegatecall 的概念:

contract Caller {
    address calledFirst = 0xF27374C91BF602603AC5C9DaCC19BE431E3501cb;
    function delegateCallToFirst() public {
        calledFirst.delegatecall(
            abi.encodeWithSignature("logSender()")
        );
    }
}

contract CalledFirst {
    event SenderAtCalledFirst(address sender);
    address constant calledLast = 0x1d142a62E2e98474093545D4A3A0f7DB9503B8BD;

    function logSender() public {
        emit SenderAtCalledFirst(msg.sender);
        calledLast.delegatecall(
            abi.encodeWithSignature("logSender()")
        );
    }
}

contract CalledLast {
    event SenderAtCalledLast(address sender);
    function logSender() public {
        emit SenderAtCalledLast(msg.sender);
    }
}

我们可能会认为在 CalledLast 中的 msg.sender 将是 CalledFirst 的地址,因为它是调用 CalledLast 的合约,但这个说法不符合我们的模型,即通过 delegatecall 调用的合约的字节码只是被借用,且始终保持执行上下文来源于发起 delegatecall 的合约。

最终结果是,CalledFirstCalledLastmsg.sender 值都对应于发起 Caller.delegateCallToFirst() 的交易的账户。可以在下面的图中观察到,当我们在 Remix 中执行这个过程并捕获日志时。

CalledFirst 和 CalledLast 的 msg.sender 结果

msg.senderCalledFirstCalledLast 中相同

一个源于混淆的说法是,有人可能会将这个操作描述为“Caller delegatecalls CalledFirstCalledFirst delegatecalls CalledLast。” 但这使得 CalledFirst 似乎在进行 delegatecall——实际上并非如此。CalledFirst 提供了字节码给 Called——该字节码正在由 Called 发出 delegatecall 到 CalledLast

通过 delegatecall 进行调用
让我们引入一个情节扭转并修改 CalledFirst 合约。现在,CalledFirst 将使用 call 而不是 delegatecall 调用 CalledLast

通过使用 delegatecall 和 logSender() 相互依赖的三个合约

换句话说,CalledFirst 合约需要更新为以下代码:

contract CalledFirst {
    event SenderAtCalledFirst(address sender);
    address constant calledLast = ...;

    function logSender() public {
        emit SenderAtCalledFirst(msg.sender);
        calledLast.call(
            abi.encodeWithSignature("logSender()")
        ); // 这是新的
    }
}

问题是:在 SenderAtCalledLast 事件中记录的 msg.sender 是什么?下面的动画展示了发生的事情:

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

通过 delegatecall() 和 call() 比较 msg.sender 输出

Caller 通过 delegatecall 调用 CalledFirst 中的函数时,该函数在 Caller 的上下文中执行。请记住,CalledFirst 只是“借用”其字节码由 Caller 执行。此时,就好像我们在 Caller 合约中执行 msg.sender,这意味着 msg.sender 是发起交易的地址。

通过 delegatecall 和 call 函数比较:msg.sender 的表现

现在,CalledFirst 调用 CalledLast,但 CalledFirstCaller 的上下文中使用,因此就像 Caller 调用 CalledLast。在这种情况下,CalledLast 中的 msg.sender 将是 Caller 的地址。

在下图中,我们在 Remix 中观察日志。注意这次 msg.sender 的值不同。

比较 delegatecall 和 call 的合约中 msg.sender 的值

msg.senderCalledLast 中是 Caller 地址

练习: 如果 Caller 调用 CalledFirst,而 CalledFirst delegatecalls CalledLast,并且每个合约记录 msg.sender,那么每个合约将记录哪个消息发送者?

低级 delegatecall

在本节中,我们将使用 delegatecallYUL 中更深入地探讨其功能。YUL 中的函数与 opcode 语法密切相关,因此首先检查 DELEGATECALL opcode 的定义是有益的。

DELEGATECALL 从堆栈中获取六个参数,按顺序分别是:gasaddressargsOffsetargsSizeretOffsetretSize,并返回一个值指示操作是否成功(1)或未成功(0)。

对每个参数的解释如下(取自 evm.codes):

  1. gas : 发送到子上下文以执行的 gas 量。未被子上下文使用的 gas 将返回到此上下文。
  2. address : 要执行其代码的账户。
  3. argsOffset : 内存中字节的偏移量,以字节为单位,表示子上下文的 calldata。
  4. argsSize : 需要复制的字节大小(calldata 的大小)。
  5. retOffset : 内存中字节的偏移量,以字节为单位,表示应存储来自子上下文的返回数据的位置。
  6. retSize : 需要复制的字节大小(返回数据的大小)。

通过 delegatecall 将 ether 发送到合约是不允许的(想象一下,如果允许,会造成什么潜在的利用!)。另一方面,CALL opcode 允许转账 Ether 并附加一个额外的参数以指示具体转账多少 Ether。

在 YUL 中,delegatecall 函数与 DELEGATECALL opcode 相似,并包含上述六个参数,语法为:

delegatecall(g, a, in, insize, out, outsize).

下面,我们展示一个合约,有两个执行相同动作的函数,分别使用 delegatecall。一个使用纯 Solidity 编写,另一个包含 YUL。

contract DelegateYUL {

    function delegateInSolidity(
        address _address
    ) public returns (bytes memory data) {
        (, data) = _address.delegatecall(
            abi.encodeWithSignature("sayOne()")
        );
    }

    function delegateInYUL(
        address _address
    ) public returns (uint data) {
        assembly {
            mstore(0x00, 0x34ee2172) // 将我打算发送的 calldata 加载到 0x00 的内存中。第一个槽将变为 0x0000000000000000000000000000000000000000000000000000000034ee2172
            let result := delegatecall(gas(), _address, 0x1c, 4, 0, 0x20) // 第三个参数表示 calldata 在内存中的起始位置,第四个参数指定其字节大小,第五个参数指定如果有应存储的返回 calldata 的位置
            data := mload(0) // 从内存中读取 delegatecall 返回值
        }
    }
}

contract Called {
    function sayOne() public pure returns (uint) {
        return 1;
    }
}

delegateInSolidity 函数中,我使用 Solidity 的 delegatecall 方法,作为参数传递 sayOne 函数的签名,这是通过 abi.encodeWithSignature 方法计算出的。

如果我们不知道返回的大小,别担心。后来可以使用 returndatacopy 函数来处理这个问题。在另一篇文章中,当我们进一步深入使用 delegatecall 编写可升级合约时,将涵盖所有这些细节。

EIP 150 和 gas 转发

关于转发 gas 的问题:我们在 delegatecall 的第一个参数中使用 gas() 函数,它返回可用的 gas。这应该表示我们打算转发所有可用 gas。然而,自 "Tangerine Whistle" 分叉以来,关于通过 delegatecall (及其他 opcodes)转发的 gas 有一个 63/64 的上限。换句话说,虽然 gas() 函数返回所有可用 gas,但只有 63/64 被转发到新的子上下文,而 1/64 被保留。

结论

总结这篇文章,让我们回顾一下我们所学到的内容。Delegatecall 允许在调用合约的上下文中执行在其他合约中定义的函数。被调用的合约(也称为实现合约)仅提供其字节码,且其内部没有被更改或从其存储中获取任何内容。Delegatecall 被用于将数据存储合约与业务逻辑或函数实现的合约分离。这构成了在 Solidity 中最常用的合约可升级性模式的基础。 然而,正如我们所观察到的,delegatecall 必须谨慎使用,因为状态变量的意外更改可能发生,这可能导致调用合约变得不可用。

了解更多,使用 RareSkills

对于新手来说,查看我们的免费 Solidity 课程。中级 Solidity 开发者请查看我们的 Solidity Bootcamp

作者

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

最初发布于 5 月 3 日

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

0 条评论

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