How to Upgrade Smart Contracts with Hardhat


23 Feb 2023  Sergio Martin Rubio  11 mins read.

Smart contracts are no upgradable by nature, so this means that once deployed on the blockchain, there is no way to make any modifications. There are scenarios where deploying a new version of the smart contract is necessary for multiple reasons like vulnerabilities or extension of the smart contract - and this can be done through Smart Contract proxies.

Hardhat deploy plugin provides support for upgrading smart contracts through proxies, and we just need to specify that we are going to use a proxy on our deployment script 🙌, so let’s see how it works with an example.

Getting Started

  1. Create a Hardhat project:
yarn add --dev hardhat
yarn hardhat
  1. Import dependencies:
yarn add --dev hardhat-deploy dotenv
  1. Update your hardhat.config.ts file. You can use this one as an example (more dependencies might be missing, so you will need to import them).

  2. Create your .env file for your environment variables.

Creating the Contracts

We are going to create two versions of a Smart Contract. The original version of the smart contract is going to have 3 functions:

  • setValue(uint256) public
  • getValue() public view returns (uint256)
  • version() public pure returns (uint256)

contract/MyContract.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract MyContract {
    uint256 internal value;

    event ValueChanged(uint256 newValue);

    function setValue(uint256 _value) public {
        value = _value;
        emit ValueChanged(_value);
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function version() public pure returns (uint256) {
        return 1;
    }
}

And the second version will have the same functions as V1 plus an increment function:

  • increment() public

contract/MyContractV2.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract MyContractV2 {
    uint256 internal value;

    event ValueChanged(uint256 newValue);

    function setValue(uint256 _value) public {
        value = _value;
        emit ValueChanged(_value);
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function increment() public {
        value++;
    }

    function version() public pure returns (uint256) {
        return 2;
    }
}

Create Deploy Script

As we already mentioned we are going to use the hardhat-deploy plugin for making our smart contract upgradable. You can use the default proxy contract by simply setting proxy: true, but we want to use an admin user to perform upgrades, which is recommended for Transparent Proxies. Therefore, we will also need a Smart Contract Admin proxy, so we are going to use the Transparent Upgradable Proxy OpenZeppelin implementation.

Firstly, we need to add the contracts from OpenZeppelin:

yarn add --dev @openzeppelin/contracts

The deployment script should look like this:

deploy/01_Deploy_MyContract.ts

import { DeployFunction } from "hardhat-deploy/dist/types"
import { network } from "hardhat"
import {
    developmentChains,
    VERIFICATION_BLOCK_CONFIRMATIONS,
} from "../helper-hardhat-config"
import { verify } from "../utils/verify"

const deployFunction: DeployFunction = async ({
    getNamedAccounts,
    deployments,
}) => {
    const { deploy, log } = deployments

    const { deployer } = await getNamedAccounts()
    const chainId: number | undefined = network.config.chainId
    if (!chainId) return

    const waitBlockConfirmations: number = developmentChains.includes(
        network.name
    )
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS
    const myContract = await deploy("MyContract", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
        proxy: {
            proxyContract: "OpenZeppelinTransparentProxy",
        },
    })

    if (
        !developmentChains.includes(network.name) &&
        process.env.ETHERSCAN_API_KEY
    ) {
        log("Verifying...")
        await verify(myContract.address, [])
    }
    log(`----------------------------------------------------`)
}

export default deployFunction
deployFunction.tags = ["all", "my-contract", "main"]

When OpenZeppelinTransparentProxy is chosen as the proxyContract option, the DefaultProxyAdmin is also used as admin since Transparent Proxy. Alternatively, you can set the proxy admin contract with viaAdminContract.

...

proxy: {
    proxyContract: "OpenZeppelinTransparentProxy",
    viaAdminContract: {
        name: "MyContractProxyAdmin",
        artifact: "MyContractProxyAdmin",
    },
},

...

You can also use the OptimizedTransparentProxy which is like OpenZeppelinTransparentProxy but it is optimized to not require storage read for the admin on every call.

Deploy Upgradable Smart Contract

Now that you have your Smart Contract implementation and the deployment script you can deploy the smart contracts to a local node.

yarn hardhat node

The deployment command deployed three contracts:

  • DefaultProxyAdmin: The admin contract that we will use for upgrading the implementation address.
  • MyContract_Implementation: The deploy implementation that was renamed and appended _Implementation.
  • MyContract_Proxy: The proxy contract. Calling this contract address will point to the address of MyContract_Implementation.

Upgrade Script

Unfortunately, Hardhat doesn’t currently have a deployment feature for upgradable contracts, so we will have to write our own script.

The following script will:

  1. Get the DefaultProxyAdmin, MyContract_Proxy and MyContractV2.
  2. Use the DefaultProxyAdmin to upgrade MyContract_Proxy implementation address to MyContractV2 address.

scripts/upgrade-my-contract.ts:

import { ContractTransaction } from "ethers"
import { ethers } from "hardhat"
import {
    MyContract,
    MyContractV2,
    ProxyAdmin,
    TransparentUpgradeableProxy,
} from "../typechain"

async function main() {
    const proxyAdmin: ProxyAdmin = await ethers.getContract("DefaultProxyAdmin")
    const transparentProxy: TransparentUpgradeableProxy =
        await ethers.getContract("MyContract_Proxy")

    // V1
    const implementation = await proxyAdmin.getProxyImplementation(
        transparentProxy.address
    )
    const proxyMyContract: MyContract = await ethers.getContractAt(
        "MyContract", // abi
        transparentProxy.address
    )
    const contractVersion = await proxyMyContract.version()
    console.log(
        `Implementation (${implementation}) version is: ${contractVersion}`
    )

    const myContractV2: MyContractV2 = await ethers.getContract("MyContractV2")
    const upgradeTx: ContractTransaction = await proxyAdmin.upgrade(
        transparentProxy.address,
        myContractV2.address
    )
    await upgradeTx.wait(1)

    // V2
    const implementationV2 = await proxyAdmin.getProxyImplementation(
        transparentProxy.address
    )
    const proxyMyContractV2: MyContractV2 = await ethers.getContractAt(
        "MyContractV2", // V2 abi
        transparentProxy.address
    )
    const newContractVersion = await proxyMyContractV2.version()
    console.log(
        `Implementation (${implementationV2}) version is: ${newContractVersion}`
    )
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })

