OpenZeppelin Upgrades Core & CLI

@openzeppelin/upgrades-core 包提供了一个 validate 命令,用于检查可升级合约的升级安全性和存储布局兼容性。它可以在你的整个开发过程中使用,以确保你的合约是升级安全的并且与以前的版本兼容。

它还提供了以编程方式执行这些检查的 API,并包含用于使用 OpenZeppelin Upgrades 插件执行这些检查的核心逻辑。

CLI: Validate 命令

从包含构建信息文件的目录中检测可升级合约,并验证它们是否升级安全。 如果你想从命令行、脚本或 CI/CD 管道中验证你项目的所有可升级合约,请使用此选项。

提示: "构建信息文件" 由你的编译工具链(Hardhat,Foundry)生成,并包含编译过程的输入和输出。

先决条件

在使用 validate 命令之前,你必须定义可升级合约以便可以检测和验证它们,定义用于存储布局比较的参考合约,并编译你的合约。

定义可升级合约

validate 命令对看起来像可升级合约的合约执行升级安全检查。 具体来说,它对满足以下任何条件的实现合约执行检查:

  • 继承 Initializable

  • 具有 upgradeTo(address)upgradeToAndCall(address,bytes) 函数。 对于继承 UUPSUpgradeable 的合约就是这种情况。

  • 具有 NatSpec 注释 @custom:oz-upgrades

  • 具有根据 定义参考合约 中的 NatSpec 注释 @custom:oz-upgrades-from <reference>

提示: 只需将 NatSpec 注释 @custom:oz-upgrades@custom:oz-upgrades-from <reference> 添加到每个实现合约,以便它可以被检测为可升级合约以进行验证。

定义参考合约

重要提示: 如果实现合约旨在作为现有代理的升级进行部署,你 必须 定义一个参考合约以进行存储布局比较。 否则,如果存在任何存储布局不兼容的情况,你将不会收到错误。

通过将 NatSpec 注释 @custom:oz-upgrades-from <reference> 添加到你的实现合约来定义参考合约,其中 <reference> 是用于存储布局比较的参考合约的合约名称或完全限定的合约名称。 该合约不需要在作用域内,并且如果合约名称在整个项目中不明确,则合约名称就足够了。

示例:

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

编译具有存储布局的合约

编译你的合约并确保你的构建已配置为在构建信息目录中输出具有 Solidity 编译器输入和输出的 JSON 文件。 编译器输出必须包括存储布局。 如果存在任何先前的构建工件,则必须先清除它们以避免重复的合约定义。

Hardhat

配置 hardhat.config.jshardhat.config.ts 以在输出选择中包含存储布局:

module.exports = {
  solidity: {
    settings: {
      outputSelection: {
        '*': {
          '*': ['storageLayout'],
        },
      },
    },
  },
};

然后编译你的合约:

npx hardhat clean && npx hardhat compile
Foundry

配置 foundry.toml 以包括构建信息和存储布局:

[profile.default]
build_info = true
extra_output = ["storageLayout"]

然后编译你的合约:

forge clean && forge build

用法

在执行先决条件后,运行 npx @openzeppelin/upgrades-core validate 命令以验证你的合约:

npx @openzeppelin/upgrades-core validate [<BUILD_INFO_DIR>] [<OPTIONS>]

如果发现任何错误,该命令将以非零退出代码退出,并将错误的详细报告打印到控制台。

参数:

  • <BUILD_INFO_DIR> - 可选的构建信息目录的路径,其中包含带有 Solidity 编译器输入和输出的 JSON 文件。 对于 Hardhat 项目,默认为 artifacts/build-info,对于 Foundry 项目,默认为 out/build-info。 如果你的项目使用自定义输出目录,则必须在此处指定其构建信息目录。

