Creating Upgradable Solidity Contract With Hardhat

Smart contracts are immutable meaning once deployed on the blockchain, they can no longer be edited. This is a good thing but what if the deployed contract is buggy or you need to add a specific function to an already deployed smart contract without de…


This content originally appeared on DEV Community and was authored by Jamiebones

Smart contracts are immutable meaning once deployed on the blockchain, they can no longer be edited. This is a good thing but what if the deployed contract is buggy or you need to add a specific function to an already deployed smart contract without deploying a new contract thereby losing the state of the previous deployed contract.

OpenZeppelin comes to the rescue by providing upgradables that allow smart contract to be updated without state loss. This tutorial makes use of Hardhat and OpenZeppelin upgradable contract.

Create a new npm project by opening your terminal and typing: ( this tutorial assumes the user already has Node and npm installed in the system )

Tutorial Code lives here

  • Project Dependencies
  • Project
  • How Upgradable Contract Works
  • Verify Upgradable Contract
  • Upgrading a smart contract
  • Things to know when working with Upgradable Contracts

Project Dependencies

npm init --y

Open the package.json file that was created when the above command was run and add the following dependencies code below to the package.json file

  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.3",
    "@nomiclabs/hardhat-etherscan": "^2.1.8",
    "@openzeppelin/hardhat-upgrades": "^1.12.0",
    "ethers": "^5.5.2",
    "hardhat": "^2.8.0"
  },
  "dependencies": {
    "dotenv": "^16.0.0"
  }

Install the above dependencies by running npm i. This installs the dependencies into the project. @openzeppelin/hardhat-upgrades provides functionality for creating and deploying upgradable contracts. @nomiclabs/hardhat-etherscan is used for verifying the contract using Etherscan. @nomiclabs/hardhat-ethers allows hardhat to work with ether.js.

Project

Create a new hardhat project by running in the terminal:

 npx hardhat

This presents us options to select a project template. Select the first option which is Create a sample project and this creates a sample project with boiler plate code.

Create a .env file in the project directory. This file will contain our environment variable. In this project, we will need values for the following environmental variables which are:
INFURA_API_KEY
PRI_KEY
ETHERSCAN_API_KEY
INFURA_API_KEY : our API key we get from Infura.We will need this to connect to infura
PRI_KEY: the primary key of your account in Meta mask. This is used for signing a transaction
ETHERSCAN_API_KEY : your API key from Etherscan. This will be used for verifying a contract.

Open the hardhat-config.js file and configure it by adding the code below.

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

