本文详细解释了 delegatecall 的工作原理。以太坊虚拟机(Ethereum Virtual Machine,简称 EVM)提供了四个操作码(opcodes)用于在合约之间进行调用:
CALL (F1)CALLCODE (F2)STATICCALL (FA)- 以及
DELEGATECALL (F4)。
值得注意的是,CALLCODE 操作码自 Solidity v5 起已被弃用,并被 DELEGATECALL 取代。这些操作码在 Solidity 中有直接的实现,并且可以作为 address 类型变量的方法来执行。
为了更好地理解 delegatecall 的工作原理,让我们首先回顾一下 CALL 操作码的功能。
CALL
为了演示 call,请考虑以下合约:
contract Called {
uint public number;
function increment() public {
number++;
}
}
从另一个合约执行 increment() 函数最直接的方法是利用 Called 合约接口(contract interface)。在这个方案中,我们可以通过像 called.increment() 这样简单的语句来执行该函数,其中 called 是 Called 的地址。但是,调用 increment() 也可以使用低级(low-level)的 call 来实现,如下面的合约所示:
contract Caller {
address constant public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138; // Called's address
function callIncrement() public {
calledAddress.call(abi.encodeWithSignature("increment()"));
}
}
每个 address 类型的变量(例如 calledAddress 变量)都有一个名为 call 的方法。该方法预期将要在交易中执行的输入数据作为参数,即 ABI encoded 的 calldata。在上述情况下,输入数据必须与 increment() 函数的签名相对应,其 function selector 为 0xd09de08a。我们使用 abi.encodeWithSignature 方法从函数定义中生成此签名。
如果你在 Caller 合约中执行 callIncrement 函数,你会观察到 Called 中的状态变量 number 将增加 1。call 方法不会验证目标地址是否实际对应于一个现有的合约,也不会验证它是否包含指定的函数。
call 交易的过程在下面的视频中进行了可视化:
Call 返回一个元组
call 方法返回一个包含两个值的元组(tuple)。第一个值是一个布尔值(boolean),指示交易的成功或失败。第二个值是 bytes 类型,保存了通过 call 执行的函数的返回值(如果有的话),该返回值经过了 ABI 编码。
为了获取 call 的返回结果,我们可以如下修改 callIncrement 函数:
function callIncrement() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("increment()")
);
}
call 方法永远不会 revert(回退)。如果交易没有 successful(成功),success 将为 false,程序员需要对此进行相应的处理。
处理 Call 失败
让我们修改上面的合约,包含另一个对不存在函数的调用,如下所示。
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");
}
}
// calls a non-existent function
function callWrong() public {
(bool success, bytes memory data) = called.call(
abi.encodeWithSignature("thisFunctionDoesNotExist()")
);
if (!success) {
revert("Something went wrong");
}
}
}
我故意创建了两个函数:一个带有正确的 increment 函数签名,另一个带有无效的签名。第一个函数的 success 将返回 true,而第二个将返回 false。返回的布尔值被显式地处理了,如果 success 为 false,交易将会 revert。
我们必须小心跟踪 call 是否成功,我们很快会再次讨论这个问题。
EVM 在底层做了什么
increment 函数的目的是将名为 number 的状态变量递增。由于 EVM 不了解状态变量,而是在存储插槽(storage slots)上操作,该函数实际执行的是增加存储的第一个插槽(即 slot 0)中的值。此操作发生在 Called 合约的存储中。

回顾了如何使用 call 方法后,将有助于我们形成关于如何使用 delegatecall 的概念。
DELEGATECALL
当一个合约对目标智能合约发起 delegatecall 时,它会在自己的环境内执行目标合约的逻辑。
一种心智模型是,它复制了目标智能合约的代码并自行运行该代码。作为目标的智能合约通常被称为“实现合约”(implementation contract)。
就像 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 和 delegatecall 执行 increment 函数之间的区别。

