# Solidity学习-可升级合约(Transparent/UUPS/Beacon)

By [rbtree](https://paragraph.com/@rbtree) · 2022-09-08

---

以太坊合约原生并不支持升级，目前的升级一般是采用代理模式实现的。

![代理模式](https://storage.googleapis.com/papyrus_images/bc6785d64ae7f8f83fcd440ad9f0f6086fb453fd5557a403853f9c4d99694eb7.png)

代理模式

在代理模式中，有2个合约：Proxy和Implementation，用户总是和Proxy进行交互。Proxy在收到用户的调用请求后，并不执行自身的代码，而是通过delegatecall去执行Implementation合约的代码。delegatecall的特殊之处就是，它并不切换上下文，因此Implementation的代码所处理的存储空间是Proxy合约的存储空间，而非自己的。

Proxy合约里存储了Implementation合约的地址，这个地址是可修改的，当我们需要进行合约升级的时候，只需要重新部署一个新的Implementation合约，同时把Proxy合约中存储的地址改成新合约的地址就行了。升级前后，合约的存储空间没有任何变化（依然是Proxy的存储空间），地址也没有变化（依然是Proxy的地址），因此升级过程对用户完全透明。

合约升级之后，要保证storage变量的兼容性。新版本可以在旧版本的变量之后增加新的变量，但是不可以删除或者修改旧版本的变量。因为自始自终，变量都只存储在Proxy合约里，升级之后，storage变量的值是不会改变的。

下面简单看一下Proxy是如何把用户请求转发到Implementation合约的(为了方便说明对代码做了一些简化)。

我们的Proxy合约是不必写出Implementation里的接口的（而且也没法写，因为后续升级可能会增加接口）。EVM有一个特性——如果调用的函数名在合约中不存在，那么会调用合约的fallback函数。因此当用户与合约交互时，会首先进入fallback中。我们看到fallback首先通过 \_implementation函数拿到了implementation合约的地址，然后通过\_delegate函数进行了delegatecall。\_delegate的代码是用汇编YUL实现的，本文不对此进行深究。

    contract Proxy {
        fallback () external payable virtual {
            _delegate(_implementation());
        }
    
        function _implementation() internal view virtual returns (address);
    
        function _delegate(address implementation) internal virtual {
            // solhint-disable-next-line no-inline-assembly
            assembly {
                // Copy msg.data. We take full control of memory in this inline assembly
                // block because it will not return to Solidity code. We overwrite the
                // Solidity scratch pad at memory position 0.
                calldatacopy(0, 0, calldatasize())
    
                // Call the implementation.
                // out and outsize are 0 because we don't know the size yet.
                let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
    
                // Copy the returned data.
                returndatacopy(0, 0, returndatasize())
    
                switch result
                // delegatecall returns 0 on error.
                case 0 { revert(0, returndatasize()) }
                default { return(0, returndatasize()) }
            }
        }
    }
    

上面只是简单说明了一下原理，如果具体考虑可升级合约的实现细节，还有几个问题需要考虑。

（1）implementation合约的地址存放在何处，后续合约不断升级可能会用到更多空间，是否可能和存放合约地址的空间相冲突？

（2）合约升级的逻辑代码放在何处？通过何种方式安全触发？

对这两个问题的不同回答产生了不同的可升级合约范式，本文将介绍3种：transparent，uups，beacon。

1 Transparent
-------------

![Transparent可升级合约](https://storage.googleapis.com/papyrus_images/013621a7bbb0c7f7e5cea11b9ba43711ca0c17160142914d70691153fe8a6420.png)

Transparent可升级合约

在Transparent模式中，除了Proxy和Implementation，还有第3个合约——ProxyAdmin，这个合约是专门用于合约升级的，它只能被管理员Admin调用。

如果Proxy合约发现自己被ProxyAdmin合约调用，那么它会调用自身的函数代码；如果调用者是ProxyAdmin之外的账户，那么它会通过delegatecall去调用Implementation的代码。这样就保障了合约升级的安全性。

在代码中，是通过下面的修饰符ifAdmin实现的，Proxy合约的所有外部函数都被加上了这样一个修饰符。如果调用者是ProxyAdmin会正常执行，如果调用者是其他人，那么会直接进入fallback。我们在上面已经解释过，fallback会通过delegatecall去调用Implementation的代码。

    modifier ifAdmin() {
        if (msg.sender == _getAdmin()) {
            _;
        } else {
            _fallback();
        }
    }
    

还有一个问题需要解决：Implementation的地址存放在何处？

这个存放的位置关键是不能和合约存放其他数据的位置冲突。定长数据类型在合约storage里是从slot0开始依次存放的，如果只有定长数据类型，那么我们选一个编号足够大的slot即可。不过变长数据类型（变长array和map）的存放位置是通过hash算出来的，所以理论上无论我们把Implementation的地址放在何处，都没有办法百分百避免碰撞的可能。不过在实践中，由于storage有2的256次方个slot，通过hash随机算出的地址冲突的概率小到可以忽略。

在openzeppelin给出的代码中，选用的存放位置是:

bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)

对于transparent合约而言，还需要存放ProxyAdmin的地址

bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)

下面我们看一个简单的例子，来体会一下transparent可升级合约的部署、调用、升级过程。

观察下面的代码，可以发现一个很明显的特点是：它并不在构造函数中实现变量初始化，而是在initialize中初始化。因为对于代理模式而言，只有Proxy合约的storage是有效的，Implementation的storage是不被使用过的。在我们部署Implementation的时候，如果写了构造函数，那么它修改的是Implementation合约的storage，并不能达到我们想要的目的。所以我们必须在部署合约之后，通过Proxy调用initialize来初始化。

下面的代码是V1和V2版本的Implementation，我们先部署V1，然后升级到V2。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    
    contract TransparentV1 is Initializable {
        event IncreaseV1();
    
        uint public val;
    
        /// @custom:oz-upgrades-unsafe-allow constructor
        constructor() {
            _disableInitializers();
        }
    
        function initialize(uint _val) public initializer {
            val = _val; // set initial value in initializer
        }
    
        function increase() public {
            val += 1;
            emit IncreaseV1();
        }
    }
    
    contract TransparentV2 is Initializable {
        event IncreaseV2();
    
        uint public val;
    
        function increase() public {
            val += 2;
            emit IncreaseV2();
        }
    }
    

（1）合约部署

首先部署implementation合约。

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

然后部署ProxyAdmin合约，部署账户自动成为ProxyAdmin的管理员。

![](https://storage.googleapis.com/papyrus_images/0645d00b2909c2a1965f6b65b84290e4b7273f416b66ccb1b18b273cde661620.png)

最后部署Proxy合约，部署时会把Proxy的Implementation地址和ProxyAdmin地址设置为上面两个合约的地址，同时调用Implementation的initialize函数进行初始化。

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

（2）合约调用

用户直接和Proxy合约交互，然后Proxy通过delegatecall调用Implementation合约。（其实上一步调用initialize函数也是通过这个逻辑实现的，不同的是initialize函数加了initializer修饰符，只能调用一次）。

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

（3）合约升级，V1→V2

首先需要部署V2的Implementation合约。

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

然后，管理员账户通过ProxyAdmin调用Proxy的upgradeTo函数进行升级。升级之后，Proxy中的存储的Implementation的V1版本地址会被改成V2版本的地址。

![](https://storage.googleapis.com/papyrus_images/3efe27ea77c358750ea57f9501ea10f454fe11ef8017b394135b58c1fb407e77.png)

2 UUPS
------

UUPS的名字来自于EIP-1822（Universal Upgradeable Proxy Standard）。上面介绍的Transparent模式把升级函数放在了Proxy合约中，而UUPS则是把升级函数放在Implementation合约中。因此该模式中，不需要再像Transparent模式那样区分管理员升级和普通函数调用，Proxy直接把所有的请求都通过delegatecall丢给Implementation（如果是升级，Implementation的升级函数会确认一下是否为管理员），因此不再需要ProxyAdmin。

![UUPS可升级合约](https://storage.googleapis.com/papyrus_images/486139d141b7c25dd481674af89826cd976d396b313d25b96af387b1859ae5f5.png)

UUPS可升级合约

普通函数的调用基本和Transparent一样，所以我们重点关注一下upgradeTo的实现。

因为需要实现upgradeTo，所以和Transparent相比，UUPS的Implementation合约会复杂一些，openzeppelin的UUPSUpgradeable给出了实现，我们写自己的Implementation时继承这个合约即可。在我们自己的合约中，我们只需要实现\_authorizeUpgrade函数以确保只有管理员账户可以进行升级。

其实这里并没有什么神秘的，具体的实现无非就是通过keccak256('eip1967.proxy.implementation')找到implementation地址的存储位置，修改为新地址而已，有兴趣的同学可以自行查看openzeppelin的完整代码研究。

    abstract contract UUPSUpgradeable is Initializable, IERC1822ProxiableUpgradeable, ERC1967UpgradeUpgradeable {
        function upgradeTo(address newImplementation) external virtual onlyProxy {
            _authorizeUpgrade(newImplementation);
            _upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
        }
    
        // implement it in your own contract
        function _authorizeUpgrade(address newImplementation) internal virtual;
    }
    

接下来依然是看一个简单的例子。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
    
    contract UUPSV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
        event IncreaseV1();
    
        uint public val;
    
        /// @custom:oz-upgrades-unsafe-allow constructor
        constructor() {
            _disableInitializers();
        }
    
        function initialize(uint _val) initializer public {
            val = _val;
            __Ownable_init();
            __UUPSUpgradeable_init();
        }
    
        function _authorizeUpgrade(address newImplementation)
            internal
            onlyOwner
            override
        {}
    
        function increase() public {
            val += 1;
            emit IncreaseV1();
        }
    }
    
    contract UUPSV2 is Initializable, OwnableUpgradeable , UUPSUpgradeable {
        event IncreaseV2();
    
        uint public val;
    
        function _authorizeUpgrade(address newImplementation)
            internal
            onlyOwner
            override
        {}
    
        function increase() public {
            val += 2;
            emit IncreaseV2();
        }
    }
    

（1）合约部署

首先部署Implementation合约。

![](https://storage.googleapis.com/papyrus_images/0a54f86fb3bbc5b6a20ce94662373ff0b6aced6268671434f85cbec64e60e02a.png)

然后部署Proxy合约，部署时会把Proxy的Implementation地址设置为上面合约的地址，同时调用Implementation的initialize函数进行初始化。

![](https://storage.googleapis.com/papyrus_images/5c564df71b87162328e87666a9d0a80cb0afedebd51f653b46e19465d79677ba.png)

（2）合约调用

用户直接和Proxy合约交互，然后Proxy通过delegatecall调用Implementation合约。

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

（3）合约升级，V1→V2

首先需要部署V2的Implementation合约。

![](https://storage.googleapis.com/papyrus_images/48e60baac8d4c25ed9cc6ceb7d5d64b42f5d22367f8b8f50359971b1433e8653.png)

然后，管理员账户通过Proxy调用implementV1的upgradeTo函数进行升级。升级之后，Proxy中的存储的Implementation的V1版本地址会被改成V2版本的地址。（注意，虽然最终调用的是implementation的upgradeTo函数，但因为是delegatecall，所以修改的依然是Proxy的storage空间的内容。）

这一步正是UUPS和Transparent的核心区别所在。

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

**Transparent** VS **UUPS**

Transparent把升级逻辑放在Proxy中，而UUPS把升级逻辑放在implementation中，这样的差异会造成哪些使用体验上的不同呢？

（1）Transparent需要多部署一个ProxyAdmin合约，所以如果不考虑后续升级，那么Transparent是更消耗gas的。但是如果考虑后续升级，情况就不是这样了。因为对于UUPS而言，升级的逻辑代码需要出现在后续的每一个版本的Implementation合约中（否则合约将失去可升级特性），所以如果考虑到多次升级，反而是Transparent更加省gas。

（2）对于升级之外的合约交互，Transparent总是需要判断是否来自ProxyAdmin账户（否则它没法知道是升级还是普通交互），因此这里会多一次storage load操作。而UUPS是把包括升级在内的所有交互都无脑丢给implementation，所以不需要这个判断。因此，对于普通用户而言，UUPS总是会更省gas一些。

（3）刚才有说到，升级的逻辑代码需要出现在后续的每一个版本的Implementation合约中，但实际上也未必。有可能在某个版本，我们决定这就是最终版本，以后再也不会再升级了，那么我们就不给这个版本添加升级函数，这样一来可升级合约就变成了不可升级合约。而Transparent的可升级特性是不可取消的（当然可以有其他间接方式做到，比如放弃ProxyAdmin合约的管理权限），所以UUPS相对Transparent具有更大的灵活性。

3 Beacon
--------

和前面2种模式不同，Beacon模式的Implementation地址并不存放在Proxy合约里，而是存放在Beacon合约里，Proxy合约里存放的是Beacon合约的地址。

在合约交互的时候，用户同样是和Proxy合约打交道，不过此时因为Proxy合约中并未保存Implementation地址，所以它要先访问Beacon合约获取Implementation地址，然后再通过delegatecall调用Implementation。

在合约升级的时候，管理员并不需要和Proxy合约打交道，而只需要交互Beacon合约，把Beacon合约存储的Implementation改掉就行了。

![Beacon可升级合约](https://storage.googleapis.com/papyrus_images/4b881241ecf87129deb487431cb30c64ea7a638c9ac9d6904c53228cd5a5dbb2.png)

Beacon可升级合约

BeaconProxy的主要函数如下，注意\_implementation()的实现，需要到Beacon合约中获取地址。

    contract BeaconProxy is Proxy, ERC1967Upgrade {
        constructor(address beacon, bytes memory data) payable {
            assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1));
            _upgradeBeaconToAndCall(beacon, data, false);
        }
    
        function _beacon() internal view virtual returns (address) {
            return _getBeacon();
        }
    
        function _implementation() internal view virtual override returns (address) {
            return IBeacon(_getBeacon()).implementation();
        }
    }
    

Implementation的地址存放在Beacon合约中，升级时调用upgradeTo改变Implementation地址。

    contract UpgradeableBeacon is IBeacon, Ownable {
        address private _implementation;
    
        function implementation() public view virtual override returns (address) {
            return _implementation;
        }
    
        function upgradeTo(address newImplementation) public virtual onlyOwner {
            _setImplementation(newImplementation);
            emit Upgraded(newImplementation);
        }
    
        function _setImplementation(address newImplementation) private {
            require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
            _implementation = newImplementation;
        }
    }
    

再看一个简单的例子。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
    import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
    
    contract ShipV1 is Initializable {
        event Move();
    
        string public name;
        uint public fuel;
    
        /// @custom:oz-upgrades-unsafe-allow constructor
        constructor() {
            _disableInitializers();
        }
    
        function initialize(string calldata _name, uint _fuel) initializer public {
            name = _name;
            fuel = _fuel;
        }
    
        function move() public {
            require(fuel > 0, "no fuel");
            fuel -= 1;
            emit Move();
        }
    }
    
    contract ShipV2 is Initializable {
        event Move();
        event Refuel();
    
        string public name;
        uint256 public fuel;
    
        function move() public {
            require(fuel > 0, "no fuel");
            fuel -= 1;
            emit Move();
        }
    
        function refuel() public {
            fuel += 1;
            emit Refuel();
        }
    }
    

（1）合约部署

首先部署Beacon合约，部署账户成为Beacon合约的管理员。

![](https://storage.googleapis.com/papyrus_images/4c487e918a3d517ec71205dd0b2da98c260772779dd5656cb79d219a48125121.png)

然后部署Proxy合约，部署时会把Proxy的Beacon地址设置为上面合约的地址。

同时在这个交易中，也会完成Implementation的部署和初始化，同时把Implementation的地址存入Beacon合约。

（在这里并没有先单独创建Implementation合约，而是在创建Proxy的时候直接把Implementation合约的creationCode带了进去。不过在我看来，这并非Beacon模式和另外两种模式的本质差异，完全可以不这样做。）

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

（2）合约调用

用户直接和Proxy合约交互，Proxy首先调用Beacon合约从中获得Implementation合约地址，然后通过delegatecall调用Implementation合约。

![](https://storage.googleapis.com/papyrus_images/2361640612e91ff3f9684a7d14cd206332564f799e3ff2562b3e463f85d38bcf.png)

（3）合约升级，V1→V2

首先需要部署V2的Implementation合约。

![](https://storage.googleapis.com/papyrus_images/9b83f77500befc07e0011d69f3b5f698a690337a52c3a3fa9043242a88877edd.png)

然后管理员账户和Beacon合约交互，把Beacon合约所记录的Implementation地址改成新地址。

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

升级过程完全不涉及Proxy合约。

和另外两种模式相比，Beacon模式最大的不同就是它的Implementation地址并不直接存放在Proxy中，着看起来似乎有点故意给自己找麻烦。不过这种模式在一种场景下有优势，就是多个Proxy共享相同的Implementation、需要批量升级的场景。此时，如果想把所有Proxy都升级，那么升级Beacon就天然可以达到升级所有Proxy的效果。如果采用transparent或者UUPS模式，每个Proxy都要进行一次升级。

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/solidity-transparent-uups-beacon)*