require('dotenv').config();
module.exports = {
  solidity: "0.8.10",
  networks: {
    ropsten: {
      url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`,
      accounts: [process.env.PRI_KEY],
    },
    rinkeby: {
      url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
      accounts: [process.env.PRI_KEY]
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

Open the contract folder in the project and delete the Greeter.sol file. Create a new file called CalculatorV1.sol. This will contain the smart contract we will deploy to the rinkeby network.

Inside the file CalculatorV1.sol replace it with the following code below:

pragma solidity 0.8.10;

import "hardhat/console.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract CalculatorV1 is Initializable {
   uint public val;
   function initialize(uint256 _val ) external initializer{
        val = _val;
    }
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }
   function getVal() public view returns (uint) {
        return val;
    }
}

This smart contract is a simple contract of a calculator. The contract inherits from the Initializable contract which is an Openzeppelin contract. It ensures that the initialize function is called only once. An upgradable contract does not have a constructor so the initialize function acts as a constructor and it must be called only once. The initializer modifier ensures the function is called once.

The contract has a public variable named val and three functions which are initialize, add and getVal. We want to deploy this contract to the Rinkeby network which we have set up in the hardhat-config.js file.

Create a new file inside the scripts folder and call it deploy_contract.js. This file will contain the code that will deploy our Calculator contract for us.

Inside the deploy_contract.js file add the following code:

//scripts/deploy_contract.js
const { ethers, upgrades } = require("hardhat");

async function main() {
   const CalculatorV1 = await ethers.getContractFactory("CalculatorV1");
    console.log("Deploying Calculator...");
    const calculator = await upgrades.deployProxy(CalculatorV1, [42], {
        initializer: "initialize",
    });
    await calculator.deployed();
    console.log("Calculator deployed to:", calculator.address);
}

main();

The code above requires ethers and upgrades from hardhat. An async function is created and inside the function, the contract factory is retrieved using ethers with the name of the contract ( CalculatorV1 ). The upgrades.deployProxy is used to deploy the contract passingin the contract factory and the initialization function with its parameter passed.

Remember in the contract, we have an initialize function that sets the value of the val variable. This function is called as the contract is being deployed passing in the value 42 as the parameter to the function.

Run on the terminal the following code to deploy the contract:

 npx hardhat run --network rinkeby scripts/deploy_contract.js

After some few second the contract is deployed with the
contract address logged to the console.

How Upgradable Contract Works

When we deployed the contract, three contracts were deployed in total. These are a Proxy contract, a Proxy Admin contract and the Implementation contract which is our CalculatorV1. When a user interacts with the contract, he is actually interacting with the Proxy contract. The Proxy contract makes a delegateCall to our CalculatorV1 contract. For example A contract named A makes a delegateCall to a contract B calling a function in contract B. The function in B is executed in the state of variable A.

For our upgradable contract, the Proxy contract calls the Implementation contract (CalculatorV1). The state change is made on the Proxy contract. The Proxy Admin contract is used for updating the address of the implementation contract
inside the Proxy contract.

Upgrading a Contract

Create a new file inside the contract folder and name it CalculatorV2.

pragma solidity 0.8.10;

import "hardhat/console.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract CalculatorV2 is Initializable {
    uint public val;

    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    function multiply(uint a, uint b) public pure returns (uint) {
        return a * b;
    }
  function getVal() public view returns (uint) {
        return val;
    }

}

We have added a new function to this version of the contract. The multiply function is added. To upgrade the deployed contract. Create a script inside the scripts folder and create a file upgrade_contract.js. Inside this file put the following code.

const { ethers, upgrades } = require("hardhat");
//the address of the deployed proxy
const PROXY = "0xaf03a6F46Eea7386F3E5481a4756efC678a624e6";

async function main() {
    const CalculatorV2 = await ethers.getContractFactory("CalculatorV2");
    console.log("Upgrading Calculator...");
    await upgrades.upgradeProxy(PROXY, CalculatorV2);
    console.log("Calculator upgraded");
}

main();

The address of the implementation Proxy and the contract factory of the new version of the contract is passed as parameters to upgrades.upgradeProxy. Run the code by typing on the terminal :

 npx hardhat run --network rinkeby scripts/upgrade_contract.js

This will update the address of the Implementation contract in the Proxy contract to make use of the new version deployed. Run the getVal contract to retrieve the value of the state variable val. You will notice that the value of val is still the value we initiated it to be when we deployed the first version of the contract. That is the beauty of upgradable contracts which is the preservation of variable state.

To verify the contract, we have to perform the same steps that was used to verify the first version of the contract.

Things to know when working with Upgradable Contracts
When working with Upgradable contracts the following points should be noted:

  • Constructor: An upgradable contract can not have a constructor. If you have code that must run when the contract is created. The code should be placed in an init function that will get called when the contract is deployed. Openaeppelin Initializable can be used to ensure a function is called once. (initializer)
function initialize(uint256 _val ) external initializer {
        val = _val;
}

The initialize function will be called only once because of the initializer modifier attached to it.

  • state variables : state variables in upgradable contracts once declared cannot be removed. Assuming we have a version one contract where we define the following state variables :
uint public val;
string public name;

When deploying version two of the contract, we must ensure that version two of the contract upgrade also contain the same variable as version one in the same order as was defined in version one. The order of the variable matters. if we want to use new state variables, they are added at the bottom.

uint public val;
string public name;
string public newVariableOne;
uint public newVariableTwo;
  • variable initialization : only state variable declared as const and immutable can be initialize. This is because initializing a state variable will attempt to create a storage for that variable. And as we know the Implementation contract don't use its state. The Proxy contract provides the storage used by the Implementation contract.

The value of variables declared as const are placed in the application code of the contract instead of in storage. That's why only const variable can be initialize.

  • Implementation contract can not contain code that will self destruct the contract. If a contract is self destruct and removed from the blockchain, the Proxy contract will no longer know where to look to execute functions.
function kill() external {
     selfdestruct(payable(address(0)));
}

Summary

Having a way to upgrade smart contracts could come in useful when you need to change and improve the contract code. Thanks for reading...


This content originally appeared on DEV Community and was authored by Jamiebones


Print Share Comment Cite Upload Translate Updates
APA

Jamiebones | Sciencx (2022-04-05T13:43:06+00:00) Creating Upgradable Solidity Contract With Hardhat. Retrieved from https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/

MLA
" » Creating Upgradable Solidity Contract With Hardhat." Jamiebones | Sciencx - Tuesday April 5, 2022, https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/
HARVARD
Jamiebones | Sciencx Tuesday April 5, 2022 » Creating Upgradable Solidity Contract With Hardhat., viewed ,<https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/>
VANCOUVER
Jamiebones | Sciencx - » Creating Upgradable Solidity Contract With Hardhat. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/
CHICAGO
" » Creating Upgradable Solidity Contract With Hardhat." Jamiebones | Sciencx - Accessed . https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/
IEEE
" » Creating Upgradable Solidity Contract With Hardhat." Jamiebones | Sciencx [Online]. Available: https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/. [Accessed: ]
rf:citation
» Creating Upgradable Solidity Contract With Hardhat | Jamiebones | Sciencx | https://www.scien.cx/2022/04/05/creating-upgradable-solidity-contract-with-hardhat/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.