使用 OpenZeppelin 升级插件部署的合约具备可升级的特性:可以升级以修改其代码,同时保留其地址,状态和余额。这使你可以迭代地向项目中添加新功能,或修复在线上版本中可能发现的任何错误。
在本文中,我们将展示使用 OpenZeppelin Truffle 升级插件和 Gnosis Safe 的生命周期,包含从创建合约,测试合约、部署合约一直到使用 Gnosis Safe 进行升级整个过程:
· 创建可升级合约
· 本地测试合约
· 将合约部署到公共网络
· 转移升级权限到 Gnosis Safe 多签
· 实现一个新的升级版本
· 本地测试升级版本
· 部署新的升级版本
· 升级合约
配置开发环境
我们将从创建一个新的 npm 项目开始:
mkdir mycontract && cd mycontract
npm init -y
安装并初始化 Truffle。注意:我们需要使用 Truffle 5.1.35 或更高版本。
npm i –save-dev truffle
npx truffle init
安装 Truffle 升级插件。
npm i –save-dev @openzeppelin/truffle-upgrades
创建可升级合约
我们将使用OpenZeppelin 学习指南[3]中最受欢迎的 Box 合约。使用以下Solidity[4]代码在你的Contracts目录中创建 Box.sol。
注意,可升级合约使用`initialize`函数而不是构造函数[5]来初始化状态。为了保持简单,任何帐户都可以多次调用的公开的store函数来初始化状态(而不是受保护的一次性 initialize 函数)。
Box.sol
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
本地测试合约
我们的合约应该始终有相应的测试。要测试合约,我们应该为合约实现创建单元测试。
我们将在测试中使用 chai(一个 Js 测试框架),因此首先需要安装它。
npm i –save-dev chai
我们将为合约实现创建单元测试。使用以下 JavaScript 在你的test目录中创建Box.test.js。
Box.test.js
// test/Box.test.js
// 加载依赖
const { expect } = require(‘chai’);
// Load compiled artifacts
const Box = artifacts.require(‘Box’);
// Start test block
contract(‘Box’, function () {
beforeEach(async function () {
// 为每个测试部署一个新的Box合约
this.box = await Box.new();
});
// 测试用
it(‘retrieve returns a value previously stored’, async function () {
// Store a value
await this.box.store(42);
// 测试是否返回了同一个设置的值
// Note that we need to use strings to compare the 256 bit integers
expect((await this.box.retrieve()).toString()).to.equal(’42’);
});
});
我们还可以通过(升级)代理创建测试进行交互。
注意:我们不需要在此处重复单元测试,这是为了测试代理交互和测试升级。
使用以下 JavaScript 在你的test目录中创建Box.proxy.test.js。
Box.proxy.test.js
// test/Box.proxy.test.js
// Load dependencies
const { expect } = require(‘chai’);
const { deployProxy } = require(‘@openzeppelin/truffle-upgrades’);
// Load compiled artifacts
const Box = artifacts.require(‘Box’);
// Start test block
contract(‘Box (proxy)’, function () {
beforeEach(async function () {
// 为每个测试部署一个新的Box合约
this.box = await deployProxy(Box, [42], {initializer: ‘store’});
});
// 测试用例
it(‘retrieve returns a value previously initialized’, async function () {
// 测试是否返回了同一个设置的值
// 注意需要使用字符串去对比256位的整数
expect((await this.box.retrieve()).toString()).to.equal(’42’);
});
});
在我们编译合约之前,我们需要在truffle-config.js中将 solc 版本更改为^0.7.0,因为我们的合约标记为pragma solidity ^0.7.0
然后,我们可以运行测试。
$ npx truffle test
…
Contract: Box (proxy)
✓ retrieve returns a value previously initialized (43ms)
Contract: Box
✓ retrieve returns a value previously stored (100ms)
2 passing (3s)
将合约部署到公共网络
我们将使用Truffle 迁移[6]来部署 Box 合约。Truffle 升级插件提供了一个 deployProxy功能来部署可升级合约。它将部署我们实现的合约,ProxyAdmin 会作为项目代理和代理管理员,并调用(任何的)初始化函数。
在 migrations 目录中创建以下2_deploy_box.js脚本。
在本文中,我们还没有initialize函数,因此我们将使用store 函数来初始化状态。
2_deploy_box.js
// migrations/2_deploy_box.js
const Box = artifacts.require(‘Box’);
const { deployProxy } = require(‘@openzeppelin/truffle-upgrades’);
module.exports = async function (deployer) {
await deployProxy(Box, [42], { deployer, initializer: ‘store’ });
};
我们通常先将合约部署到本地测试环境(例如ganache-cli),然后手动与之交互。为了节省时间,我们将跳过这步,而直接部署到公共测试网络。
在本文中,我们将部署到 Rinkeby。如果需要配置方面的帮助,请参阅使用 Truffle 连接到公共测试网络[7]。注意:助记符或 Infura 项目 ID 之类的机密内容都不应提交给版本控制。
使用 Rinkeby 网络运行truffle migration进行部署。我们可以看到 3 个合约:Box.sol、ProxyAdmin 和 代理合约AdminUpgradeabilityProxy。
$ npx truffle migrate –network rinkeby
…
2_deploy_box.js
===============
Deploying ‘Box’
—————
> transaction hash: 0x3263d01ce2e3eb4ba51abf882abbdd9252364b51eb972f82958719d60a8b9ebe
> Blocks: 0 Seconds: 5
> contract address: 0xd568071213Ea31B01AA2247BC9eC7285087cf882
…
Deploying ‘ProxyAdmin’
———————-
> transaction hash: 0xf39e8cb97c332b8bbdf0c66b13f26a9a3dc97b207d2caec73ba6df8d5bb6b211
> Blocks: 1 Seconds: 17
> contract address: 0x2A210B6d5EffC0A3BB47dD3791a4C26B8E31f161
…
Deploying ‘AdminUpgradeabilityProxy’
————————————
> transaction hash: 0x439711597b694f03b1065582ab44ac0bea5e22b0c6e3c460ae7b4536f004c355
> Blocks: 1 Seconds: 17
> contract address: 0xF325bB49f91445F97241Ec5C286f90215a7E3BC6
…
我们可以使用 Truffle 控制台(truffle console)与我们的合约进行交互。
注意: Box.deployed() 是我们的代理合约的地址。
$ npx truffle console –network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> box.address
‘0xF325bB49f91445F97241Ec5C286f90215a7E3BC6’
truffle(rinkeby)> (await box.retrieve()).toString()
’42’
转移升级权限到 Gnosis Safe 多签
我们将使用 Gnosis Safe 来控制合约的升级。
译者注:Gnosis Safe 是一款多签名钱包,可设置满足 n/m (例如:2/3)的签名才可以进行交易。
首先,我们需要在 Rinkeby 网络上为自己创建一个 Gnosis Safe。可参考文档:创建 Safe Multisig[8]的说明。为简单起见,在本文中,本例使用 1/1,在正式产品中,你应考虑使用至少 2/3。
在 Rinkeby 上创建 Gnosis Safe 之后,请复制地址,以便我们转移所有权。
当前代理的管理员(可以执行升级)是 ProxyAdmin 合约。只有 ProxyAdmin 的所有者可以升级代理。警告:ProxyAdmin 所有权转移时请确保转到我们控制的地址上。
使用以下 JavaScript 在migrations目录中创建3_transfer_ownership.js。将 gnosisSafe 的值更改为你的 Gnosis Safe 地址。
3_transfer_ownership.js
// migrations/3_transfer_ownership.js
const { admin } = require(‘@openzeppelin/truffle-upgrades’);
module.exports = async function (deployer, network) {
// 使用你的 Gnosis Safe 地址
const gnosisSafe = ‘0x1c14600daeca8852BA559CC8EdB1C383B8825906’;
// Don’t change ProxyAdmin ownership for our test network
if (network !== ‘test’) {
// The owner of the ProxyAdmin can upgrade our contracts
await admin.transferProxyAdminOwnership(gnosisSafe);
}
};
我们可以在 Rinkeby 网络上运行迁移。
$ npx truffle migrate –network rinkeby
…
3_transfer_ownership.js
=======================
> Saving migration to chain.
————————————-
…
实现一个新的升级版本
一段时间后,我们决定要向合约添加功能。在本文中,我们将添加一个increment函数。
注意:我们无法更改之前合约实现的存储布局,有关技术限制的更多详细信息,请参阅升级[9]。
使用以下 Solidity 代码在你的contracts目录中创建新的实现BoxV2.sol 。
BoxV2.sol
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract BoxV2 {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
// Increments the stored value by 1
function increment() public {
value = value + 1;
emit ValueChanged(value);
}
}
本地测试升级的版本
为了测试我们的升级版本,我们应该为新的合约创建单元测试,并为通过代理测试交互,并检查升级之间是否保持状态。
我们将为新的合约实现创建单元测试。我们可以在已经创建的单元测试中添加新测试,以确保高覆盖率。使用以下 JavaScript 在你的test目录中创建BoxV2.test.js。
BoxV2.test.js
// test/BoxV2.test.js
// Load dependencies
const { expect } = require(‘chai’);
// Load compiled artifacts
const BoxV2 = artifacts.require(‘BoxV2’);
// Start test block
contract(‘BoxV2’, function () {
beforeEach(async function () {
// Deploy a new BoxV2 contract for each test
this.boxV2 = await BoxV2.new();
});
// Test case
it(‘retrieve returns a value previously stored’, async function () {
// Store a value
await this.boxV2.store(42);
// 测试是否返回了同一个设置的值
// 注意需要使用字符串去对比256位的整数
expect((await this.boxV2.retrieve()).toString()).to.equal(’42’);
});
// Test case
it(‘retrieve returns a value previously incremented’, async function () {
// Increment
await this.boxV2.increment();
// 测试是否返回了同一个设置的值
// 注意需要使用字符串去对比256位的整数
expect((await this.boxV2.retrieve()).toString()).to.equal(‘1’);
});
});
升级后,我们还可以通过代理进行交互来创建测试。注意:我们不需要在此处重复单元测试,这是为了测试代理交互和测试升级后的状态。
使用以下 JavaScript 在你的test目录中创建BoxV2.proxy.test.js。
BoxV2.proxy.test.js
// test/Box.proxy.test.js
// Load dependencies
const { expect } = require(‘chai’);
const { deployProxy, upgradeProxy} = require(‘@openzeppelin/truffle-upgrades’);
// Load compiled artifacts
const Box = artifacts.require(‘Box’);
const BoxV2 = artifacts.require(‘BoxV2’);
// Start test block
contract(‘BoxV2 (proxy)’, function () {
beforeEach(async function () {
// Deploy a new Box contract for each test
this.box = await deployProxy(Box, [42], {initializer: ‘store’});
this.boxV2 = await upgradeProxy(this.box.address, BoxV2);
});
// Test case
it(‘retrieve returns a value previously incremented’, async function () {
// Increment
await this.boxV2.increment();
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await this.boxV2.retrieve()).toString()).to.equal(’43’);
});
});
然后,我们可以运行测试。
$ npx truffle test
Using network ‘test’.
…
Contract: Box (proxy)
✓ retrieve returns a value previously initialized (38ms)
Contract: Box
✓ retrieve returns a value previously stored (87ms)
Contract: BoxV2 (proxy)
✓ retrieve returns a value previously incremented (90ms)
Contract: BoxV2
✓ retrieve returns a value previously stored (91ms)
✓ retrieve returns a value previously incremented (86ms)
5 passing (1s)
部署新的升级版本
一旦测试了新的实现,就可以准备升级。这将验证并部署新合约。注意:我们仅是准备升级。我们将使用 Gnosis Safe 执行实际升级。
使用以下 JavaScript 在migrations目录中创建4_prepare_upgrade_boxv2.js。
4_prepare_upgrade_boxv2.js
// migrations/4_prepare_upgrade_boxv2.js
const Box = artifacts.require(‘Box’);
const BoxV2 = artifacts.require(‘BoxV2’);
const { prepareUpgrade } = require(‘@openzeppelin/truffle-upgrades’);
module.exports = async function (deployer) {
const box = await Box.deployed();
await prepareUpgrade(box.address, BoxV2, { deployer });
};
我们可以在 Rinkeby 网络上运行迁移,以部署新的合约实现。注意:运行此迁移时,我们需要跳过之前运行过的迁移。
$ npx truffle migrate –network rinkeby
…
4_prepare_upgrade_boxv2.js
==========================
Deploying ‘BoxV2’
—————–
> transaction hash: 0x078c4c4454bb15e3791bc80396975e6e8fc8efb76c6f54c321cdaa01f5b960a7
> Blocks: 1 Seconds: 17
> contract address: 0xEc784bE1CC7F5deA6976f61f578b328E856FB72c
…
升级合约
要管理我们在 Gnosis Safe 中的升级,我们使用 OpenZeppelin 应用程序(找一下 OpenZeppelin 的 logo)。
首先,我们需要代理的地址(box.address)和新实现的地址(boxV2.address)。我们可以从 truffle 迁移的输出或 truffle console 中获得。
$ npx truffle console –network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> boxV2 = await BoxV2.deployed()
truffle(rinkeby)> box.address
‘0xF325bB49f91445F97241Ec5C286f90215a7E3BC6’
truffle(rinkeby)> boxV2.address
‘0xEc784bE1CC7F5deA6976f61f578b328E856FB72c’
在“应用程序(APPS)”选项卡中,选择“ OpenZeppelin”应用程序,然后将代理地址粘贴到“合约地址(Contract address)”字段中,然后将新实现的地址粘贴到“新实现的地址( New implementation address)”字段中。
该应用程序应显示该合约是EIP1967[10]兼容的。
仔细检查地址,然后按“升级(Upgrade)”按钮。
确认显示对话框以提交交易。
然后,我们需要在 MetaMask(或你正使用的钱包)中签署交易。
现在,我们可以与升级后的合约进行交互。我们需要使用代理地址与 BoxV2 进行交互。然后,我们可以调用新的“增量”功能,观察到整个升级过程中都保持了状态。
$ npx truffle console –network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> boxV2 = await BoxV2.at(box.address)
truffle(rinkeby)> (await boxV2.retrieve()).toString()
’42’
truffle(rinkeby)> await boxV2.increment()
{ tx:
…
truffle(rinkeby)> (await boxV2.retrieve()).toString()
’43’
接下来
我们已经创建了一个可升级的合约,将升级的控制权转移到了 Gnosis Safe,并升级了我们的合约。
可以在主网上执行相同的过程。注意:我们应该始终首先在公共测试网上测试升级。
如果你对本文有任何疑问或建议的改进,请发布在openzeppelin 社区论坛[11]中。
参考资料
[1]登链翻译计划: https://github.com/lbc-team/Pioneer
[2]Tiny 熊: https://learnblockchain.cn/people/15
[3]OpenZeppelin 学习指南: https://docs.openzeppelin.com/learn/developing-smart-contracts#setting-up-a-solidity-project
[4]Solidity: https://learnblockchain.cn/docs/solidity/
[5]initialize函数而不是构造函数: https://docs.openzeppelin.com/learn/upgrading-smart-contracts#initialization
[6]Truffle 迁移: https://learnblockchain.cn/docs/truffle/getting-started/running-migrations.html
[7]使用 Truffle 连接到公共测试网络: https://forum.openzeppelin.com/t/connecting-to-public-test-networks-with-truffle/2960
[8]创建 Safe Multisig: https://help.gnosis-safe.io/en/articles/3876461-create-a-safe-multisig
[9]升级: https://docs.openzeppelin.com/learn/upgrading-smart-contracts#upgrading
[10]EIP1967: https://learnblockchain.cn/docs/eips/eip-1967.html
[11]openzeppelin 社区论坛: https://forum.openzeppelin.com/
[12]Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain
[13]abcoathup: https://forum.openzeppelin.com/u/abcoathup