Skip to main content
In this tutorial, you’ll learn how to upgrade your ERC-721 smart contract using the OpenZeppelin UUPS (Universal Upgradeable Proxy Standard) pattern and Hardhat. We’ll first cover how the upgradeable proxy pattern works, then go through step-by-step implementation and upgrade verification, explaining each part clearly.
You can take a look at the complete code in the Hedera-Code-Snippets repository.

Understanding the Upgradeable Proxy Pattern (Simplified)

In traditional smart contracts, once deployed, the code is immutable, meaning bugs can’t be fixed and new features can’t be added. The upgradeable proxy pattern solves this by separating the contract into two components:
  1. Proxy Contract: Stores the contract’s state (data) and delegates all function calls to a logic contract using delegatecall.
  2. Logic Contract: Contains the actual business logic and can be upgraded.
When you upgrade your smart contract, you deploy a new logic contract and point your proxy contract to this new logic. The proxy stays at the same address, retaining your data and allowing seamless upgrades. Important Note: In upgradeable contracts, constructors aren’t used because the proxy doesn’t call the constructor of the logic contract. Instead, we use an initialize function marked with the initializer modifier. This function serves the role of the constructor—setting up initial values and configuring inherited modules like ERC721 or Ownable. The initializer modifier ensures this function can only be called once, helping protect against accidental or malicious re-initialization.

Prerequisites


Table of Contents

  1. Set Up Your Project
  2. Create Your Initial Upgradeable ERC-721 Contract
  3. Deploy Your Upgradeable Contract
  4. Upgrade Your ERC-721 Contract
  5. Deploy the Upgrade and Verify
  6. Run tests
  7. Why Use the UUPS Pattern?

Video Tutorial

You can watch the video tutorial (which uses Hardhat version 2) or follow the step-by-step tutorial below (which uses Hardhat version 3).
Key differences in Hardhat 3:
  • compile → build
    npx hardhat compile is now npx hardhat build. This is the big one. The v3 migration guide explicitly shows using the build task.
  • project init switch
    v2 commonly used npx hardhat or npx hardhat init to bootstrap. In v3 it’s npx hardhat --init.
  • keystore helper commands are new
    v3’s recommended flow includes a keystore plugin with commands like npx hardhat keystore set HEDERA_RPC_URL and npx hardhat keystore set HEDERA_PRIVATE_KEY. These weren’t standard in v2.
  • Foundry-compatiable Solidity tests
    In addition to offering Javascript/Typescript integration tests, Hardhat v3 also integrates Foundry-compatible Solidity tests that allows developers to write unit tests directly in Solidity
  • Enhanced Network Management
    v3 allows tasks to create and manage multiple network connections simultaneously which is a significant improvement over the single, fixed connection available in version 2. This provides greater flexibility for scripts and tests that interact with multiple networks.
📚 Learn more from the official Hardhat documentation.

Step 1: Set Up Your Project

Install necessary dependencies if you haven’t done so.
For part 3 of this tutorial series, we’re adding one extra dependency:
  • @openzeppelin/contracts-upgradeable : This is a version of the OpenZeppelin Contracts library designed for upgradeable contracts. It contains modular and reusable smart contract components that are compatible with proxy deployment patterns, such as UUPS.
Files overview (what each file does):
  • contracts/MyTokenUpgradeable.sol
    • Upgradeable ERC-721 logic (initializer-based), Ownable, UUPS-ready. Holds functions like initialize and safeMint.
  • contracts/MyTokenUpgradeableV2.sol
    • Upgrade version that inherits V1 and adds version() for verification. No new storage variables to preserve layout.
  • contracts/OZTransparentUpgradeableProxy.sol
    • Thin wrapper so Hardhat has an artifact to deploy the proxy. Constructor takes logic, admin EOA, and initializer calldata.
  • scripts/deploy-upgradeable.ts
    • Deploys V1 logic, encodes initialize, deploys the Transparent proxy with your EOA as admin, sanity-checks via proxy, prints PROXY_ADDRESS.
  • scripts/upgrade-upgradeable.ts
    • Deploys V2 logic and upgrades the proxy in-place by calling upgradeToAndCall as the admin EOA, then verifies version().

