Delegatecall: 详细且生动的指南

  • RareSkills
  • 更新于 2024-07-25 16:47
  • 阅读 1365

Delegatecall: 详细且生动的指南

本文详细解释了 delegatecall 的工作原理。以太坊虚拟机(EVM)提供了四个用于在合约之间进行调用的操作码:

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

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

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

CALL

为了演示 call,请考虑以下合约:

contract Called {  
  uint public number;  

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

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

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

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

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

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

视频中展示了 call 交易的可视化过程:

视频:https://youtu.be/Wh-6hHTRO_I

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 函数签名,另一个具有无效签名。第一个函数将返回true表示success,而第二个将返回false。返回的布尔值被显式处理,如果success为 false,交易将回滚。

我们必须小心跟踪调用是否成功,我们稍后会重新讨论这个问题。

在底层 EVM 做了什么

increment函数的目的是增加名为number的状态变量。由于 EVM 不关心状态变量,而是在存储槽上操作,所以函数实际上是增加存储的第一个槽中的值,即槽 0。此操作发生在Called合约的存储中。

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

DELEGATECALL

一个合约对目标智能合约进行 delegatecall 时,会在自己的环境中执行目标合约的逻辑。

一种思维模型是它复制目标智能合约的代码并自行运行该代码。目标智能合约通常被称为“实现合约”。

动画视频:https://youtu.be/uwBXltZvFA4

call一样,delegatecall也将要由目标合约执行的输入数据(input data)作为参数。

以下是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的存储。

delegatecall

下图说明了使用calldelegatecall执行 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变量。以下视频展示了这个 bug:

https://youtu.be/oWvqq5_ONIk

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

因此,在使用delegatecall时必须谨慎,因为它可能会无意中破坏我们的合约。在上面的例子中,程序员可能并不打算通过callIncrement函数更改calledAddress变量。

让我们通过将状态变量myNumber移动到槽 0 来对Caller进行一个小改动。

contract Caller {      

  uint public myNumber;      

  address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;      

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

现在,当执行callIncrement函数时,myNumber变量将被递增,因为这是increment函数的目的。我故意选择了与Called中的变量不同的变量名,以证明变量名并不重要;重要的是它在哪个槽中。对齐两个合约的状态变量对于 delegatecall 的正常运行至关重要。

将实现与数据解耦

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

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

contract NewCalled {      

  uint public number;      

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

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

在创建新的实现合约NewCalled后,只需部署这个新合约并更改Caller中的calledAddress状态变量。当然,Caller 需要有一个机制来更改它发出delegateCall的地址,我们没有包括这个机制以保持代码简洁。

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

在上图中,左边的合约处理数据和逻辑。右边,顶部的合约持有数据,但更新数据的机制在逻辑合约中。要更新数据,需要对逻辑合约进行 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函数来利用这个逻辑。这个函数返回一个值,我们必须使用abi.decode进行解码。在根据这个返回值做出任何决定之前,必须检查函数是否成功执行,否则我们可能会尝试解析一个不存在的返回值或最终解析一个回退原因字符串。

当 call 和 delegatecall 返回 false 时

一个关键点是理解成功值何时为truefalse本质上,这取决于被执行的函数是否会回退。 有三种方式可以导致执行回退:

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

如果通过delegatecall(或call)执行的函数遇到任何这些情况,它将回退,并且delegatecall的返回值将为 false。

注意: 一个经常困扰开发者的问题是为什么对不存在的合约进行delegatecall不会回退并且仍然报告执行成功。根据我们所说的,一个空地址永远不会满足回退的三个条件之一,所以它永远不会回退。

另一个存储变量陷阱的例子

让我们对上面的代码稍作修改,以给出另一个与存储布局相关的 bug 的例子。

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 中的不可变和常量变量:一个 bug 故事

另一个与 delegatecall 相关的棘手问题出现在涉及不可变或常量变量时。让我们来看一个许多经验丰富的 Solidity 程序员容易误解的例子:

contract Caller {  
    uint256 private immutable a = 3;  
    function getValueDelegate(address called) public pure returns (uint256) {  
        (bool success, bytes memory data) = B.delegatecall(  
            abi.encodewithSignature("getValue()"));  
        return abi.decode(data, (uint256)); // is this 3 or 2?  
    }  
}  

contract Called {  
    uint256 private immutable a = 2;  

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

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

  • getValueDelegate 函数执行 getValue 函数,该函数应该返回槽 0 中对应的状态变量的值。
  • 由于是 delegatecall,我们应该检查调用合约中的槽,而不是被调用合约中的槽。
  • Caller 中变量 a 的值是 3,所以响应必须是 3。搞定了。

令人惊讶的是,正确答案是 2。为什么?!

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

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

如果我们在 Called 合约中使用 msg.sendermsg.valueaddress(this),所有这些值将对应于 Caller 合约的 msg.sendermsg.valueaddress(this) 值。让我们记住 delegatecall 的操作方式:一切都在调用合约的上下文中发生。实现合约仅提供要执行的字节码,仅此而已。

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

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 函数中返回这些值。在下图中,使用 Remix 执行 getDelegateInfo,显示了返回的值。

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

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

msg.data 和 delegatecall 中的输入数据

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

当我们执行 call 或 delegatecall 时,我们将作为参数指定将在实现合约中执行的输入数据。因此,原始 calldata 与由 delegatecall 创建的子上下文中的 calldata 不同,因此 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 函数反过来 delegatecalls returnMsgData 函数。为此,传递给运行时的 calldata 必须包含 returnMsgData 的签名。因此,returnMsgData 内部的 msg.data 的值是其自身的签名,即 0x0b1c837f

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

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

Codesize 作为反例

我们提到可以通过借用实现合约的字节码并在调用合约中执行它来构思 delegatecall。有一个例外,即 CODESIZE 操作码。

假设一个智能合约的字节码中有 CODESIZECODESIZE 返回该合约的大小。Codesize 不会在 delegatecall 期间返回调用者代码的大小——它返回被 delegatecalled 的代码的大小。

为了演示这一特性,我们提供了下面的代码。在 Solidity 中,CODESIZE 可以通过 codesize() 函数在汇编中执行。我们有两个实现合约,CalledACalledB,它们仅在局部变量(ContractB 中的 unused——该变量在 ContractA 中不存在)上有所不同,目的是确保合约大小不同。这些合约通过 Caller 合约的 getSizes 函数使用 delegatecall 调用。

// 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 函数返回的是 Caller 合约的大小,那么通过委托调用 ContractAContractBgetSizes() 返回的值将是相同的。也就是说,它们将是 Caller 的大小,即 1103。然而,如下图所示,返回的值是不同的,这明确表明这些是 CalledACalledB 的大小。

委托一个委托调用会如何

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

其工作原理如下:

  • Caller 合约委托调用 CalledFirst 合约中的 logSender() 函数。
  • 该函数旨在发出一个事件,记录 msg.sender
  • 此外,CalledFirst 合约除了创建这个日志外,还委托调用 CalledLast 合约。
  • CalledLast 合约也会发出一个事件,同样记录 msg.sender

下面是描述此流程的图表。

请记住,所有的委托调用只是借用了被委托调用合约的字节码。可以这样想象,这些字节码暂时被“吸收”到调用合约中。当我们这样看时,我们会发现 msg.sender 始终是原始的 msg.sender,因为所有事情都发生在 Caller 内部。请参见下面的动画:https://youtu.be/Xbf5GIQTJvA

下面我们提供一些源代码来测试委托调用的委托调用的概念:

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 的合约。

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

msg.sender 在 CalledFirst 和 CalledLast 中是相同的

一个混淆的来源是,有人可能会描述这个操作为“Caller 委托调用 CalledFirst,而 CalledFirst 委托调用 CalledLast。”但这听起来像是 CalledFirst 在进行委托调用——事实并非如此。CalledFirst 提供字节码给 Called——而该字节码从 Called 发出委托调用给 CalledLast

从委托调用中call 调用

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

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

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

    function logSender() public {  
        emit SenderAtCalledFirst(msg.sender);  
        calledLast.call(  
            abi.encodeWithSignature("logSender()")  
        ); // this is new  
    }  
}

问题来了:在 SenderAtCalledLast 事件中记录的 msg.sender 会是什么?以下动画说明了会发生什么: 视频: https://youtu.be/PLy3zEdc9t0

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

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

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

CalledLast 中的 msg.sender 是 Caller 的地址

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

低层委托调用

在本节中,我们将使用 YUL 中的 delegatecall 来深入探索其功能。YUL 中的函数与操作码语法非常相似,因此首先查看 DELEGATECALL 操作码的定义是有益的。

DELEGATECALL 从堆栈中获取 6 个参数,依次为:gasaddressargsOffsetargsSizeretOffsetretSize,并返回一个值到堆栈,指示操作是否成功(1)或不成功(0)。

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

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

使用委托调用向合约发送以太币是不允许的(想象一下如果允许的话可能的漏洞!)。另一方面,CALL 操作码允许以太币转移,并包含一个额外的参数来指示应发送多少以太币。

在 YUL 中,delegatecall 函数与 DELEGATECALL 操作码相似,并包含上述相同的 6 个参数。其语法为:

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)             // 在0x00处加载我打算发送到内存中的calldata. 第一个槽将变成 0x0000000000000000000000000000000000000000000000000000000034ee2172  
            let result := delegatecall(gas(), _address, 0x1c, 4, 0, 0x20) // 第三个参数指示calldata在内存中的起始位置,第四个参数指定其大小(以字节为单位),第五个参数指定返回的calldata(如果有)应该存储在内存中的位置 
            data := mload(0)  // 读取委派调用从内存返回
        }  
    }  
}  

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

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

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

