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

  • RareSkills
  • 更新于 2024-09-09 15:46
  • 阅读 1058

在 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);

    console.log("----------------------------------");
    console.log("升级前 Proxy 1 的值 --> ", instance1.value());
    console.log("升级前 Proxy 2 的值 --> ", instance2.value());
    console.log("----------------------------------");

    // 在升级前验证新实现
    Options memory opts;
    opts.referenceContract = "ContractA.sol";
    Upgrades.validateUpgrade("ContractB.sol", opts);

    // 升级信标以使用 ContractB
    Upgrades.upgradeBeacon(beacon, "ContractB.sol", msg.sender);

    // 升级后获取信标的新实现地址
    address implAddrV2 = IBeacon(beacon).implementation();

    // 在两个代理中激活 increaseValue 函数
    ContractB(address(instance1)).increaseValue();
    ContractB(address(instance2)).increaseValue();

    console.log("----------------------------------");
    console.log("升级后 Proxy 1 的值 --> ", instance1.value());
    console.log("升级后 Proxy 2 的值 --> ", instance2.value());
    console.log("----------------------------------");

    // 检查值是否已正确增加
    assertEq(instance1.value(), 25);
    assertEq(instance2.value(), 30);

    // 检查实现地址是否已更改
    assertFalse(implAddrV1 == implAddrV2);
}

Upgrades 插件的支持功能

该插件支持比我们在上述两个示例中使用的更多功能。我们在下面提供了 Upgrades 的其他函数概述:

初始部署函数:

这些函数主要用于部署合约的初始版本并设置其代理结构:

  • deployUUPSProxy(*string contractName, bytes initializerData, struct Options opts*): 使用给定合约作为实现部署 UUPS 代理。

  • deployTransparentProxy(*string contractName, address initialOwner, bytes initializerData, struct Options opts*): 使用给定合约作为实现部署透明代理。

  • deployBeaconProxy(*address beacon, bytes data, struct Options opts*): 使用给定信标和调用数据部署信标代理。

  • deployBeacon(*string contractName, address initialOwner, struct Options opts*): 使用给定合约作为实现部署可升级信标。

验证和实现函数:

这些函数用于确保在升级过程中实现的兼容性和安全性:

  • validateImplementation(*string contractName, struct Options opts*): 验证实现合约,但不部署它。

  • deployImplementation(*string contractName, struct Options opts*): 验证并部署实现合约,并返回其地址。

  • validateUpgrade(*string contractName, struct Options opts*): 验证新实现合约与参考合约的比较,但不部署它。

  • prepareUpgrade(*string contractName, struct Options opts*): 验证新实现合约与参考合约的比较,部署新实现合约并返回其地址。

升级函数:

这些函数用于管理和实施已部署合约的升级:

  • upgradeProxy(*address proxy, string contractName, bytes data, struct Options opts*): 将代理升级到新实现合约。此函数在内部调用 validateUpgrade(),因此如果验证不成功,将会失败。如果用户希望绕过某些验证,可以在 opts 参数中进行配置。

  • upgradeBeacon(*address beacon, string contractName, struct Options opts*): 将信标升级到新实现合约。

其他函数:

  • getAdminAddress(*address proxy*): 从其 ERC1967 管理存储槽获取透明代理的管理员地址。

在 Sepolia 测试网部署和验证

本节指导你通过部署 ContractA,将其升级到 ContractB 使用透明代理,并在 Sepolia Explorer 上验证合约。

第 1 步:添加环境变量

首先,设置部署到测试网所需的配置:

  • 创建一个 .env 文件以存储敏感数据。

  • 将 .env 文件包含在你的 .gitignore 中,以防止在版本控制平台(如 Github)上暴露私钥或其他敏感信息。

  • 用你的特定数据填充 .env 文件:

SEPOLIA\_RPC\_URL=your-sepolia-endpoint
PRIVATE\_KEY=your-private-key
ETHERSCAN\_API\_KEY=your-etherscan-api
SENDER=your-EOA-address

配置细节:

  • SEPOLIA_RPC_URL: 连接到 Sepolia 网络的 RPC URL。

  • PRIVATE_KEY: 你的私钥,用于签署交易。确保该钱包有以太币以支付部署的 gas 费用。

  • ETHERSCAN_API_KEY: 你的 Etherscan API 密钥,用于合约验证。

  • SENDER: 将发起部署交易的以太坊地址。

