Smart contracts are immutable by nature, meaning their code cannot be altered after deployment. While this immutability ensures security and trust in a blockchain, it also presents challenges when you need to fix bugs, add new features, or adapt to evolving regulations. Upgradable smart contracts address these challenges by allowing developers to update the logic of a deployed contract without affecting its state or address.

This guide will show you how to build and deploy upgradable smart contracts using OpenZeppelin’s Upgrades Plugin and Hardhat for deployment.

Prerequisites

  • You have followed the previous tutorials and are confident on learning about new contract types.
  • You have an understanding on how to deploy contracts using Hardhat.
  • JavaScript programming language experience.
  • Understanding about upgradable smart contracts.

Why Upgradable Smart Contracts?

Traditional smart contracts are inherently limited in flexibility. Once deployed, they are immutable pieces of code residing on the blockchain. Only the state of the contract can change while the code itself remains fixed. This means that if bugs are discovered or if improvements are needed, you’re left without an option to update the contract.

Upgradable smart contracts address this limitation by offering flexibility and adaptability throughout the contract’s lifecycle. They allow for seamless bug fixes and security patches on a deployed contract, helping to mitigate vulnerabilities and address security concerns. In an ever-evolving blockchain environment—where best practices and regulatory standards are continuously being redefined—using upgradable smart contracts enables you to update your code to comply with new regulations, ultimately facilitating broader adoption.

Instructions

Building an Upgradable Smart Contract

For the creation of the upgradable smart contract we will be using OpenZeppelin’s deployProxy plugin.

1. Install upgrades plugin

  • After creating a Hardhat environment you will need to download the OpenZeppelin package that contains the upgrade logic.
npm install --save-dev @openzeppelin/hardhat-upgrades
  • Next you will need to add the following import in the hardhat.config.js to configure Hardhat to use this package
// hardhat.config.js
...
require('@openzeppelin/hardhat-upgrades');
...
module.exports = {
...
};

2. Create the contract

Now we need to create the contract that will be upgradable. For simplicity sake, let us again use the SimpleStorage.sol contract from before. Please place this file in the /contracts directory of your project. The code is as follows:

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

contract SimpleStorage {
    uint256 private data;
    
    // Emitted when the stored value changes
    event ValueChanged(uint256 _data);

    function set(uint256 _data) public {
        data = _data;
        emit ValueChanged(_data);
    }

    function get() public view returns (uint256) {
        return data;
    }
}

Please note how we added an event which will be helpful later.

3. Write the deployment script

Next, we need to deploy the contract as upgradable. This uses a bit different logic so please make sure to take special note on the code below. Rather than putting the deployment script in the ignition/modules/ directory path, we place it in the scripts/.

  • Create a new file in the /scripts directory called upgrade_storage.js.
  • Paste the following code in the file:
// scripts/upgrade_storage.js
const { ethers, upgrades } = require('hardhat');

async function main () {
  const SimpleStorage = await ethers.getContractFactory('SimpleStorage');
  console.log('Deploying SimpleStorage...');
  const simple_storage = await upgrades.deployProxy(SimpleStorage, [10], { initializer: 'store' });
  await simple_storage.waitForDeployment();
  console.log('SimpleStorage deployed to:', await simple_storage.getAddress());
}

main();

4. Deploy the contract

  • Run the following cmd to deploy the contract. For the upgradable contract we use the npx hardhat run cmd instead of the ignition deploy.
npx hardhat run --network agung scripts/upgrade_storage.js
  • After successfully doing so, the deployed contract address will be displayed. Save this value so you can access the contract in the future.

5. Interact the contract

  • Hardhat console will be used to interact with the smart contract we have deployed. To launch the console you can run the cmd:
npx hardhat console --network agung
  • A new terminal will open up where you can execute the following the check the deployment:
Welcome to Node.js v22.4.1.
Type ".help" for more information.
> const SimpleStorage = await ethers.getContractFactory('SimpleStorage')
undefined
> const simple_storage = await SimpleStorage.attach('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D');
undefined
> (await simple_storage.get()).toString();
'10'
  • Confirms that the contract has been properly stored an initialized at 10.

Upgrade previously deployed contract

Let us say down the line, we want to upgrade this smart contract to have a new feature - increment the stored value.

1. Create a new contract

  • Create a new Solidity contract in the contracts/ directory with the name SimpleStorageV2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorageV2 {
    uint256 private data;
    
    // Emitted when the stored value changes
    event ValueChanged(uint256 _data);

    function set(uint256 _data) public {
        data = _data;
        emit ValueChanged(_data);
    }

    function get() public view returns (uint256) {
        return data;
    }

    // new function added
    function increment() public {
        data = data + 1;
        emit ValueChanged(data);
    }
}

Take special note how to contract is exactly the same, except for the name of the contract and the new increment function that was added.

2. Upgrade previously deployed contract

Use the updateProxy function from OpenZeppelin to update the contract,

  • Create a new file at scripts/ called upgrade_storagev2. Use the following code:
// scripts/upgrade_box.js
const { ethers, upgrades } = require('hardhat');

async function main () {
  const SimpleStorageV2 = await ethers.getContractFactory('SimpleStorageV2');
  console.log('Upgrading SimpleStorage...');
  await upgrades.upgradeProxy('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D', SimpleStorageV2);
  console.log('SimpleStorage upgraded');
}

main();

3. Deploy upgradable contract

Again use the run cmd to upgrade the contract from the set network.

$ npx hardhat run --network agung scripts/upgrade_storagev2.js

Upgrading SimpleStorage...
SimpleStorage upgraded

The above will be displayed on a successful upgrade. The code have been updated to the latest version, while maintaining the same address and state from before.

4. Interact with the contract

Again, let us use the Hardhat console to interact with the contract. Notice how we are able to call increment() that we added in our update. Launch the console:

npx hardhat console --network agung

Terminal will open and execute the following:

Welcome to Node.js v22.4.1.
Type ".help" for more information.
> const SimpleStorageV2 = await ethers.getContractFactory('SimpleStorageV2');
undefined
> const simple_storage = await SimpleStorageV2.attach('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D');
undefined
> await simple_storage.increment();
...
> (await simple_storage.get()).toString();
'11'

🎉 Congratulations! You have created a smart contract and successfully upgraded it. Notice how the address and the stayed stayed the same, until the increment() function was called. Now you have more flexibility when dealing with your smart contracts. If you would like to learn more about how these updates work, please checkout the OpenZeppelin documentation.