在 Foundry 使用 OpenZeppelin 插件进行智能合约升级

  • RareSkills
  • 发布于 2024-09-09 14:47
  • 阅读 1931

在 Foundry 使用 OpenZeppelin 插件进行智能合约升级

升级智能合约是一个多步骤且容易出错的过程,因此为了尽量减少人为错误的可能性,使用一个尽可能自动化该过程的工具是理想的。

因此, OpenZeppelin Upgrade Plugin 简化了使用 Foundry 或 Hardhat 构建的智能合约的部署、升级和管理。

在本文中,我们将学习如何使用 Upgrade Plugin 与 Foundry 来管理合约升级,包括本地和在 Sepolia 测试网。我们还将讨论这些插件如何防范常见的升级问题。

前提条件

为了充分利用本指南,读者应熟悉:

Foundry 的升级插件

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 函数的详细信息将在本指南后面讨论。

在 Foundry 中本地测试智能合约升级

现在我们展示在本地部署和升级透明可升级代理的步骤。稍后我们将展示如何在测试网上执行此操作。以下是我们将采取的高层次步骤:

  1. 部署代理和实现ContractA:

    • 部署 ContractA 并初始化它。

    • 此步骤自动设置 ContractA、一个 TransparentUpgradeableProxy 和一个 Proxy Admin 合约。

    • ContractA 作为此升级过程的参考合约。

  2. 升级到ContractB:

    • 工具使用代理上的 upgradeAndCall 方法将合约更新为 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

第 3 步:创建可升级合约

现在我们将创建两个智能合约,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 &lt;REFERENCE\_CONTRACT>\\\` annotation to the contract, or include the reference contract name when running the validate command or function.`

尽管我们为此示例准备了 ContractA 和 ContractB,但这并不意味着我们需要“预见” ContractA 的未来升级。我们只是为了方便起见提前创建 ContractB。

第 4 步:测试可升级功能

此步骤涉及编译合约、将其作为透明代理模式部署、执行升级,并验证合约状态是否按预期更新。

准备测试环境

首先,导航到 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
}

初始测试涉及两个主要操作:

  1. 使用透明代理部署 ContractA,并使用初始值进行初始化。

  2. 升级到 ContractB。

  3. 最后,调用 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 cleanforge clean

使用 Beacon Proxy 的示例

我们现在演示如何使用此插件与 Beacon Proxy 模式。我们将使用前一个示例中的相同实现合约 ContractAContractB

测试概述

  1. 将 ContractA 部署为 Beacon 的初始实现。

  2. 创建两个 beacon proxies,每个代理初始化为不同的值。请记住,在 beacon proxy 模式中,多个代理指向单个实现,但它们具有各自独立的状态。

  3. 验证新实现(ContractB) 与原始实现(ContractA)使用 validateUpgrade 函数。

  4. 将信标的实现升级到 ContractB,同时升级两个代理。

  5. 在两个代理上测试新功能,以确保升级按预期应用更改。

将此函数添加到 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);...

剩余50%的内容订阅专栏后可查看

点赞 5
收藏 4
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/