Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

通用升级合约(UUPS)

通用升级合约(UUPS)

通用升级合约(UUPS)相比透明代理合约,UUPS 代理合约最终可以移出其升级能力,使代码真正不可变,而且 UUPS 在技术上部署成本稍低一些。

OpenZeppelin 的文档中详细介绍了这两种代理的区别: https://docs.openzeppelin.com/contracts/5.x/api/proxy#transparent-vs-uups

构建一个可升级合约

构建 V1 合约,V2 合约

构建一个 UUPS 合约,可以引入 OpenZeppelin 的标准库,对于 Foundry 开发环境:

1
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit

即可安装 Foundry 到项目中,使用时,导入即可(记得 Foundry.toml 重映射)

1
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

UUPSUpgradeable 是一个抽象合约,需要我们在我们自己的合约中重写_authorizeUpgrade函数:

1
function _authorizeUpgrade(address newImplementation) internal override {}

关于重写该函数的原因,在 OpenZeppelin 库中给出了原因:

1
2
3
4
5
6
7
8
9
10
11
/**
* @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 中的一些函数,所以,代理合约或者打算通过代理使用的合约,不使用构造函数,满足下面的流程:

1
Proxy ---> deploy implementation ---> call some "initializable" function

所以,很多可升级合约做的第一步是调用这个已禁用的 initializers 函数:

1
2
3
4
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

而当我们确实需要使用构造函数时,我们之秀添加一个初始化器:

1
2
3
4
function initialize() public initializer {
__Ownable_init(msg.sender); // sets owner
__UUPSUpgradeable_init();
}

完整的 V1 合约如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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 合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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 合约的脚本合约如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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) -> 升级成功。

完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 的测试:

不多赘述,直接上源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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());
}
}