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:- Proxy Contract: Stores the contract’s state (data) and delegates all function calls to a logic contract using delegatecall.
- Logic Contract: Contains the actual business logic and can be upgraded.
Prerequisites
- ⚠️ Complete tutorial part 1 as we continue from this example. Part 2 is optional.
- Basic understanding of smart contracts.
- Basic understanding of Node.js and JavaScript.
- Basic understanding of Hardhat EVM Development Tool and Ethers.
- ECDSA account from the Hedera Portal.
Table of Contents
- Set Up Your Project
- Create Your Initial Upgradeable ERC-721 Contract
- Deploy Your Upgradeable Contract
- Upgrade Your ERC-721 Contract
- Deploy the Upgrade and Verify
- Run tests
- 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).🚧 What's new: Hardhat 2 → 3
🚧 What's new: Hardhat 2 → 3
Key differences in Hardhat 3:
- compile → build
npx hardhat compileis nownpx hardhat build. This is the big one. The v3 migration guide explicitly shows using thebuildtask. - project init switch
v2 commonly usednpx hardhatornpx hardhat initto bootstrap. In v3 it’snpx hardhat --init.
- keystore helper commands are new
v3’s recommended flow includes a keystore plugin with commands likenpx hardhat keystore set HEDERA_RPC_URLandnpx 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.
Step 1: Set Up Your Project
Install necessary dependencies if you haven’t done so.@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.
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
CreateMyTokenUpgradeable.sol in the contracts/ directory:
- Uses initializer pattern; constructor disables initializers and initialize() sets up ERC721, Ownable, and UUPS.
- UUPS gate:
_authorizeUpgradeisonlyOwner, ensuring only the owner can upgrade when using UUPS flows. safeMintincrements_nextTokenIdand mints; keep storage layout stable across future versions.- No constructor state writes; all initialization happens via
initialize().
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.
Step 3: Deploy Your Upgradeable Contract
Createdeploy-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.
Step 4: Upgrade Your ERC-721 Contract
Let’s upgrade your contract by adding a newversion 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.
Step 5: Deploy the Upgrade and Verify
Createupgrade-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 viaethers.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.
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.solcontracts/MyTokenUpgradeableV2.t.soltest/MyTokenUpgradeable.tstest/MyTokenUpgradeableV2.ts
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.