Skip to content
On this page

合约升级

如果你的合约部署后想支持升级功能,可以使用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个内存插槽)。

Released under the MIT License.