DEV Community

fangjun
fangjun

Posted on

Tutorial: write upgradeable smart contract (proxy contract) with OpenZeppelin

There are great articles about upgradeable smart contracts or proxy pattern.

In this tutorial, I will try to list several steps you can follow to develop and deploy proxy contracts to local testnet and public testnet Ropsten.

We will use OpenZeppelin proxy contracts and OpenZeppelin Upgrades plugin for Hardhat (or Truffle) here.

Special thanks for two how-to guide in OpenZeppelin ecosystem: OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat and Gnosis Safe by abcoathup, OpenZeppelin Defender guide Upgrading a contract via a multisig and Defender.


How upgradeable smart contract works?

how proxy contract works

This illustation explaines how upgradeable smart contract work. To be specific, this is transparent proxy pattern, and the other one is UUPS proxy pattern(Universal Upgradeable Proxy Standard).

Upgradeable smart contracts need 3 contracts:

  • Proxy contract. The smart contract which user interacts with. It will keep data/state. This is an EIP1967 standard proxy contract.
  • Implementation contract. The smart contract provides functionality and logic. Please note that the data is also define in this contract.
  • ProxyAdmin contract. The contract links Proxy and Implementation. When transferring ProxyAmin ownership to a multi-sig account, the authority to upgrade the Proxy contract is transferred to it.

When we first deploy upgradeable contract using OpenZeppelin Upgrade plugin for Hardhat, we actually deploy all these three contracts:

  1. deploy "Implementation contract"
  2. deploy "ProxyAdmin contract"
  3. deploy "Proxy contract"

In the ProxyAdmin contract, Implementation and Proxy is linked.

When a user call the proxy contract, the call is delegate call to the implementation.

ProxyAmdin explained:

What is a proxy admin?
A ProxyAdmin is a contract that acts as the owner of all your proxies. Only one per network gets deployed. When you start your project, the ProxyAdmin is owned by the deployer address, but you can transfer ownership of it by calling transferOwnership. (link)

When we upgrade the contract, what we do is:

  1. deploy a new "Implementation contract"
  2. upgrade in "ProxyAdmin contract" which redirect all call to proxy to new Implementation contract.

OpenZeppelin Upgrades plugins for Hardhat/Truffle can help us to get these jobs done.

If you need to know about how to modify a contract to be upgradeable, you can refer to OpenZeppelin docs: link.

Let's begin to write and deploy an upgradeable smart contract. You can find the repo at Github: https://github.com/fjun99/proxy-contract-example


Task 1: Write upgradable smart contract

Task 1.1 Init hardhat project

We will use Hardhat, Hardhat Network local testnet and OpenZeppelin Upgrades plugin.

STEP 1: install hardhat and init a project

mkdir solproxy && cd solproxy
yarn init -y
yarn add harthat

yarn hardhat
// choose option: sample typescript 
Enter fullscreen mode Exit fullscreen mode

STEP 2: add @openzeppelin/hardhat-upgrades

yarn add @openzeppelin/hardhat-upgrades
Enter fullscreen mode Exit fullscreen mode

Edit hardhat.config.ts to use Upgrades plugins.

// hardhat.config.ts
import '@openzeppelin/hardhat-upgrades';
Enter fullscreen mode Exit fullscreen mode

We will use three functions of hardhat upgrade plugin(API reference link):

deployProxy
upgradeProxy
prepareUpgrade
Enter fullscreen mode Exit fullscreen mode

1.2: Write an upgradable smart contract

We use the Box.sol contract from OpenZeppelin guide. We will build several version of this contract:

  • Box.sol
  • BoxV2.sol
  • BoxV3.sol
  • BoxV4.sol

The big diffference between a normal contract and an upgradeable contract is that an upgradeable contract does not have a consctructor(), doc link.

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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;
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • User can store() a value to this contract can retrieve() it later.

1.3 Unit test script for Box.sol

Let's write unit test scripts for Box.sol. The following hardhat unit test script is adapted from (
OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat). And we make some minor changes to use Typescript.

