Appearance
合约升级
如果你的合约部署后想支持升级功能,可以使用OpenZeppelin升级插件,你需要用到OpenZeppelin合约的可升级变量。 此变量是在一个独立的模块中@openzeppelin/contracts-upgradeable
,你可以在软件仓库中得到它OpenZeppelin/openzeppelin-contracts-upgradeable。 它遵循了编写可升级合约的原则:构造函数由初始化方法代替,状态变量在初始化方法中进行初始化,同时我们还增加了升级后镜像版本与当前版本存储一致性检查。
提示
OpenZeppelin提供一整套部署,编写安全的可升级合约工具。可以在这里获取
概览
安装
sh
$ npm install @openzeppelin/contracts-upgradeable
示例
可升级合约包的结构复制了主要的OpenZeppelin合约,只是在每个合约加了Upgradeable
后缀。
solidity
-import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
-contract MyCollectible is ERC721 {
+contract MyCollectible is ERC721Upgradeable {
构造函数将由内部的初始化方法代替,方法名按照惯例叫__{ContractName}_init
。因为这些都是内部方法,因此你需要定义一个自己的公开的初始化方法,并在其中调用所继承父合约的初始化方法。
solidity
- constructor() ERC721("MyCollectible", "MCO") public {
+ function initialize() initializer public {
+ __ERC721_init("MyCollectible", "MCO");
}
警告
使用多重继承需要特别注意。具体内容可以参考多重继承。
当合约编写完毕并编译后,你可以通过升级插件来进行部署。下面的代码片段展示了用Hardhat来部署合约的示例。
js
// 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.deployed();
console.log("MyCollectible deployed to:", mc.address);
}
main();
进一步说明
多重继承
初始化方法并不会像构造函数一样被编译器顺序执行。因此,每一个__{ContractName}_init
方法都会嵌入到父初始化方法中被顺序调用。这样的话,调用这两个init
方法可能会导致初始化相同的合约两次。
每个合约中的__{ContractName}_init_unchained
初始化方法,是减去了对父合约初始化方法的调用,这样可以避免双重初始化的问题,但是这种手工处理的方式并不推荐。我们希望在将来的升级插件中能实现对此的安全检查。
存储缝隙
你可能已经发现每个合约中都包含一个__gap
的状态变量。这是在存储空间中预设的,为了存放可升级合约。目的是为了将来在升级合约时,添加新的状态变量后,不用对已部署的老合约中的存储一致性进行折中处理。
简单的添加一个状态变量是不安全的,因为它会“下移”已继承的链上的所有状态变量。这会让存储空间方式不一致,例如在编写可升级合约中说明的。__gap
数组的大小是通过计算得出的,因此合约使用的存储空间量通常加起来是一个固定数(在这里是50个内存插槽)。