# 深入理解合约升级(3) - call 与 delegatecall

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-05-18

---

### call 与 delegatecall 的区别

`call` 和 `delegatecall` 是 Solidity 中调用外部合约的方法，但是它俩却有挺大的区别。假设 A 合约调用 B 合约，当在 A 合约中使用 `call` 调用 B 合约时，使用的是 B 合约的上下文，修改的是 B 合约的内存插槽值。而在如果在 A 合约中使用 `delegatecall` 调用 B 合约，那么在 B 合约的函数执行过程中，使用的是 A 合约的上下文，同时修改的也是 A 合约的内存插槽值。这么说有些抽象，我们来看一个简单的示意图：

![通过 call 调用](https://storage.googleapis.com/papyrus_images/ee9e40fe735e66c390fd21f913fbde6778b3eb4b41ad02d23bf418cc1078c348.png)

通过 call 调用

![通过 delegatecall 调用](https://storage.googleapis.com/papyrus_images/29f6745969e2d350fd85fecc28eb820684e645e21ae0aa6b22356de572fb2bba.png)

通过 delegatecall 调用

从上面的图中我们可以看出，在使用 `call` 调用时，B 合约使用的上下文数据均是 B 本身的。而当使用 `delegatecall` 调用时，B 合约使用了 A 合约中的上下文数据。我们来写段代码测试一下：

    pragma solidity 0.8.13;
    
    
    contract A {
        address public b;
        constructor(address _b) {
            b = _b;
        }
    
        function foo() external {
            (bool success, bytes memory data) = 
                b.call(abi.encodeWithSignature("foo()"));
            require(success, "Tx failed");
        }
    }
    
    contract B {
        event Log(address sender, address me);
        function foo() external {
            emit Log(msg.sender, address(this));
        }
    }
    

上面代码中，我们在 A 合约中使用 `call` 调用 B 合约，通过 `Log` 事件记录一些信息。先部署 B 合约，然后将其地址作为参数部署 A 合约，接着我们调用 `foo` 函数，可以获取到 `Log` 事件的内容为：

![通过 call 调用](https://storage.googleapis.com/papyrus_images/d0c141bc9a7a3e2861adef21f43d9f1533d2916d2fc44565061bd069ff598fa6.png)

通过 call 调用

与我们前面的说的规则一致，使用 `call` 调用时，使用的是 B 本身的上下文。接下来我们将 `call` 改成 `delegatecall`：

    function foo() external {
        (bool success, bytes memory data) = 
            b.delegatecall(abi.encodeWithSignature("foo()"));
        require(success, "Tx failed");
    }
    

再来看看执行结果：

![通过 delegatecall 调用](https://storage.googleapis.com/papyrus_images/a172e4eeb2a77b6be1a00ad40f272345f58940cf8e8a7671c25ca20971d61cce.png)

通过 delegatecall 调用

可以看到当使用了 `delegatecall` 调用时，使用了 A 合约的上下文。

上面我们还提到，当使用 `delegatecall` 时，修改的是调用合约的内存插槽值，这是什么意思呢，我们来看一个例子：

    pragma solidity 0.8.13;
    
    
    contract A {
        uint256 public alice;
        uint256 public bob;
    
        address public b;
        constructor(address _b) {
            b = _b;
        }
    
        function foo(uint256 _alice, uint256 _bob) external {
            (bool success, bytes memory data) = 
                b.delegatecall(abi.encodeWithSignature("foo(uint256,uint256)", 
                _alice, _bob));
            require(success, "Tx failed");
        }
    }
    
    contract B {
        uint256 public alice;
        uint256 public bob;
        function foo(uint256 _alice, uint256 _bob) external {
            alice = _alice;
            bob = _bob;
        }
    }
    

这段代码中，我们使用 `delegatecall` 来调用 `foo` 函数，`foo` 函数的作用是给 B 合约的两个变量赋值。但是实际调用后的结果是，A 合约的两个变量被赋值，而 B 中的变量仍为空。这就是我们前面说的，`delegatecall` 会修改调用合约的内存插槽值，我们来看一个图示：

![内存插槽](https://storage.googleapis.com/papyrus_images/5353ed8f2691916ed2ce0139f228cf6263e5aba02ea1fae8646249f6e6f09e51.png)

内存插槽

在 A 合约中有三个状态变量，B 合约中有两个状态变量。当 A 合约使用 `delegatecall` 调用 B 合约时，对 B 合约状态变量的赋值会通过插槽顺序分别影响 A 合约的各个变量。也就是说，对 B 合约插槽 0 的变量 `alice` 赋值，实际上是把值赋给了 A 合约插槽 0 的变量 `alice`。同理，对 B 合约的第 n 个插槽赋值，实际上会对 A 合约的第 n 个插槽赋值。注意，这里仅仅和插槽顺序有关，而和变量名无关。如果我们将 B 合约改为：

    contract B {
        // 调换了变量声明顺序
        uint256 public bob;
        uint256 public alice;
        function foo(uint256 _alice, uint256 _bob) external {
            // 调换了赋值内容
            bob = _alice;
            alice = _bob;
        }
    }
    

这段代码中，虽然变量声明以及赋值的顺序调换，但是 `foo` 的内容仍然是将 `_alice` 赋值给插槽 0 的变量，将 `_bob` 赋值给插槽 1 的变量，因此 A 合约的结果不变。

### delegatecall 在合约升级方面的应用

学习理解 `delegatecall` 是我们后面学习合约升级的基础，合约升级的原理就是代理合约通过 `delegatecall` 调用逻辑合约，此时逻辑合约的上下文以及数据都是来自于代理合约，那么即使升级，更换了逻辑合约，所有的数据仍然存在于代理合约中，没有影响。可升级合约还有一个限制是，在升级合约时，不能更改已有的状态变量的顺序，如果需要新添变量，只能放在当前所有变量之后，不能在其中插入，原因就是这会改变插槽对应关系，使变量内容混乱。例如，若升级前的插槽为：

![](https://storage.googleapis.com/papyrus_images/18c28501674ea0cc61c969d549f95dea9fde77b868a8691c119960ef162ecaa0.png)

此时，变量 a 和 b 的值分别存储在代理合约的插槽 0，1 中。若添加变量 c，将其放在 a 和 b 中间，那么后续对于 c 的修改实际修改的是 b 的插槽，而对于 b 的修改则是在一个新的插槽上操作，造成数据混乱。

### 总结

`delegatecall` 会在被调用合约中使用调用合约的上下文，同时影响的是调用合约的内存插槽，这有时会对合约开发带来一些困扰。在使用时，一定要多考虑各方面的影响。同时，`delegatecall` 也是代理合约升级模式的基石，要理解合约升级，必须要明白这种调用方式的方方面面。

### 合约升级系列文章

1.  [深入理解合约升级(1) - 概括](https://mirror.xyz/xyyme.eth/RZscMYGkeGTY8z6ccHseY8HKu-ER3pX0mFYoWXRqXQ0)
    
2.  [深入理解合约升级(2) - Solidity 内存布局](https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec)
    
3.  [深入理解合约升级(3) - call 与 delegatecall](https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk)
    
4.  [深入理解合约升级(4) - 合约升级原理的代码实现](https://mirror.xyz/xyyme.eth/VSyU0JfmVrcqN-F28tX5mzYjxFFAosl8tDAQX3vB5Dg)
    
5.  [深入理解合约升级(5) - 部署一个可升级合约](https://mirror.xyz/xyyme.eth/kM9ld2u0D1BpHAfXTiaSPGPtDnOd6vrxJ5_tW4wZVBk)
    

### 关于我

欢迎[和我交流](https://linktr.ee/xyymeeth)

### 参考

[

DelegateCall() - Blockchain Academy
-----------------------------------

This topic focuses on the functionality of delegate calls in Solidity.

https://blockchain-academy.hs-mittweida.de

![](https://storage.googleapis.com/papyrus_images/577cddb808bb986f7f7533ef8a68ee416aa9986b9a646ae020027020e079a23a.webp)

](https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-5-calling-other-contracts-visibility-state-access/topic/delegatecall/)

[

Solidity中的delegatecall杂谈-安全KER - 安全资讯平台
---------------------------------------

安全KER - 安全资讯平台

https://www.anquanke.com

![](https://storage.googleapis.com/papyrus_images/812af69f9a537c10cf532c46338d77049202b2d5aaf550500d7653a1330e0451.png)

](https://www.anquanke.com/post/id/152590)

[

Delegatecall and Upgradeable Contract
-------------------------------------

UUPS ( EIP-1822) 和 Transparent Proxy Pattern 基本相似，区别只在于： 对于 UUPS 来说，升级相关代码位于 Implementation 合约（或称 Logic 合约）中，而对于 Transparent Proxy Pattern 来说，升级相关代码位于 Proxy 合约中。 下面是 UUPS 的例子（摘自 https://eips.ethereum.org/EIPS/eip-1822#erc-20-token ）。其中 Proxy 合约为： pragma solidity ^0.5.1; contract Proxy { // Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" constructor(bytes memory constructData, address contractLogic) public {

http://aandds.com



](http://aandds.com/blog/eth-delegatecall.html)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/3-call-delegatecall)*
