upgrade-solidity-contracts by openzeppelin/openzeppelin-skills
npx skills add https://github.com/openzeppelin/openzeppelin-skills --skill upgrade-solidity-contracts| 模式 | 升级逻辑位于 | 最适合 |
|---|---|---|
UUPS (UUPSUpgradeable) | 实现合约 (重写 _authorizeUpgrade) | 大多数项目 — 代理更轻量,部署 gas 更低 |
| 透明代理 | 独立的 ProxyAdmin 合约 | 当管理员/用户调用分离至关重要时 — 管理员不会意外调用实现函数 |
| 信标代理 | 共享的信标合约 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 多个代理共享一个实现 — 升级信标会原子性地升级所有代理 |
所有三种模式都使用 EIP-1967 存储槽来存放实现地址、管理员和信标。
透明代理 — v5 构造函数变更: 在 v5 中,
TransparentUpgradeableProxy会自动部署其自身的ProxyAdmin合约,并将管理员地址存储在一个不可变变量中(在构造时设置,永远不可更改)。第二个构造函数参数是那个自动部署的ProxyAdmin的所有者地址 — 请不要在此处传递现有的ProxyAdmin合约地址。升级能力的转移完全通过ProxyAdmin的所有权来处理。这与 v4 不同,在 v4 中ProxyAdmin是单独部署的,其地址被传递给代理构造函数。
不支持将代理的实现从使用 OpenZeppelin Contracts v4 升级到使用 v5。
v4 使用顺序存储(按声明顺序的存储槽);v5 使用命名空间存储(ERC-7201,位于确定性存储槽的结构体)。v5 实现无法安全地读取 v4 实现写入的状态。理论上可以进行手动数据迁移,但通常不可行 — mapping 条目无法枚举,因此无法重新定位在任意键下写入的值。
推荐方法: 部署使用 v5 实现的新代理,并将用户迁移到新地址 — 不要升级当前指向 v4 实现的代理。
鼓励将您的代码库更新到 v5。 上述限制仅适用于已部署的代理。完全支持基于 v5 的新部署,以及同一主要版本内的升级。
代理合约通过 delegatecall 调用实现合约。构造函数仅在实现合约本身部署时运行,而不是在创建代理时运行。用初始化函数替换构造函数:
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 来选择启用从 @openzeppelin/contracts-upgradeable 导入基础合约(例如,ERC20Upgradeable、OwnableUpgradeable)。从 @openzeppelin/contracts 导入接口和库。在 v5.5+ 中,Initializable 和 UUPSUpgradeable 也应直接从 @openzeppelin/contracts 导入 — 可升级包中的别名将在下一个主要版本中移除。
升级时,新实现必须与旧实现存储兼容:
现代方法 — 所有 @openzeppelin/contracts-upgradeable 合约(v5+)都使用此方法。状态变量被分组到一个位于确定性存储槽的结构体中,隔离了每个合约的存储,并消除了对存储间隙的需求。推荐用于所有可能作为基础合约被导入的合约。
/// @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 }
}
使用命名空间存储中的变量:
function _getBalance(address account) internal view returns (uint256) {
MainStorage storage $ = _getMainStorage();
return $.balances[account];
}
相比传统存储间隙的优势:可以安全地向基础合约添加变量,继承顺序更改不会破坏布局,每个合约的存储完全隔离。
升级时,切勿通过从继承链中删除命名空间来移除它。插件会将删除的命名空间标记为错误 — 存储在该命名空间中的状态将变为孤立状态:数据保留在链上,但新实现无法读取或写入它。如果一个命名空间不再被主动使用,请将旧合约保留在继承链中。未使用的命名空间不会增加运行时成本,也不会引起存储冲突。没有针对性的标志来抑制此错误;唯一的绕过方法是使用 unsafeSkipStorageCheck,它会禁用所有存储布局兼容性检查,是一种危险的最后手段。
生成命名空间存储代码时,始终计算实际的 STORAGE_LOCATION 常量。使用 Bash 工具运行以下命令,使用实际的命名空间 id,并将计算出的值直接嵌入生成的代码中。切勿留下像 0x... 这样的占位符值。
公式是:keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)),其中 id 是命名空间字符串(例如,"example.main")。
使用 ethers 的 Node.js:
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,运行命令,并将输出用作常量值。
selfdestruct — 在 Dencun 之前的链上,会销毁实现合约并使所有代理失效。Dencun 之后(EIP-6780),selfdestruct 仅在创建合约的同一交易中被调用时才会销毁代码,但插件仍会将其标记为不安全delegatecall 调用不受信任的合约 — 恶意目标可能会 selfdestruct 或破坏存储此外,避免在可升级合约内部使用 new 创建合约 — 创建的合约将不可升级。应注入预先部署的地址。
安装插件:
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers # 对等依赖
在 hardhat.config 中注册:
require('@openzeppelin/hardhat-upgrades'); // JS
import '@openzeppelin/hardhat-upgrades'; // TS
工作流概念 — 插件在 upgrades 对象上提供函数 (deployProxy、upgradeProxy、deployBeacon、upgradeBeacon、deployBeaconProxy)。每个函数:
插件在 .openzeppelin/ 目录下的每个网络文件中跟踪已部署的实现。请将非开发网络的文件提交到版本控制。
使用 prepareUpgrade 来验证和部署新实现,而不执行升级 — 当多签或治理合约持有升级权限时很有用。
请阅读已安装插件的 README 或源代码以获取确切的 API 签名和选项,因为这些内容会随着版本更新而变化。
安装依赖:
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
配置 foundry.toml:
[profile.default]
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
需要 Node.js — 该库会调用 OpenZeppelin Upgrades CLI 进行验证。
在脚本/测试中导入并使用:
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 结构体中传递 referenceContractUnsafeUpgrades 变体跳过所有验证(接收地址而不是名称)— 切勿在生产脚本中使用forge clean 或使用 --force请阅读已安装库的
Upgrades.sol以获取完整的 API 和Options结构体。
当插件标记警告或错误时,请按此层次结构处理:
UnsafeUpgrades(Foundry)或通用的 unsafeAllow 条目这样的选项会跳过受影响范围内的所有验证。如果使用它们,请注释原因,并手动验证 — 插件不再保护您。initialize 函数使用 initializer 修饰符。实现合约构造函数调用 _disableInitializers()。__X_init 在 initialize 中恰好被调用一次。selfdestruct 或 delegatecall 到不受信任的目标。_authorizeUpgrade:已用适当的访问控制(例如,onlyOwner)重写。忘记这一点会使代理不可升级或任何人都可升级。reinitializer(2) 修饰符(而不是 initializer,它只能运行一次)。每周安装量
128
代码仓库
GitHub 星标数
158
首次出现
2026年3月5日
安全审计
安装于
opencode127
gemini-cli95
codex95
kimi-cli95
cline95
github-copilot95
| Pattern | Upgrade logic lives in | Best for |
|---|---|---|
UUPS (UUPSUpgradeable) | Implementation contract (override _authorizeUpgrade) | Most projects — lighter proxy, lower deploy gas |
| Transparent | Separate ProxyAdmin contract | 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,
TransparentUpgradeableProxyautomatically deploys its ownProxyAdmincontract 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.
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.
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:
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:
initialize uses the initializer modifier__X_init) use onlyInitializing internally — call them explicitly, the compiler does not auto-linearize initializers like constructors_disableInitializers() in a constructor to prevent attackers from initializing the implementation directlyuint256 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 intendedImport 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.
When upgrading, the new implementation must be storage-compatible with the old one:
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.
/// @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:
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.
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 :
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.
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 unsafedelegatecall to untrusted contracts — a malicious target could selfdestruct or corrupt storageAdditionally, avoid using new to create contracts inside an upgradeable contract — the created contract won't be upgradeable. Inject pre-deployed addresses instead.
Install the plugin:
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers # peer dependencies
Register in hardhat.config:
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:
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.
Install dependencies:
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
Configure foundry.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:
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:
@custom:oz-upgrades-from or pass referenceContract in the Options structUnsafeUpgrades variant skips all validation (takes addresses instead of names) — never use in production scriptsforge clean or use --force before running scriptsRead the installed library's
Upgrades.solfor the full API andOptionsstruct.
When the plugins flag a warning or error, work through this hierarchy:
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.initialize uses initializer modifier. Implementation constructor calls _disableInitializers().__X_init is called exactly once in initialize.selfdestruct or delegatecall to untrusted targets.Weekly Installs
128
Repository
GitHub Stars
158
First Seen
Mar 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode127
gemini-cli95
codex95
kimi-cli95
cline95
github-copilot95
Azure RBAC 权限管理工具:查找最小角色、创建自定义角色与自动化分配
135,700 周安装
_authorizeUpgradeonlyOwnerreinitializer(2) modifier (not initializer, which can only run once).