通用升级合约(UUPS)
通用升级合约(UUPS)
通用升级合约(UUPS)相比透明代理合约,UUPS 代理合约最终可以移出其升级能力,使代码真正不可变,而且 UUPS 在技术上部署成本稍低一些。
OpenZeppelin 的文档中详细介绍了这两种代理的区别: https://docs.openzeppelin.com/contracts/5.x/api/proxy#transparent-vs-uups
构建一个可升级合约
构建 V1 合约,V2 合约
构建一个 UUPS 合约,可以引入 OpenZeppelin 的标准库,对于 Foundry 开发环境:
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit
即可安装 Foundry 到项目中,使用时,导入即可(记得 Foundry.toml 重映射)
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
UUPSUpgradeable
是一个抽象合约,需要我们在我们自己的合约中重写_authorizeUpgrade
函数:
function _authorizeUpgrade(address newImplementation) internal override {}
关于重写该函数的原因,在 OpenZeppelin 库中给出了原因:
/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upgradeToAndCall}.
*
* Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeUpgrade(address) internal onlyOwner {}
* ```
*/
function _authorizeUpgrade(address newImplementation) internal virtual;
当需要需要构造函数的功能时(即在合约部署时的一些初始化操作时),就需要导入另一个库:Initializable
。注意:在代理合约(Proxy)中不使用构造函数(constructor
)
因为:状态变量存储在 Proxy 合约中,而不是 implementation (实现合约)中,Proxy 只是借用了 Implementation 中的一些函数,所以,代理合约或者打算通过代理使用的合约,不使用构造函数,满足下面的流程:
Proxy ---> deploy implementation ---> call some "initializable" function
所以,很多可升级合约做的第一步是调用这个已禁用的 initializers
函数:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
而当我们确实需要使用构造函数时,我们之秀添加一个初始化器:
function initialize() public initializer {
__Ownable_init(msg.sender); // sets owner
__UUPSUpgradeable_init();
}
完整的 V1 合约如下:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract BoxV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 internal number;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() public initializer {
__Ownable_init(msg.sender); // sets owner
__UUPSUpgradeable_init();
}
function getNumber() external view returns (uint256) {
return number;
}
function version() external pure returns (uint256) {
return 1;
}
function _authorizeUpgrade(address newImplementation) internal override {}
}
V2 合约:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract BoxV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 internal number;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() public initializer {
__Ownable_init(msg.sender); // sets owner
__UUPSUpgradeable_init();
}
function getNumber() external view returns (uint256) {
return number;
}
function version() external pure returns (uint256) {
return 2;
}
function setNumber(uint256 _number) external {
number = _number;
}
function _authorizeUpgrade(address newImplementation) internal override {}
}
部署 V1 合约
接下来完成部署 V1 合约的脚本。
这里又回到了最开始的点,实现可升级(不改变合约地址),实际上不改变的是 Proxy 合约的地址,也就是说,部署 V1 合约实际上是部署了两个合约:一个是Implementation(Logic)合约,一个是 Proxy(代理合约)。而这里使用的代理合约是 ERC1967 代理合约(还是,从 OpenZeppelin 标准合约库中引用即可)。
实际上部署完 V1 合约后,所有的交互都是和 Proxy 交互,Proxy 通过 Delegatecall 将 Implementation 合约中的逻辑引用过来,所有的状态变量也是存储在 Proxy 合约的 Storage 中。
完整的部署 V1 合约的脚本合约如下:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import {Script} from "forge-std/Script.sol";
import {BoxV1} from "../src/BoxV1.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract DeployBox is Script {
function run() external returns (address) {
address proxy = deployBox();
return proxy;
}
function deployBox() public returns (address) {
vm.startBroadcast();
BoxV1 box = new BoxV1(); // implementation (Logic)
// here we will use ERC1967 Proxy Contract
ERC1967Proxy proxy = new ERC1967Proxy(address(box), "");
vm.stopBroadcast();
return address(proxy);
}
}
将 V1 合约升级到 V2
对合约进行升级时,涉及到这么一个过程:
deploy V2 -> call function upgradeToAndCall(V2address, "")
(in Proxy) -> 升级成功。
完整脚本:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import {Script} from "forge-std/Script.sol";
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";
import {BoxV2} from "../src/BoxV2.sol";
import {BoxV1} from "../src/BoxV1.sol";
contract UpgradeBox is Script {
function run() external returns (address) {
address mostRecentlyDeployed = DevOpsTools.get_most_recent_deployment("ERC1967Proxy", block.chainid);
vm.startBroadcast();
BoxV2 newBox = new BoxV2();
vm.stopBroadcast();
address proxy = upgradeBox(mostRecentlyDeployed, address(newBox));
return proxy;
}
function upgradeBox(address proxyAddress, address newBox) public returns (address) {
vm.startBroadcast();
BoxV1 proxy = BoxV1(proxyAddress);
proxy.upgradeToAndCall(address(newBox), "");
vm.stopBroadcast();
return address(proxy);
}
}
一些关于 UUPS 的测试:
不多赘述,直接上源代码:
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import {Test} from "forge-std/Test.sol";
import {DeployBox} from "../script/DeployBox.s.sol";
import {UpgradeBox} from "../script/UpgradeBox.s.sol";
import {BoxV1} from "../src/BoxV1.sol";
import {BoxV2} from "../src/BoxV2.sol";
contract DeployAndUpgradeTest is Test {
DeployBox public deployer;
UpgradeBox public upgrader;
address public OWNER = makeAddr("owner");
address public proxy;
function setUp() public {
deployer = new DeployBox();
upgrader = new UpgradeBox();
proxy = deployer.run(); // right now, Proxy point to V1
}
function testProxyStartAsBoxV1() public {
vm.expectRevert();
BoxV2(proxy).setNumber(7);
}
function testUpgrades() public {
BoxV2 box2 = new BoxV2();
upgrader.upgradeBox(proxy, address(box2));
uint256 expectValue = 2;
assertEq(expectValue, BoxV2(proxy).version());
BoxV2(proxy).setNumber(7);
assertEq(7, BoxV2(proxy).getNumber());
}
}