DEV Community

fangjun
fangjun

Posted on • Updated on

Web3 Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin

There are several good articles about upgradeable smart contracts or proxy pattern. But I can't find a step-by-step how-to guide to build, deploy a proxy contract and interact with it. So I wrote one and here it is.

In this tutorial, I list 7 details tasks which you can follow to deploy proxy contract to local testnet as well as public testnet Ropsten. Each task has several sub-tasks.

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

Special thanks for two related guides in OpenZeppelin ecosystem: OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat and Gnosis Safe by abcoathup, OpenZeppelin Defender guide Upgrading a contract via a multi-sig and Defender.


How upgradeable smart contract works?

how proxy contract works

This illustration explains how upgradeable smart contracts work. To be specific, this is transparent proxy pattern. Another one is UUPS proxy pattern (Universal Upgradeable Proxy Standard).

Upgradeable smart contracts are composed of 3 contracts:

  • Proxy contract. The smart contract which user interacts with. It will keep data/state which means data is stored in the context of this proxy contract account. This is an EIP1967 standard proxy contract.

  • Implementation contract. The smart contract provides functionality and logic. Please note that the data is also defined in this contract. This is the smart contract you are building.

  • ProxyAdmin contract. The contract links Proxy and Implementation.

ProxyAmdin explained in OpenZeppelin docs:

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.

If you transfer ProxyAmin ownership to a multi-sig account, the authority to upgrade the Proxy contract (link proxy to new implementation) is transferred to it.


How to deploy proxy? How to upgrade proxy?

When we first deploy upgradeable contract using OpenZeppelin Upgrades plugin for Hardhat, we deploy 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 calls the proxy contract, the call is delegated to the implementation contract (delegate call).

When upgrading the contract, what we do is:

  1. deploy a new "Implementation contract"
  2. upgrade in "ProxyAdmin contract" by redirecting all calls to proxy to the new Implementation contract.

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

If you want 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

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 plugin @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 learn guide. We will build several versions of this contract:

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

The big difference between a normal contract and an upgradeable contract is that an upgradeable contract does not have a constructor(), docs 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

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

Task 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). We made some changes in it.

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

Task 2: Deploy upgradeable smart contract

Task 2.1: write deploy script with "OpenZeppelin Upgrades plugin for Hardhat"

When we write a 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() instead. 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 an initial value 42 by calling store() as initializer.

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().
  • Three contracts will be deployed: Implementation, ProxyAdmin, Proxy. We log their addresses for inspection.

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 terminal:

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 is always the same: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

Task 2.3: test whether Box.sol using proxy pattern works correctly

Users interact with the implementation contract through the box(proxy) contract.

To make sure that they work correctly, let's add unit test for this scenario. In the unit test, we 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")
    //initialize 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 is working correctly now.

Later, we find that we need an increment() function. Instead of re-deploying this contract, migrate data to the new contract and ask all users to access the new contract address. We can upgrade the contract easily thanks to the proxy pattern.


Task #3

Task 3: Upgrade smart contract to BoxV2

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 for normal deployment

We write a 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 retrieve value 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 retrieve value 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 deployment

We write a unit test script for BoxV2 deployed in proxy pattern:

  • first, we deploy 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(proxy) to 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0.

In this sub-task, we will upgrade it to BoxV2 (deploy a new contract , and link proxy to a new implementation contract in ProxyAdmin):

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 script

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(proxy) will be 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

Task 4: Play with contract in hardhat console

Let's play with Box(proxy) 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 the 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. This is because the data is stored in the context of Proxy contract (0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0) under the hood, not in the context of Implementation contract(0x5fc8d32690cc91d4c39d9d3abcbd16989f875707).


Task #5

Task 5: Build BoxV3 to add new state variable

We can add new state variables 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 test for proxy deployment

We skip local unit test here. We will test the proxy deployment only.

// 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")

    //initialize 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.


Image description

Task 6: Write BoxV4 with prepareUpgrade script

We write BoxV4 and prepare upgrade script for Task 7 in which we will deploy and upgrade these versions of Box to public testnet Ropsten manually call ProxyAdmin.upgrade().

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 state variable:

  • We change the 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

