Skip to main content

How to Create and Deploy an Upgradeable Smart Contract using OpenZeppelin and Hardhat

Updated on
Dec 11, 2023

16 min read

Overview​

One hard rule about developing on the blockchain is that any smart contracts that are deployed cannot be altered. Smart contracts are often called "immutable" which ensures that the code that developers are interacting with is tamper-proof and transparent. This philosophy is beneficial to those interacting with smart contracts but not always to those writing them. Developers writing smart contracts must always ensure that it is all-encompassing, error-free, and covers every edge case. This is often the case, but not always, and that is where the need for upgradeable smart contracts arises.

Using the upgradeable smart contract approach, if there is an error, faulty logic or a missing feature in your contract, a developer has the option to upgrade this smart contract and deploy a new one to be used instead.

In this tutorial, we will demonstrate exactly how this is done by creating and deploying an upgradeable smart contract from scratch using OpenZeppelin and Hardhat.

What We Will Do

  • Create an upgradeable smart contract using OpenZeppelin’s Plug-ins for Hardhat
  • Compile and deploy the contract on the Mumbai Testnet using Hardhat
  • Verify the contract using Polygonscan API
  • Upgrade the contract and verify the results

What You Will Need

  • NPM (Node Package Manager) and Node.js (Version 16.15 recommended)
  • MetaMask with the Polygon Mumbai Testnet selected (you can learn how to add the network to your wallet here)
  • MATIC tokens on Mumbai Testnet (you can get some at this faucet)
  • Previous experience with Solidity
  • Knowledge of upgradeable smart contracts. You can refer to our "An Introduction to Upgradeable Smart Contracts" guide to learn more about the theory behind upgradeable smart contracts.

Setting up the Development Environment​

We will need a new folder locally where our project for this tutorial will live. We will name ours UpgradeableContracts, but you can call it anything you like. Run these commands in your terminal to create the folder and navigate into it:

Great! Now that we have a blank canvas to work on, let us get down to painting it. Execute these two commands in your terminal:

The first command, npm init -y, initializes an empty package.json file in your directory, while the second command installs Hardhat as a development dependency which allows you to set up an Ethereum development environment easily.

At this point, you can open and view your folder in your code editor of choice. We’ll be using VScode and will continue running our commands in the embedded terminal. Feel free to use the original terminal window you’ve initialized your project in.

Now, run the following command in your terminal to start Hardhat:

If everything is installed correctly, your terminal will look like this:

Hardhat Configuration Setup

Congratulations! You just successfully installed and initialized Hardhat.

Next, click on Create a basic sample project, and press Enter through all the questions Hardhat asks. This will choose the default settings which will allow Hardhat to create a basic sample project in your projects root directory. Additionally, Hardhat will create a .env file and install the sample projects dependency (e.g., @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers). It usually takes a while to install them all.

Once the installation is complete, you should now have everything you need to develop, test and deploy smart contracts on the blockchain. Since we’ll be working with upgradeable smart contracts, we will need to install two more dependencies. Execute the following lines in your terminal:

@openzeppelin/hardhat-upgrades is the package that allows us to deploy our smart contracts in a way that allows them to be upgradeable. (We’ll touch more on this later). @nomiclabs/hardhat-etherscan is a hardhat plugin that allows us to verify our contracts in the blockchain. This allows anyone to interact with your deployed contracts and provides transparency. Using the hardhat plugin is the most convenient way to verify our contracts.

Kudos if you were able to follow the tutorial up to here. You just set up a smart contract development environment using Hardhat and installed additional dependencies that will allow us to deploy and verify upgradeable smart contracts.

Accessing a Polygon Mumbai Testnet Node​

We'll need to deploy our contract on the Polygon Mumbai Testnet. We can simply sign up with a free QuickNode account here and create an Ethereum endpoint.

QuickNode Endpoints page

Copy the HTTP URL and paste it into the RPC_URL variable in your .env file.

Next, go to your profile on PolygonScan and navigate to the API KEYS tab. If you do not have an account, create one here. Here you will create an API key that will help you verify your smart contracts on the blockchain. Copy the API key and paste it into the ETHERSCAN_API_KEY variable in your .env file.

Lastly, go into your MetaMask and copy the private key of one of your accounts. To learn how to access your private key, check out this short guide. Paste this private key into the PRIVATE_KEY variable in your .env file.