第 2 步:配置 foundry.toml 文件

更新 foundry.toml 文件以包含 Sepolia 测试网和 Etherscan 验证的设置:

[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}" }

[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"

此配置完成两个任务:

  • Etherscan 配置: 将 Etherscan API 密钥与 Sepolia 网络链接,以便在部署后进行合约验证。

  • RPC 端点: 指定 Sepolia 网络的 RPC URL。

第 3 步:脚本部署和升级

导航到 script 文件夹并打开 Upgrades.s.sol 文件以插入部署和升级任务所需的代码。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Script} from "forge-std/Script.sol";

import {ContractA} from '../src/ContractA.sol';
import {ContractB} from '../src/ContractB.sol';

import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

contract UpgradesScript is Script {
      function setUp() public {}

    function run() public {
        uint256 deployerPrivateKey \= vm.envUint("PRIVATE\_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy \`ContractA\` as a transparent proxy using the Upgrades Plugin
        address transparentProxy \= Upgrades.deployTransparentProxy(
            "ContractA.sol",
            msg.sender,
            abi.encodeCall(ContractA.initialize, 10)
        );

    }
}

在后台,这将为我们部署代理、ProxyAdmin 和 ContractA。

deployTransparentProxy 函数接受 3 个参数:

  • contractName(string): 实现合约的名称,例如“MyContract.sol”、“MyContract.sol:MyContract”或相对工件路径。

  • initialOwner(address): 设置为 ProxyAdmin 合约的所有者地址,该合约会与代理一起自动部署。

  • initializerData(bytes): 在代理创建期间要执行的初始化函数的编码调用数据;如果不需要初始化,则留空。

该函数返回已部署代理的地址。

执行脚本

使用以下命令在 Sepolia 网络上部署并验证升级。

forge clean && forge script script/UpgradesScript.s.sol --rpc-url sepolia --private-key $PRIVATE_KEY --broadcast --verify --sender $SENDER

此命令清理以前的构建,执行脚本,并在 Sepolia Explorer 上验证合约。

执行后,你应该期望输出指示合约成功部署和验证的内容:

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Total Paid: 0.00356888630073128 ETH (862220 gas * avg 4.139182924 gwei)
##
Start verification for (3) contracts
Start verifying contract `0x427186c574B5fA11cB9d796871861EF87c74Ad37` deployed on sepolia

Submitting verification for [src/ContractA.sol:ContractA] 0x427186c574B5fA11cB9d796871861EF87c74Ad37.
Submitted contract for verification:
        Response: `OK`
        GUID: `hf2dplvhjmjpj3nixun3kupamtsgmbacngfeygpm9p34mbzb3g`
        URL: https://sepolia.etherscan.io/address/0x427186c574b5fa11cb9d796871861ef87c74ad37
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Start verifying contract `0xbA58580452Bc758C9a044584F6CEa468e5569a13` deployed on sepolia

Submitting verification for [lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy] 0xbA58580452Bc758C9a044584F6CEa468e5569a13.
Submitted contract for verification:
        Response: `OK`
        GUID: `biaqcdgrhwjfu8d7b3n9jg6btmsjpktgd61rdx42n7ptttuvre`
        URL: https://sepolia.etherscan.io/address/0xba58580452bc758c9a044584f6cea468e5569a13
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
Start verifying contract `0x8bB6A51C24ad9b6bA276c2bf0380e5E8Ce31E866` deployed on sepolia

Submitting verification for [lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol:ProxyAdmin] 0x8bB6A51C24ad9b6bA276c2bf0380e5E8Ce31E866.
Submitted contract for verification:
        Response: `OK`
        GUID: `rfnvjnxa8j2rqxxgwtnx9if17rdlyky2nk9eixrmzjbp5pn4gy`
        URL: https://sepolia.etherscan.io/address/0x8bb6a51c24ad9b6ba276c2bf0380e5e8ce31e866
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `NOTOK`
Details: `Already Verified`
Contract source code already verified
All (3) contracts were verified!

Transactions saved to: /Users/nest/rareskills/rareskills-foundry-upgrades/broadcast/UpgradesScript.s.sol/11155111/run-latest.json

Sensitive values saved to: /Users/nest/rareskills/rareskills-foundry-upgrades/cache/UpgradesScript.s.sol/11155111/run-latest.json

在此脚本中执行的 deployTransparentProxy 命令部署了 ContractA 和一个透明可升级代理以及一个 Proxy Admin 合约。

这些交易可以在 Sepolia Explorer 上查看:

升级合约

在升级到 ContractB 之前,我们将使用插件的 validateUpgrade 函数验证新实现与参考合约 ContractA 的兼容性。

一旦确认验证,接下来我们将使用 upgradeProxy 函数进行升级。请阅读下面代码中的注释:

import {Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";

function run() public {
    uint256 deployerPrivateKey \= vm.envUint("PRIVATE\_KEY");
    vm.startBroadcast(deployerPrivateKey);

    // 指定现有透明代理的地址
    address transparentProxy \= 'your-transparent-proxy-address';

     // 设置验证升级的选项
    Options memory opts;
    opts.referenceContract \= "ContractA.sol";

    // 验证升级的兼容性
    Upgrades.validateUpgrade("ContractB.sol", opts);

    // 升级到 ContractB 并尝试增加值
    Upgrades.upgradeProxy(transparentProxy, "ContractB.sol", abi.encodeCall(ContractB.increaseValue, ()));
}

如果新的合约实现与参考合约不兼容,它将抛出以下错误:

revert: Upgrade safety validation failed:

再次使用相同的脚本部署和升级 ContractB,并在 Explorer 上验证。

Etherscan foundry upgrade transaciton

升级交易可以在这里查看。

OpenZeppelin Foundry 插件如何帮助解决升级问题

本节列出了特定于代理升级的潜在安全问题以及该工具如何防范这些问题。

1. 不可变变量

在 Solidity 中,immutable 关键字允许在合约创建期间通过将其值直接嵌入到字节码中来永久设置变量。这迫使未来的部署使用完全相同的构造函数参数,以便未来的部署具有相同的字节码。由于这很难跟踪,插件不鼓励使用不可变变量。

为了在可升级合约中允许不可变变量,开发者可以使用 @custom:oz-upgrades-unsafe-allow 注释绕过此安全检查,如下所示。

contract ImmutableVar {
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    uint256 public immutable a;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(uint256 _a) {
        a = _a;
    }
}

这允许使用不可变变量,但要求谨慎管理以保持部署一致性。

2. 存储布局

在更新智能合约时,保持一致的存储布局至关重要。状态变量的顺序或类型的任何变化都可能导致数据损坏。

这意味着如果你有一个初始合约如下所示:

contract MyContract {
    uint256 private x;
    string private y;
}

那么你不能改变变量的类型:

contract MyContract {
    string private x; // uint256 变成了 string
    string private y;
}

或者改变它们声明的顺序:

contract MyContract {
    string private y; // x 和 y 交换了位置
    uint256 private x;
}

如果你需要引入一个新变量,请确保你总是在最后添加:

contract MyContract {
    uint256 private x;
    string private y;
    bytes private z;
}

使用我们在本文开头的 ContractA 和 ContractB 示例,假设我们错误地在 ContractB 中插入了一个存储变量,如下所示:

在 ContractB 升级时错误插入存储变量

我们将从 OpenZeppelin 插件获得以下错误(我们使用与第一个示例相同的测试,使用透明可升级代理):

OpenZeppelin 插件的错误,存储变量不正确

3. 验证 __gap 是否正确使用

正如我们在 存储命名空间 的文章中讨论的,使用 __gap  变量是一种防止父合约在插入新存储变量时移动子合约存储变量的策略。要正确使用__gap 变量,插入新变量时必须减少间隙的大小。在以下示例中,开发者插入了一个新的存储变量,却忘记减少 __gap 的大小:

存储命名空间中未减少的间隙变量

工具会捕捉到这一点:

OpenZeppelin 的错误,间隙变量未减少

OpenZeppelin 工具并不强制使用__gap。相反,如果存在__gap变量,工具会检查升级是否遵循 __gap变量的预期使用方式,即保持存储变量的对齐。

一种更稳健的替代__gap 策略的方法是使用命名存储布局,正如我们在 存储命名空间 教程中讨论的,我们在下一节中提供了一个示例。

4. 验证 ERC-7201 是否正确遵循

使用我们开头的 ContractA 和 ContractB 的运行示例,让我们修改合约以使用 ERC-7201。

注意以下更改:

  • value 现在是一个公共函数,而不是公共变量

  • value 的底层存储已移动到 MyStorage 结构中

  • 设置函数现在必须与 MyStorage 结构交互(注意该结构用于分组变量,实际上从未初始化)。

以下是调整后的 ContractA,以遵循 ERC-7201:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract ContractA is Initializable {

    /// @custom:storage-location erc7201:ContractA.storage.MyStorage
    struct MyStorage {
        uint256 value;
    }

    // keccak256(abi.encode(uint256(keccak256("ContractA.storage.MyStorage")) - 1)) & ~bytes32(uint256(0xff));
    bytes32 private constant MyStorageLocation = 0xd255ccbed1486709ef10c220c9b584c9ad5cacd00961bdfc2156c2c7f2e4fc00;

    function _getMyStorage() private pure returns (MyStorage storage $) {
        assembly {
            $.slot := MyStorageLocation
        }
    }

    function value() public view returns (uint256) {
        MyStorage storage $ = _getMyStorage();
        return $.value;
    }

    function initialize(uint256 _setValue) public initializer {
        MyStorage storage $ = _getMyStorage();
        $.value = _setValue;
    }
}

以及 ContractB:

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

    /// @custom:storage-location erc7201:ContractA.storage.MyStorage
    struct MyStorage {
        uint256 value;
    }

    // keccak256(abi.encode(uint256(keccak256("ContractA.storage.MyStorage")) - 1)) & ~bytes32(uint256(0xff));
    bytes32 private constant MyStorageLocation = 0xd255ccbed1486709ef10c220c9b584c9ad5cacd00961bdfc2156c2c7f2e4fc00;

    function _getMyStorage() private pure returns (MyStorage storage $) {
        assembly {
            $.slot := MyStorageLocation
        }
    }

    function value() public view returns (uint256) {
        MyStorage storage $ = _getMyStorage();
        return $.value;
    }

    function initialize(uint256 _setValue) public initializer {
        MyStorage storage $ = _getMyStorage();
        $.value = _setValue;
    }

    function increaseValue() public {
        MyStorage storage $ = _getMyStorage();
        $.value += 10;
    }
}

假设我们在 ContractB 中搞乱了 MyStorage 结构,在结构中插入了一个变量,而不是放在最后:

    /// @custom:storage-location erc7201:ContractA.storage.MyStorage
    struct MyStorage {
            uint256 badInsert; // 这不应该在这里,它应该在最后
        uint256 value;
    }

进行此更改后,工具将显示以下错误:

OpenZeppelin 的错误,结构 MyStorage 不正确

工具还会防止重命名命名空间槽。假设我们将结构上方的注释更改如下:

/// @custom:storage-location erc7201:ContractA.storage.MyStorage
/// @custom:storage-location erc7201:ContractA.storage.MyStorage2

我们现在会得到以下错误:

OpenZeppelin 错误,重命名命名空间槽

命名空间在升级之间不应被移除。

未能调用父初始化器无法自动捕获

在以下示例中,ContractA 和 ContractB 都继承自 Base。然而,它们都没有调用父类的初始化器,这是一个错误。由于理解一个函数是否作为初始化器需要语义解释,因此工具无法自动捕获此问题。开发者或审计员必须手动检查所有初始化器是否被正确调用(通常这种问题可以通过单元测试轻松捕获,如果变量被初始化为非零值)。

以下代码即使未调用 __Base_init 函数,也不会触发工具的任何问题。

无效的合约代码以调用父初始化器

结论

本文详细介绍了如何使用 OpenZeppelin 升级 Foundry 插件,插件如何用于单元测试和 Foundry 脚本,以及它如何简化部署和升级的多步骤过程。我们展示了如何设置部署环境,并给出了一些示例,说明该工具如何自动捕获各种错误。

本文由 Pari Tomar 与 RareSkills 合作撰写。

我们要感谢来自 OpenZeppelin 的 Eric Lau 对本文草稿的有益评论。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

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

0 条评论

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