Storage 插槽冲突
发起 delegatecall 的合约必须极其小心地预测其哪些存储插槽会被修改。前面的例子之所以完美运行,是因为 Caller 没有使用 slot 0 中的状态变量。在使用 delegatecall 时的一个常见 bug 就是忘记了这一点。让我们来看一个例子。
contract Called {
uint public number;
function increment() public {
number++;
}
}
contract Caller {
// there is a new storage variable here
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
uint public myNumber;
function callIncrement() public {
calledAddress.delegatecall(
abi.encodeWithSignature("increment()")
);
}
}
请注意,在上面更新后的合约中,slot 0 的内容是 Called 合约的地址,而 myNumber 变量现在存储在 slot 1 中。
如果你部署提供的合约并执行 callIncrement 函数,Caller 存储的 slot 0 将会递增,但位于那里的却是 calledAddress 变量,而不是 myNumber 变量。
下面的视频演示了这个 bug:
让我们在下面图解发生的事情。

因此,在使用 delegatecall 时必须谨慎,因为它可能会无意中破坏我们的合约。在上面的例子中,程序员的意图多半不是要通过 callIncrement 函数来更改 calledAddress 变量。
让我们对 Caller 做一个小改动,将状态变量 myNumber 移到 slot 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 不再受限于它拥有的函数,它可以 delegatecall 它需要的来自其他合约的函数。
如果需要更改执行逻辑,例如,将 myNumber 的值减 1 而不是加 1,你可以创建一个新的实现合约,如下所示。
contract NewCalled {
uint public number;
function increment() public {
number = number - 1;
}
}
遗憾的是,无法更改将被调用的函数的名称,因为这样做会改变其签名。
创建了新的实现合约 NewCalled 之后,只需部署这个新合约并更改 Caller 中的 calledAddress 状态变量即可。当然,Caller 需要有一个机制来更改它发出 delegateCall 的地址,为了保持代码简洁,我们没有包含该机制。
我们已经成功修改了 Caller 合约所利用的业务逻辑。将数据与执行逻辑分离,使我们能够在 Solidity 中创建可升级的智能合约(upgradable smart contracts)。