Step 2: Create Your Initial Upgradeable ERC-721 Contract

Create MyTokenUpgradeable.sol in the contracts/ directory:
  • Uses initializer pattern; constructor disables initializers and initialize() sets up ERC721, Ownable, and UUPS.
  • UUPS gate: _authorizeUpgrade is onlyOwner, ensuring only the owner can upgrade when using UUPS flows.
  • safeMint increments _nextTokenId and mints; keep storage layout stable across future versions.
  • No constructor state writes; all initialization happens via initialize().
We also need to create OZTransparentUpgradeableProxy.sol in the contracts/ directory:
  • Thin wrapper so Hardhat produces an artifact to deploy the Transparent proxy.
  • Constructor takes logic address, admin EOA, and initializer calldata to run once at deployment.
  • We use the Transparent proxy path here for a straightforward upgrade on Hedera; user calls go to logic via delegatecall, admin calls manage upgrades.
Now, let’s build the contracts:

Step 3: Deploy Your Upgradeable Contract

Create deploy-upgradeable.ts under the scripts directory:
  • Deploys V1 logic, encodes initialize(initialOwner), and deploys the Transparent proxy with your EOA as admin.
  • Validates the deployment by calling ERC‑721 functions through the proxy and minting a token.
  • Prints the PROXY_ADDRESS to use in the upgrade step.
Deploy your contract:
Make sure to copy the smart contract address for your ERC-721 token.

Step 4: Upgrade Your ERC-721 Contract

Let’s upgrade your contract by adding a new version function. Create MyTokenUpgradeableV2.sol in your contracts folder:
  • Inherits from V1 and adds only behavior (version()); no new storage variables to keep layout compatible.
  • Verifies that after upgrade, calls through the proxy hit the new implementation.
  • Safe pattern for upgrades: extend behavior, avoid touching existing state ordering.
Build the upgraded version:

Step 5: Deploy the Upgrade and Verify

Create upgrade-upgradeable.ts script to upgrade and verify the new functionality. Make sure to update Your_Proxy_Address to your own from Step 3:
  • Deploys V2 and constructs upgradeToAndCall(newImpl, "0x") calldata via ethers.Interface.
  • Sends the upgrade tx directly to the proxy from the admin EOA; this path is reliable on Hedera.
  • Verifies by calling version() through the proxy; expect "v2" after a successful upgrade.
Run this upgrade script:
Output confirms the upgrade:
Congratulations! 🎉 You’ve successfully implemented and upgraded an ERC-721 smart contract using OpenZeppelin’s UUPS proxy pattern with Hardhat.

Step 6: Run tests(Optional)

You can find both types of tests in the Hedera-Code-Snippets repository. You will find the following files:
  • contracts/MyTokenUpgradeable.t.sol
  • contracts/MyTokenUpgradeableV2.t.sol
  • test/MyTokenUpgradeable.ts
  • test/MyTokenUpgradeableV2.ts
Copy these files and then run the tests:
You can also run tests individually with either of these

Why Use the UUPS Pattern?

  • Security: Upgrade functions can be restricted, ensuring only authorized roles can perform upgrades.
  • Data Retention: Maintains all token balances and stored data during upgrades.
  • Flexibility: Enables easy updates for new features, improvements, or critical fixes without redeploying a completely new contract.

Note

This tutorial’s contracts follow the UUPS initializer and authorization best practices (UUPSUpgradeable + \_authorizeUpgrade), while the example scripts perform the upgrade using an admin function TransparentUpgradeableProxy for a straightforward, reliable flow on Hedera. If you later switch to a pure UUPS proxy (ERC1967Proxy), upgrades would be triggered via the logic contract’s UUPS mechanism instead of the Transparent proxy’s admin API.

Additional Resources

Writer: Michiel, DevRel Engineer

Editor: Luis, Sr Software Developer

Editor: Krystal, Senior DX Engineer

Editor: Kiran, Developer Advocate