如何使用 OpenZeppelin Contracts Upgradeable 包:以 initializer 替换构造函数并部署代理合约?
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
如果你要把合约部署成可升级的(例如配合 OpenZeppelin Upgrades Plugins 使用),就不能直接用@openzeppelin/contracts里的普通合约,而要改用专门的 Upgradeable 变体包@openzeppelin/contracts-upgradeable。这个任务包含三步:安装 Upgradeable 包、把构造函数改写为 initializer 函数、用 Upgrades Plugins 部署代理合约。本文基于仓库中的 upgradeable 文档、Initializable 源码 和 Proxy 模块说明 给出完整的操作路径。
一个必须先知道的前提:OpenZeppelin Contracts 用语义化版本来承诺 API 与存储布局的向后兼容,不同大版本之间的存储布局应视为不兼容,例如从 4.9.3 升级到 5.0.0 是不安全的(见 index 文档)。所以升级前先在文档中确认版本约束。
安装 Upgradeable 包
Upgradeable 变体是独立发布的 npm 包@openzeppelin/contracts-upgradeable,并且以@openzeppelin/contracts作为 peer dependency,因此两个包要一起安装:
$ npm install @openzeppelin/contracts-upgradeable @openzeppelin/contracts包的结构与主包一致,只是每个文件和合约名都带Upgradeable后缀。唯一的例外:接口(interface)和库(library)不包含在 Upgradeable 包里,仍从主包@openzeppelin/contracts导入。
把构造函数替换为 initializer 函数
改写分两部分:导入与继承,以及构造函数改写成 initializer。
以 ERC721 为例,导入和继承的改动是:
-import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -contract MyCollectible is ERC721 { +contract MyCollectible is ERC721Upgradeable {构造函数的改写遵循固定命名约定:Upgradeable 包中每个合约的构造函数对应一个内部函数__{ContractName}_init。因为它是internal的,你必须在自己的合约里定义一个public的 initializer 函数,并调用父合约的 init 函数:
- constructor() ERC721("MyCollectible", "MCO") { + function initialize() initializer public { + __ERC721_init("MyCollectible", "MCO"); }initializer修饰符保证该函数最多只能被成功调用一次,这是代理部署替代构造函数的核心保护机制(见 Initializable.sol)。
为什么部署前建议锁定实现合约
Initializable 源码 的文档明确警告:未初始化的合约可能被攻击者接管,这对代理和它背后的实现合约都适用。为防止实现合约本身被(误)初始化,推荐在构造器中调用_disableInitializers()自动上锁:
/// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); }_disableInitializers()会把合约锁定,阻止其被初始化到任何版本,文档建议用于"设计为通过代理调用的实现合约"。
用 Upgrades Plugins 部署代理合约
合约写好并编译后,用 OpenZeppelin Upgrades Plugins 部署代理。Proxy 模块说明 指出:正确使用升级代理需要深入理解代理模式、Solidity 和 EVM,除非你想做低层控制,否则推荐直接使用 Hardhat 和 Foundry 的 Upgrades Plugins。
以下是 upgradeable 文档 给出的 Hardhat 部署脚本示例,放在scripts/目录下:
// scripts/deploy-my-collectible.js const { ethers, upgrades } = require("hardhat"); async function main() { const MyCollectible = await ethers.getContractFactory("MyCollectible"); const mc = await upgrades.deployProxy(MyCollectible); await mc.waitForDeployment(); console.log("MyCollectible deployed to:", await mc.getAddress()); } main();执行脚本后,终端会打印代理地址(上面console.log输出的MyCollectible deployed to:即为代理合约地址),这是文档给出的部署完成标志。
部署后如何验证初始化成功
两个来自文档的验证依据:
Initialized事件。initializer修饰符在成功初始化后会发出event Initialized(uint64 version)事件(见 Initializable.sol)。在部署交易的回执中确认该事件,版本为 1,说明initialize()已成功执行过一次。- 重复调用应失败。
initializer修饰符的语义是最多调用一次,重复调用会 revertInvalidInitialization()。如果意外发现还能再次调用成功,说明保护机制没有生效,属于异常情况。
另外注意底层代理的行为:如果不用 Upgrades Plugins 而直接使用 ERC1967Proxy,其构造函数要求传入_data(即编码好的初始化调用),_data为空时构造会失败——文档建议把initialize的编码调用作为_data尽早传入,以避免代理停留在未初始化状态。使用deployProxy时插件会自动处理这一步。
限制与边界
- 多继承需要特别注意。initializer 函数不像构造函数那样由编译器线性化,每个
__{ContractName}_init内嵌了对所有父合约 initializer 的线性化调用,因此两个init函数可能把同一个合约初始化两次。每个合约都提供__{ContractName}_init_unchained(即去掉父调用后的 initializer),可以手动规避双重初始化,但文档不推荐手动这么做。 - 命名空间存储(ERC-7201)。Upgradeable 包中的合约用带
@custom:storage-location erc7201:<NAMESPACE_ID>注解的 struct 存放状态变量,每个合约拥有独立的存储命名空间。这使得日后新增状态变量不会"推移"继承链中下方变量的存储位置,从而保持存储布局兼容。 - 大版本存储不兼容。跨大版本(如 4.x 到 5.0.0)升级前,按 Backwards Compatibility 文档确认存储布局,并使用 Upgrades Plugins 检查存储兼容性。
完成以上步骤后,你就有了一个已完成初始化、地址可从部署日志或Initialized事件中确认的代理合约。后续的升级操作(如upgradeProxy到新版本实现、用reinitializer(n)初始化新增模块)属于 OpenZeppelin Upgrades Plugins 文档的范畴,本仓库 upgradeable 文档 将其列为延伸阅读。
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考