Contracts deployed using the OpenZeppelin upgrade plugin are upgradeable: they can be upgraded to modify their code, while preserving their addresses, state, and balances. New features can be added to the project iteratively, or any bugs that may be found in the live version can be fixed.
Configure the development environment
Create a new npm project
mkdir mycontract && cd mycontract
npm init -y
Install and initialize Truffle
npm i --save-dev truffle
npx truffle init
Install the Truffle upgrade plugin
npm i --save-dev @openzeppelin/truffle-upgrades
Create upgradable contracts
Note that upgradable contracts use the initialize function instead of the constructor to initialize the state.
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;
}
}
Deploy the contract to the public network
We will use the Truffle migration to deploy the Box contract. The Truffle upgrade plugin provides a deployProxy function to deploy upgradeable contracts. It will deploy the contract we implemented, ProxyAdmin will act as the project proxy and proxy administrator, and call the initialization function.
Create the following 2_deploy_contracts.js script in the migrations directory.
In this article, we don't have an initialize function yet, so the store function will be used to initialize the state.
2_deploy_contracts.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' });
};
Run truffle migrate with Rinkeby network to deploy. We can see 3 contracts: Box.sol, ProxyAdmin and the proxy contract TransparentUpgradeableProxy.
truffle migrate --network rinkeby
...
2_deploy_contracts.js
===============Deploying 'Box'
---------------
> transaction hash: 0x1e5a61c2d4560d6ffe5cc60d7badbfef6d5e420708eebff6dc580bb3f9f9f3e1
> Blocks: 1 Seconds: 14
> contract address: 0x7f7dc11961fCD81f53e9F45D9DfBb745832c0657
...Deploying 'ProxyAdmin'
----------------------
> transaction hash: 0x298b429391c5a98701bf79df00f4f5526c61570f3091b3d6693e3a4f12a88409
> Blocks: 1 Seconds: 14
> contract address: 0x7Bd40e62aEe2c5e232152351f57068038761E20F
...Deploying 'TransparentUpgradeableProxy'
---------------------------------------
> transaction hash: 0x7a0043dbe9a35ab9eab8cf0eac1856418cd0c359e330448df016150d293e6716
> Blocks: 2 Seconds: 26
> contract address: 0xc2ea7DE43F194bB397761a30a05CEDcF28835F24
...
Publish the verification contract
truffle run verify Box --network rinkeby
We can use the truffle console to interact with our contracts.
Note: Box.deployed() is the address of our proxy contract.
truffle console --network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> box.address
'0xc2ea7DE43F194bB397761a30a05CEDcF28835F24'
truffle(rinkeby)> (await box.retrieve()).toString()
'42'
The administrator of the current proxy (who can perform upgrades) is the ProxyAdmin contract. Only the owner of the ProxyAdmin can upgrade the proxy. Warning: Please make sure to go to an address we control when ProxyAdmin ownership is transferred.
Create the following 3_transfer_ownership.js script in the migrations directory.
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);
}
};
Run the migration on the Rinkeby network
truffle migrate --network rinkeby
...
3_transfer_ownership.js
=======================> Saving migration to chain.
-------------------------------------
...
Implement a new upgrade version
After some time, we decided that we wanted to add functionality to the contract. In this article, we will add an increment function.
Note: We cannot change the storage layout of previous contract implementations, see Upgrades for more details on technical limitations.
Create a new implementation BoxV2.sol in your contracts directory with the following Solidity code.
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);
}
}
Deploy the new upgrade version
Once the new implementation has been tested, the upgrade is ready. This will validate and deploy the new contract. NOTE: We are only preparing to upgrade.
Create the following 4_prepare_upgrade_boxv2.js script in the migrations directory.
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 });
};
Run migrations on the Rinkeby network to deploy new contract implementations
truffle migrate --network rinkeby
...
4_prepare_upgrade_boxv2.js
==========================Deploying 'BoxV2'
-----------------
> transaction hash: 0x078c4c4454bb15e3791bc80396975e6e8fc8efb76c6f54c321cdaa01f5b960a7
> Blocks: 1 Seconds: 17
> contract address: 0xEc784bE1CC7F5deA6976f61f578b328E856FB72c
...
Post-deployment address
Enter the truffle console
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'
upgrade contract
Execute the upgrade method of the ProxyAdmin contract
proxy: the address of the TransparentUpgradeableProxy contract
implementation: the address of the BoxV2 contract
Then the transaction needs to be signed in MetaMask (or the wallet you are using).
Now, we can interact with the upgraded contract. We need to use the proxy address to interact with BoxV2. We can then call the new "incremental" function and observe that the state is maintained throughout the upgrade.
Enter the truffle console
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'