upgrade-solidity-contracts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Solidity Upgrades

Solidity合约升级

Contents

目录

Proxy Patterns Overview

代理模式概述

PatternUpgrade logic lives inBest for
UUPS (
UUPSUpgradeable
)
Implementation contract (override
_authorizeUpgrade
)
Most projects — lighter proxy, lower deploy gas
TransparentSeparate
ProxyAdmin
contract
When admin/user call separation is critical — admin cannot accidentally call implementation functions
BeaconShared beacon contractMultiple proxies sharing one implementation — upgrading the beacon atomically upgrades all proxies
All three use EIP-1967 storage slots for the implementation address, admin, and beacon.
Transparent proxy — v5 constructor change: In v5,
TransparentUpgradeableProxy
automatically deploys its own
ProxyAdmin
contract and stores the admin address in an immutable variable (set at construction time, never changeable). The second constructor parameter is the owner address for that auto-deployed
ProxyAdmin
— do not pass an existing
ProxyAdmin
contract address here. Transfer of upgrade capability is handled exclusively through
ProxyAdmin
ownership. This differs from v4, where
ProxyAdmin
was deployed separately and its address was passed to the proxy constructor.
模式升级逻辑所在位置适用场景
UUPS
UUPSUpgradeable
实现合约(需重写
_authorizeUpgrade
大多数项目——代理更轻量,部署gas成本更低
Transparent独立的
ProxyAdmin
合约
需严格区分管理员/用户调用场景——管理员不会意外调用实现合约的函数
Beacon共享的Beacon合约多个代理共享同一实现——升级Beacon可一次性完成所有代理的升级
三种模式均使用EIP-1967存储槽来存储实现合约地址、管理员地址和Beacon地址。
Transparent代理——v5构造函数变更:在v5版本中,
TransparentUpgradeableProxy
会自动部署专属的
ProxyAdmin
合约,并将管理员地址存储在不可变变量中(在构造时设置,不可修改)。构造函数的第二个参数是该自动部署的
ProxyAdmin
所有者地址——请勿在此处传入已有的
ProxyAdmin
合约地址。升级权限的转移完全通过
ProxyAdmin
的所有权管理实现。这与v4版本不同,v4中
ProxyAdmin
是单独部署的,其地址会传入代理的构造函数。

Upgrade Restrictions Between Major Versions (v4 → v5)

主版本间的升级限制(v4 → v5)

Upgrading a proxy's implementation from one using OpenZeppelin Contracts v4 to one using v5 is not supported.
v4 uses sequential storage (slots in declaration order); v5 uses namespaced storage (ERC-7201, structs at deterministic slots). A v5 implementation cannot safely read state written by a v4 implementation. Manual data migration is theoretically possible but often infeasible —
mapping
entries cannot be enumerated, so values written under arbitrary keys cannot be relocated.
Recommended approach: Deploy new proxies with v5 implementations and migrate users to the new address — do not upgrade proxies that currently point to v4 implementations.
Updating your codebase to v5 is encouraged. The restriction above applies only to already-deployed proxies. New deployments built on v5, and upgrades within the same major version, are fully supported.
不支持将代理的实现合约从基于OpenZeppelin Contracts v4的版本升级到基于v5的版本。
v4使用顺序存储(按声明顺序分配存储槽);v5使用命名空间存储(ERC-7201,结构体存储在确定性槽位)。v5的实现合约无法安全读取v4实现合约写入的状态。理论上可手动迁移数据,但通常不可行——
mapping
条目无法枚举,因此无法重定位任意键下存储的值。
推荐方案:部署基于v5实现合约的新代理,并引导用户迁移至新地址——不要升级当前指向v4实现合约的代理。
建议将代码库升级到v5版本。上述限制仅适用于已部署的代理。基于v5的新部署以及同一主版本内的升级均完全支持。

Writing Upgradeable Contracts

编写可升级合约

Use initializers instead of constructors

使用初始化函数替代构造函数

Proxy contracts delegatecall into the implementation. Constructors run only when the implementation itself is deployed, not when a proxy is created. Replace constructors with initializer functions:
solidity
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers(); // lock the implementation
    }

    function initialize(address initialOwner) public initializer {
        __ERC20_init("MyToken", "MTK");
        __Ownable_init(initialOwner);
    }
}
Key rules:
  • Top-level
    initialize
    uses the
    initializer
    modifier
  • Parent init functions (
    __X_init
    ) use
    onlyInitializing
    internally — call them explicitly, the compiler does not auto-linearize initializers like constructors
  • Always call
    _disableInitializers()
    in a constructor to prevent attackers from initializing the implementation directly
  • Do not set initial values in field declarations (e.g.,
    uint256 x = 42
    ) — these compile into the constructor and won't execute for the proxy.
    constant
    is safe (inlined at compile time).
    immutable
    values are stored in bytecode and shared across all proxies — the plugins flag them as unsafe by default; use
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    to opt in when a shared value is intended
代理合约通过delegatecall调用实现合约。构造函数仅在实现合约自身部署时执行,不会在代理创建时执行。请用初始化函数替代构造函数:
solidity
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers(); // 锁定实现合约
    }

    function initialize(address initialOwner) public initializer {
        __ERC20_init("MyToken", "MTK");
        __Ownable_init(initialOwner);
    }
}
核心规则:
  • 顶层
    initialize
    函数需使用
    initializer
    修饰器
  • 父类初始化函数(
    __X_init
    )内部使用
    onlyInitializing
    ——需显式调用这些函数,编译器不会像构造函数那样自动线性化初始化逻辑
  • 务必在构造函数中调用
    _disableInitializers()
    ,防止攻击者直接初始化实现合约
  • 不要在字段声明中设置初始值(如
    uint256 x = 42
    )——这些代码会编译到构造函数中,不会为代理执行。
    constant
    是安全的(编译时内联)。
    immutable
    值存储在字节码中,所有代理共享——插件默认会标记它们为不安全;若确实需要共享值,可使用
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    来开启权限

