OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat

OpenZeppelin Hardhat 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 Hardhat Upgrades and Gnosis Safe from creating, testing and deploying, all the way through to upgrading with Gnosis Safe:

  1. Create an upgradeable contract
  2. Test the contract locally
  3. Deploy the contract to a public network
  4. Transfer control of upgrades to a Gnosis Safe
  5. Create a new version of our implementation
  6. Test the upgrade locally
  7. Deploy the new implementation
  8. Upgrade the contract

Setting up the Environment

We will begin by creating a new npm project:

mkdir mycontract && cd mycontract
npm init -y

We will install Hardhat (previously called Buidler).
When running Hardhat select the option to “Create an empty hardhat.config.js”

npm install --save-dev hardhat
npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.0.3

✔ What do you want to do? · Create an empty hardhat.config.js
Config file created

Install the Hardhat Upgrades plugin.

npm install --save-dev @openzeppelin/hardhat-upgrades

We use ethers, so we also need to install.

npm install --save-dev @nomiclabs/hardhat-ethers ethers

We then need to configure Hardhat to use the @nomiclabs/hardhat-ethers and our @openzeppelin/hardhat-upgrades, as well as setting the compiler version to solc 0.7.3. To do this add the plugins and set the solc version in your hardhat.config.js file as follows.

hardhat.config.js

// hardhat.config.js
require("@nomiclabs/hardhat-ethers");
require('@openzeppelin/hardhat-upgrades');

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.7.3",
};

Create upgradeable contract

We will use our beloved Box contract from the OpenZeppelin Learn guides. Create a contracts directory in our project root and then create Box.sol in the 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 use chai expect in our tests, so we also need to install.

npm install --save-dev chai

We will create unit tests for the implementation contract. Create a test directory in our project root and then create Box.js in the test directory with the following JavaScript.

Box.js

// test/Box.js
// Load dependencies
const { expect } = require('chai');
 
let Box;
let box;
 
// Start test block
describe('Box', function () {
  beforeEach(async function () {
    Box = await ethers.getContractFactory("Box");
    box = await Box.deploy();
    await box.deployed();
  });
 
  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value
    await 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 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.js in your test directory with the following JavaScript.

Box.proxy.js

// test/Box.proxy.js
// Load dependencies
const { expect } = require('chai');
 
let Box;
let box;
 
// Start test block
describe('Box (proxy)', function () {
  beforeEach(async function () {
    Box = await ethers.getContractFactory("Box");
    box = await upgrades.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 box.retrieve()).toString()).to.equal('42');
  });
});

We can then run our tests.

$ npx hardhat test
Downloading compiler 0.7.3
Compiling 1 file with 0.7.3
Compilation finished successfully


  Box
    ✓ retrieve returns a value previously stored (63ms)

  Box (proxy)
    ✓ retrieve returns a value previously initialized


  2 passing (2s)

Deploy the contract to a public network

To deploy our Box contract we will use a script. The OpenZeppelin Hardhat 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 a scripts directory in our project root and then create the following deploy.js script in the scripts directory.

In this guide we don’t have an initialize function so we will initialize state using the store function.

deploy.js

// scripts/deploy.js
async function main() {
    const Box = await ethers.getContractFactory("Box");
    console.log("Deploying Box...");
    const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
    console.log("Box deployed to:", box.address);
  }
  
  main()
    .then(() => process.exit(0))
    .catch(error => {
      console.error(error);
      process.exit(1);
    });

We would normally first deploy our contract to a local test 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 and Hardhat: Deploying to a live network. Note: any secrets such as mnemonics or Alchemy API keys should not be committed to version control.

We will use the following hardhat.config.js for deploying to Rinkeby.

hardhat.config.js

// hardhat.config.js
const { alchemyApiKey, mnemonic } = require('./secrets.json');

require("@nomiclabs/hardhat-ethers");
require('@openzeppelin/hardhat-upgrades');

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.7.3",
  networks: {
    rinkeby: {
      url: `https://eth-rinkeby.alchemyapi.io/v2/${alchemyApiKey}`,
      accounts: {mnemonic: mnemonic}
    }
  }
};

Run our deploy.js with the Rinkeby network to deploy. Our implementation contract (Box.sol), a ProxyAdmin and the proxy will be deployed.

Note: We need to keep track of our proxy address, we will need it later.

$ npx hardhat run --network rinkeby scripts/deploy.js
Deploying Box...
Box deployed to: 0xFF60fd044dDed0E40B813DC7CE11Bed2CCEa501F

We can interact with our contract using the Buidler console.
Note: Box.attach(“PROXY ADDRESS”) takes the address of our proxy contract.

