Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Upgrading Smart Contracts with Hardhat on Testnet

Hello! Welcome back to our module on Governance and Upgradability.

In our last lesson, you successfully implemented an upgradeable contract using the UUPS proxy pattern. We wrote the Solidity code, understood the key components like UUPSUpgradeable and _authorizeUpgrade, and used Hardhat scripts with the OpenZeppelin Upgrades plugin to perform a deployment and upgrade on a local, simulated network.

Today, we're taking the crucial next step to bring our work into a more realistic environment. Our goal is to perform a simulated contract upgrade on a testnet using Hardhat. This moves us from a private simulation to a public, shared blockchain, which is exactly how you would prepare for a mainnet launch. We'll use the Sepolia testnet for this. The core logic from our last lesson remains the same, but we'll focus on configuring our environment to interact with a live network.

Part 1: Configuring Hardhat for a Testnet

To communicate with a public Ethereum network like Sepolia, our Hardhat project needs two key pieces of information:

  1. A Network Endpoint (RPC URL): A URL that points to an Ethereum node, which acts as our gateway to the network.
  2. A Private Key: The key for an account (your MetaMask account) that will pay for the gas fees and own the deployed contracts.

Your experience as a backend developer will be helpful here, as this process is very similar to managing API keys and database credentials in a traditional application. We must never hardcode private keys directly into our configuration files. Instead, we'll use environment variables.

Step-by-step Configuration

  1. Install dotenv: This is a standard Node.js utility to load environment variables from a .env file.

    npm install dotenv
    
  2. Create a .env file: In the root of your project, create a new file named .env. This file will store your secrets.

    • Get an RPC URL: Services like Infura or Alchemy provide reliable RPC URLs. Sign up for a free account, create a new project for the Sepolia network, and copy the RPC URL.
    • Export Your Private Key: From MetaMask, select your account, go to "Account details", and click "Show private key". Copy this key. Handle this with extreme care.
    • Add these to your .env file:
      SEPOLIA_RPC_URL="YOUR_ALCHEMY_OR_INFURA_URL"
      PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"
      
  3. Create .gitignore: If you don't have one, create a .gitignore file in your project root and add .env to it. This prevents you from accidentally committing your private key to version control.

    node_modules
    .env
    
  4. Update hardhat.config.js: Modify your Hardhat configuration to load these variables and define the Sepolia network.

    require("@nomicfoundation/hardhat-toolbox");
    require("@openzeppelin/hardhat-upgrades");
    require("dotenv").config(); // Load environment variables
    
    /** @type import('hardhat/config').HardhatUserConfig */
    module.exports = {
      solidity: "0.8.20",
      networks: {
        sepolia: {
          url: process.env.SEPOLIA_RPC_URL || "",
          accounts:
            process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
        },
      },
    };
    

With this setup, Hardhat is now ready to deploy to Sepolia.

Part 2: The Testnet Upgrade Workflow

The beauty of the Hardhat Upgrades plugin is that our scripts from the previous lesson need almost no changes. The plugin and Hardhat handle the complexity of targeting a different network.

1. The Contracts and Scripts

We'll use the same BoxV1.sol and BoxV2.sol contracts from our previous lesson. We'll also use the same deploy.js and upgrade.js scripts.

  • BoxV1.sol: The initial contract with a store function.
  • BoxV2.sol: The new version inheriting from BoxV1 and adding an increment function.
  • deploy.js: The script using upgrades.deployProxy to deploy BoxV1.
  • upgrade.js: The script using upgrades.upgradeProxy to upgrade to BoxV2.

2. Deploying V1 to Sepolia

First, ensure you have some Sepolia ETH in your MetaMask account. You can get this from a public faucet like sepoliafaucet.com or infura.io/faucet/sepolia.

Now, run the deployment script, specifying the sepolia network with the --network flag:

npx hardhat run scripts/deploy.js --network sepolia

You will see output indicating the contracts are being deployed, and finally, the address of your proxy contract. Copy this proxy address! We'll need it for the upgrade and for verification.

3. The .openzeppelin Directory

After the deployment, you'll notice a new directory in your project: .openzeppelin.

Hardhat Project Structure with OpenZeppelin Contracts
This image shows a typical Hardhat project structure. The arrow points to the `.openzeppelin` folder, which is automatically created and managed by the OpenZeppelin Hardhat Upgrades plugin to track your deployments.

This folder contains a JSON file for each network you deploy to (e.g., sepolia-xxxxxxxx.json). This file is the plugin's brain. It stores critical information, including:

  • The address of your proxy.
  • The address of the current implementation contract (BoxV1).
  • The ABI and other metadata.