Edit test/1.Box.test.ts:

// test/1.Box.test.ts
import { expect } from "chai";
import { ethers } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box", function () {
  let box:Contract;

  beforeEach(async function () {
    const Box = await ethers.getContractFactory("Box")
    box = await Box.deploy()
    await box.deployed()
  })

  it("should retrieve value previously stored", async function () {
    await box.store(42)
    expect(await box.retrieve()).to.equal(BigNumber.from('42'))

    await box.store(100)
    expect(await box.retrieve()).to.equal(BigNumber.from('100'))
  })
})

// NOTE: should also add test for event: event ValueChanged(uint256 newValue)

Enter fullscreen mode Exit fullscreen mode

Run test:

yarn hardhat test test/1.Box.test.ts
Enter fullscreen mode Exit fullscreen mode

Results:

  Box
    ✓ should retrieve value previously stored
  1 passing (505ms)
✨  Done in 3.34s.
Enter fullscreen mode Exit fullscreen mode

Task 2. Deploy an upgradeable smart contract

Task 2.1 Write deploy script with "OpenZeppelin Upgrades plugin for Hardhat"

When we write script to deploy a smart contract, we write:

  const Greeter = await ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, Hardhat!");
Enter fullscreen mode Exit fullscreen mode

To deploy an upgradeable contract, will call deployProxy(), docs can be found at link:

  const Box = await ethers.getContractFactory("Box")
  const box = await upgrades.deployProxy(Box,[42], { initializer: 'store' })
Enter fullscreen mode Exit fullscreen mode

In the second line, we use OpenZeppelin Upgrades plugin to deploy Box with a initial value 42 by calling store().

Edit scripts/1.deploy_box.ts

// scripts/1.deploy_box.ts
import { ethers } from "hardhat"
import { upgrades } from "hardhat"

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.address," box(proxy) address")
  console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
  console.log(await upgrades.erc1967.getAdminAddress(box.address)," getAdminAddress")    
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We deploy an upgradeable contract Box.sol using upgrades.deployProxy().
  • It will deploy three contracts: Implementation, ProxyAdmin, Proxy.

Task 2.2 Deploy the contract to local testnet

Let's run the deploy script in local testnet.

STEP 1: run a stand-alone hardhat testnet in another terminial:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

STEP 2: run the deploy script

yarn hardhat run scripts/1.deploy_box.ts --network localhost
Enter fullscreen mode Exit fullscreen mode

Results:

Deploying Box...
0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0  box(proxy) address
0x5FbDB2315678afecb367f032d93F642f64180aa3  getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512  getAdminAddress
✨  Done in 3.83s.
Enter fullscreen mode Exit fullscreen mode

Users can interact with the box contract through box(proxy) address: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0.

Note: if you run this deployment for several times, you can find that ProxyAdmin remains same: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

2.3 test script to test whether Box.sol via proxy pattern work correctly.

You may find that user interact with the implementation contract through the box(proxy) contract.

To make sure this work correctly, let's add a unit test for this scenario. In the unit test, we will deploy our contract using upgrades.deployProxy() and interact through the box(proxy) contract.

Edit test/2.BoxProxy.test.ts

// test/2.BoxProxy.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box (proxy)", function () {
  let box:Contract

  beforeEach(async function () {
    const Box = await ethers.getContractFactory("Box")
    //initilize with 42
    box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
    })

  it("should retrieve value previously stored", async function () {    
    // console.log(box.address," box(proxy)")
    // console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
    // console.log(await upgrades.erc1967.getAdminAddress(box.address), " getAdminAddress")   

    expect(await box.retrieve()).to.equal(BigNumber.from('42'))

    await box.store(100)
    expect(await box.retrieve()).to.equal(BigNumber.from('100'))
  })

})
Enter fullscreen mode Exit fullscreen mode

Run test

yarn hardhat test test/2.BoxProxy.test.ts
Enter fullscreen mode Exit fullscreen mode