选项:

  • --contract <CONTRACT> - 要验证的合约的名称或完全限定的名称。 如果未指定,将验证构建信息目录中的所有可升级合约。

  • --reference <REFERENCE_CONTRACT> - 只能在同时提供 --contract 选项时使用。 用于存储布局比较的参考合约的名称或完全限定的名称。 如果未指定,则如果正在验证的合约中定义了 @custom:oz-upgrades-from 注释,则使用该注释。

  • --requireReference - 只能在同时提供 --contract 选项时使用。 与 --unsafeSkipStorageCheck 不兼容。 如果指定,则要求提供 --reference 选项或合约具有 @custom:oz-upgrades-from 注释。

  • --referenceBuildInfoDirs "<BUILD_INFO_DIR>[,<BUILD_INFO_DIR>…​]" - 用于存储布局比较的项目的早期版本的可选构建信息目录路径。 使用此选项时,在使用 --reference 选项或 @custom:oz-upgrades-from 注释中的合约名称或完全限定名称之前,使用前缀 <dirName>: 引用这些目录之一,其中 <dirName> 是目录短名称。 每个目录短名称必须是唯一的,包括与主构建信息目录相比。 如果传入多个目录,请用逗号分隔它们或多次调用该选项,每个目录一次。

  • --exclude "<GLOB_PATTERN>" [--exclude "<GLOB_PATTERN>"…​] - 排除对源文件路径中与任何给定 glob 模式匹配的合约的验证。 例如,--exclude "contracts/mocks/**/*.sol"。 不适用于参考合约。 如果传入多个模式,请多次调用该选项,每个模式一次。

  • --unsafeAllow "<VALIDATION_ERROR>[,<VALIDATION_ERROR>…​]" - 选择性地禁用一个或多个验证错误或警告。 以逗号分隔的列表,包含以下一个或多个:

    • 错误:state-variable-assignment, state-variable-immutable, external-library-linking, struct-definition, enum-definition, constructor, delegatecall, selfdestruct, missing-public-upgradeto, internal-function-storage, missing-initializer, missing-initializer-call, duplicate-initializer-call

    • 警告:incorrect-initializer-order

  • --unsafeAllowRenames - 配置存储布局检查以允许变量重命名。

  • --unsafeSkipStorageCheck - 跳过检查存储布局兼容性错误。 这是一个危险的选项,旨在作为最后的手段使用。

高级 API

高级 API 在编程上等效于 validate 命令。 如果你想从 JavaScript 或 TypeScript 环境中验证你项目的所有可升级合约,请使用此 API。

先决条件

validate 命令 相同的先决条件。

用法

导入 validateUpgradeSafety 函数:

import { validateUpgradeSafety } from '@openzeppelin/upgrades-core';

然后调用该函数以验证你的合约并获取包含验证结果的项目报告。

validateUpgradeSafety

validateUpgradeSafety(
  buildInfoDir?: string,
  contract?: string,
  reference?: string,
  opts: ValidateUpgradeSafetyOptions = {},
  referenceBuildInfoDirs?: string[],
  exclude?: string[],
): Promise<ProjectReport>

从构建信息目录中检测可升级合约,并验证它们是否升级安全。 返回一个带有结果的 项目报告

请注意,此函数不会直接抛出验证错误。 相反,你必须使用项目报告来确定是否找到任何错误。

参数:

  • buildInfoDir - 构建信息目录的路径,其中包含带有 Solidity 编译器输入和输出的 JSON 文件。 对于 Hardhat 项目,默认为 artifacts/build-info,对于 Foundry 项目,默认为 out/build-info。 如果你的项目使用自定义输出目录,则必须在此处指定其构建信息目录。

  • contract - 要验证的合约的名称或完全限定的名称。 如果未指定,将验证构建信息目录中的所有可升级合约。

  • reference - 只能在同时提供 contract 参数时使用。 用于存储布局比较的参考合约的名称或完全限定的名称。 如果未指定,则如果正在验证的合约中定义了 @custom:oz-upgrades-from 注释,则使用该注释。

  • opts - 一个对象,其中包含 通用选项 中定义的以下选项:

    • unsafeAllow

    • unsafeAllowRenames

    • unsafeSkipStorageCheck

    • requireReference - 只能在同时提供 contract 参数时使用。 与 unsafeSkipStorageCheck 选项不兼容。 如果指定,则要求提供 reference 参数或合约具有 @custom:oz-upgrades-from 注释。

  • referenceBuildInfoDirs - 用于存储布局比较的项目的早期版本的可选构建信息目录路径。 使用此选项时,在使用 reference 参数或 @custom:oz-upgrades-from 注释中的合约名称或完全限定名称之前,使用前缀 <dirName>: 引用这些目录之一,其中 <dirName> 是目录短名称。 每个目录短名称必须是唯一的,包括与主构建信息目录相比。

  • exclude - 排除对源文件路径中与任何给定 glob 模式匹配的合约的验证。

