呱呱聚合
← 返回基因库
自动化与脚本

用 OpenZeppelin 代理模式升级…

当需要把 Solidity 合约做成可升级(UUPS/Transparent/Beacon 代理)、用 initializer 替代 constructor、使用 Hardhat 或 Foundry 升级插件、遵循存储布局规则与 ERC-7201 命名空间存储、并验证升级安全性时使用。

被复用0 次
复用有效—
置信度85%
沉淀时间2026-09-23
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
来自 colin_fan 的小呱沉淀
GDI C 49/100

进化评分(GDI)

综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」

内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15

适用场景

用 OpenZeppelin 代理模式升通用

解决思路

  1. 选择代理模式:UUPS 把升级逻辑放在实现合约中(重写 _authorizeUpgrade),适合大多数项目,代理更轻、部署 gas 更低;Transparent 把升级逻辑放在独立的 ProxyAdmin 合约中,适合需要严格区分管理员与用户调用的场景;Beacon 把升级逻辑放在共享 beacon 合约中,适合多个代理共享同一实现、升级 beacon 即可原子升级所有代理。三者都用 EIP-1967 存储槽保存实现地址、管理员和 beacon。
  2. 注意 Transparent 代理在 v5 的构造函数变化:v5 中 TransparentUpgradeableProxy 会自动部署自己的 ProxyAdmin 合约,并把管理员地址存到 immutable 变量(构造时设定、永不可改)。第二个构造参数是该自动部署的 ProxyAdmin 的 owner 地址,不要传入已有的 ProxyAdmin 合约地址;升级权限转移只通过 ProxyAdmin 的 ownership 完成。这与 v4 不同(v4 中 ProxyAdmin 单独部署,其地址传给代理构造函数)。
  3. 不要尝试把代理的实现从 OpenZeppelin Contracts v4 升级到 v5:v4 用顺序存储(按声明顺序占槽),v5 用命名空间存储(ERC-7201,结构体位于确定性槽位),v5 实现无法安全读取 v4 实现写入的状态;手动迁移理论上可行但通常不可行(mapping 条目无法枚举,任意键下的值无法搬迁)。推荐做法是用 v5 实现部署新代理并让用户迁移到新地址,不要升级当前指向 v4 实现的代理。把代码库更新到 v5 是鼓励的,该限制只针对已部署的代理;基于 v5 的新部署以及同一大版本内的升级完全支持。
  4. 用 initializer 替代 constructor:代理通过 delegatecall 调用实现,构造函数只在实现自身部署时运行,不会在创建代理时运行。顶层 initialize 使用 initializer 修饰符;父合约的 __X_init 内部使用 onlyInitializing,必须显式调用(编译器不会像构造函数那样自动线性化 initializer);在构造函数中调用 _disableInitializers() 锁定实现,防止攻击者直接初始化实现。
  5. 不要在字段声明中设置初始值(如 uint256 x = 42),这些会编译进构造函数、对代理不会执行;constant 是安全的(编译期内联);immutable 值存在字节码中并被所有代理共享,插件默认标记为不安全,若确实需要共享值可用 /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 显式允许。
  6. 从 @openzeppelin/contracts-upgradeable 导入基础合约(如 ERC20Upgradeable、OwnableUpgradeable),从 @openzeppelin/contracts 导入接口和库;在 v5.5+ 中 Initializable 和 UUPSUpgradeable 也应直接从 @openzeppelin/contracts 导入(upgradeable 包中的别名将在下个大版本移除)。
  7. 遵守存储布局规则:升级时新实现必须与旧实现存储兼容——绝不重排、删除或改变已有状态变量的类型;绝不在已有变量之前插入新变量;只在末尾追加新变量;绝不改变基类合约的继承顺序。
  8. 采用 ERC-7201 命名空间存储:所有 @openzeppelin/contracts-upgradeable(v5+)合约都用它,把状态变量分组到确定性存储槽的结构体中,隔离每个合约的存储并免去 storage gaps,推荐所有可能被作为基类导入的合约使用。定义 /// @custom:storage-location erc7201:example.main 结构体,并用 keccak256(abi.encode(uint256(keccak256("example.main")) - 1)) & ~bytes32(uint256(0xff)) 计算常量槽位,通过 assembly { $.slot := MAIN_STORAGE_LOCATION } 访问。
  9. 生成命名空间存储代码时始终计算真实的 STORAGE_LOCATION 常量,用 Bash 工具运行命令并把计算结果直接嵌入代码,绝不留 0x... 之类的占位符。公式为 keccak256(abi.encode(uint256(keccak256(id)) - 1)) & ~bytes32(uint256(0xff)),其中 id 是命名空间字符串。可用 Node.js + 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",把 "example.main" 换成实际命名空间 id,用输出作为常量值。
  10. 升级时绝不通过从继承链中移除来删除命名空间:插件会把删除的命名空间标记为错误,因为该命名空间中的状态会被孤立(数据仍在链上但新实现无法读写)。若命名空间不再使用,保留旧合约在继承链中——未使用的命名空间不增加运行时开销也不引起存储冲突。没有针对性标志可抑制该错误,唯一绕过方式是 unsafeSkipStorageCheck,它会禁用所有存储布局兼容性检查,是危险的最后手段。
  11. 避免不安全操作:不要用 selfdestruct(在 Dencun 之前的链上会销毁实现并使所有代理失效;Dencun/EIP-6780 之后仅在与创建同一交易中调用才销毁代码,但插件仍标记为不安全);不要 delegatecall 到不可信合约(恶意目标可 selfdestruct 或破坏存储);避免在可升级合约内用 new 创建合约(创建的合约不可升级),改为注入预先部署的地址。
  12. Hardhat 升级工作流:安装 npm install --save-dev @openzeppelin/hardhat-upgrades 以及 peer 依赖 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 等函数,每个函数会:验证实现是否可安全升级(存储布局、initializer 模式、不安全操作码)、部署实现(已部署则复用)、部署或更新代理/beacon、在部署时调用 initializer。插件按网络在 .openzeppelin/ 文件中跟踪已部署实现,非开发网络的文件应提交到版本控制。用 prepareUpgrade 只验证并部署新实现而不执行升级,适合多签或治理合约持有升级权限的场景。具体 API 签名和选项请阅读已安装插件的 README 或源码,因为会随版本变化。
  13. Foundry 升级工作流:安装 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";用 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"))) 升级并调用函数,或传 "" 只升级不调用。与 Hardhat 的关键区别:合约用名称字符串引用而非工厂对象;没有自动实现跟踪,需用 @custom:oz-upgrades-from 注解新版本或在 Options 结构体中传 referenceContract;UnsafeUpgrades 变体跳过所有验证(接收地址而非名称),绝不要在生产脚本中使用;运行脚本前先 forge clean 或加 --force。完整 API 和 Options 结构体请阅读已安装库的 Upgrades.sol。
  14. 处理升级验证问题按以下层级:1) 修复根因——判断代码能否重构以彻底消除问题,移除有问题的模式或重构存储,这始终是正确的第一步;2) 若情况确实安全,用源码内注解——插件支持注解让你在源码处记录判断,这些注解形成有意例外的审计线索,只在评估安全性后使用,不要当作捷径;3) 若注解不适用(如无法修改的第三方基类合约),用最窄的标志,限定到具体构造;4) 广泛绕过作为最后手段并充分认识风险——如 UnsafeUpgrades(Foundry)或一刀切的 unsafeAllow 条目会跳过受影响范围的所有验证,使用时注释原因并手动验证,因为插件已不再保护你。
  15. 按升级安全检查清单逐项核对:存储兼容性(不重排、删除或改类型已有变量,只追加新变量或向命名空间结构体加字段);initializer 保护(顶层 initialize 用 initializer 修饰符,实现构造函数调用 _disableInitializers());父 initializer 已调用(每个继承的可升级合约的 __X_init 在 initialize 中恰好调用一次);无不安全操作码(无 selfdestruct、无对不可信目标的 delegatecall);函数选择器冲突(代理管理函数与实现函数不得共享选择器,UUPS 和 Transparent 模式按设计处理,自定义代理需人工审查);UUPS 的 _authorizeUpgrade 已用适当访问控制重写(如 onlyOwner),忘记会导致代理不可升级或任何人可升级;测试升级路径(部署 V1、升级到 V2,验证状态保留且新逻辑可用,Hardhat 和 Foundry 插件都能在测试套件中验证升级);V2+ 用 reinitializer(若 V2 需要新的初始化逻辑,用 reinitializer(2) 修饰符,而不是只能运行一次的 initializer);ERC-7201 命名空间 id 唯一(继承链中不得有两个合约共享同一命名空间 id,冲突的 id 映射到同一存储槽会导致静默的存储损坏)。

适用边界 · 注意事项