Vechain
Openzeppelin Upgradable

Upgradable Contracts with OpenZeppelin

Create upgradeable contracts

Upgrading a contract to extend functionality or fix bugs is important.

The standard tools for Hardhat are linked with the ethereum-standard, for example @openzeppelin/hardhat-upgrades works like a charm but fails in the VeChain environment.

The magic in @openzeppelin/hardhat-upgrades however is deploying contracts and calling functions. The same can be done manually.

The following steps will show how to deploy a contract and upgrade it with new functionality using all the standard tools and contracts.

Setup project from scratch using Hardhat

yarn init -y
yarn add --dev hardhat @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers @vechain.energy/hardhat-thor @openzeppelin/contracts @openzeppelin/contracts-upgradeable web3-eth-abi
npx hardhat
 
 What do you want to do? · Create an empty hardhat.config.js
 Config file created 

Configure to vechain network

Configure the network in hardhat.config.js:

require("@nomiclabs/hardhat-waffle");
require('@vechain.energy/hardhat-thor')
 
module.exports = {
 solidity: "0.8.4",
 defaultNetwork: "vechain",
 networks: {
    vechain: {
      url: 'https://testnet.veblocks.net',
      privateKey: "0xb79c7c145881219876d4e624f725c439f832b89cbea4e7a7b9cb1f43d8e203f9",
      delegateUrl: 'https://sponsor-testnet.vechain.energy/by/90',
      blockGasLimit: 10000000
    }
  }
};

Configure first contract

With OpenZeppelin (opens in a new tab) configure an upgradeable contract and put it in the project at contracts/MyToken_v1.sol.

This version will be unable to mint tokens:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
 
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
 
contract MyToken_v1 is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}
 
    function initialize() initializer public {
        __ERC721_init("MyToken", "MTK");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }
 
    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}

Deploy with proxy

  1. The NFT Contract is deployed first
  2. The ERC1967Proxy from OpenZeppelin is imported and deployed 3. A reference to the previously deployed NFT Contract is given 4. And the NFT contracts initialize() is called thru the proxy
  3. The address of both deployments is written to status.json for future reference

The script is put into scripts/01-deploy.js:

const hre = require("hardhat")
const fs = require('fs')
const ERC1967Proxy = require('@openzeppelin/contracts/build/contracts/ERC1967Proxy.json')
const Web3EthAbi = require('web3-eth-abi')
 
async function main() {
  await hre.run('compile')
 
  // deploy initial contract  
  const MyToken = await hre.thor.getContractFactory("MyToken")
  const myToken = await MyToken.deploy()
  console.log("MyToken 1.0 deployed to:", myToken.address)
 
  // calculate initialize() call during deployment
  const { abi } = await hre.artifacts.readArtifact("MyToken");
  const callInitialize = Web3EthAbi.encodeFunctionCall(
    abi.find(({ name }) => name === 'initialize'), []
  )
 
  // deploy proxy
  const Proxy = await hre.thor.getContractFactory(ERC1967Proxy.abi, ERC1967Proxy.bytecode)
  const proxy = await Proxy.deploy(myToken.address, callInitialize)
  console.log("Proxy deployed to:", proxy.address)
 
  fs.writeFileSync('./status.json', JSON.stringify({ proxyAddress: proxy.address, myToken_v1Address: myToken.address }, "", 2))
}
 
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

The result:

$ node scripts/01-deploy.js 
Compiled 17 Solidity files successfully
MyToken 1.0 deployed to: 0x03c41F547eB5A45C90208eFbb130252F7128264f
Proxy deployed to: 0x39fa815f8e3d095789E730D08D1E250cf0e002ca

Configure new contract

To fix the missing minting-functionality the wizard is used to create a modified contract MyToken_v2 in contracts/MyToken_v2.sol.

Using a different contract name is important to access the correct version in each deployment step.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
 
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
 
contract MyToken_v2 is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}
 
    function initialize() initializer public {
        __ERC721_init("MyToken", "MTK");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }
 
    function safeMint(address to, uint256 tokenId) public onlyOwner {
        _safeMint(to, tokenId);
    }
 
    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}
}

Deploy upgrade

EIP1967 (opens in a new tab) is the standard that defines how proxies behave and its bytecode was used during the initial deployment.

With upgradeTo the proxy can point to a different contract address. The ABI for this function definition is at a different location. UUPSUpgradeable is therefore imported and used to communicate with the deployed proxy.

In addition the first token is minted to the deploying address:

const hre = require("hardhat")
const fs = require('fs')
const UUPSUpgradeable = require('@openzeppelin/contracts/build/contracts/UUPSUpgradeable.json')
const status = require('../status.json')
 
async function main() {
  await hre.run('compile')
 
  const MyToken_v2 = await hre.thor.getContractFactory("MyToken_v2")
  const myToken_v2 = await MyToken_v2.deploy()
  console.log("MyToken 2.0 deployed to:", myToken_v2.address)
 
  const proxy = await hre.thor.getContractAt(UUPSUpgradeable.abi, status.proxyAddress)
  await proxy.upgradeTo(myToken_v2.address)
  console.log("Proxy upgraded to:", myToken_v2.address)
 
  fs.writeFileSync('./status.json', JSON.stringify({ ...status, myToken_v2Address: myToken_v2.address }, "", 2))
 
  const [deployer] = await hre.thor.getSigners()
  const address = await deployer.getAddress()
  const proxiedMyToken = await hre.thor.getContractAt("MyToken_v2", status.proxyAddress)
  await proxiedMyToken.safeMint(address, 1)
  console.log(`Minted Token #1 to ${address}`)
}
 
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

This time the result looks like this:

$ node scripts/02-upgrade.js
Compiled 1 Solidity file successfully
MyToken 2.0 deployed to: 0x3697DD85Eb46f4C61323111089972Cf753910BDb
Proxy upgraded to: 0x3697DD85Eb46f4C61323111089972Cf753910BDb
Minted Token #1 to 0x1dF12f7c3c2ed2339409388Da9050c73C90Eb938

Links