在 Foundry 使用 OpenZeppelin 插件进行智能合约升级
升级智能合约是一个多步骤且容易出错的过程,因此为了尽量减少人为错误的可能性,使用一个尽可能自动化该过程的工具是理想的。
因此, OpenZeppelin Upgrade Plugin 简化了使用 Foundry 或 Hardhat 构建的智能合约的部署、升级和管理。
在本文中,我们将学习如何使用 Upgrade Plugin 与 Foundry 来管理合约升级,包括本地和在 Sepolia 测试网。我们还将讨论这些插件如何防范常见的升级问题。
为了充分利用本指南,读者应熟悉:
Solidity 中的 delegatecall 操作。
像 ERC1967 这样的标准,以及 initializers 在可升级合约中设置状态的作用。
常见的升级失败,包括 function selector 和 storage slot 冲突。
对常见代理模式的了解,例如 Transparent Upgradeable Proxy、 UUPS Proxy 或 Beacon Proxy。
Namespace 存储布局或 EIP-7201。
OpenZeppelin 的 Foundry 插件是一个实用工具,可以导入到 Foundry Solidity 脚本或单元测试中,导入方式如下:
import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol";
该库公开了用于部署代理、实现合约和其他相关合约的函数。在下一节中,我们将提供其功能的高层次概述,随后展示如何为升级编写单元测试,并创建一个使用此插件帮助部署和升级智能合约的脚本。
给定对先前智能合约实现的引用,插件将先前实现与新实现进行比较,以检查潜在问题,如存储槽冲突和我们稍后将讨论的其他问题。
插件支持部署和升级 UUPS、透明和 Beacon 代理模式。不支持钻石代理模式。
当使用此插件首次部署可升级合约时,最多会自动创建三个组件(具体取决于升级模式是 UUPS、透明还是 Beacon 代理):
实现合约:包含合约的实际逻辑。
代理:如果部署新的代理,插件会处理其创建并将其链接到指定的实现合约。但是,如果代理已经存在,插件通过将现有代理链接到新实现来促进升级过程。
ProxyAdmin:此管理组件管理谁可以专门为 透明代理 升级代理(只有透明可升级代理使用 Proxy Admin)。
Beacon 代理:Beacon 代理模式不为代理分配单独的管理员地址。相反,单个 beacon 具有一个所有者,负责更新所有链接代理的实现。插件自动化了 beacon 和代理的设置,并更新 beacon 的实现。
插件旨在与 Hardhat 和 Foundry 一起使用。虽然 Hardhat 环境详细记录了部署日志,但 Foundry 专注于使用 reference contracts 来确保升级的安全性。
与 OpenZeppelin 的 Hardhat 升级插件不同,后者通过 JSON 自动跟踪实现合约及其版本,Foundry 插件要求开发人员显式定义 参考合约。
考虑下面合约中的 NatSpec:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @custom:oz-upgrades-from MyContractV1
contract MyContractV2 {
...
}
或
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @custom:oz-upgrades-from contracts/MyContract.sol:MyContractV1
contract MyContractV2 {
...
}
Natspec 参考了 reference contract,这是 Foundry 插件用来获取对参考合约的引用的。
参考合约是要升级的合约,并作为基线以确保新实现与原始状态和布局兼容。
一般来说,你可以将“参考合约”视为“先前的实现”。
插件中的 validateUpgrade 函数检查新实现合约是否与参考合约兼容。它要求设置 referenceContract 选项,或者在新合约上存在 @custom:oz-upgrades-from <reference> 注释。
关于如何使用 Upgrades 工具中的 validateUpgrade 函数的详细信息将在本指南后面讨论。
现在我们展示在本地部署和升级透明可升级代理的步骤。稍后我们将展示如何在测试网上执行此操作。以下是我们将采取的高层次步骤:
部署代理和实现ContractA:
部署 ContractA 并初始化它。
此步骤自动设置 ContractA、一个 TransparentUpgradeableProxy 和一个 Proxy Admin 合约。
ContractA 作为此升级过程的参考合约。
升级到ContractB:
首先创建一个新的项目目录并初始化 Foundry。打开终端并执行以下命令:
mkdir rareskills-foundry-upgrades && cd rareskills-foundry-upgrades
forge init
接下来,我们需要准备项目的基本文件。这些包括智能合约文件、测试文件和依赖映射文件。
执行以下命令在项目目录中创建这些文件:
touch src/ContractA.sol && touch src/ContractB.sol && touch test/Upgrades.t.sol && touch remappings.txt
项目初始化后,下一步是设置处理可升级合约所需的 OpenZeppelin 库。
按如下方式更新 remappings 文件:
forge remappings > remappings.txt
在终端中运行以下命令以安装所需的库。
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit
forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit
no-commit 标志避免了在使用上述命令之前提交当前状态的麻烦。
接下来,我们需要确保项目知道在哪里找到 OpenZeppelin
文件。这是通过配置我们之前创建的 remappings.txt
文件来完成的。
打开 remappings.txt 并插入以下行:
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
如果 remapping.txt 文件中已经存在 OpenZeppelin remappings,请用上述内容替换它们。
接下来,删除自动创建的 test/Counter.t.sol 测试和 src/Counter.sol:
rm test/Counter.t.sol
rm src/Counter.sol
最后,打开 foundry.toml 并添加以下配置:
\[profile.default\]
src \= "src"
out \= "out"
libs \= \["node\_modules", "lib"\]
build\_info \= true
extra\_output \= \["storageLayout"\]
ffi \= true
ast \= true
现在我们将创建两个智能合约,ContractA 和 ContractB,以演示升级过程。
从 ContractA.sol 开始。该合约包含一个公共变量 value 和一个方法 initialize,用于替代构造函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract ContractA is Initializable{
uint256 public value;
function initialize(uint256 _setValue) public initializer {
value = _setValue;
}
}
接下来,我们将创建 ContractB.sol 以展示从 ContractA 的升级路径。
ContractB 通过添加一个方法来递增 value,从而扩展了 ContractA 的功能。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
/// @custom:oz-upgrades-from ContractA
contract ContractB is Initializable {
uint256 public value;
function initialize(uint256 _setValue) public initializer {
value = _setValue;
}
function increaseValue() public {
value += 10;
}
}
通过使用 @custom:oz-upgrades-from ContractA 注释,我们指定 ContractB 是 ContractA 的升级版本。
此注释不要求 ContractA 和 ContractB 在同一目录中。它通过名称识别 ContractA,只要在项目中唯一定义,否则需要完全限定名称。
没有此注释,插件将无法继续,并会显示如下错误:
`The contract ${sourceContract.fullyQualifiedName} does not specify what contract it upgrades from. Add the \\\`@custom:oz-upgrades-from <REFERENCE\_CONTRACT>\\\` annotation to the contract, or include the reference contract name when running the validate command or function.`
尽管我们为此示例准备了 ContractA 和 ContractB,但这并不意味着我们需要“预见” ContractA 的未来升级。我们只是为了方便起见提前创建 ContractB。
此步骤涉及编译合约、将其作为透明代理模式部署、执行升级,并验证合约状态是否按预期更新。
准备测试环境
首先,导航到 test 文件夹,并将以下代码插入 Upgrades.t.sol 文件中。
此设置测试从 ContractA 到 ContractB 的可升级性。
// 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 {
// future code will go here
}
初始测试涉及两个主要操作:
使用透明代理部署 ContractA,并使用初始值进行初始化。
升级到 ContractB。
最后,调用 increaseValue 来修改状态。
以下是测试的代码。请阅读下面代码中的注释以理解工作流程:
function testTransparent() public {
// Deploy a transparent proxy with ContractA as the implementation and initialize it with 10
address proxy = Upgrades.deployTransparentProxy(
"ContractA.sol",
msg.sender,
abi.encodeCall(ContractA.initialize, (10))
);
// Get the instance of the contract
ContractA instance = ContractA(proxy);
// Get the implementation address of the proxy
address implAddrV1 = Upgrades.getImplementationAddress(proxy);
// Get the admin address of the proxy
address adminAddr = Upgrades.getAdminAddress(proxy);
// Ensure the admin address is valid
assertFalse(adminAddr == address(0));
// Log the initial value
console.log("----------------------------------");
console.log("Value before upgrade --> ", instance.value());
console.log("----------------------------------");
// Verify initial value is as expected
assertEq(instance.value(), 10);
// Upgrade the proxy to ContractB
Upgrades.upgradeProxy(proxy, "ContractB.sol", "", msg.sender);
// Get the new implementation address after upgrade
address implAddrV2 = Upgrades.getImplementationAddress(proxy);
// Verify admin address remains unchanged
assertEq(Upgrades.getAdminAddress(proxy), adminAddr);
// Verify implementation address has changed
assertFalse(implAddrV1 == implAddrV2);
// Invoke the increaseValue function separately
ContractB(address(instance)).increaseValue();
// Log and verify the updated value
console.log("----------------------------------");
console.log("Value after upgrade --> ", instance.value());
console.log("----------------------------------");
assertEq(instance.value(), 20);
}
作为总结,该工具执行以下操作:
通过透明代理使用 Upgrades.deployTransparentProxy("ContractA.sol", msg.sender, abi.encodeCall(ContractA.initialize, (10))); 部署 ContractA,并使用特定值初始化 ContractA。
使用 Upgrades.upgradeProxy(proxy, "ContractB.sol", "", msg.sender); 升级代理以使用 ContractB。
验证更新后的值和升级后管理员地址的一致性。
执行测试
要运行测试,请在终端中输入以下命令:
forge clean && forge test -vvv --ffi
你将看到类似以下内容的输出:
pari@MacBook-Air rareskills-foundry-upgrades % forge clean && forge test --mt testTransparent -vvv
\[⠢\] Compiling...
\[⠊\] Compiling 54 files with 0.8.24
\[⠆\] Solc 0.8.24 finished in 4.94s
Compiler run successful!
Ran 1 test for test/Upgrades.t.sol:UpgradesTest
\[PASS\] testTransparent() (gas: 1355057)
Logs:
----------------------------------
Value before upgrade --> 10
----------------------------------
----------------------------------
Value after upgrade --> 20
----------------------------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.43s (1.43s CPU time)
你可能需要在第二次运行测试之前运行 forge cache clean
和forge clean
。
我们现在演示如何使用此插件与 Beacon Proxy 模式。我们将使用前一个示例中的相同实现合约 ContractA
和 ContractB
。
测试概述
将 ContractA 部署为 Beacon 的初始实现。
创建两个 beacon proxies,每个代理初始化为不同的值。请记住,在 beacon proxy 模式中,多个代理指向单个实现,但它们具有各自独立的状态。
验证新实现(ContractB) 与原始实现(ContractA)使用 validateUpgrade 函数。
将信标的实现升级到 ContractB,同时升级两个代理。
在两个代理上测试新功能,以确保升级按预期应用更改。
将此函数添加到 Upgrades.t.sol 文件中以进行测试。注释解释了工作流程:
import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol";
import {Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
function testBeacon() public {
// 使用 ContractA 作为初始实现部署一个信标
address beacon = Upgrades.deployBeacon("ContractA.sol", msg.sender);
// 获取信标的初始实现地址
address implAddrV1 = IBeacon(beacon).implementation();
// 部署第一个信标代理并初始化
address proxy1 = Upgrades.deployBeaconProxy(beacon, abi.encodeCall(ContractA.initialize, 15));
ContractA instance1 = ContractA(proxy1);
// 部署第二个信标代理并初始化
address proxy2 = Upgrades.deployBeaconProxy(beacon, abi.encodeCall(ContractA.initialize, 20));
ContractA instance2 = ContractA(proxy2);
// 检查两个代理是否指向同一个信标
assertEq(Upgrades.getBeaconAddress(proxy1), beacon);
assertEq(Upgrades.getBeaconAddress(proxy2), beacon);...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!