从源码分析openzeppelin的三种可升级模式和两种代理模式
本文作者: gasshadow
https://github.com/OpenZeppelin/openzeppelin-labs
源码里有三种可升级模式:
主要是为了解决:如何确保逻辑合约不会覆盖代理中用于升级的状态变量。
下文图中的内容,绿色文字表示 function 是 public 的。方框表示每个合约声明的存储
upgradeability_using_eternal_storage
用户和EternalStorageProxy
交互。
这种情况,是通过逻辑合约全部使用 map 映射来和升级变量进行隔离
部署过程
- 部署
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
这种情况,是通过继承,让逻辑合约不会占用升级变量
部署过程
- 部署
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
这种情况,是通过固定的槽位分散存储,和逻辑合约进行隔离,赌的是逻辑合约本身的存储不会和升级数据的槽位冲突
部署过程
- 部署
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
这两者解决的不是存储冲突的问题,而是谁(代理合约还是逻辑合约)来升级(调用upgradeTo(address)
)的问题。他们底层都用的unstructured_storage
透明代理
合约的升级,由代理合约来进行,也就是逻辑合约根本不关心升级的事情。所以,需要代理合约来完成。
透明代理工作原理
透明代理,需要部署三个合约:逻辑合约(业务代码)、代理合约(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;
有问题欢迎留言,相互探讨
本文首发于登链社区: gasshadow 从源码分析 openzeppelin 的三种可升级模式和两种代理模式
登链社区-区块链技术爱好者的家园 twitter Discord