EIP 150 和 gas 转发

关于转发 gas 的问题说明:我们使用 gas() 函数作为 delegatecall 的第一个参数,它返回可用的 gas。这应该表明我们打算转发所有可用的 gas。然而,自从 Tangerine Whistle 分叉以来,通过 delegatecall(和其他操作码)转发的 gas 总量被限制为 总可能 gas 的 63/64。换句话说,尽管 gas() 函数返回所有可用的 gas,但只有 63/64 被转发到新的子上下文,而 1/64 被保留。

结论

总结本文,我们学到了什么。Delegatecall 允许在调用合约的上下文中执行其他合约中定义的函数。被调用的合约,也称为实现合约,仅提供其字节码,其存储中的任何内容都不会被更改或获取。

Delegatecall 被用来将存储数据的合约与存放业务逻辑或函数实现的合约分开。这是 Solidity 中最常用的合约可升级模式的基础。然而,正如我们所看到的,delegatecall 必须非常谨慎地使用,因为可能会发生对状态变量的意外更改,可能会导致调用合约无法使用。

了解更多 RareSkills

对于 Solidity 新手,请参阅我们的免费 Solidity 课程。中级 Solidity 开发人员请参阅我们的 Solidity Bootcamp。 登链社区是RareSkills 官方合作伙伴, 合约开发线下培训请参阅 OpenSpace 集训营

作者

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

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 3
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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