本文介绍了以太坊中三种常见的可升级合约模式:Transparent Proxy、UUPS以及Beacon Proxy。Transparent Proxy通过代理合约中的管理员来升级合约;UUPS将升级逻辑放在实现合约中;Beacon Proxy则使用一个Beacon合约来管理实现合约的地址,从而实现多个代理合约的同步升级。
本文档是一篇学习笔记。如果它也能帮助到一些读者,我将很高兴。背景是在进行智能合约审计时,我有时需要回忆不同类型的可升级合约。因此,我在此记录下来以供将来参考。
首先,什么是代理(proxy):
代理合约是一种只保留存储并将逻辑委托给另一个称为实现的合约的合约类型。
使用代理合约的目的有:
代理合约的基础是 ERC1967Proxy
,它将实现地址存储在一个特殊的存储位置(以避免插槽冲突)。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/ERC1967/ERC1967Proxy.sol)
pragma solidity ^0.8.22;
import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "./ERC1967Utils.sol";
/**
* @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an
* implementation address that can be changed. This address is stored in storage in the location specified by
* https://learnblockchain.cn/docs/eips/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the
* implementation behind the proxy.
*/
contract ERC1967Proxy is Proxy {
constructor(address implementation, bytes memory _data) payable {
ERC1967Utils.upgradeToAndCall(implementation, _data);
}
function _implementation() internal view virtual override returns (address) {
return ERC1967Utils.getImplementation();
}
}
就我所见,可升级合约有三种不同的结构:
TransparentProxy
在 openzepplein-contract 中,TransparentProxy
继承了 ERC1967Proxy
。
function fallback() external {
_fallback();
}
// openzepplein's implementation
// openzepplein 的实现
function _fallback() internal override {
if (isUpgrader(msg.sender)) {
require(msg.sig == bytes4(keccak256("upgradeToAndCall(address, bytes)")));
_dispatchUpgradeToAndCall();
} else {
super._fallback();
}
}
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
背后的逻辑: 代理的 admin
可以设置一个 upgrader
。upgrader
是唯一允许执行代理升级的 upgradeToAndCall
命令的人(并且他们只能调用 upgradeToandCall
,不能调用其他函数),而其他人可以执行实现逻辑。
注意: 无论我们调用什么函数,它都会执行额外的检查,以查看调用者是否是 upgrader
。这不是很节省 Gas。
UUPS 可升级
根据我的理解,UUPS 可升级指的是实现合约。在这种情况下,代理就是简单的 ERC1967Proxy
。但是,这一次,与 TransParentProxy
相比,升级逻辑位于实现中,而不是代理中。核心是以下函数:
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data);
}
function _upgradeToAndCallUUPS(address newImplementation, bytes memory data) private {
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) {
revert UUPSUnsupportedProxiableUUID(slot);
}
ERC1967Utils.upgradeToAndCall(newImplementation, data);
} catch {
// The implementation is not UUPS
// 该实现不是 UUPS
revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation);
}
}
代理将调用实现的 upgradeToAndCall
函数来升级合约。请注意,我们升级到的实现需要实现 IERC1822Proxiable
的 proxiableUUID
函数,该函数返回 ERC1967Utils.IMPLEMENTATION_SLOT
;否则,升级将失败。
另一个重要的注意事项是,我们升级到的实现也需要是 UUPS 可升级的。否则,它将不再可升级(升级链将被中断)。
信标代理
代理
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/beacon/BeaconProxy.sol)
pragma solidity ^0.8.22;
import {IBeacon} from "./IBeacon.sol";
import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol";
/**
* @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}.
*
* The beacon address can only be set once during construction, and cannot be changed afterwards. It is stored in an
* immutable variable to avoid unnecessary storage reads, and also in the beacon storage slot specified by
* https://learnblockchain.cn/docs/eips/EIPS/eip-1967[ERC-1967] so that it can be accessed externally.
*
* CAUTION: Since the beacon address can never be changed, you must ensure that you either control the beacon, or trust
* the beacon to not upgrade the implementation maliciously.
*
* IMPORTANT: Do not use the implementation logic to modify the beacon storage slot. Doing so would leave the proxy in
* an inconsistent state where the beacon storage slot does not match the beacon address.
*/
contract BeaconProxy is Proxy {
address private immutable _beacon;
constructor(address beacon, bytes memory data) payable {
ERC1967Utils.upgradeBeaconToAndCall(beacon, data);
_beacon = beacon;
}
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
function _getBeacon() internal view virtual returns (address) {
return _beacon;
}
}
信标
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/UpgradeableBeacon.sol)
pragma solidity ^0.8.20;
import {IBeacon} from "./IBeacon.sol";
import {Ownable} from "../../access/Ownable.sol";
/**
* @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their
* implementation contract, which is where they will delegate all function calls.
*
* An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon.
*/
contract UpgradeableBeacon is IBeacon, Ownable {
address private _implementation;
error BeaconInvalidImplementation(address implementation);
event Upgraded(address indexed implementation);
constructor(address implementation_, address initialOwner) Ownable(initialOwner) {
_setImplementation(implementation_);
}
function implementation() public view virtual returns (address) {
return _implementation;
}
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
}
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert BeaconInvalidImplementation(newImplementation);
}
_implementation = newImplementation;
emit Upgraded(newImplementation);
}
关键区别在于,代理不存储实现本身;相反,它存储一个 beacon
合约的地址,该合约持有实现地址。beacon
合约可以通过调用 upgradeTo
来更新实现。
这种设计背后的原因是允许通过更改信标中的实现地址来同时升级多个代理(假设所有代理都指向同一个信标)。否则,我们需要一个一个地升级代理 1、2、3...
- 原文链接: blog.blockmagnates.com/u...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!