使用OpenZeppelin升级插件部署的合约具备可升级的特性:可以升级以修改其代码,同时保留其地址,状态和余额。 可以迭代地向项目中添加新功能,或修复在线上版本中可能发现的任何错误。
配置开发环境
创建一个新的npm项目
mkdir mycontract && cd mycontract
npm init -y
安装并初始化Truffle
npm i --save-dev truffle
npx truffle init
安装Truffle升级插件
npm i --save-dev @openzeppelin/truffle-upgrades
创建可升级合约
注意,可升级合约使用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;
}
}
将合约部署到公共网络
我们将使用Truffle迁移来部署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' });
};
使用Rinkeby网络运行truffle migration进行部署。 我们可以看到3 个合约:Box.sol、ProxyAdmin 和 代理合约AdminUpgradeabilityProxy。
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 run verify Box --network rinkeby
我们可以使用Truffle控制台(truffle console)与我们的合约进行交互。
注意: Box.deployed() 是我们的代理合约的地址。
truffle console --network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> box.address
'0xc2ea7DE43F194bB397761a30a05CEDcF28835F24'
truffle(rinkeby)> (await box.retrieve()).toString()
'42'
当前代理的管理员(可以执行升级)是ProxyAdmin合约。 只有ProxyAdmin的所有者可以升级代理。 警告:ProxyAdmin 所有权转移时请确保转到我们控制的地址上。
3_transfer_ownership.js
// migrations/3_transfer_ownership.js
const { admin } = require('@openzeppelin/truffle-upgrades');
module.exports = async function (deployer, network) {
// 使用你的 钱包 地址
const admin = '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(admin);
}
};
在Rinkeby网络上运行迁移
truffle migrate --network rinkeby
...
3_transfer_ownership.js
=======================> Saving migration to chain.
-------------------------------------
...
实现一个新的升级版本
一段时间后,我们决定要向合约添加功能。 在本文中,我们将添加一个increment函数。
注意:我们无法更改之前合约实现的存储布局,有关技术限制的更多详细信息,请参阅升级。
使用以下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);
}
}
部署新的升级版本
一旦测试了新的实现,就可以准备升级。 这将验证并部署新合约。 注意:我们仅是准备升级。。
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网络上运行迁移,以部署新的合约实现
truffle migrate --network rinkeby
...
4_prepare_upgrade_boxv2.js
==========================Deploying 'BoxV2'
-----------------
> transaction hash: 0x078c4c4454bb15e3791bc80396975e6e8fc8efb76c6f54c321cdaa01f5b960a7
> Blocks: 1 Seconds: 17
> contract address: 0xEc784bE1CC7F5deA6976f61f578b328E856FB72c
...
进入truffle控制台
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'
升级合约
执行ProxyAdmin合约的upgrade方法
proxy:TransparentUpgradeableProxy合约的地址
implementation:BoxV2合约的地址
然后需要在MetaMask(或你正使用的钱包)中签署交易。
现在,我们可以与升级后的合约进行交互。 我们需要使用代理地址与BoxV2进行交互。 然后,我们可以调用新的“增量”功能,观察到整个升级过程中都保持了状态。
进入truffle控制台
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'