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

主要是为了解决:<mark>**如何确保逻辑合约不会覆盖代理中用于升级的状态变量**</mark>。
> 下文图中的内容,绿色文字表示function 是public的。方框表示每个合约声明的存储
# upgradeability_using_eternal_storage

用户和`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

<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

<mark>**这种情况,是通过固定的槽位分散存储,和逻辑合约进行隔离,赌的是逻辑合约本身的存储不会和升级数据的槽位冲突**</mark>
## 部署过程
1. 部署`OwnedUpgradeabilityProxy`合约,地址为O1
2. 部署`Token_V0`合约,地址为T1
3. 调用`OwnedUpgradeabilityProxy`合约的`upgradeTo`或者`upgradeToAndCall`方法进行升级,向`implementationPosition`槽位写入值
## 升级过程
1. 部署`Token_V1`合约,地址为T2
2. 调用`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<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;
“`
——
有问题欢迎留言,相互探讨
https://github.com/OpenZeppelin/openzeppelin-labs
源码里有三种可升级模式:
主要是为了解决:<mark>如何确保逻辑合约不会覆盖代理中用于升级的状态变量</mark>。
下文图中的内容,绿色文字表示function 是public的。方框表示每个合约声明的存储
upgradeability_using_eternal_storage
用户和
EternalStorageProxy
交互。
<mark>这种情况,是通过逻辑合约全部使用map映射来和升级变量进行隔离</mark>
部署过程
- 部署
EternalStorageProxy
合约,地址为E1 - 部署
Token_V0
合约,地址为T1 - 调用
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
里
升级过程
- 部署
Token_V1
合约,地址为T2 - 调用
EternalStorageProxy
合约的upgradeTo()
或者upgradeToAndCall()
方法
在这里,实际修改的就是代理合约EternalStorageProxy
的_implementation
成员指向了Token_V1
地址。后续调用就会转到Token_V1
合约。
小结
可以看到,这种代理模式,要求合约里的所有数据存储,都放到map里面来。编码不够灵活。
upgradeability_using_inherited_storage
<mark>这种情况,是通过继承,让逻辑合约不会占用升级变量</mark>
部署过程
- 部署
Registry
合约,地址R1 - 部署
Token_V0
合约,地址为T1 - 调用
Registry
合约的addVersion
将T1进行注册 - 调用
Registry
合约的createProxy(version)
方法,获取一个UpgradeabilityProxy
合约地址,地址U1。此时U1里的registry
变量就是R1(构造函数里赋值为msg.sender
) - 调用U1的
upgradeTo
方法,设置_implementation
变量
部署完以后,代理合约UpgradeabilityProxy
的_implementation
就指向了T1,后续的调用都会转到T1上来。
升级过程
- 部署
Token_V1
合约,地址为T2 - 调用
Registry
合约的addVersion
将T2进行注册 - 调用
UpgradeabilityProxy
合约的upgradeTo
方法,进行升级,修改_implementation
变量。
upgradeability_using_unstructured_storage
<mark>这种情况,是通过固定的槽位分散存储,和逻辑合约进行隔离,赌的是逻辑合约本身的存储不会和升级数据的槽位冲突</mark>
部署过程
- 部署
OwnedUpgradeabilityProxy
合约,地址为O1 - 部署
Token_V0
合约,地址为T1 - 调用
OwnedUpgradeabilityProxy
合约的upgradeTo
或者upgradeToAndCall
方法进行升级,向implementationPosition
槽位写入值
升级过程
- 部署
Token_V1
合约,地址为T2 - 调用
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#L9roxyexport async function getProxyFactory(hre: HardhatRuntimeEnvironment, signer?: Signer): Promise<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 )
- 分类:智能合约