Use the upgradeable package

使用可升级合约包

Import from
@openzeppelin/contracts-upgradeable
for base contracts (e.g.,
ERC20Upgradeable
,
OwnableUpgradeable
). Import interfaces and libraries from
@openzeppelin/contracts
. In v5.5+,
Initializable
and
UUPSUpgradeable
should also be imported directly from
@openzeppelin/contracts
— aliases in the upgradeable package will be removed in the next major release.
@openzeppelin/contracts-upgradeable
导入基础合约(如
ERC20Upgradeable
OwnableUpgradeable
)。从
@openzeppelin/contracts
导入接口和库。在v5.5+版本中,
Initializable
UUPSUpgradeable
也应直接从
@openzeppelin/contracts
导入——可升级合约包中的别名将在下一个主版本中移除。

Storage layout rules

存储布局规则

When upgrading, the new implementation must be storage-compatible with the old one:
  • Never reorder, remove, or change the type of existing state variables
  • Never insert new variables before existing ones
  • Only append new variables at the end
  • Never change the inheritance order of base contracts
升级时,新的实现合约必须与旧合约的存储布局兼容:
  • 绝对不要重新排序、删除或修改现有状态变量的类型
  • 绝对不要在现有变量前插入新变量
  • 仅能在末尾追加新变量
  • 绝对不要修改基础合约的继承顺序

Namespaced storage (ERC-7201)

命名空间存储(ERC-7201)

The modern approach — all
@openzeppelin/contracts-upgradeable
contracts (v5+) use this. State variables are grouped into a struct at a deterministic storage slot, isolating each contract's storage and eliminating the need for storage gaps. Recommended for all contracts that may be imported as base contracts.
solidity
/// @custom:storage-location erc7201:example.main
struct MainStorage {
    uint256 value;
    mapping(address => uint256) balances;
}

// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant MAIN_STORAGE_LOCATION = 0x...;

function _getMainStorage() private pure returns (MainStorage storage $) {
    assembly { $.slot := MAIN_STORAGE_LOCATION }
}
Using a variable from namespaced storage:
solidity
function _getBalance(address account) internal view returns (uint256) {
    MainStorage storage $ = _getMainStorage();
    return $.balances[account];
}
Benefits over legacy storage gaps: safe to add variables to base contracts, inheritance order changes don't break layout, each contract's storage is fully isolated.
When upgrading, never remove a namespace by dropping it from the inheritance chain. The plugin flags deleted namespaces as an error — the state stored in that namespace becomes orphaned: the data remains on-chain but the new implementation has no way to read or write it. If a namespace is no longer actively used, keep the old contract in the inheritance chain. An unused namespace adds no runtime cost and causes no storage conflict. There is no targeted flag to suppress this error; the only bypass is
unsafeSkipStorageCheck
, which disables all storage layout compatibility checks and is a dangerous last resort.
这是现代方案——所有
@openzeppelin/contracts-upgradeable
合约(v5+)均使用该方案。状态变量被分组到结构体中,存储在确定性槽位,隔离每个合约的存储,无需使用存储间隙。推荐所有可能被作为基础合约导入的合约使用该方案。
solidity
/// @custom:storage-location erc7201:example.main
struct MainStorage {
    uint256 value;
    mapping(address => uint256) balances;
}

// keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant MAIN_STORAGE_LOCATION = 0x...;

function _getMainStorage() private pure returns (MainStorage storage $) {
    assembly { $.slot := MAIN_STORAGE_LOCATION }
}
使用命名空间存储中的变量:
solidity
function _getBalance(address account) internal view returns (uint256) {
    MainStorage storage $ = _getMainStorage();
    return $.balances[account];
}
相比传统存储间隙的优势:可安全地向基础合约添加变量,继承顺序变更不会破坏布局,每个合约的存储完全隔离。
升级时,不要通过从继承链中移除合约来删除命名空间。插件会将已删除的命名空间标记为错误——该命名空间中存储的状态会变成“孤儿数据”:数据仍在链上,但新的实现合约无法读取或写入。如果某个命名空间不再被主动使用,需将旧合约保留在继承链中。未使用的命名空间不会增加运行时成本,也不会导致存储冲突。目前没有针对性的标记来抑制此错误;唯一的绕过方式是
unsafeSkipStorageCheck
,它会禁用所有存储布局兼容性检查,是一种危险的最后手段。

Computing ERC-7201 storage locations

计算ERC-7201存储位置

When generating namespaced storage code, always compute the actual
STORAGE_LOCATION
constant. Use the Bash tool to run the command below with the actual namespace id and embed the computed value directly in the generated code. Never leave placeholder values like
0x...
.
The formula is:
keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))
where
id
is the namespace string (e.g.,
"example.main"
).
Node.js with ethers:
bash
node -e "const{keccak256,toUtf8Bytes,zeroPadValue,toBeHex}=require('ethers');const id=process.argv[1];const h=BigInt(keccak256(toUtf8Bytes(id)))-1n;console.log(toBeHex(BigInt(keccak256(zeroPadValue(toBeHex(h),32)))&~0xffn,32))" "example.main"
Replace
"example.main"
with the actual namespace id, run the command, and use the output as the constant value.
生成命名空间存储代码时,务必计算实际的
STORAGE_LOCATION
常量。请使用Bash工具运行以下命令,传入实际的命名空间ID,并将计算出的值直接嵌入生成的代码中。绝对不要保留
0x...
这类占位符值。
计算公式为:
keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))
,其中
id
是命名空间字符串(如
"example.main"
)。
使用Node.js和ethers
bash
node -e "const{keccak256,toUtf8Bytes,zeroPadValue,toBeHex}=require('ethers');const id=process.argv[1];const h=BigInt(keccak256(toUtf8Bytes(id)))-1n;console.log(toBeHex(BigInt(keccak256(zeroPadValue(toBeHex(h),32)))&~0xffn,32))" "example.main"
"example.main"
替换为实际的命名空间ID,运行命令后将输出结果作为常量值使用。

Unsafe operations

不安全操作

  • No
    selfdestruct
    — on pre-Dencun chains, destroys the implementation and bricks all proxies. Post-Dencun (EIP-6780),
    selfdestruct
    only destroys code if called in the same transaction as creation, but the plugins still flag it as unsafe
  • No
    delegatecall
    to untrusted contracts — a malicious target could
    selfdestruct
    or corrupt storage
Additionally, avoid using
new
to create contracts inside an upgradeable contract — the created contract won't be upgradeable. Inject pre-deployed addresses instead.
  • 禁止使用
    selfdestruct
    ——在Dencun升级前的链上,该操作会销毁实现合约并导致所有代理失效。Dencun升级后(EIP-6780),
    selfdestruct
    仅在创建合约的同一交易中调用时才会销毁代码,但插件仍会将其标记为不安全操作
  • 禁止向不可信合约发起
    delegatecall
    ——恶意目标合约可能执行
    selfdestruct
    或破坏存储
此外,避免在可升级合约内部使用
new
创建合约——创建的合约将无法升级。应注入预部署的合约地址替代。

Hardhat Upgrades Workflow

Hardhat升级工作流

