本文作者:gasshadow[1]
可以对照https://learnblockchain.cn/article/4899 一起看,算是那一篇的细化版。
Openzeppelin[2]:
https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy
有三种代理模式:
图如下
用户和代理合约交互,代理合约不直接实现upgradeTo
和upgradeToAndCall
,由逻辑合约实现。
// 这里需要用逻辑合约地址来构建代理合约。所以需要先部署逻辑合约。
constructor(address _logic, bytes memory _data) payable {
_upgradeToAndCall(_logic, _data, false);// 直接设置代理合约的implementation槽位
}
function _implementation() internal view virtual override returns (address impl) {
return ERC1967Upgrade._getImplementation();// 返回implementation槽位的值
}
modifier onlyProxy() {
// 要求不能直接调用,通过delegatecall 那么address(this)就是代理合约
require(address(this) != __self, "Function must be called through delegatecall");
// 要求代理合约实现的_getImplementation() 指向我这个逻辑合约
require(_getImplementation() == __self, "Function must be called through active proxy");
_;
}
// 这个是要求直接调用,不通过delegatecall 调用
modifier notDelegated() {
require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall");
_;
}
// 注意这里的notDelegated,这个函数是在升级时父类ERC1967Upgrade里调用的方法。
// 父类要求子类实现这个方法并返回_IMPLEMENTATION_SLOT用以证明是UUPS
function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
return _IMPLEMENTATION_SLOT;
}
合约升级上,openzeppelin 的 mocks 里,有一个实现,可以校验升级的合约是否可回滚。代码简单,就是先升级到新合约,然后设置回滚标记,然后回滚(升级)到旧合约,对比旧合约地址,然后再升级到新合约。
// 可以参考:mocks/UUPS/UUPSLegacy.sol
function _upgradeToAndCallSecureLegacyV1(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
address oldImplementation = _getImplementation();
// Initial upgrade and setup call
__setImplementation(newImplementation);
if (data.length > 0 || forceCall) {
Address.functionDelegateCall(newImplementation, data);
}
// Perform rollback test if not already in progress
StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
if (!rollbackTesting.value) {
// Trigger rollback using upgradeTo from the new implementation
rollbackTesting.value = true;
Address.functionDelegateCall(
newImplementation,
abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
);
rollbackTesting.value = false;
// Check rollback was effective
require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
// Finally reset to the new implementation and log the upgrade
_upgradeTo(newImplementation);
}
}
透明代理,有三方参与:代理合约、逻辑合约和管理合约。图如下:
// 代理合约的构造,需要逻辑合约和管理合约地址,所以需要先部署逻辑合约和管理合约。
constructor(
address _logic,
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
_changeAdmin(admin_);
}
// 只能让管理合约调用,如果不是管理合约,就直接转到逻辑合约执行(_fallback)。
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
// 每次fallback调用,都要检查是不是admin
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
就算逻辑合约里有upgradeTo
等方法,也不会影响调用。因为普通用户就直接在代理合约判断ifAdmin
的时候就转到了逻辑合约里,而管理合约的调用,就会放行到代理合约直接执行。
仅仅是回调参数传过来的 proxy 的同名函数。整个管理合约是否可以调用 proxy 的函数,是在 proxy 代理合约里判断的。所以管理合约很轻量。
用代理合约地址,直接调用管理合约的upgrade
或者upgradeAndCall
方法即可。
以上两种代理,都存在一种缺陷,就是如果我要升级一批具有相同逻辑合约的代理合约,那么需要在每个代理合约都执行一遍升级(因为每个代理合约独立存储了_implementation)。信标合约,就是将所有的具有相同逻辑合约的代理合约的_implementation 只存一份在信标合约中,所有的代理合约通过和信标合约接口调用,获取_implementation,这样,在升级的时候,就可以只升级信标合约,就能搞定所有的代理合约的升级。如图:
// 构造函数需要信标合约的地址,所以信标合约要先部署。将信标合约的地址传给代理合约进行构造。
constructor(address beacon, bytes memory data) payable {
_upgradeBeaconToAndCall(beacon, data, false);
}
// 从信标合约获取实现。
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
// 构造函数需要逻辑合约的实现,所以先要部署逻辑合约,再部署信标合约。
constructor(address implementation_) {
_setImplementation(implementation_);
}
// 升级直接升级信标合约的implementation即可。
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
直接调用信标合约的upgradeTo
方法即可(当然只能是onlyOwner
)
[1]
gasshadow: https://learnblockchain.cn/people/11678
[2]
Openzeppelin: https://learnblockchain.cn/article/727