## 0x00:介绍
最近要对使用了UUPS可升级模式的合约进行测试部署,打算先用个小demo熟悉一下。openzeppelin为Foundry框架实现了Upgrades插件,目的是使得**透明模式**、**信标模式**、**UUPS模式** 3种可升级代理模式的合约,可以更加方便、安全地测试部署。
本篇文章,会先执行一个demo。然后介绍Upgrades插件的相关函数,最后说说一些注意点。目前Upgrades插件还处于完善中,遇到一些意外报错,请查阅官方文档或仓库,了解最新内容。
<br>
**Foundry Upgrades相关资料:**
官方仓库:https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
官方文档:https://docs.openzeppelin.com/upgrades-plugins/foundry-upgrades
RareSkills的教程文章:https://www.rareskills.io/post/openzeppelin-foundry-upgrades
<br>
## 0x01:Demo
Demo的话,直接基于RareSkills文章中的修改了。RareSkills文章中,讲了透明模式和信标模式,但没有UUPS模式,所以也算是一个补充了。
<br>
初始化一个项目:
```shell
$ forge init upgrade_demo
```
下载依赖(这里用的OZ版本为v5):
```shell
$ forge install foundry-rs/forge-std
$ forge install OpenZeppelin/openzeppelin-foundry-upgrades
$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable
```
下载完依赖后,在项目根目录(`upgrade_demo`)下创建`remappings.txt`,添加如下路径解析:
```
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
```
修改foundry.toml,配置为:
```toml
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
```
把`/script`、`/src`、`/test`中的默认文件删掉。添加合约文件`/src/ContractA.sol`、 `/src/ContractB.sol` 和测试文件`/test/Upgrades.t.sol`。
<br>
ContractA会作为一开始的实现合约,在后续会升级为ContractB,作为新的实现合约。而Upgrades.t.sol则编写测试代码,模拟这一过程。
文件结构:
```
├── foundry.toml
├── lib
│ ├── forge-std
│ ├── openzeppelin-contracts-upgradeable
│ └── openzeppelin-foundry-upgrades
├── README.md
├── remappings.txt
├── script
├── src
│ ├── ContractA.sol
│ ├── ContractB.sol
└── test
└── Upgrades.t.sol
```
<br>
**ContractA.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract ContractA is OwnableUpgradeable, UUPSUpgradeable {
uint256 public value;
function initialize(uint256 _setValue) public initializer {
__Ownable_init(msg.sender); // 设置调用者为 owner
__UUPSUpgradeable_init(); // 初始化 UUPS
value = _setValue;
}
// 需要重写(这里添加了修饰符onlyOwner,表示只有owner才能升级合约)
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
```
<br>
**ContractB.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
/// @custom:oz-upgrades-from ContractA
contract ContractB is OwnableUpgradeable, UUPSUpgradeable {
uint256 public value;
function initialize(uint256 _setValue) public initializer {
__Ownable_init(msg.sender); // 设置调用者为 owner
__UUPSUpgradeable_init(); // 初始化 UUPS
value = _setValue;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function increaseValue() public { // 升级,添加了个函数
value += 10;
}
}
```
`/// @custom:oz-upgrades-from ContractA`,这个文档注释在使用Upgrades插件时,是必要的,为了告诉Upgrades插件**ContractB**是想升级**ContractA**。
<br>
**Upgrades.t.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-foundry-upgrades/Upgrades.sol";
import "../src/ContractA.sol";
import "../src/ContractB.sol";
contract UpgradesTest is Test {
function testUUPS() public {
address proxy = Upgrades.deployUUPSProxy(
"ContractA.sol",
abi.encodeCall(ContractA.initialize, (10))
);
ContractA aProxy = ContractA(proxy);
address implAddrV1 = Upgrades.getImplementationAddress(proxy);
address adminAddr = aProxy.owner();
console.log("------------Before Upgrade-------------");
console.log("proxy address: ", proxy);
console.log("imple address: ", implAddrV1);
console.log("admin address: ", adminAddr);
console.log("Value = ", aProxy.value());
console.log("---------------------------------------");
Upgrades.upgradeProxy(
proxy,
"ContractB.sol",
"" // without calling any additional function
);
ContractB bProxy = ContractB(proxy);
address implAddrV2 = Upgrades.getImplementationAddress(proxy);
adminAddr = bProxy.owner();
bProxy.increaseValue();
console.log("----------------After------------------");
console.log("proxy address: ", proxy);
console.log("imple address: ", implAddrV2);
console.log("admin address: ", adminAddr);
console.log("Value = ", bProxy.value());
console.log("---------------------------------------");
}
}
```
测试代码我就不用断言来判断了,直接打印出来,直观一点。
<br>
执行测试:
```shell
$ forge clean && forge test --match-test testUUPS -vvv --ffi
```

看打印结果,算是成功部署并升级了。
但在实际过程中,遇到了一些坑,后面会说说一些需要注意的点。
<br>
## 0x02:Upgrades中与UUPS相关的函数
Upgrades.t.sol中的一些函数还没做解释,这里先来介绍一下。
<br>
```solidity
deployUUPSProxy(contractName, initializerData, opts)
deployUUPSProxy(contractName, initializerData)
```
这两个函数是在最开始时调用的,用于部署实现合约和代理合约。内部具体流程为:
1. 部署实现合约,由“contractName”指定;
2. 部署代理合约,并将实现合约地址设为上一步中部署的实现合约;
3. 通过代理合约**委托调用**实现合约中的initizlize函数,进行初始化;
(opts参数暂未了解)
<br>
升级函数:
```solidity
upgradeProxy(proxy, contractName, data)
upgradeProxy(proxy, contractName, data, opts)
upgradeProxy(proxy, contractName, data, opts, tryCaller)
upgradeProxy(proxy, contractName, data, tryCaller)
```
将"proxy"代理的实现合约升级为合约"contractName"。这里的data和上面的initializerData作用是一样的,是新的实现合约的调用数据,通常是调用initialize函数。至于`tryCaller`是指定哪个账户去调用升级,通常是合约管理员。
<br>
```
getAdminAddress(proxy)
getImplementationAddress(proxy)
```
从函数名即可得知,分别用于获取代理合约的admin和实现合约地址。
<br>
## 0x03:proxy的Admin
我们在实现UUPS可升级模式的代理合约时,通常是使用openzeppelin的[ERC1967Proxy.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/ERC1967/ERC1967Proxy.sol)。代理合约需要存储一个admin数据,根据ERC-1967规范进行随机化。其存储**slot**为:

与该slot配套的,有`changeAmdin(newAdmin)`和`getAdmin()`函数,用于设置和获取该slot的值,即相对于设置和获取admin。
<br>
同时值得注意的是,代理合约**ERC1967Proxy.sol**在部署、执行构造函数时,是默认没有对admin进行设置的:

如果我们是另外写个`MyProxy`合约去继承`ERC1976Proxy`作为代理合约,那么就可以在`MyProxy`的构造函数中调用`changeAdmin`函数来设置admin。
但在使用Foundry的upgrades插件,在执行`deployUUPSProxy`函数部署代理时,是直接部署一个ERC1976Proxy.sol。代码段为:

这意味着,但我们使用Upgrades插件时,无法通过常规方法(changeAdmin函数)去初始化。至于非常规的方法,可以直接在实现合约初始化(执行initialize)时,通过内嵌汇编直接更改admin对应slot的值。
<br>
## 0x04:与ownable库的owner区分
另外,出于习惯或方便,我们经常使用ownable库,对应到其升级实现,即`OwnableUpgradeable`。
但`OwnableUpgradeable`中,owner数据是根据ERC-7201规范存储的,其存储的**slot**为:

这与代理合约中的`ADMIN_SLOT`的存储位置是不同的,应该区别开来。
如果在一开始是对`ADMIN_SLOT`设置管理员,那么检查权限时,得是检查`ADMIN_SLOT`,而不是`OwnableStorageLocation`。相反同理,需要对应上。
<br>
而Upgrades插件有个函数:`getAdminAddress(proxy)`,但是在Upgrades.t.sol中我并没有用到。这是因为`getAdminAddress(proxy)`函数获取的是`ADMIN_SLOT`的值,而我们合约中是使用了`OwnableUpgradeable`的数据。

<br>
## 0x05:区别Test合约地址和调用者地址
看如下这个测试代码,你能发现有什么问题吗?
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "openzeppelin-foundry-upgrades/Upgrades.sol";
import "../src/ContractA.sol";
import "../src/ContractB.sol";
contract UpgradesTest is Test {
function testUUPS() public {
address proxy = Upgrades.deployUUPSProxy(
"ContractA.sol",
abi.encodeCall(ContractA.initialize, (10))
);
Upgrades.upgradeProxy(
proxy,
"ContractB.sol",
"",
msg.sender
);
}
}
```
<br>
会在升级时,显示权限不通过:

<br>
这里有个需要注意的概念是,测试合约`UpgradesTest`也是一个合约,也有一个地址。当我们执行测试时,可以理解为,我们是用一个EOA账户来调用该测试合约执行。
对于测试代码,在执行`deployUUPSProxy`时,是UpgradesTest合约来部署合约。也就是说:在设置owner为`msg.sender`时,这个`msg.sender`是UgradesTest合约的地址。
而在`upgradeProxy`升级时,却指定了`msg.sender`(调用UpgradesTest合约的EOA账户)来调用,但EOA账户地址是不同于UpgradesTest合约的地址,所以权限验证不通过。
<br>
可以在demo的测试代码中,加入如下逻辑验证一下:
```solidity
console.log("-------------------------------------");
console.log("Upgrades Address: ", address(this));
console.log("MsgSender Address: ", msg.sender);
console.log("-------------------------------------");
```
