---
# System prepended metadata

title: Foundry Upgrades插件部署UUPS
tags: [Smart Contract, EVM]

---

## 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
```

![image-20250316104336656](https://hackmd.io/_uploads/B19zrHE2yx.png)

看打印结果，算是成功部署并升级了。

但在实际过程中，遇到了一些坑，后面会说说一些需要注意的点。

<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**为：

![image-20250315160836978](https://hackmd.io/_uploads/ryBBHBN2ye.png)

与该slot配套的，有`changeAmdin(newAdmin)`和`getAdmin()`函数，用于设置和获取该slot的值，即相对于设置和获取admin。

<br>

同时值得注意的是，代理合约**ERC1967Proxy.sol**在部署、执行构造函数时，是默认没有对admin进行设置的：

![image-20250316145911939](https://hackmd.io/_uploads/ryWwSHN2kg.png)

如果我们是另外写个`MyProxy`合约去继承`ERC1976Proxy`作为代理合约，那么就可以在`MyProxy`的构造函数中调用`changeAdmin`函数来设置admin。

但在使用Foundry的upgrades插件，在执行`deployUUPSProxy`函数部署代理时，是直接部署一个ERC1976Proxy.sol。代码段为：

![image-20250316150541135](https://hackmd.io/_uploads/Byv5HrNnye.png)

这意味着，但我们使用Upgrades插件时，无法通过常规方法（changeAdmin函数）去初始化。至于非常规的方法，可以直接在实现合约初始化（执行initialize）时，通过内嵌汇编直接更改admin对应slot的值。

<br>

## 0x04：与ownable库的owner区分

另外，出于习惯或方便，我们经常使用ownable库，对应到其升级实现，即`OwnableUpgradeable`。

但`OwnableUpgradeable`中，owner数据是根据ERC-7201规范存储的，其存储的**slot**为：

![image-20250315162327191](https://hackmd.io/_uploads/SJqYHr4hkx.png)

这与代理合约中的`ADMIN_SLOT`的存储位置是不同的，应该区别开来。

如果在一开始是对`ADMIN_SLOT`设置管理员，那么检查权限时，得是检查`ADMIN_SLOT`，而不是`OwnableStorageLocation`。相反同理，需要对应上。

<br>

而Upgrades插件有个函数：`getAdminAddress(proxy)`，但是在Upgrades.t.sol中我并没有用到。这是因为`getAdminAddress(proxy)`函数获取的是`ADMIN_SLOT`的值，而我们合约中是使用了`OwnableUpgradeable`的数据。

![image-20250316152014925](https://hackmd.io/_uploads/H1QTBH4h1l.png)

<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>

会在升级时，显示权限不通过：

![image-20250316152509929](https://hackmd.io/_uploads/SyU1UHE3kx.png)

<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("-------------------------------------");
```

![image-20250316153751659](https://hackmd.io/_uploads/HJblISV2Jx.png)

