Recommended resources
The freely available resources this lesson will direct you to.
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:
- A Network Endpoint (RPC URL): A URL that points to an Ethereum node, which acts as our gateway to the network.
- 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
-
Install
dotenv: This is a standard Node.js utility to load environment variables from a.envfile.npm install dotenv -
Create a
.envfile: 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
.envfile:SEPOLIA_RPC_URL="YOUR_ALCHEMY_OR_INFURA_URL" PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"
-
Create
.gitignore: If you don't have one, create a.gitignorefile in your project root and add.envto it. This prevents you from accidentally committing your private key to version control.node_modules .env -
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 astorefunction.BoxV2.sol: The new version inheriting fromBoxV1and adding anincrementfunction.deploy.js: The script usingupgrades.deployProxyto deployBoxV1.upgrade.js: The script usingupgrades.upgradeProxyto upgrade toBoxV2.
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.

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.
- Open your
scripts/upgrade.jsfile. - Paste the proxy address you copied from the V1 deployment into the
PROXY_ADDRESSconstant. - Run the upgrade script, again targeting the
sepolianetwork:
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.
- Find Your Proxy Contract: Go to Sepolia Etherscan and search for your proxy address.
- Check the Implementation (Before Upgrade): Navigate to the
Contracttab. 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 yourBoxV1contract. - Perform the Upgrade: Run the
upgrade.jsscript as shown above. - 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
BoxV2contract 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:
- Insufficient Testnet ETH: The most common reason. The account specified by your
PRIVATE_KEYmight not have enough Sepolia ETH to cover the gas cost of deployingBoxV2and calling theupgradeTofunction. The solution is to get more funds from a faucet. - Low Gas Price/Limit in Config: While less common with modern Hardhat, your
hardhat.config.jscould have a manually setgasPriceorgaslimit that is too low for the current network conditions. You can try increasing these values in thesepolianetwork configuration. - Complex V2 Contract: If
BoxV2is significantly more complex thanBoxV1, 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.jsfile 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
.envfiles and.gitignore. - The
--networkFlag: This simple flag is all you need to direct Hardhat's commands (likerun) to the desired network. - The
.openzeppelinDirectory: 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.