$ npx hardhat console --network rinkeby
> const Box = await ethers.getContractFactory("Box")
undefined
> const box = await Box.attach("0xFF60fd044dDed0E40B813DC7CE11Bed2CCEa501F")
undefined
> (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 transfer_ownership.js in the scripts directory with the following JavaScript. Change the value of gnosisSafe to your Gnosis Safe address.

transfer_ownership.js

// scripts/transfer_ownership.js
async function main() {
  const gnosisSafe = '0x1c14600daeca8852BA559CC8EdB1C383B8825906';
 
  console.log("Transferring ownership of ProxyAdmin...");
  // The owner of the ProxyAdmin can upgrade our contracts
  await upgrades.admin.transferProxyAdminOwnership(gnosisSafe);
  console.log("Transferred ownership of ProxyAdmin to:", gnosisSafe);
}
 
main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

We can run the transfer on the Rinkeby network.

$ npx hardhat run --network rinkeby scripts/transfer_ownership.js
Transferring ownership of ProxyAdmin...
Transferred ownership of ProxyAdmin to: 0x1c14600daeca8852BA559CC8EdB1C383B8825906

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.js in your test directory with the following JavaScript.

BoxV2.js

// test/BoxV2.js
// Load dependencies
const { expect } = require('chai');
 
let BoxV2;
let boxV2;
 
// Start test block
describe('BoxV2', function () {
  beforeEach(async function () {
    BoxV2 = await ethers.getContractFactory("BoxV2");
    boxV2 = await BoxV2.deploy();
    await boxV2.deployed();
  });
 
  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value
    await 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 boxV2.retrieve()).toString()).to.equal('42');
  });
 
  // Test case
  it('retrieve returns a value previously incremented', async function () {
    // Increment
    await 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 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.js in your test directory with the following JavaScript.

BoxV2.proxy.js

// test/BoxV2.proxy.js
// Load dependencies
const { expect } = require('chai');
 
let Box;
let BoxV2;
let box;
let boxV2;
 
// Start test block
describe('BoxV2 (proxy)', function () {
  beforeEach(async function () {
    Box = await ethers.getContractFactory("Box");
    BoxV2 = await ethers.getContractFactory("BoxV2");
 
    box = await upgrades.deployProxy(Box, [42], {initializer: 'store'});
    boxV2 = await upgrades.upgradeProxy(box.address, BoxV2);
  });
 
  // Test case
  it('retrieve returns a value previously incremented', async function () {
    // Increment
    await 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 boxV2.retrieve()).toString()).to.equal('43');
  });
});

We can then run our tests.

$ npx hardhat test
Compiling 1 file with 0.7.3
Compilation finished successfully


  Box
    ✓ retrieve returns a value previously stored (59ms)

  Box (proxy)
    ✓ retrieve returns a value previously initialized

  BoxV2
    ✓ retrieve returns a value previously stored (40ms)
    ✓ retrieve returns a value previously incremented

  BoxV2 (proxy)
    ✓ retrieve returns a value previously incremented (40ms)


  5 passing (2s)

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 prepare_upgrade.js in the scripts directory with the following JavaScript.
Note: We need to update the script to specify our proxy address.

prepare_upgrade.js

// scripts/prepare_upgrade.js
async function main() {
  const proxyAddress = '0xFF60fd044dDed0E40B813DC7CE11Bed2CCEa501F';
 
  const BoxV2 = await ethers.getContractFactory("BoxV2");
  console.log("Preparing upgrade...");
  const boxV2Address = await upgrades.prepareUpgrade(proxyAddress, BoxV2);
  console.log("BoxV2 at:", boxV2Address);
}
 
main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

We can run the migration on the Rinkeby network to deploy the new implementation.

$ npx hardhat run --network rinkeby scripts/prepare_upgrade.js
Preparing upgrade...
BoxV2 at: 0xE8f000B7ef04B7BfEa0a84e696f1b792aC526700

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 and the address of the new implementation. We can get these from the output of when we ran our deploy.js and prepare_upgrade.js scripts.

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. Note: BoxV2.attach(“PROXY ADDRESS”) takes the address of our proxy contract.

We can then call our new increment function, observing that state has been maintained across the upgrade.

$ npx hardhat console --network rinkeby

>
>
(To exit, press ^C again or type .exit)
>
abcoathup@OpenZeppelin:~/projects/upgradeplugins/mycontract$ npx hardhat console --network rinkeby
> const BoxV2 = await ethers.getContractFactory("BoxV2")
undefined
> const boxV2 = await BoxV2.attach("0xFF60fd044dDed0E40B813DC7CE11Bed2CCEa501F")
undefined
> (await boxV2.retrieve()).toString()
'42'
> await boxV2.increment()
{ hash:
...
> (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.

We could also manage the upgrade using OpenZeppelin Defender Admin


Sign up for OpenZeppelin Defender: https://defender.openzeppelin.com/

If you have any questions or suggested improvements for this guide please post in the Community Forum.

3 Likes

A post was split to a new topic: Error verifying proxy with plugin @nomiclabs/hardhat-etherscan