Install the plugin:
bash
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers  # peer dependencies
Register in
hardhat.config
:
javascript
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades';   // TS
Workflow concept — the plugin provides functions on the
upgrades
object (
deployProxy
,
upgradeProxy
,
deployBeacon
,
upgradeBeacon
,
deployBeaconProxy
). Each function:
  1. Validates the implementation for upgrade safety (storage layout, initializer patterns, unsafe opcodes)
  2. Deploys the implementation (reuses if already deployed)
  3. Deploys or updates the proxy/beacon
  4. Calls the initializer (on deploy)
The plugin tracks deployed implementations in
.openzeppelin/
per-network files. Commit non-development network files to version control.
Use
prepareUpgrade
to validate and deploy a new implementation without executing the upgrade — useful when a multisig or governance contract holds upgrade rights.
Read the installed plugin's README or source for exact API signatures and options, as these evolve across versions.
安装插件:
bash
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers  # 依赖包
hardhat.config
中注册:
javascript
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades';   // TS
工作流概念——插件在
upgrades
对象上提供函数(
deployProxy
upgradeProxy
deployBeacon
upgradeBeacon
deployBeaconProxy
)。每个函数会:
  1. 验证实现合约的升级安全性(存储布局、初始化模式、不安全操作码)
  2. 部署实现合约(若已部署则复用)
  3. 部署或更新代理/Beacon
  4. 调用初始化函数(部署时)
插件会按网络将已部署的实现合约记录在
.openzeppelin/
目录下的文件中。请将非开发网络的文件提交到版本控制系统。
使用
prepareUpgrade
可验证并部署新的实现合约,但不执行升级——当多签或治理合约持有升级权限时,此方法非常实用。
请查阅已安装插件的README或源码获取准确的API签名和选项,这些内容会随版本迭代而更新。

Foundry Upgrades Workflow

Foundry升级工作流

Install dependencies:
bash
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
Configure
foundry.toml
:
toml
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
Node.js is required — the library shells out to the OpenZeppelin Upgrades CLI for validation.
Import and use in scripts/tests:
solidity
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

// Deploy
address proxy = Upgrades.deployUUPSProxy(
    "MyContract.sol",
    abi.encodeCall(MyContract.initialize, (args))
);

// IMPORTANT: Before upgrading, annotate MyContractV2 with: /// @custom:oz-upgrades-from MyContract

// Upgrade and call a function
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", abi.encodeCall(MyContractV2.foo, ("arguments for foo")));

// Upgrade without calling a function
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", "");
Key differences from Hardhat:
  • Contracts are referenced by name string, not factory object
  • No automatic implementation tracking — annotate new versions with
    @custom:oz-upgrades-from
    or pass
    referenceContract
    in the
    Options
    struct
  • UnsafeUpgrades
    variant skips all validation (takes addresses instead of names) — never use in production scripts
  • Run
    forge clean
    or use
    --force
    before running scripts
Read the installed library's
Upgrades.sol
for the full API and
Options
struct.
安装依赖:
bash
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
配置
foundry.toml
toml
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
需要安装Node.js——该库会调用OpenZeppelin Upgrades CLI进行验证。
在脚本/测试中导入并使用:
solidity
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";

// 部署
address proxy = Upgrades.deployUUPSProxy(
    "MyContract.sol",
    abi.encodeCall(MyContract.initialize, (args))
);

// 重要提示:升级前,请为MyContractV2添加注解:/// @custom:oz-upgrades-from MyContract

// 升级并调用函数
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", abi.encodeCall(MyContractV2.foo, ("arguments for foo")));

// 仅升级不调用函数
Upgrades.upgradeProxy(proxy, "MyContractV2.sol", "");
与Hardhat的主要区别:
  • 通过名称字符串引用合约,而非工厂对象
  • 不会自动跟踪实现合约——需为新版本添加
    @custom:oz-upgrades-from
    注解,或在
    Options
    结构体中传入
    referenceContract
  • UnsafeUpgrades
    变体跳过所有验证(接收地址而非名称)——绝对不要在生产脚本中使用
  • 运行脚本前请执行
    forge clean
    或使用
    --force
    参数
请查阅已安装库的
Upgrades.sol
获取完整API和
Options
结构体说明。

Handling Upgrade Validation Issues

处理升级验证问题