在上面的图片中,左侧的合约同时处理数据和逻辑。在右侧,顶部的合约持有数据,但更新数据的机制保存在逻辑合约中。为了更新数据,会对逻辑合约发起 delegatecall。
处理 delegatecall 返回值
就像 call 一样,delegatecall 也返回一个包含两个值的元组:一个指示执行是否成功的布尔值,以及通过 delegatecall 执行的函数的返回值(以 bytes 形式)。来看看如何处理这个返回值,让我们编写一个新例子。
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 合约包含计算折扣价格的逻辑。我们通过 delegatecall 执行 calculateDiscountPrice 函数来利用此逻辑。该函数返回一个值,我们必须使用 abi.decode 对其进行解码。在根据此返回值做出任何决定之前,至关重要的是检查该函数是否成功执行,否则我们可能会尝试解析一个不存在的返回值,或者最终解析出 revert 原因字符串。
当 call 和 delegatecall 返回 false 时
一个需要理解的关键点是,success 值何时会是 true 或 false。本质上,这取决于正在执行的函数是否会 revert。 执行可以通过三种方式 revert:
- 如果遇到 REVERT 操作码,
- 如果耗尽了 gas,
- 如果尝试了被禁止的操作,例如除以零。
如果通过 delegatecall(或 call)执行的函数遇到了这些条件中的任何一个,它将会 revert,并且 delegatecall 的返回值将为 false。
一个经常困扰开发者的问题是,为什么对一个不存在的合约进行 delegatecall 不会 revert,并且仍然报告执行成功。基于我们刚才所说的,一个空地址永远不会满足上述三种 revert 条件之一,因此它永远不会 revert。
另一个 storage 变量陷阱的例子
让我们对上面的代码进行轻微修改,以给出另一个与存储布局(storage layout)相关 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 正在读取一个状态变量,特别是位于 slot 0 的状态变量。请记住,在 delegatecall 中,函数是在调用合约的存储中执行的。换句话说,你可能认为你正在使用 Called 合约中的 discountRate 变量来计算新的 price,但实际上你使用的是 Caller 合约中的 price 变量!存储变量 Called.discountRate 和 Called.price 都占据了 slot 0。
你将获得 200% 的折扣,这非常可观(并且会导致函数 revert,因为新计算出的价格将变为负数,这对于 uint 类型变量是不允许的)。
delegatecall 中的 Immutable 和 Constant 变量:一个 Bug 故事
delegatecall 的另一个棘手问题发生在涉及 immutable(不可变)或 constant(常量)变量时。让我们检查一个许多经验丰富的 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)); // 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函数,据推测该函数返回对应于 slot 0 中的状态变量的值。- 既然它是 delegatecall,我们应该检查调用合约中的插槽,而不是被调用合约。
Caller中变量a的值是 3,因此响应必须是 3。完全正确(Nailed it)。
令人惊讶的是,正确答案是 2。为什么?!
Immutable 或 constant 状态变量不是真正的状态变量:它们不占用插槽。当我们声明 immutable 变量时,它们的值被硬编码在合约字节码中,该字节码在 delegatecall 期间执行。因此,getValue 函数返回硬编码的值 2。
msg.sender,msg.value 和 address(this)
如果我们在 Called 合约中使用 msg.sender,msg.value 和 address(this),所有这些值都将对应于 Caller 合约的 msg.sender,msg.value 和 address(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.sender,msg.value 和 address(this),并在 getInfo 函数中返回了这些值。在下图中,描绘了使用 Remix 执行 getDelegateInfo 的过程,显示了返回的值。
msg.sender对应于执行该交易的账户,具体来说就是第一个 Remix 默认账户,即0x5B38Da6a701c568545dCfcB03FcB875f56beddC4。msg.value反映了在原始交易中发送的 1 ether 的值。address(this)是 Caller 合约的地址,正如在图片左侧可以看到的那样,而不是 Called 合约的地址。

在 Remix 中,我们显示了 msg.sender (0),msg.value (1) 和 address(this) (2) 的日志值。
delegatecall 中的 msg.data 和 input data
msg.data 属性返回正在执行的上下文的 calldata。当在一个直接由 EOA 通过交易执行的函数中调用 msg.data 时,msg.data 代表该交易的输入数据(input data)。
当我们执行 call 或 delegatecall 时,我们将即将在实现合约中执行的输入数据指定为参数。因此,原始的 calldata 与由 delegatecall 创建的子上下文(sub-context)中的 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 函数,该函数需要一个 address 类型的参数。结果,输入数据将由函数签名和经过 ABI 编码的地址组成。
delegateMsgData 函数反过来 delegatecall 了 returnMsgData 函数。为了实现这一点,传递给运行时的 calldata 必须包含 returnMsgData 的签名。因此,在 returnMsgData 内部 msg.data 的值是其自身的签名,即 0x0b1c837f。
在下图中,我们可以观察到 returnMsgData 的返回结果是其自身经过 ABI 编码的签名。

解码后的输出是 returnMsgData 函数的签名,经过 ABI 编码为 bytes。
作为一个反例的 Codesize
我们提到过,我们可以用这样的观念来理解 delegatecall:我们正在从实现合约中借用字节码,并在调用合约中执行它。这有一个例外,那就是 CODESIZE 操作码。
假设一个智能合约的字节码中包含 CODESIZE,CODESIZE 返回的是那个合约的大小。在 delegatecall 期间,Codesize 并不会返回调用者代码的大小——它返回的是被 delegatecall 的代码的大小。
为了证明这个属性,我们提供了以下代码。在 Solidity 中,CODESIZE 可以通过汇编(assembly)中的 codesize() 函数执行。我们有两个实现合约 CalledA 和 CalledB,它们之间的唯一区别是一个局部变量(在 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()
}
}
}
// You can use this contract to check the size of contracts
contract MeasureContractSize {
function measureConctract(address c) external view returns (uint256 size){
size = c.code.length;
}
}
如果 codesize 函数返回的是 Caller 合约的大小,那么通过 delegatecall 调用 ContractA 和 ContractB 时,getSizes() 返回的值将会是相同的。也就是说,它们都将是 Caller 的大小,即 1103。然而,正如我们在下图中看到的,这些值是不同的,这明确表明它们是 CalledA 和 CalledB 的大小。

Delegatecall 另一个 delegatecall
人们可能会好奇:如果一个合约向第二个合约发出 delegatecall,而第二个合约又向第三个合约发出 delegatecall,会发生什么?在这种情况下,上下文将始终保持为发起第一次 delegatecall 的合约的上下文,而不是中间合约。
它的工作方式如下:
Caller合约 delegatecall 了CalledFirst合约中的logSender()函数。- 该函数旨在触发一个记录
msg.sender的事件。 - 此外,
CalledFirst合约除了创建此日志之外,还 delegatecall 了CalledLast合约。 CalledLast合约也触发了一个事件,同样记录了msg.sender。
下面是一个描绘此流程的图解。

