从源码分析openzeppelin的三种可升级模式和两种代理模式

openzeppelin 源码里有三种可升级模式和两种代理模式。从源码分析实现原理,对比几种模式的优缺点

https://github.com/OpenZeppelin/openzeppelin-labs

源码里有三种可升级模式:

image.png

主要是为了解决:<mark>如何确保逻辑合约不会覆盖代理中用于升级的状态变量</mark>。

下文图中的内容,绿色文字表示function 是public的。方框表示每个合约声明的存储

upgradeability_using_eternal_storage

image.png 用户和EternalStorageProxy交互。

<mark>这种情况,是通过逻辑合约全部使用map映射来和升级变量进行隔离</mark>

部署过程

  1. 部署EternalStorageProxy合约,地址为E1
  2. 部署Token_V0合约,地址为T1
  3. 调用EternalStorageProxy合约的upgradeTo()或者upgradeToAndCall()方法(call some function to redo the setup),传入T1。

完成部署后:

  • 合约里的_upgradeabilityOwner是合约部署人 —— 只能是部署者(msg.sender,图中的Actor)才能升级,因为OwnedUpgradeabilityProxy构造函数里会设置。
  • 合约里的_implementation 指向了Token_V0合约地址。
  • 后续的函数调用,会通过proxy的fallback函数调到Token_V0里。

在这里,Token_V0是逻辑合约,数据存在代理合约EternalStorageProxy

升级过程

  1. 部署Token_V1合约,地址为T2
  2. 调用EternalStorageProxy合约的upgradeTo()或者upgradeToAndCall()方法

在这里,实际修改的就是代理合约EternalStorageProxy_implementation成员指向了Token_V1地址。后续调用就会转到Token_V1合约。

小结

可以看到,这种代理模式,要求合约里的所有数据存储,都放到map里面来。编码不够灵活。

upgradeability_using_inherited_storage

image.png

<mark>这种情况,是通过继承,让逻辑合约不会占用升级变量</mark>

部署过程

  1. 部署Registry合约,地址R1
  2. 部署Token_V0合约,地址为T1
  3. 调用Registry合约的addVersion将T1进行注册
  4. 调用Registry合约的createProxy(version)方法,获取一个UpgradeabilityProxy合约地址,地址U1。此时U1里的registry变量就是R1(构造函数里赋值为msg.sender
  5. 调用U1的upgradeTo方法,设置_implementation变量

部署完以后,代理合约UpgradeabilityProxy_implementation就指向了T1,后续的调用都会转到T1上来。

升级过程

  1. 部署Token_V1合约,地址为T2
  2. 调用Registry合约的addVersion将T2进行注册
  3. 调用UpgradeabilityProxy合约的upgradeTo方法,进行升级,修改_implementation变量。

upgradeability_using_unstructured_storage

image.png

<mark>这种情况,是通过固定的槽位分散存储,和逻辑合约进行隔离,赌的是逻辑合约本身的存储不会和升级数据的槽位冲突</mark>

部署过程

  1. 部署OwnedUpgradeabilityProxy合约,地址为O1
  2. 部署Token_V0合约,地址为T1
  3. 调用OwnedUpgradeabilityProxy合约的upgradeTo或者upgradeToAndCall方法进行升级,向implementationPosition槽位写入值

    升级过程

  4. 部署Token_V1合约,地址为T2
  5. 调用OwnedUpgradeabilityProxy合约的upgradeTo或者upgradeToAndCall方法进行升级

关于TransparentUpgradeableProxy和UUPSUpgradeable

https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy

<mark>这两者解决的不是存储冲突的问题,而是谁(代理合约还是逻辑合约)来升级(调用upgradeTo(address))的问题。他们底层都用的unstructured_storage</mark>

透明代理

合约的升级,由代理合约来进行,也就是逻辑合约根本不关心升级的事情。所以,需要代理合约来完成。

透明代理工作原理

透明代理,需要部署三个合约:逻辑合约(业务代码)、代理合约(TransparentUpgradeableProxy)、管理合约(ProxyAdmin)

用户和代理合约进行交互。代理合约里有_IMPLEMENTATION_SLOT槽位数据。

升级过程就是用逻辑合约和代理合约的地址,调用管理合约的upgrade或者upgradeAndCall方法。然后在方法里,会调用代理合约的updateTo或者upgradeToAndCall方法,修改代理合约里的_IMPLEMENTATION_SLOT槽位对应的变量

透明代理的问题

因为由透明代理来完成升级工作,那么透明代理合约里,必然有处理升级的函数,比如upgradeTo,所以,就存在代理合约和逻辑合约两者有函数名冲突的情况。比如都有upgradeTo函数,那么针对普通用户,应该需要调用到逻辑合约,对于管理员(负责升级)应该要调用代理合约。所以,在透明代理模式上,一定要有用户权限的判断。也就是TransparentUpgradeableProxy的如下代码:

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

至于是不是每一次用户函数调用是否都需要如上的判断,是有的。原因是不能让管理员能调到逻辑合约的函数。代码如下:

function _beforeFallback() internal virtual override {
        require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); // 这里
        super._beforeFallback();                                                                                                    
    }
    function _fallback() internal virtual {
        _beforeFallback();
        _delegate(_implementation());
    }
    fallback() external payable virtual {
        _fallback();
    }

所以,透明代理会比普通的合约要更费gas(多一次存储数据读取)。

UUPS代理

合约的升级,由逻辑合约来进行,不需要代理合约参与。

UUPS代理工作原理

UUPS代理,只需要部署两个合约:逻辑合约(业务代码)、代理合约。没有管理合约(因为直接在逻辑合约里升级即可)。逻辑合约从UUPSUpgradeable继承。

由于所有的用户调用都是通过代理合约直接转到逻辑合约,所以代理合约本身不需要什么数据。代理合约的生成,可以使用plugin-hardhat生成。

实际上plugin-hardhat生成的代理合约就是ERC1967Proxy,参考代码: https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/3069545ba53b8130596ee290a3e8ca71b921b44a/packages/plugin-hardhat/src/utils/factories.ts#L9roxy

export async function getProxyFactory(hre: HardhatRuntimeEnvironment, signer?: Signer): Promise&lt;ContractFactory> {
 return hre.ethers.getContractFactory(ERC1967Proxy.abi, ERC1967Proxy.bytecode, >signer);
}

可以看到确实没有对外的函数

用户和代理合约进行交互。代理合约里有_IMPLEMENTATION_SLOT槽位数据。

升级过程就是直接通过代理合约,调用逻辑合约里的upgradeTo方法或者upgradeToAndCall方法。

UUPS代理的问题

由于升级过程是调用逻辑合约的升级方法,如果逻辑合约没有该升级方法,那么就可能导致后续无法升级。为了解决这个问题,要求逻辑合约必须继承UUPSUpgradeable合约。该合约要求子类必须实现_authorizeUpgrade方法

    /**
     * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
     * {upgradeTo} and {upgradeToAndCall}.
     *
     * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
     *
     * ```solidity
     * function _authorizeUpgrade(address) internal override onlyOwner {}
     * ```
     */
    function _authorizeUpgrade(address newImplementation) internal virtual;

有问题欢迎留言,相互探讨

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-10-18 19:19
  • 阅读 ( 323 )
  • 学分 ( 14 )
  • 分类:智能合约
© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发
区块链技术的头像-区块链开发网

昵称

取消
昵称表情代码图片