OpenZeppelin Truffle Upgrades
Smart contracts deployed with the OpenZeppelin Upgrades plugins can be upgraded to modify their code, while preserving their address, state, and balance. This allows you to iteratively add new features to your project, or fix any bugs you may find in production.
In this guide, we will show the lifecycle using OpenZeppelin Truffle Upgrades and Gnosis Safe from creating, testing and deploying, all the way through to upgrading with Gnosis Safe:
- Create an upgradeable contract
- Test the contract locally
- Deploy the contract to a public network
- Transfer control of upgrades to a Gnosis Safe
- Create a new version of our implementation
- Test the upgrade locally
- Deploy the new implementation
- Upgrade the contract
Setting up the Environment
We will begin by creating a new npm project:
mkdir mycontract && cd mycontract
npm init -y
Install and initialize Truffle. Note: We need to use Truffle version 5.1.35 or greater.
npm i --save-dev truffle
npx truffle init
Install the Truffle Upgrades plugin.
npm i --save-dev @openzeppelin/truffle-upgrades
Create upgradeable contract
We will use our beloved Box contract from the OpenZeppelin Learn guides. Create Box.sol
in your contracts
directory with the following Solidity code.
Note, upgradeable contracts use initialize
functions rather than constructors to initialize state. To keep things simple we will initialize our state using the public store
function that can be called multiple times from any account rather than a protected single use initialize
function.
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;
}
}
Test the contract locally
We should always appropriately test our contracts.
To test upgradeable contracts we should create unit tests for the implementation contract, along with creating higher level tests for testing interaction via the proxy.
We will use chai expect in our tests, so first we need to install.
npm i --save-dev chai
We will create unit tests for the implementation contract. Create Box.test.js
in your test
directory with the following JavaScript.
Box.test.js
// test/Box.test.js
// Load dependencies
const { expect } = require('chai');
// Load compiled artifacts
const Box = artifacts.require('Box');
// Start test block
contract('Box', function () {
beforeEach(async function () {
// Deploy a new Box contract for each test
this.box = await Box.new();
});
// Test case
it('retrieve returns a value previously stored', async function () {
// Store a value
await this.box.store(42);
// 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.box.retrieve()).toString()).to.equal('42');
});
});
We can also create tests for interacting via the proxy.
Note: We don’t need to duplicate our unit tests here, this is for testing proxy interaction and testing upgrades.
Create Box.proxy.test.js
in your test
directory with the following JavaScript.
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 () {
// Deploy a new Box contract for each test
this.box = await deployProxy(Box, [42], {initializer: 'store'});
});
// Test case
it('retrieve returns a value previously initialized', async function () {
// 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.box.retrieve()).toString()).to.equal('42');
});
});
Before we can compile our contract, we will need to change the solc version to ^0.7.0
in truffle-config.js
as our contract has pragma solidity ^0.7.0
We can then run our tests.
$ 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)
Deploy the contract to a public network
To deploy our Box contract we will use Truffle migrations. The Truffle Upgrades plugin provides a deployProxy
function to deploy our upgradeable contract. This deploys our implementation contract, a ProxyAdmin to be the admin for our projects proxies and the proxy, along with calling any initialization.
Create the following 2_deploy_box.js
script in the migrations directory.
In this guide we don’t have an initialize
function so we will initialize state using the store
function.
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' });
};
We would normally first deploy our contract to a local test (such as ganache-cli
) and manually interact with it. For the purposes of time we will skip ahead to deploying to a public test network.
In this guide we will deploy to Rinkeby. If you need assistance with configuration, see Connecting to Public Test Networks. Note: any secrets such as mnemonics or Infura project IDs should not be committed to version control.
Run truffle migrate
with the Rinkeby network to deploy. We can see our implementation contract (Box.sol), a ProxyAdmin and the proxy being deployed.
$ 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
...
We can interact with our contract using the Truffle console.
Note: Box.deployed()
is the address of our proxy contract.
$ npx truffle console --network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> box.address
'0xF325bB49f91445F97241Ec5C286f90215a7E3BC6'
truffle(rinkeby)> (await box.retrieve()).toString()
'42'
Transfer control of upgrades to a Gnosis Safe
We will use Gnosis Safe to control upgrades of our contract.
First we need to create a Gnosis Safe for ourselves on Rinkeby network. Follow the Create a Safe Multisig instructions. For simplicity in this guide we will use a 1 of 1, in production you should consider using at least 2 of 3.
Once you have created your Gnosis Safe on Rinkeby, copy the address so we can transfer ownership.
The admin (who can perform upgrades) for our proxy is a ProxyAdmin contract. Only the owner of the ProxyAdmin can upgrade our proxy. Warning: Ensure to only transfer ownership of the ProxyAdmin to an address we control.
Create 3_transfer_ownership.js
in the migrations
directory with the following JavaScript. Change the value of gnosisSafe
to your Gnosis Safe address.
3_transfer_ownership.js
// migrations/3_transfer_ownership.js
const { admin } = require('@openzeppelin/truffle-upgrades');
module.exports = async function (deployer, network) {
// Use address of your 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);
}
};
We can run the migration on the Rinkeby network.
$ npx truffle migrate --network rinkeby
...
3_transfer_ownership.js
=======================
> Saving migration to chain.
-------------------------------------
...
Create a new version of our implementation
After a period of time, we decide that we want to add functionality to our contract. In this guide we will add an increment
function.
Note: We cannot change the storage layout of our implementation contract, see Upgrading for more details on the technical limitations.
Create the 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);
}
}
Test the upgrade locally
To test our upgrade we should create unit tests for the new implementation contract, along with creating higher level tests for testing interaction via the proxy, checking that state is maintained across upgrades.
We will create unit tests for the new implementation contract. We can add to the unit tests we already created to ensure high coverage.
Create BoxV2.test.js
in your test
directory with the following JavaScript.
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);
// 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('42');
});
// 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('1');
});
});
We can also create tests for interacting via the proxy after upgrading.
Note: We don’t need to duplicate our unit tests here, this is for testing proxy interaction and testing state after upgrades.
Create BoxV2.proxy.test.js
in your test
directory with the following JavaScript.
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');
});
});
We can then run our tests.
$ 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)
Deploy the new implementation
Once we have tested our new implementation, we can prepare the upgrade. This will validate and deploy our new implementation contract. Note: We are only preparing the upgrade. We will use our Gnosis Safe to perform the actual upgrade.
Create 4_prepare_upgrade_boxv2.js
in the migrations
directory with the following JavaScript.
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 });
};
We can run the migration on the Rinkeby network to deploy the new implementation. Note: We need to skip dry run when running this migration.
$ npx truffle migrate --network rinkeby
...
4_prepare_upgrade_boxv2.js
==========================
Deploying 'BoxV2'
-----------------
> transaction hash: 0x078c4c4454bb15e3791bc80396975e6e8fc8efb76c6f54c321cdaa01f5b960a7
> Blocks: 1 Seconds: 17
> contract address: 0xEc784bE1CC7F5deA6976f61f578b328E856FB72c
...
Upgrade the contract
To manage our upgrade in Gnosis Safe we use the OpenZeppelin app (look for the OpenZeppelin logo).
First, we need the address of the proxy (box.address
) and the address of the new implementation (boxV2.address
). We can get these from the output of truffle migrate or from the 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'
In the Apps tab, select the OpenZeppelin application and paste the address of the proxy in the Contract address field, and paste the address of the new implementation in the New implementation address field.
The app should show that the contract is EIP1967-compatible.
Double check the addresses, and then press the Upgrade button.
We will be shown a confirmation dialog to Submit the transaction.
We then need to sign the transaction in MetaMask (or the wallet that you are using).
We can now interact with our upgraded contract. We need to interact with BoxV2 using the address of the proxy. We can then call our new increment
function, observing that state has been maintained across the upgrade.
$ 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'
Next Steps
We have created an upgradeable contract, transferred control of the upgrade to a Gnosis Safe and upgraded our contract.
The same process can be performed on mainnet. Note: we should always test the upgrade on a public testnet first.
If you have any questions or suggested improvements for this guide please post in the Community Forum.