Task 6.2 write test for proxy deployment

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")

    //initialize 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

Task 6.3: write script to prepare upgrade

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

Task 7: Deploy Box to public testnet Ropsten

Task 7.1: preparations 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 contracts 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 our 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 the Box(proxy) 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 our upgrade script:

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

Let's interact with the Box(proxy) with BoxV3 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 https://ropsten.etherscan.io manually.

ProxyAdmin is already verified on Etherscan block explorer. 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

Task 7.6: read ProxyAdmin contract

You can also read ProxyAdmin contract to get more information about the Box(proxy) contract:

  • getProxyAdmin
  • getProxyImplementation
  • owner

read ProxyAdmin

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

Install plugin:

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 have built and deploy proxy contract in local testnet and public testnet Ropsten in 7 tasks. You can continue to do more:

  • Use OpenZeppelin Defender to manage upgrading.

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


References you may be interested in:


Tutorial List:

1. A Concise Hardhat Tutorial(3 parts)

https://dev.to/yakult/a-concise-hardhat-tutorial-part-1-7eo

2. Understanding Blockchain with Ethers.js(5 parts)

https://dev.to/yakult/01-understanding-blockchain-with-ethersjs-4-tasks-of-basics-and-transfer-5d17

3. Tutorial : build your first DAPP with Remix and Etherscan (7 Tasks)

https://dev.to/yakult/tutorial-build-your-first-dapp-with-remix-and-etherscan-52kf

4. Tutorial: build DApp with Hardhat, React and Ethers.js (6 Tasks)

https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi

5. Tutorial: build DAPP with Web3-React and SWR

https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0

6. Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin(7 Tasks)

https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916

7. Tutorial: Build a NFT marketplace DApp like Opensea(5 Tasks)

https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9


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

Top comments (6)

Collapse
 
chubidurcel profile image
Daniel Kozhevnikov

Awesome! Greate job

Collapse
 
tuantuismyname profile image
Nguyen Tuan Tu

Thank you so much, this is the first time I see a nitty-gritty article like that.

Collapse
 
germavinsmoke profile image
GermaVinsmoke • Edited

Great article!
I've a question. At the time of v4, we discarded v3 and instead took v2 as the base contract - the reason was that we wanted to change the implementation of setName function and the access modifier of name variable right?

So, what if we add function "A" in v3.
And we reached till v20, in that, we added functions - B to N
But now, in v20 we want to change the implementation of function "A".
Then in that case, we need to use v2 as the base of v21 contract? And what will happen to all the contracts from B to N?

Collapse
 
jamillhallak profile image
Jamill-hallak

I think:
_v2 must be the base all the time ,
_from B to N it will live on chain but the user only interact with v21 by the base address (v2).
_if you want to interact with B to N it will be done by itself address .

Collapse
 
linghuccc profile image
LingHu Chong

All the above worked fine until Task 7: Deploy Box to public testnet Ropsten.
When I am following Task 7.2: deploy Box V1, and try to deploy the code on sepolia/bnb test net. I received below error:
Error: Contract at 0xbE1d86B2e28e8214EeCE8f2292bd36D3a8bb2F42 doesn't look like an ERC 1967 proxy with a logic contract address.

The strange thing is that when I go to etherscan, the contract is there and it is indeed a proxy! I searched online and could not find out the reason. Please assist. Thank you!

Collapse
 
arynyestos profile image
arynyestos

Probably too late, since it's been two and a half months, but just created a dev account to respond to this hahaha

I had the same issue you did. This is because online testnets are not as fast as Hardhat's localhost testnet, therefore, even though you can see your contract in Etherscan and it is a perfectly normal proxy contract, the getImplementationAddress function run in your script won't be detected as such, since some block confirmations must be missing when the function get executed or something.

I was hoping to find a solution to this that used some sort of query to the blockchain in order to wait for everything to be in place before trying getImplementationAddress, but I couldn't find a way, so I just rewrote the script to try that line a maximum of ten times with a 1 second delay and that worked.

Hope this helps!