Results:

  Box (proxy)
    ✓ should retrieve value previously stored
  1 passing (579ms)
✨  Done in 3.12s.`
Enter fullscreen mode Exit fullscreen mode

Our box.sol works correctly. And we find that we need an increment() function. Instead of re-deploying this contract and asking all users to use the new one. We can upgrade the contract easily.


Task 3: upgrade smart contract

Task 3.1: write new implementation

We write a new version of Box BoxV2.sol by inherit Box.sol.

Edit contracts/BoxV2.sol

// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./Box.sol";

contract BoxV2 is Box{
    // Increments the stored value by 1
    function increment() public {
        store(retrieve()+1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Task 3.2 test script to test locally

We write unit test script to test BoxV2 when deployed locally.

Edit test/3.BoxV2.test.ts

// test/3.BoxV2.test.ts
import { expect } from "chai"
import { ethers } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box V2", function () {
  let boxV2:Contract

  beforeEach(async function () {
    const BoxV2 = await ethers.getContractFactory("BoxV2")
    boxV2 = await BoxV2.deploy()
    await boxV2.deployed()
  });

  it("should retrievevalue previously stored", async function () {
    await boxV2.store(42)
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))

    await boxV2.store(100)
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
  });

  it('should increment value correctly', async function () {
    await boxV2.store(42)
    await boxV2.increment()
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))
  })

})

Enter fullscreen mode Exit fullscreen mode

Run test

yarn hardhat test test/3.BoxV2.test.ts
Enter fullscreen mode Exit fullscreen mode

results:

  Box V2
    ✓ should retrievevalue previously stored
    ✓ should increment value correctly
  2 passing (579ms)
✨  Done in 3.38s.
Enter fullscreen mode Exit fullscreen mode

Task 3.3 test script for upgradeable deploy

We write unit test script to test BoxV2 when it deployed in proxy mode:

  • first, we deply the Box.sol
  • then we upgrade it to BoxV2.sol
  • test whether BoxV2 works correctly.

Edit test/4.BoxProxyV2.test.ts

// test/4.BoxProxyV2.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box (proxy) V2", function () {
  let box:Contract
  let boxV2:Contract

  beforeEach(async function () {
    const Box = await ethers.getContractFactory("Box")
    const BoxV2 = await ethers.getContractFactory("BoxV2")

    //initilize with 42
    box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
    // console.log(box.address," box/proxy")
    // console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
    // console.log(await upgrades.erc1967.getAdminAddress(box.address), " getAdminAddress")   

    boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
    // console.log(boxV2.address," box/proxy after upgrade")
    // console.log(await upgrades.erc1967.getImplementationAddress(boxV2.address)," getImplementationAddress after upgrade")
    // console.log(await upgrades.erc1967.getAdminAddress(boxV2.address)," getAdminAddress after upgrade")   
  })

  it("should retrieve value previously stored and increment correctly", async function () {
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))

    await boxV2.increment()
    //result = 42 + 1 = 43
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))

    await boxV2.store(100)
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
  })

})
Enter fullscreen mode Exit fullscreen mode

Run test

yarn hardhat test test/4.BoxProxyV2.test.ts
Enter fullscreen mode Exit fullscreen mode

Results:

  Box (proxy) V2
    ✓ should retrieve value previously stored and increment correctly


  1 passing (617ms)

✨  Done in 3.44s.
Enter fullscreen mode Exit fullscreen mode

Task 3.4 write upgrade script

In sub-task 2.2, we deploy Box to 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0.

In this sub-task, we will upgrade it with BoxV2

Edit

// scripts/2.upgradeV2.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'

async function main() {
  console.log(proxyAddress," original Box(proxy) address")
  const BoxV2 = await ethers.getContractFactory("BoxV2")
  console.log("upgrade to BoxV2...")
  const boxV2 = await upgrades.upgradeProxy(proxyAddress, BoxV2)
  console.log(boxV2.address," BoxV2 address(should be the same)")

  console.log(await upgrades.erc1967.getImplementationAddress(boxV2.address)," getImplementationAddress")
  console.log(await upgrades.erc1967.getAdminAddress(boxV2.address), " getAdminAddress")    
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

Enter fullscreen mode Exit fullscreen mode

Task 3.5 run the upgrade

We will start over from the beginning:

STEP 1: run a new local testnet in another terminal:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

STEP 2: deploy Box V1

yarn hardhat run scripts/1.deploy_box.ts --network localhost
Enter fullscreen mode Exit fullscreen mode

The box will deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0.

STEP 3: upgrade to Box V2

yarn hardhat run scripts/2.upgradeV2.ts --network localhost
Enter fullscreen mode Exit fullscreen mode

results:

0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0  original Box(proxy) address
upgrade to BoxV2...
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0  BoxV2 address(should be the same)
0x5FC8d32690cc91D4c39d9d3abcBD16989F875707  getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512  getAdminAddress
✨  Done in 3.64s.
Enter fullscreen mode Exit fullscreen mode

Task 4 Play with contract in hardhat console

Let's play with contract in hardhat console.

Task 4.1 play with the proxy contract

Run hardhat console connecting to local testnet.

yarn hardhat console --network localhost
Enter fullscreen mode Exit fullscreen mode

In hardhat console:

address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv2 = await ethers.getContractAt("BoxV2", address)
await boxv2.retrieve()
//BigNumber { value: "42" }

await boxv2.increment()
// tx response
// {
//  hash: '0x3e8c9dd8842d3315cadad2a80b592ac369e644edc5cec16f7a22c76d49e4b921',
//  blockNumber: 6,

await boxv2.retrieve()
//BigNumber { value: "43" }

await boxv2.store(100)
// tx response ...
await boxv2.retrieve()
//BigNumber { value: "100" }
Enter fullscreen mode Exit fullscreen mode

Task 4.2 try to interact with implementation contact

Let's try to interact with the implementation contact to see what's happening.

addressimp = '0x5fc8d32690cc91d4c39d9d3abcbd16989f875707'
boximp = await ethers.getContractAt("BoxV2", addressimp)

await boximp.retrieve()
//BigNumber { value: "0" }
await boximp.increment()
// tx response ...
await boximp.retrieve()
BigNumber { value: "1" }
Enter fullscreen mode Exit fullscreen mode

We will find that the original value in the implementation is 0. Under the hood the data is stored in the context of Proxy contract (0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0), not in the context of Implementatoin contract(0x5fc8d32690cc91d4c39d9d3abcbd16989f875707).


Task 5: Build BoxV3 to add new state variable

We can add new state variable as long as you don't change the the layout of the state variables.

To put it simply, you can add a new one. We will add a state variable string public name.

Task 5.1 Write BoxV3.sol

We write BoxV3.sol by inheriting BoxV2:

// contracts/BoxV3.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./BoxV2.sol";

contract BoxV3 is BoxV2{
    string public name;

    event NameChanged(string name);
    function setName(string memory _name) public {
        name = _name;
        emit NameChanged(name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Task 5.2 write proxy test

We skip local unit test here. We only add a unit test for proxy.

// test/5.BoxProxyV3.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box (proxy) V3 with name", function () {
  let box:Contract
  let boxV2:Contract
  let boxV3:Contract

  beforeEach(async function () {
    const Box = await ethers.getContractFactory("Box")
    const BoxV2 = await ethers.getContractFactory("BoxV2")
    const BoxV3 =  await ethers.getContractFactory("BoxV3")

    //initilize with 42
    box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
    boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
    boxV3 = await upgrades.upgradeProxy(box.address, BoxV3)
  })

  it("should retrieve value previously stored and increment correctly", async function () {
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('42'))
    await boxV3.increment()
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('43'))

    await boxV2.store(100)
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
  })

  it("should set name correctly in V3", async function () {
    expect(await boxV3.name()).to.equal("")

    const boxname="my Box V3"
    await boxV3.setName(boxname)
    expect(await boxV3.name()).to.equal(boxname)
  })

})

// NOTE: should also add test for event: event NameChanged(string name)
Enter fullscreen mode Exit fullscreen mode

Run Test

yarn hardhat test test/5.BoxProxyV3.test.ts
Enter fullscreen mode Exit fullscreen mode

results

  Box (proxy) V3 with name
    ✓ should retrieve value previously stored and increment correctly
    ✓ should set name correctly in V3
  2 passing (748ms)
✨  Done in 3.12s.
Enter fullscreen mode Exit fullscreen mode

Task 5.3 write upgrade script

Edit scripts/3.upgradeV3.ts:

// scripts/3.upgradeV3.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
// const proxyAddress = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
async function main() {
  console.log(proxyAddress," original Box(proxy) address")
  const BoxV3 = await ethers.getContractFactory("BoxV3")
  console.log("upgrade to BoxV3...")
  const boxV3 = await upgrades.upgradeProxy(proxyAddress, BoxV3)
  console.log(boxV3.address," BoxV3 address(should be the same)")

  console.log(await upgrades.erc1967.getImplementationAddress(boxV3.address)," getImplementationAddress")
  console.log(await upgrades.erc1967.getAdminAddress(boxV3.address), " getAdminAddress")    
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

Enter fullscreen mode Exit fullscreen mode

Task 5.4 deploy and interact with BoxV3

STEP 1: upgrade to BoxV3

yarn hardhat run scripts/3.upgradeV3.ts --network localhost
Enter fullscreen mode Exit fullscreen mode

results:

0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0  original Box(proxy) address
upgrade to BoxV3...
0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0  BoxV3 address(should be the same)
0xa513E6E4b8f2a923D98304ec87F64353C4D5C853  getImplementationAddress
0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512  getAdminAddress
✨  Done in 3.52s.
Enter fullscreen mode Exit fullscreen mode

STEP 2: play with V3 in hardhat console

yarn hardhat console --network localhost
Enter fullscreen mode Exit fullscreen mode
address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv3 = await ethers.getContractAt("BoxV3", address)
await boxv3.retrieve()
//BigNumber { value: "42" }

await boxv3.setName("mybox")
// tx response
await boxv3.name()
//'mybox'
Enter fullscreen mode Exit fullscreen mode

STEP 3: try to play with the implementation contract directly again

addressimp = '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853'
boximp = await ethers.getContractAt("BoxV3", addressimp)
await boximp.retrieve()
//BigNumber { value: "0" }
await boximp.name()
//''
Enter fullscreen mode Exit fullscreen mode

You can find explanations about data change in openzeppelin docsopenzeppelin docs:

Due to technical limitations, when you upgrade a contract to a new version you cannot change the storage layout of that contract.

This means that, if you have already declared a state variable in your contract, you cannot remove it, change its type, or declare another variable before it. In our Box example, it means that we can only add new state variables after value.


Task 6 BoxV4

We write Box4 and prepare upgrade script for Task 7 in which we will deploy and upgrade these versions of Box to public testnet Ropsten.

Let's write BoxV4.sol, unit test and upgrade script.

Task 6.1 write BoxV4.sol

We write a BoxV4 which change how we interact with name variable:

  • We change state variable name from public to private.
  • When user getName(), a prefix is added.
// contracts/BoxV4.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./BoxV2.sol";

contract BoxV4 is BoxV2{
    string private name;

    event NameChanged(string name);
    function setName(string memory _name) public {
        name = _name;
        emit NameChanged(name);
    }

   function getName() public view returns(string memory){
      return string(abi.encodePacked("Name: ",name));
    }
}
Enter fullscreen mode Exit fullscreen mode

6.2 write test

Edit test/6.BoxProxyV4.test.ts:

// test/6.BoxProxyV4.test.ts
import { expect } from "chai"
import { ethers, upgrades } from "hardhat"
import { Contract, BigNumber } from "ethers"

describe("Box (proxy) V4 with getName", function () {
  let box:Contract
  let boxV2:Contract
  let boxV3:Contract
  let boxV4:Contract

  beforeEach(async function () {
    const Box = await ethers.getContractFactory("Box")
    const BoxV2 = await ethers.getContractFactory("BoxV2")
    const BoxV3 =  await ethers.getContractFactory("BoxV3")
    const BoxV4 =  await ethers.getContractFactory("BoxV4")

    //initilize with 42
    box = await upgrades.deployProxy(Box, [42], {initializer: 'store'})
    boxV2 = await upgrades.upgradeProxy(box.address, BoxV2)
    boxV3 = await upgrades.upgradeProxy(box.address, BoxV3)
    boxV4 = await upgrades.upgradeProxy(box.address, BoxV4)
  })

  it("should retrieve value previously stored and increment correctly", async function () {
    expect(await boxV4.retrieve()).to.equal(BigNumber.from('42'))
    await boxV4.increment()
    expect(await boxV4.retrieve()).to.equal(BigNumber.from('43'))

    await boxV2.store(100)
    expect(await boxV2.retrieve()).to.equal(BigNumber.from('100'))
  })

  it("should setName and getName correctly in V4", async function () {
    //name() removed, getName() now
    // expect(boxV4).to.not.have.own.property("name")
    expect(boxV4.name).to.be.undefined
    expect(await boxV4.getName()).to.equal("Name: ")

    const boxname="my Box V4"
    await boxV4.setName(boxname)
    expect(await boxV4.getName()).to.equal("Name: "+boxname)
  })

})

Enter fullscreen mode Exit fullscreen mode

Run Test

yarn hardhat test test/6.BoxProxyV4.test.ts
Enter fullscreen mode Exit fullscreen mode

results:

  Box (proxy) V4 with getName
    ✓ should retrieve value previously stored and increment correctly
    ✓ should setName and getName correctly in V4
  2 passing (771ms)
✨  Done in 2.46s.
Enter fullscreen mode Exit fullscreen mode

6.3 prepare script

We will write this script using upgrades.prepareUpgrade.

When calling upgrades.upgradeProxy(), two jobs are done:

  • an implementation contract is deployed
  • ProxyAdmin upgrade() is called to link Proxy and implementation contract.

When calling upgrades.prepareUpgrade(), only the first job is done, and the second is left for developers to do manually.

Edit scripts/4.prepareV4.ts

// scripts/4.prepareV4.ts
import { ethers } from "hardhat";
import { upgrades } from "hardhat";

const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
// const proxyAddress = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
async function main() {
  console.log(proxyAddress," original Box(proxy) address")
  const BoxV4 = await ethers.getContractFactory("BoxV4")
  console.log("Preparing upgrade to BoxV4...");
  const boxV4Address = await upgrades.prepareUpgrade(proxyAddress, BoxV4);
  console.log(boxV4Address, " BoxV4 implementation contract address")
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})
Enter fullscreen mode Exit fullscreen mode

Run deployment:

yarn hardhat run scripts/4.prepareV4.ts --network localhost
Enter fullscreen mode Exit fullscreen mode

Results:

0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0  original Box(proxy) address
Preparing upgrade to BoxV4...
0x610178dA211FEF7D417bC0e6FeD39F05609AD788  BoxV4 implementation contract address
✨  Done in 3.90s.
Enter fullscreen mode Exit fullscreen mode

You can interact with proxy contract (0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0) in hardhat console by running yarn hardhat console --network localhost

address = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'
boxv4 = await ethers.getContractAt("BoxV4", address)

await boxv4.getName()
//We will get error:
//ProviderError: Error: Transaction reverted: function selector was not recognized
Enter fullscreen mode Exit fullscreen mode

Task 7: Deploy Box to public testnet Ropsten

Task 7.1 Prepare to deploy to Ropsten

We can deploy smart contract to Ropsten directly using Hardhat.

STEP 1: Edit Alchemy/Infura endpoint in .env

ROPSTEN_URL=https://eth-ropsten.alchemyapi.io/v2/{YOURS}
PRIVATE_KEY={YOURS HERE}
Enter fullscreen mode Exit fullscreen mode

STEP 2: Make sure you have Ropsten set in hardhat.config.ts:

  networks: {
    ropsten: {
      url: process.env.ROPSTEN_URL || "",
      accounts:
        process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
    },
  },
Enter fullscreen mode Exit fullscreen mode

Task 7.2 deploy Box V1

Run deploy script:

yarn hardhat run scripts/1.deploy_box.ts --network ropsten
Enter fullscreen mode Exit fullscreen mode

deploy box v1

Three contract are deployed:

  • 0x7fcb5f0898ee1394c6cb44e3a62b9e9fc19d0e1c, implementation contract
  • 0x9ce0fdc88df321c804ed0ad9cefe87d97a30479e, ProxyAmin contract
  • 0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268, proxy contract

We can interact with the proxy contract from hardhat console by running yarn hardhat console --network ropsten

In console:

address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("Box", address)
await box.retrieve()
//BigNumber { value: "43" }
Enter fullscreen mode Exit fullscreen mode

Task 7.3 upgrade to Box V2

Run upgrade script

yarn hardhat run scripts/2.upgradeV2.ts --network ropsten
Enter fullscreen mode Exit fullscreen mode

The script does two jobs:

  • deploy new implementation contract
  • call ProxyAmdin.upgrade()

upgrade to BoxV2

Let's interact with it in console:

address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV2", address)

await box.increment()
//wait for tx to be mined in a new block...

await box.retrieve()
//BigNumber { value: "44" }
Enter fullscreen mode Exit fullscreen mode

Task 7.4 upgrade to Box V3

Run upgrade script

yarn hardhat run scripts/3.upgradeV3.ts --network ropsten
Enter fullscreen mode Exit fullscreen mode

Let's interact with it in console:

address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV3", address)

box = await ethers.getContractAt("BoxV3", address)
await box.setName("mybox")
//wait for tx to be mined in a new block...
Enter fullscreen mode Exit fullscreen mode

upgrade to V3

Task 7.5 prepare upgrade to Box V4

Run the prepare upgrade script:

yarn hardhat run scripts/4.prepareV4.ts --network ropsten
Enter fullscreen mode Exit fullscreen mode

This script will deploy a BoxV4 contract at 0xA0726E6e045f84dEe8D7cA4CdD427A68dd336458.

We will upgrade in block explorer manually. In ProxyAmdin contract write page, connect to wallet and run upgrade() like in the screenshot.

prepare BoxV4 and manually upgrade

address = '0x1CD0c84b7C7C1350d203677Bb22037A92Cc7e268'
box = await ethers.getContractAt("BoxV4", address)

box = await ethers.getContractAt("BoxV4", address)
await box.getName()
//'Name: mynewbox'
Enter fullscreen mode Exit fullscreen mode

7.6 read ProxyAdmin

You can also read ProxyAdmin contract to get some information:

  • getProxyAdmin
  • getProxyImplementation
  • owner

read ProxyAdmin

If you would like to verify Box, BoxV2, BoxV3, BoxV4 contract in block explore, you can use hardhat plugin hardhat-etherscan.

Install:

yarn add @nomiclabs/hardhat-etherscan
Enter fullscreen mode Exit fullscreen mode

Verify BoxV4:

yarn hardhat verify 0xA0726E6e045f84dEe8D7cA4CdD427A68dd336458 --network ropsten
Enter fullscreen mode Exit fullscreen mode

You can continue to do more:

  • Use OpenZeppelin Defender to manage upgrading.

  • Transfer ProxyAdmin owner to a multi-sig such as Gnosis Safe multi-sig wallet.


References you may be interested in:


If you find this tutorial helpful, follow me at Twitter @fjun99

Discussion (0)