本指南将向您展示如何使用 Foundry 来管理 {oz} Contracts 的升级。
您将学习到的内容:
-
使用 {oz} Upgrades 插件和库升级合约。
-
使用 Foundry 测试升级。
-
将升级部署到测试网。
本指南假定您熟悉 Foundry。如果您不熟悉 Foundry,请查看 Foundry Book。
设置
本指南需要安装 Foundry。您可以通过运行以下命令来安装 Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup
接下来,您需要创建一个新的 Foundry 项目:
forge init --template https://github.com/OpenZeppelin/openzeppelin-foundry-template oz-foundry-upgrades
cd oz-foundry-upgrades
此命令将使用 {oz} Foundry 模板创建一个新的 Foundry 项目。该模板包含 {oz} Upgrades 插件和库。
现在,安装 Foundry 项目的依赖项:
forge install
定义合约
首先,您将定义您的合约。为简单起见,您将定义一个只有一个变量 value
的简单合约。
创建一个名为 contracts/Box.sol
的新文件,内容如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Box {
uint256 private value;
function store(uint256 newValue) public {
value = newValue;
}
function retrieve() public view returns (uint256) {
return value;
}
}
这是合约的初始版本。稍后您将修改此合约以添加新功能。
部署合约
现在您将部署您的合约。您将使用 {oz} Upgrades 插件来部署您的合约。该插件提供了一个方便的函数来部署您的合约并对其进行代理。
创建一个名为 script/Deploy.s.sol
的新文件,内容如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import {Box} from "../src/Box.sol";
import {console} from "forge-std/console.sol";
import { upgrades } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {Deployments} from "../src/Deployments.sol";
contract Deploy is Script {
function setUp() public {}
function run() public {
// 获取部署者的私钥
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// 使用私钥启动广播
vm.startBroadcast(deployerPrivateKey);
// 部署 Box 合约
Box box = new Box();
// 停止广播
vm.stopBroadcast();
// 输出部署地址
console.log("Box deployed to:", address(box));
}
}
此脚本将部署 Box
合约。
要运行此脚本,您需要设置 PRIVATE_KEY
环境变量。您可以使用 Foundry 来生成一个帐户,然后将该帐户的私钥设置为环境变量。
forge account create deployer
export PRIVATE_KEY=<deployer 私钥>
forge script script/Deploy.s.sol --broadcast --rpc-url $ETH_RPC_URL
该脚本将部署 Box
合约并输出合约的地址。
使用插件部署合约
现在您将使用 {oz} Upgrades 插件来部署您的合约。使用该插件,您可以部署一个代理合约,该合约将您的合约的逻辑分离到一个单独的合约中。这允许您稍后升级合约的逻辑,而无需更改代理合约的地址。
首先,修改您的部署脚本以使用 {oz} Upgrades 插件。
--- a/script/Deploy.s.sol
+++ b/script/Deploy.s.sol
@@ -4,7 +4,7 @@
import {Box} from "../src/Box.sol";
import {console} from "forge-std/console.sol";
import { upgrades } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
-
+import { IERC1967 } from "@openzeppelin/contracts/interfaces/IERC1967.sol";
import {Deployments} from "../src/Deployments.sol";
contract Deploy is Script {
@@ -17,13 +17,13 @@
vm.startBroadcast(deployerPrivateKey);
// 部署 Box 合约
- Box box = new Box();
+ Box box = Box.deploy();
// 停止广播
vm.stopBroadcast();
// 输出部署地址
- console.log("Box deployed to:", address(box));
+ console.log("Box deployed to:", address(Box.getAddress()));
}
}
您还需要添加一个 constructor
函数到 Box
合约中,这样 {oz} Upgrades 插件才能正常工作。
--- a/src/Box.sol
+++ b/src/Box.sol
@@ -7,6 +7,10 @@
contract Box {
uint256 private value;
+ constructor() {
+ _disableInitializers();
+ }
+
function store(uint256 newValue) public {
value = newValue;
}
您还需要创建一个 Box
部署库。这将包含部署 Box
合约的逻辑。创建一个名为 src/BoxDeployment.s.sol
的新文件,内容如下:
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol";
import {Box} from "./Box.sol";
library BoxDeployment {
function deploy() internal returns (Box) {
console.log("Deploying Box...");
Box box = Upgrades.deployProxy(new Box(), "initialize", abi.encode(uint256(42)));
console.log("Box deployed to:", address(box));
return box;
}
function getAddress() internal view returns (address) {
return Upgrades.getProxyAddress();
}
}
此库使用 Upgrades.deployProxy
函数来部署 Box
合约。deployProxy
函数将部署一个代理合约,该合约将 Box
合约的逻辑委托给它。
deployProxy
函数还将调用 initialize
函数,该函数用于初始化合约的状态。
Upgrades.deployProxy
的第二个参数是 initialize
函数的选择器。在本例中,您将使用 abi.encode(uint256(42))
作为 initialize
函数的参数。这意味着 initialize
函数将使用 42
作为参数调用。
您需要修改 Box
合约以包含 initialize
函数:
--- a/src/Box.sol
+++ b/src/Box.sol
@@ -11,6 +11,10 @@
_disableInitializers();
}
+ function initialize(uint256 initialValue) public initializer {
+ value = initialValue;
+ }
+
function store(uint256 newValue) public {
value = newValue;
}
现在,您可以像之前一样运行部署脚本:
forge script script/Deploy.s.sol --broadcast --rpc-url $ETH_RPC_URL
该脚本将部署 Box
合约并输出代理合约的地址。
升级合约
现在您已经部署了您的合约,您可以升级它。假设您想向您的合约添加一个新函数,该函数会递增存储的值。
首先,创建一个名为 contracts/BoxV2.sol
的新文件,内容如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract BoxV2 is Initializable {
uint256 private value;
function initialize(uint256 initialValue) public initializer {
value = initialValue;
}
function store(uint256 newValue) public {
value = newValue;
}
function retrieve() public view returns (uint256) {
return value;
}
function increment() public {
value = value + 1;
}
}
该合约与 Box
合约类似,但它还包含一个 increment
函数。请注意,您需要从 {oz} Contracts 导入 Initializable
合约,并使用 initializer
修饰符修饰 initialize
函数。
接下来,您需要修改您的部署脚本以升级到 BoxV2
合约。
--- a/script/Deploy.s.sol
+++ b/script/Deploy.s.sol
@@ -5,6 +5,7 @@
import {console} from "forge-std/console.sol";
import { upgrades } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { IERC1967 } from "@openzeppelin/contracts/interfaces/IERC1967.sol";
+import { BoxV2 } from "../src/BoxV2.sol";
import {Deployments} from "../src/Deployments.sol";
contract Deploy is Script {
@@ -16,10 +17,11 @@
// 使用私钥启动广播
vm.startBroadcast(deployerPrivateKey);
- // 部署 Box 合约
- Box box = Box.deploy();
+ // 部署或升级 Box 合约
+ BoxV2 box = BoxV2.deploy();
+ console.log("Box deployed to:", address(BoxV2.getAddress()));
// 停止广播
vm.stopBroadcast();
您还需要创建 BoxV2
部署库。这将包含升级到 BoxV2
合约的逻辑。创建一个名为 src/BoxV2Deployment.s.sol
的新文件,内容如下:
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Upgrades} from "@openzeppelin/foundry-upgrades/Upgrades.sol";
import {BoxV2} from "./BoxV2.sol";
library BoxV2Deployment {
function deploy() internal returns (BoxV2) {
address proxyAddress = Upgrades.getProxyAddress();
BoxV2 boxV2 = new BoxV2(proxyAddress);
if (proxyAddress == address(0)) {
console.log("Deploying BoxV2...");
boxV2 = Upgrades.deployProxy(new BoxV2(), "initialize", abi.encode(uint256(42)));
} else {
console.log("Upgrading Box to BoxV2...");
boxV2 = Upgrades.upgradeProxy(new BoxV2(), proxyAddress);
}
console.log("BoxV2 deployed to:", address(boxV2));
return boxV2;
}
function getAddress() internal view returns (address) {
return Upgrades.getProxyAddress();
}
}
此库使用 Upgrades.upgradeProxy
函数将代理合约升级到 BoxV2
合约。upgradeProxy
函数将把代理合约指向的新逻辑合约的地址更改为 BoxV2
合约的地址。
现在,您可以像之前一样运行部署脚本:
forge script script/Deploy.s.sol --broadcast --rpc-url $ETH_RPC_URL
该脚本将升级到 BoxV2
合约并输出代理合约的地址。请注意,代理合约的地址与之前的地址相同。
测试升级
现在您已经升级了您的合约,您需要测试它以确保它按预期工作。
创建一个名为 test/Box.t.sol
的新文件,内容如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import {Box} from "../src/Box.sol";
import {BoxV2} from "../src/BoxV2.sol";
import {console} from "forge-std/console.sol";
contract BoxTest is Test {
Box public box;
BoxV2 public boxV2;
function setUp() public {
// 部署 Box 合约
box = new Box(address(0));
boxV2 = new BoxV2(address(0));
}
function test_retrieve() public {
// 存储一个值
box.store(42);
// 检查该值是否被检索到
assertEq(box.retrieve(), 42, "value not equal to 42");
}
function test_increment() public {
// 将 Box 合约转换为 BoxV2 合约
boxV2 = BoxV2(address(box));
// 存储一个值
box.store(42);
// 递增该值
boxV2.increment();
// 检查该值是否已递增
assertEq(box.retrieve(), 43, "value not equal to 43");
}
}
此测试用例测试 retrieve
和 increment
函数是否按预期工作。
要运行测试用例,请运行以下命令:
forge test
测试用例应该通过。