upgrade-solidity-contracts
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSolidity Upgrades
Solidity合约升级
Contents
目录
Proxy Patterns Overview
代理模式概述
| Pattern | Upgrade logic lives in | Best for |
|---|---|---|
UUPS ( | Implementation contract (override | Most projects — lighter proxy, lower deploy gas |
| Transparent | Separate | When admin/user call separation is critical — admin cannot accidentally call implementation functions |
| Beacon | Shared beacon contract | Multiple 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,automatically deploys its ownTransparentUpgradeableProxycontract 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-deployedProxyAdmin— do not pass an existingProxyAdmincontract address here. Transfer of upgrade capability is handled exclusively throughProxyAdminownership. This differs from v4, whereProxyAdminwas deployed separately and its address was passed to the proxy constructor.ProxyAdmin
| 模式 | 升级逻辑所在位置 | 适用场景 |
|---|---|---|
UUPS( | 实现合约(需重写 | 大多数项目——代理更轻量,部署gas成本更低 |
| Transparent | 独立的 | 需严格区分管理员/用户调用场景——管理员不会意外调用实现合约的函数 |
| Beacon | 共享的Beacon合约 | 多个代理共享同一实现——升级Beacon可一次性完成所有代理的升级 |
三种模式均使用EIP-1967存储槽来存储实现合约地址、管理员地址和Beacon地址。
Transparent代理——v5构造函数变更:在v5版本中,会自动部署专属的TransparentUpgradeableProxy合约,并将管理员地址存储在不可变变量中(在构造时设置,不可修改)。构造函数的第二个参数是该自动部署的ProxyAdmin的所有者地址——请勿在此处传入已有的ProxyAdmin合约地址。升级权限的转移完全通过ProxyAdmin的所有权管理实现。这与v4版本不同,v4中ProxyAdmin是单独部署的,其地址会传入代理的构造函数。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 — entries cannot be enumerated, so values written under arbitrary keys cannot be relocated.
mappingRecommended 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 uses the
initializemodifierinitializer - Parent init functions () use
__X_initinternally — call them explicitly, the compiler does not auto-linearize initializers like constructorsonlyInitializing - Always call in a constructor to prevent attackers from initializing the implementation directly
_disableInitializers() - Do not set initial values in field declarations (e.g., ) — these compile into the constructor and won't execute for the proxy.
uint256 x = 42is safe (inlined at compile time).constantvalues are stored in bytecode and shared across all proxies — the plugins flag them as unsafe by default; useimmutableto opt in when a shared value is intended/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
代理合约通过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 for base contracts (e.g., , ). Import interfaces and libraries from . In v5.5+, and should also be imported directly from — aliases in the upgradeable package will be removed in the next major release.
@openzeppelin/contracts-upgradeableERC20UpgradeableOwnableUpgradeable@openzeppelin/contractsInitializableUUPSUpgradeable@openzeppelin/contracts从导入基础合约(如、)。从导入接口和库。在v5.5+版本中,和也应直接从导入——可升级合约包中的别名将在下一个主版本中移除。
@openzeppelin/contracts-upgradeableERC20UpgradeableOwnableUpgradeable@openzeppelin/contractsInitializableUUPSUpgradeable@openzeppelin/contractsStorage 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 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.
@openzeppelin/contracts-upgradeablesolidity
/// @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 , which disables all storage layout compatibility checks and is a dangerous last resort.
unsafeSkipStorageCheck这是现代方案——所有合约(v5+)均使用该方案。状态变量被分组到结构体中,存储在确定性槽位,隔离每个合约的存储,无需使用存储间隙。推荐所有可能被作为基础合约导入的合约使用该方案。
@openzeppelin/contracts-upgradeablesolidity
/// @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];
}相比传统存储间隙的优势:可安全地向基础合约添加变量,继承顺序变更不会破坏布局,每个合约的存储完全隔离。
升级时,不要通过从继承链中移除合约来删除命名空间。插件会将已删除的命名空间标记为错误——该命名空间中存储的状态会变成“孤儿数据”:数据仍在链上,但新的实现合约无法读取或写入。如果某个命名空间不再被主动使用,需将旧合约保留在继承链中。未使用的命名空间不会增加运行时成本,也不会导致存储冲突。目前没有针对性的标记来抑制此错误;唯一的绕过方式是,它会禁用所有存储布局兼容性检查,是一种危险的最后手段。
unsafeSkipStorageCheckComputing ERC-7201 storage locations
计算ERC-7201存储位置
When generating namespaced storage code, always compute the actual 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 .
STORAGE_LOCATION0x...The formula is: where is the namespace string (e.g., ).
keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff))id"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 with the actual namespace id, run the command, and use the output as the constant value.
"example.main"生成命名空间存储代码时,务必计算实际的常量。请使用Bash工具运行以下命令,传入实际的命名空间ID,并将计算出的值直接嵌入生成的代码中。绝对不要保留这类占位符值。
STORAGE_LOCATION0x...计算公式为:,其中是命名空间字符串(如)。
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"将替换为实际的命名空间ID,运行命令后将输出结果作为常量值使用。
"example.main"Unsafe operations
不安全操作
- No — on pre-Dencun chains, destroys the implementation and bricks all proxies. Post-Dencun (EIP-6780),
selfdestructonly destroys code if called in the same transaction as creation, but the plugins still flag it as unsafeselfdestruct - No to untrusted contracts — a malicious target could
delegatecallor corrupt storageselfdestruct
Additionally, avoid using to create contracts inside an upgradeable contract — the created contract won't be upgradeable. Inject pre-deployed addresses instead.
new- 禁止使用——在Dencun升级前的链上,该操作会销毁实现合约并导致所有代理失效。Dencun升级后(EIP-6780),
selfdestruct仅在创建合约的同一交易中调用时才会销毁代码,但插件仍会将其标记为不安全操作selfdestruct - 禁止向不可信合约发起——恶意目标合约可能执行
delegatecall或破坏存储selfdestruct
此外,避免在可升级合约内部使用创建合约——创建的合约将无法升级。应注入预部署的合约地址替代。
newHardhat Upgrades Workflow
Hardhat升级工作流
Install the plugin:
bash
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers # peer dependenciesRegister in :
hardhat.configjavascript
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades'; // TSWorkflow concept — the plugin provides functions on the object (, , , , ). Each function:
upgradesdeployProxyupgradeProxydeployBeaconupgradeBeacondeployBeaconProxy- Validates the implementation for upgrade safety (storage layout, initializer patterns, unsafe opcodes)
- Deploys the implementation (reuses if already deployed)
- Deploys or updates the proxy/beacon
- Calls the initializer (on deploy)
The plugin tracks deployed implementations in per-network files. Commit non-development network files to version control.
.openzeppelin/Use to validate and deploy a new implementation without executing the upgrade — useful when a multisig or governance contract holds upgrade rights.
prepareUpgradeRead 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.configjavascript
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades'; // TS工作流概念——插件在对象上提供函数(、、、、)。每个函数会:
upgradesdeployProxyupgradeProxydeployBeaconupgradeBeacondeployBeaconProxy- 验证实现合约的升级安全性(存储布局、初始化模式、不安全操作码)
- 部署实现合约(若已部署则复用)
- 部署或更新代理/Beacon
- 调用初始化函数(部署时)
插件会按网络将已部署的实现合约记录在目录下的文件中。请将非开发网络的文件提交到版本控制系统。
.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-upgradeableConfigure :
foundry.tomltoml
[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 or pass
@custom:oz-upgrades-fromin thereferenceContractstructOptions - variant skips all validation (takes addresses instead of names) — never use in production scripts
UnsafeUpgrades - Run or use
forge cleanbefore running scripts--force
Read the installed library'sfor the full API andUpgrades.solstruct.Options
安装依赖:
bash
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable配置:
foundry.tomltoml
[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结构体中传入OptionsreferenceContract - 变体跳过所有验证(接收地址而非名称)——绝对不要在生产脚本中使用
UnsafeUpgrades - 运行脚本前请执行或使用
forge clean参数--force
请查阅已安装库的获取完整API和Upgrades.sol结构体说明。Options
Handling Upgrade Validation Issues
处理升级验证问题
When the plugins flag a warning or error, work through this hierarchy:
- 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.
- 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.
- 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.
- Broad bypass as a last resort, with full awareness of the risk. Options like (Foundry) or blanket
UnsafeUpgradesentries skip all validation for the affected scope. If you use them, comment why, and verify manually — the plugin is no longer protecting you.unsafeAllow
当插件标记警告或错误时,请按以下优先级处理:
- 修复根本原因。判断是否可重构代码以彻底消除问题——移除有问题的模式或重构存储。这始终是首选方案。
- 若确认场景安全,使用代码内注解。若无法重构且已确认标记的模式实际安全,插件支持在源码中添加注解来记录此判断。请查阅已安装插件的文档了解可用注解。这些注解会创建清晰的审计轨迹,记录有意的例外情况——仅在评估安全性后使用,不要作为捷径。
- 若注解无法解决,使用窄范围标记。某些情况(如无法修改的第三方基础合约)无法通过源码处理。请使用最具针对性的标记,仅作用于特定结构。
- 万不得已时使用宽范围绕过,且需充分了解风险。如Foundry的或Hardhat的全局
UnsafeUpgrades条目会跳过影响范围内的所有验证。若使用这些选项,请添加注释说明原因,并手动验证——插件将不再提供保护。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 uses
initializemodifier. Implementation constructor callsinitializer._disableInitializers() - Parent initializers called: Every inherited upgradeable contract's is called exactly once in
__X_init.initialize - No unsafe opcodes: No or
selfdestructto untrusted targets.delegatecall - 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 : Overridden with proper access control (e.g.,
_authorizeUpgrade). Forgetting this makes the proxy non-upgradeable or upgradeable by anyone.onlyOwner - 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 modifier (not
reinitializer(2), which can only run once).initializer - 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会映射到同一存储槽,导致静默的存储损坏。