When the plugins flag a warning or error, work through this hierarchy:
  1. Fix the root cause. Determine whether the code can be restructured to eliminate the concern entirely — remove the problematic pattern or refactor storage. This is always the right first step.
  2. Use in-code annotations if the situation is genuinely safe. If restructuring isn't appropriate and you've determined the flagged pattern is actually safe, the plugins support annotations that let you document that judgment directly at the source. Check the installed plugin's docs for what's available. These annotations create a clear audit trail of intentional exceptions — use them only after evaluating the safety, not as a shortcut.
  3. Use a narrow flag if an annotation won't work. Some cases (e.g., a third-party base contract you can't modify) can't be addressed in source. Use the most targeted flag available, scoped to the specific construct.
  4. Broad bypass as a last resort, with full awareness of the risk. Options like
    UnsafeUpgrades
    (Foundry) or blanket
    unsafeAllow
    entries skip all validation for the affected scope. If you use them, comment why, and verify manually — the plugin is no longer protecting you.
当插件标记警告或错误时,请按以下优先级处理:
  1. 修复根本原因。判断是否可重构代码以彻底消除问题——移除有问题的模式或重构存储。这始终是首选方案。
  2. 若确认场景安全,使用代码内注解。若无法重构且已确认标记的模式实际安全,插件支持在源码中添加注解来记录此判断。请查阅已安装插件的文档了解可用注解。这些注解会创建清晰的审计轨迹,记录有意的例外情况——仅在评估安全性后使用,不要作为捷径。
  3. 若注解无法解决,使用窄范围标记。某些情况(如无法修改的第三方基础合约)无法通过源码处理。请使用最具针对性的标记,仅作用于特定结构。
  4. 万不得已时使用宽范围绕过,且需充分了解风险。如Foundry的
    UnsafeUpgrades
    或Hardhat的全局
    unsafeAllow
    条目会跳过影响范围内的所有验证。若使用这些选项,请添加注释说明原因,并手动验证——插件将不再提供保护。

Upgrade Safety Checklist

升级安全检查清单

  • Storage compatibility: No reordering, removal, or type changes of existing variables. Only append new variables (or add fields to namespaced structs).
  • Initializer protection: Top-level
    initialize
    uses
    initializer
    modifier. Implementation constructor calls
    _disableInitializers()
    .
  • Parent initializers called: Every inherited upgradeable contract's
    __X_init
    is called exactly once in
    initialize
    .
  • No unsafe opcodes: No
    selfdestruct
    or
    delegatecall
    to untrusted targets.
  • Function selector clashes: Proxy admin functions and implementation functions must not share selectors. UUPS and Transparent patterns handle this by design; custom proxies need manual review.
  • UUPS
    _authorizeUpgrade
    : Overridden with proper access control (e.g.,
    onlyOwner
    ). Forgetting this makes the proxy non-upgradeable or upgradeable by anyone.
  • Test the upgrade path: Deploy V1, upgrade to V2, verify state is preserved and new logic works. Both Hardhat and Foundry plugins can validate upgrades in test suites.
  • Reinitializer for V2+: If V2 needs new initialization logic, use
    reinitializer(2)
    modifier (not
    initializer
    , which can only run once).
  • Unique ERC-7201 namespace ids: No two contracts in the inheritance chain share the same namespace id. Colliding ids map to the same storage slot, causing silent storage corruption.
  • 存储兼容性:不要重新排序、删除或修改现有变量的类型。仅可追加新变量(或向命名空间结构体添加字段)。
  • 初始化函数保护:顶层
    initialize
    函数使用
    initializer
    修饰器。实现合约的构造函数调用
    _disableInitializers()
  • 调用父类初始化函数:每个继承的可升级合约的
    __X_init
    函数在
    initialize
    中被精确调用一次。
  • 无不安全操作码:不使用
    selfdestruct
    ,也不向不可信目标发起
    delegatecall
  • 函数选择器无冲突:代理管理员函数与实现合约的函数不得共享选择器。UUPS和Transparent模式会自动处理此问题;自定义代理需手动检查。
  • UUPS的
    _authorizeUpgrade
    :已重写并配置适当的访问控制(如
    onlyOwner
    )。遗漏此步骤会导致代理无法升级或任何人都可升级。
  • 测试升级路径:部署V1版本,升级到V2版本,验证状态是否保留且新逻辑正常工作。Hardhat和Foundry插件均可在测试套件中验证升级。
  • V2+版本使用重初始化函数:若V2版本需要新的初始化逻辑,请使用
    reinitializer(2)
    修饰器(不要使用
    initializer
    ,它仅能运行一次)。
  • ERC-7201命名空间ID唯一:继承链中的任意两个合约不得使用相同的命名空间ID。冲突的ID会映射到同一存储槽,导致静默的存储损坏。