请记住,delegatecall 所做的只是借用被 delegatecall 合约的字节码。一种可视化的方式是,字节码被暂时“吸收”到了调用合约中。当用这种方式看待它时,我们会看到 msg.sender 始终是原始的 msg.sender,因为一切都发生在 Caller 内部。请参阅下面的动画:
下面我们提供了一些源代码来测试 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 的合约。
最终的结果是,这两个 msg.sender 值都对应于使用 Caller.delegateCallToFirst() 发起交易的账户。这可以在下图中观察到,我们在 Remix 中执行此过程并捕获了日志。

msg.sender 在 CalledFirst 和 CalledLast 中是相同的。
导致混淆的一个原因是,有些人可能会将此操作描述为“Caller delegatecall 了 CalledFirst,而 CalledFirst delegatecall 了 CalledLast。”但这听起来像是 CalledFirst 正在执行 delegatecall——事实并非如此。CalledFirst 正在向 Caller 提供字节码——而该字节码正在从 Caller 向 CalledLast 发起 delegatecall。
在 delegatecall 中发起 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 会是什么?以下动画说明了所发生的情况:

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

现在,CalledFirst 调用了 CalledLast,但 CalledFirst 是在 Caller 的上下文中使用的,所以这就像是 Caller 对 CalledLast 发起了一次 call 调用。在这种情况下,CalledLast 中的 msg.sender 将会是 Caller 的地址。
在下图中,我们在 Remix 中观察到了日志。请注意,这一次 msg.sender 的值是不同的。

CalledLast 中的 msg.sender 是 Caller 的地址
练习: 如果 Caller call 了 CalledFirst,而 CalledFirst delegatecall 了 CalledLast,并且每个合约都记录了 msg.sender,那么每个合约记录的 message sender 分别是什么?
低级(Low-level) delegatecall
在本节中,我们将利用 YUL 中的 delegatecall 来更深层次地探索其功能。YUL 中的函数与操作码语法非常相似,因此首先检查 DELEGATECALL 操作码的定义将大有裨益。
DELEGATECALL 从栈(stack)中按顺序接收 6 个参数:gas、address、argsOffset、argsSize、retOffset 和 retSize,并向栈返回一个值,指示该操作是否成功执行(1 表示成功,0 表示失败)。
各个参数的解释如下(取自 evm.codes):
- gas: 发送到子上下文执行的 gas 数量。子上下文未使用的 gas 会返回到当前上下文。
- address: 要执行其代码的账户。
- argsOffset: 内存中的字节偏移量,即子上下文的 calldata 的起始位置。
- argsSize: 要复制的字节大小(calldata 的大小)。
- retOffset: 内存中的字节偏移量,即存放子上下文返回数据的位置。
- retSize: 要复制的字节大小(返回数据的大小)。
不允许使用 delegatecall 向合约发送 ether(想象一下如果允许的话潜在的漏洞利用!)。另一方面,CALL 操作码允许传输 ether,并包含一个额外的参数来指示应该发送多少 ether。
在 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) // Load the calldata I intend to send into memory at 0x00. The first slot will become 0x0000000000000000000000000000000000000000000000000000000034ee2172
let result := delegatecall(gas(), _address, 0x1c, 4, 0, 0x20) // The third parameter indicates the starting position in memory where the calldata is located, the fourth parameter specifies its size in bytes, and the fifth parameter specifies where the returned calldata, if any, should be stored in memory
data := mload(0) // Read delegatecall return from memory
}
}
}
contract Called {
function sayOne() public pure returns (uint) {
return 1;
}
}
在 delegateInSolidity 函数中,我使用了 Solidity 中的 delegatecall 方法,将 sayOne 函数的签名作为参数传入,该签名是使用 abi.encodeWithSignature 方法计算得出的。
如果我们事先不知道返回值的大小,不用担心,我们可以稍后使用 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。
作者信息
本文由 João Paulo Morais 与 RareSkills 合作撰写。
最初发布于 5 月 3 日