When you run upgrades.upgradeProxy, the plugin reads this file to know which proxy to update and to validate that the new implementation is compatible with the old one.

4. Upgrading to V2 on Sepolia

Now, let's perform the upgrade.

  1. Open your scripts/upgrade.js file.
  2. Paste the proxy address you copied from the V1 deployment into the PROXY_ADDRESS constant.
  3. Run the upgrade script, again targeting the sepolia network:
npx hardhat run scripts/upgrade.js --network sepolia

The script will deploy BoxV2 and then send a transaction to your proxy contract, calling upgradeTo to point it at the new BoxV2 implementation address.

To see this process in action, you can review this part of the OpenZeppelin video, which demonstrates using upgradeProxy in a test script. The logic is identical to what our upgrade.js script is doing.

Deploying More Efficient Upgradeable Contracts

This clip from 'Deploying More Efficient Upgradeable Contracts' shows the upgradeProxy function in action within a Hardhat test. This is the core function call our script is executing on the testnet.

Watch the segment from 28:58 to 32:20. The presenter creates a MarsV2 contract and uses upgrades.upgradeProxy to upgrade the previously deployed proxy. Notice how the function takes the existing proxy instance and the new contract factory as arguments.

Part 3: Verifying the Upgrade on a Block Explorer

How do we know the upgrade actually worked? We use a block explorer, like Sepolia Etherscan, to inspect the state of our contract on the blockchain.

  1. Find Your Proxy Contract: Go to Sepolia Etherscan and search for your proxy address.
  2. Check the Implementation (Before Upgrade): Navigate to the Contract tab. Etherscan is smart enough to detect proxies. You should see a message indicating it's a proxy and a link to the implementation address. This will be your BoxV1 contract.
  3. Perform the Upgrade: Run the upgrade.js script as shown above.
  4. Check the Implementation (After Upgrade): Wait for the transaction to be confirmed. Now, refresh the Etherscan page for your proxy address. You will see that the implementation address has changed! It now points to your newly deployed BoxV2 contract address.

This is the definitive proof: the logic of your contract has been updated, while the contract's address and its storage (the value variable) have been preserved. You can even use Etherscan's "Write Proxy" functionality to call the new increment() function, which didn't exist in BoxV1.

Test your understanding!

You run npx hardhat run scripts/upgrade.js --network sepolia, but the transaction fails with an "out of gas" error. What are some potential reasons and how might you troubleshoot this in your Hardhat project?

Show answer

There are a few possibilities:

  1. Insufficient Testnet ETH: The most common reason. The account specified by your PRIVATE_KEY might not have enough Sepolia ETH to cover the gas cost of deploying BoxV2 and calling the upgradeTo function. The solution is to get more funds from a faucet.
  2. Low Gas Price/Limit in Config: While less common with modern Hardhat, your hardhat.config.js could have a manually set gasPrice or gas limit that is too low for the current network conditions. You can try increasing these values in the sepolia network configuration.
  3. Complex V2 Contract: If BoxV2 is significantly more complex than BoxV1, its deployment cost could be much higher than anticipated. Hardhat's automatic gas estimation is usually reliable, but in rare cases, manual adjustment might be needed.

The first step is always to check your account balance on Etherscan and the gas cost of the failed transaction.

Conclusion

Excellent work! You have successfully bridged the gap between local development and live testnet deployment. You configured your Hardhat project for a public network, managed private keys securely, and performed a UUPS contract upgrade that you could verify on a public block explorer. This workflow is fundamental to professional smart contract development and maintenance.

Key Takeaways:

  • Testnet Configuration: The hardhat.config.js file is where you define connections to live networks using an RPC URL and a private key.
  • Security First: Private keys must be kept out of version control using .env files and .gitignore.
  • The --network Flag: This simple flag is all you need to direct Hardhat's commands (like run) to the desired network.
  • The .openzeppelin Directory: This is a critical artifact that tracks your proxy deployments, enabling the upgrade plugin to function correctly.
  • Block Explorers are Your Source of Truth: Etherscan provides the ultimate confirmation that your on-chain operations, like an upgrade, were successful.

Preview of the Next Lesson:

Now that we have a stable, upgradeable contract living on a testnet, we can start building services that interact with it. In our next lesson, "Write a Node.js script to listen for and react to specific smart contract events," we will build an off-chain service that monitors our contract for activity, a core component of nearly every dApp backend.

Can't find a good explanation? Sign up and we'll make it for you

Sign up