You will also need to have a few Mumbai Testnet MATIC in your account to deploy your contracts. You can get some at this faucet.

Finally, open your hardhat.config file, and replace the entire code with this:

require("@nomiclabs/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config()

module.exports = {
solidity: "0.8.4",
networks: {
mumbai: {
url: process.env.RPC_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
}
};

The first few lines we've used to import several libraries we'll need. The hardhat-upgrades package is the plugin that allows us to call the function that deploys upgradeable contracts.

To confirm everything runs correctly, save all your files and compile the contracts once more by running the command:

If you followed all the steps correctly, Hardhat will compile your contracts again and give you a confirmation message. We’re now ready to deploy our contracts.

Creating our Smart Contracts​

In this section, we will create two basic smart contracts. We will deploy the first smart contract, and later we will upgrade it to the second smart contract.

Go into the contracts folder, and delete the pre-existing Greeter.sol file. That is a default smart contract template provided by Hardhat and we don’t need it. Now create a new file in the contracts folder, named contractV1.sol, and paste the following code in the file:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

contract V1 {
uint public number;

function initialValue(uint _num) external {
number=_num;
}

function increase() external {
number += 1;
}
}

This contract is pretty simple. It has one state variable of type unsigned integer and two functions. The function initialValue() simply sets the initial value of the variable, while the function increase() increments its value by 1.

Create another file in the contracts folder, and name it contractV2.sol. Paste the following code into the file:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

contract V2 {
uint public number;

function initialValue(uint _num) external {
number=_num;
}

function increase() external {
number += 1;
}

function decrease() external {
number -= 1;
}
}

After deploying the contract V1, we will be upgrading it to contract V2. In the second contract, we merely add a function decrease(), which will decrease the value of the variable by 1.

Save the files that you have been working with and navigate back to the terminal. Confirm that you are in the project directory (e.g, UpgradeableContracts) and then run this command in your terminal:

If you did everything correctly, the terminal should tell you that it has compiled two solidity files successfully. We are now ready to configure our deployment tools. The next section will teach you the best practices when it comes to deploying your contracts.

Setting Up the HardHat Configuration File​

When Hardhat is run, it searches for the nearest hardhat.config file. This is the file that contains the specifications for compiling and deploying our code. It is very important to work with this file carefully. Before we work with the file, however, we need to install one last package.

Open up your terminal, and run these commands in succession:

This installs the dotenv library and sets up an .env file in our hardhat project, which we will use to store sensitive data. Open the .env file and paste the following content:

We'll fill in these empty variables in the following sections.

Deploying Contract V1​

We are now ready to deploy our upgradeable smart contract! Under the scripts folder, delete the sample-script.js file and create a new file named deployV1.js.

In this new file, paste the following code:

const { ethers, upgrades } = require("hardhat");

async function main() {
const gas = await ethers.provider.getGasPrice()
const V1contract = await ethers.getContractFactory("V1");
console.log("Deploying V1contract...");
const v1contract = await upgrades.deployProxy(V1contract, [10], {
gasPrice: gas,
initializer: "initialvalue",
});
await v1contract.deployed();
console.log("V1 Contract deployed to:", v1contract.address);
}

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

Let’s break down this code:

  • Line 1: First, we import the relevant plugins from Hardhat.
  • Lines 3-5: We then create a function to deploy our V1 smart contract and then print a status message. Keep in mind that the parameter passed to the getContractFactory() function should be the name of the contract, not the name of the file it is written in. In our case, the contract is named V1 and is stored in the file contractV1.sol.
  • Lines 6-8: We then deploy our contract V1 by calling deployProxy from the upgrades plugin. We pass a couple of parameters to the deployProxy. First the variable that holds the contract we want to deploy then the value we want to set. The initializer function is provided to us by upgrades, and whatever function we pass to it will be executed only once at the time of the contract deployment.
  • Lines 9-10: Then we call the deploy function and print a status message with the deployed contract address to our terminal.
  • Lines 13-16: We can now simply call our function main() which will run the logic in our function.

Look back to contract V1 and see what the initialValue function does. 10 is the parameter that will be passed to our initialValue function. Hence, after deployment, the initial value of our variable will be 10.

Now, go back to your project's root directory and run this command in your terminal:

This is a typical hardhat command to run a script, along with the network flag that ensures that our contract is deployed to the Mumbai testnet. This command will deploy your smart contract to the Mumbai Testnet and return an address. Your terminal should look like this:

Terminal output from deploying deployV1.sol

If you are returned an address, that means the deployment was successful. Congrats! You just deployed a smart contract to the Polygon Mumbai Testnet using Openzeppelin’s Transparent Upgradeable proxy.

You may be wondering what exactly is happening behind the scenes. Let’s pause and find out.

Peeking Under the Hood​

Ignore the address the terminal returned to us for now, we will get back to it in a minute. Instead, go to MetaMask and copy the public address of the account that you used to deploy the smart contract. Open the Mumbai Testnet explorer, and search for your account address.

You will see that your account has deployed not one but three different contracts.

PolygonScan Account Transactions Page

To see each individual contract, you can click the Contract Creation link under the To field on the Transactions tab.

Open all three contract addresses in three different tabs. So what’s happening here? In the three contract addresses that you opened, click on the contract tab on each of their pages. You should have something like this:

PolygonScan Contract Proxy Tab

To check if your contract is verified, you'll see a checkmark logo on the Contract tab and the smart contracts source code will be available. You will note that all the contracts (e.g, ProxyAdmin, TransparentUpgradeableProxy & V1) should already be verified if you used the same code. This is because PolygonScan detects the same bytecode already existing on the network and verifies the contract for us automatically, thanks PolygonScan!

However note, if you changed any code in the implementation contract (e.g, V1), you'll need to verify it before you can continue. To quickly verify the contract, run this command in the terminal:

npx hardhat verify --contract "contracts/contractV1.sol:V1" <insert V1 address> --network mumbai

If you have named your files or contracts differently from us, edit that command accordingly. Through this command, we point to the exact code of the contract we want to verify and use the hardhat-etherscan package to send a verification request.

Your terminal should now look like this:

Verifying deployV1 contract with Hardhat and Etherscan

Now refresh the webpage of your implementation contract (V1), and you should see a green checkmark there too.

Whenever you deploy a smart contract using the deployProxy function, OpenZeppelin deploys two additional contracts for you, namely TransparentUpgradeableProxy and ProxyAdmin. TransparentUpgradeableProxy is the main contract here. This contract holds all the state variable changes for our implementation contract. This means that the implementation contract does not maintain its own state and actually relies on the proxy contract for storage.

In this scenario, the proxy contract (TransparentUpgradeableProxy) is the wrapper for our implementation contract (V1), and if and when we need to upgrade our smart contract (via ProxyAdmin), we simply deploy another contract and have our proxy contract point to that contract, thus upgrading its state and future functionality. How cool is that!

In the end, we did not actually alter the code in any of our smart contracts, yet from the user’s perspective, the main contract has been upgraded. This flow chart will give you a better understanding:

Upgradeable Smart Contracts Flowchart

You may recall that the terminal returned us an address when we initially deployed our smart contract. If you go back to it, you will find that it is actually the address of our TransparentUpgradeableProxy contract. That is because, as of now, any user who wants to interact with our implementation contract will actually have to send their calls through the proxy contract. So it makes sense to just use that particular address.

Let us follow through with a few more steps to better cement these concepts in our minds. On the implementation contract (i.e, the contract named V1) webpage, go to the Read Contract tab on Etherscan:

Etherscan Read Tab for deployV1.sol

As you can see, our only state variable has the value zero. This is because even though we did initialize the state variable correctly, the value of the variable simply isn’t stored in the implementation contract. Only code is stored in the implementation contract itself, while the state is maintained by the TransparentUpgradeableProxy contract.

So now go to the TransparentUpgradeableProxy contract and try to read from it. But you won’t be able to read it, despite it being verified. Why? Well, that’s because we need to tell the block explorer that the contract indeed is a proxy, even though the explorer usually already suspects it.

Under the Contract > Code tab on the contract’s page, click on more options and then click Is this a Proxy?.

Here you can verify the contract as a proxy. Then, return to the original page. You should now see a few additional options on the TransparentUpgradeableProxy’s contract page. Click on Read as Proxy.

Read as Proxy tab on Etherscan

Voila! You can see that the value of the state variable of our contract has been stored as 10 over here, which shows that this is the smart contract responsible for maintaining our implementation contract’s state. Go to the Write as Proxy page and call the increase function. After the transaction is successful, check out the value of number again. It increases by 1, which means our function is being successfully called from the implementation contract.

Upgrading Contract V1 to V2​

Now that we have a solid understanding of what's happening on the backend, let us return to our code and upgrade our contract! Under the scripts folder, create a new file named upgradeV1.js. Inside, paste the following code:

const { ethers, upgrades } = require("hardhat");

const UPGRADEABLE_PROXY = "Insert your proxy contract address here";

async function main() {
const gas = await ethers.provider.getGasPrice()
const V2Contract = await ethers.getContractFactory("V2");
console.log("Upgrading V1Contract...");
let upgrade = await upgrades.upgradeProxy(UPGRADEABLE_PROXY, V2Contract, {
gasPrice: gas
});
console.log("V1 Upgraded to V2");
console.log("V2 Contract Deployed To:", upgrade.address)
}

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

There is just one change in this script as compared to our first one. Here, we don’t call the deployProxy function. Instead, we call the upgradeProxy function. This is because our proxy contract (e.g, TransparentUpgradeableProxy) has already been deployed, here we just deploy a new implementation contract (V2) and pass that to the proxy contract. We do NOT redeploy the proxy here.

Before we upgrade our contract, remember to paste your proxy contract address (e.g, TransparentUpgradeableProxy address) in the variable UPGRADEABLE_PROXY above.

Now, let us run this script in the terminal:

npx hardhat run --network mumbai scripts/upgradeV1.js

What basically happened here is that we called the upgrade function inside the proxy admin contract. Do note that only the account that deployed the proxy contracts can call the upgrade function, and that is for obvious reasons. This causes the TransparentUpgradeableProxy proxy contract to now point to the address of the newly deployed V2 contract. Check out the flow chart below:

Upgradeable Smart Contracts Flowchart

Please note that the address of the user who calls a particular function (msg.sender) is critical here. The address determines the entire logic flow.

If the msg.sender is any other user besides the admin, then the proxy contract will simply delegate the call to the implementation contract, and the relevant function will execute. Thus, the proxy contract calls the appropriate function from the implementation contract on behalf of msg.sender, the end-user. As explained before, the state of the implementation contract is meaningless, as it does not change. What does change is the state of the proxy contract, which is determined on the basis of what is returned from the implementation contract when the required function executes.

This means that if the caller is not an admin, the proxy contract will not even consider executing any sort of upgrade function. If the caller is not an admin, the call is forwarded or ‘delegated’ to the implementation contract without any further delay. This is called a ‘delegate call’ and is an important concept to understand. If the caller is however the admin, in this case, our ProxyAdmin contract, the call is not automatically delegated, and any of the functions of the proxy contract can be executed, including the upgrade function.

Now the final steps. Go to your transparent proxy contract and try to read the value of ‘number’ again. You will not be able to do so. This is because the proxy now points to a new address, and we need to re-verify the contract as a proxy to read the state variable.

However, for that, you need to verify the contract V2 beforehand. Run this command in the terminal:

npx hardhat verify --contract "contracts/contractV2.sol:V2" <insert V2 address> --network mumbai

Note, you'll need to input the V2 contract address in the command above. The V2 address was previously logged in your terminal after you ran the upgradeV1.js script.

After you verify the V2 contract, navigate to the TransparentUpgradeableProxy contract on the Mumbai block explorer and under the Contract - Write as Proxy tab, this is what your screen should look like:

Etherscan Read Tab for deployV1.sol

As you can see, the proxy contract now points to the new implementation contract (V2) we just deployed. Furthermore, we now have the decrease function too. We can call that and decrease the value of our state variable.

That’s it. You just deployed an upgradeable smart contract and then upgraded it to include a new function. Now push the code to Github and show it off! One last caveat, remember how we used a .env file to store our sensitive data? The purpose of the file was to prevent our sensitive data from being published publicly, thus compromising our assets on the blockchain. After verifying that you have the .env file name listed in your .gitignore, you can then push your code to GitHub without worries since you have no private data in your hardhat.config file.

Conclusion​

Give yourselves a pat on the back. You have earned it. This was a fairly advanced tutorial, and if you followed it thoroughly, you now understand how to deploy a basic upgradeable contract using the OpenZeppelin library.

Subscribe to our newsletter for more articles and guides on Ethereum. If you have any feedback, feel free to reach out to us via Twitter. You can always chat with us on our Discord community server, featuring some of the coolest developers you’ll ever meet 😊

Share this guide