返回值:

ProjectReport

interface ProjectReport {
  ok: boolean;
  explain(color?: boolean): string;
  numPassed: number;
  numTotal: number;
}

一个对象,表示升级安全检查和存储布局比较的结果,并包含所有已发现错误的报告。

成员:

  • ok - 如果发现任何错误,则为 false,否则为 true

  • explain() - 返回一条消息,详细说明错误(如果有)。

  • numPassed - 通过升级安全检查的合约数量。

  • numTotal - 检测到的可升级合约的总数。

低级 API

注意: 此低级 API 已弃用。 请改用 高级 API

低级 API 使用 Solidity 输入和输出 JSON 对象,并允许你对单个合约执行升级安全检查和存储布局比较。 如果你想验证特定的合约而不是整个项目,请使用此 API。

先决条件

编译你的合约以生成 Solidity 输入和输出 JSON 对象。 编译器输出必须包括存储布局。

请注意,不需要 validate 命令 中的其他先决条件,因为低级 API 不会自动检测可升级合约。 相反,你必须为你想要验证的每个实现合约创建一个 UpgradeableContract 实例,并调用它上面的函数以获取升级安全性和存储布局报告。

用法

导入 UpgradeableContract 类:

import { UpgradeableContract } from '@openzeppelin/upgrades-core';

然后,为你想要验证的每个实现合约创建一个 UpgradeableContract 实例,并调用它上面的 .getErrorReport() 和/或 .getStorageLayoutReport() 以分别获取升级安全性和存储布局报告。

UpgradeableContract

此类表示可升级合约的实现,并提供对错误报告的访问。

constructor UpgradeableContract
constructor UpgradeableContract(
  name: string,
  solcInput: SolcInput,
  solcOutput: SolcOutput,
  opts?: {
    unsafeAllow?: ValidationError[],
    unsafeAllowRenames?: boolean,
    unsafeSkipStorageCheck?: boolean,
    kind?: 'uups' | 'transparent' | 'beacon',
  },
  solcVersion?: string,
): UpgradeableContract

创建 UpgradeableContract 的新实例。

参数:

  • name - 实现合约的名称,可以是完全限定的名称或合约名称。 如果多个合约具有相同的名称,则必须使用完全限定的名称,例如 contracts/Bar.sol:Bar

  • solcInput - 实现合约的 Solidity 输入 JSON 对象。

  • solcOutput - 实现合约的 Solidity 输出 JSON 对象。

  • opts - 一个对象,其中包含 通用选项 中定义的以下选项:

    • kind

    • unsafeAllow

    • unsafeAllowRenames

    • unsafeSkipStorageCheck

  • solcVersion - 用于编译实现合约的 Solidity 版本。

提示: 在 Hardhat 中,solcInputsolcOutput 可以从构建信息文件中获取,构建信息文件本身可以使用 hre.artifacts.getBuildInfo 检索。

.getErrorReport
getErrorReport(): Report

返回值:

  • 关于与代理合约相关的错误的报告,例如使用 selfdestruct

.getStorageUpgradeReport
getStorageUpgradeReport(
  upgradedContract: UpgradeableContract,
  opts?: {
    unsafeAllow?: ValidationError[],
    unsafeAllowRenames?: boolean,
    unsafeSkipStorageCheck?: boolean,
    kind?: 'uups' | 'transparent' | 'beacon',
  },
): Report

将可升级合约的存储布局与提议的升级的存储布局进行比较。

参数:

  • upgradedContract - UpgradeableContract 的另一个实例,表示提议的升级。

  • opts - 一个对象,其中包含 通用选项 中定义的以下选项:

    • kind

    • unsafeAllow

    • unsafeAllowRenames

    • unsafeSkipStorageCheck

返回值:

  • 关于与代理合约相关的错误的报告,例如使用 selfdestruct 和存储布局冲突。

Report

interface Report {
  ok: boolean;
  explain(color?: boolean): string;
}

一个对象,表示分析的结果。

成员:

  • ok - 如果发现任何错误,则为 false,否则为 true

  • explain() - 返回一条消息,详细说明错误(如果有)。