So, let’s run the upgrade script with:

yarn hardhat run scripts/upgrade-my-contract.ts --network localhost

Unit Testing

Finally we can write some unit tests.

test/unit/MyContract.spec.ts:

import { expect } from "chai"
import { ContractTransaction } from "ethers"
import { network, deployments, ethers } from "hardhat"
import { developmentChains } from "../../helper-hardhat-config"
import {
    MyContract,
    MyContractV2,
    ProxyAdmin,
    TransparentUpgradeableProxy,
} from "../../typechain"

!developmentChains.includes(network.name)
    ? describe.skip
    : describe("Upgrade MyContract Unit Tests", () => {
          let transparentProxy: TransparentUpgradeableProxy
          let proxyMyContractV1: MyContract
          let proxyMyContractV2: MyContractV2
          let proxyAdmin: ProxyAdmin
          beforeEach(async () => {
              await deployments.fixture(["all"])
              transparentProxy = await ethers.getContract("MyContract_Proxy")
              proxyMyContractV1 = await ethers.getContractAt(
                  "MyContract", // abi
                  transparentProxy.address
              )
              proxyMyContractV2 = await ethers.getContractAt(
                  "MyContractV2", // abi
                  transparentProxy.address
              )
              proxyAdmin = await ethers.getContract("DefaultProxyAdmin")
          })

          it("Should upgrade contract to V2", async () => {
              // GIVEN
              const contractV1Version = await proxyMyContractV1.version()
              expect(contractV1Version).to.equal(1)

              // WHEN
              const myContractV2: MyContractV2 = await ethers.getContract(
                  "MyContractV2"
              )
              const upgradeTx: ContractTransaction = await proxyAdmin.upgrade(
                  transparentProxy.address,
                  myContractV2.address
              )
              await upgradeTx.wait(1)

              // THEN
              const contractV2Version = await proxyMyContractV2.version()
              expect(contractV2Version).to.equal(2)
          })

          it("Should increment when upgraded to V2", async () => {
              // GIVEN
              const value = await proxyMyContractV2.getValue()
              expect(value).to.equal(0)
              const myContractV2: MyContractV2 = await ethers.getContract(
                  "MyContractV2"
              )
              const upgradeTx: ContractTransaction = await proxyAdmin.upgrade(
                  transparentProxy.address,
                  myContractV2.address
              )
              await upgradeTx.wait(1)

              // WHEN
              proxyMyContractV2.increment()

              // THEN
              const newValue = await proxyMyContractV2.getValue()
              expect(newValue).to.equal(1)
          })
      })

Source Code