How to Upgrade an ERC-721 Token with OpenZeppelin UUPS Proxies and Hardhat (Part 3)
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.
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.
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
⚠️ 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
Video Tutorial
You can either watch the video tutorial or follow the step-by-step tutorial below.
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 two extra dependencies:
@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.@openzeppelin/hardhat-upgrades
: This Hardhat plugin simplifies deploying and managing upgradeable contracts. It provides utilities likedeployProxy
andupgradeProxy
and automatically manages the underlying proxy contracts. This plugin is imported in thehardhat.config.js
file, so we can use it.
// hardhat.config.js
require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");
require("@openzeppelin/hardhat-upgrades"); // Plugin for upgradeable contracts
require("@nomicfoundation/hardhat-ethers");
Step 2: Create Your Initial Upgradeable ERC-721 Contract
Create erc-721-upgrade.sol
in the contracts/
directory:
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyTokenUpgradeable is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
uint256 private _nextTokenId;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) public initializer {
__ERC721_init("MyTokenUpgradeable", "MTU");
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function safeMint(address to) public onlyOwner returns (uint256) {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
return tokenId;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
initialize
function: Replaces the constructor in upgradeable contracts, setting initial values and calling necessary initializers.initializer
modifier: Ensures the initialize function is only called once._authorizeUpgrade
: Ensures only the owner can authorize upgrades.
Compile the contract:
npx hardhat compile
Step 3: Deploy Your Upgradeable Contract
Create deploy-upgradeable.js
under the scripts
directory:
const { ethers, upgrades } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyTokenUpgradeable");
const token = await upgrades.deployProxy(Token, [deployer.address], { initializer: "initialize" });
await token.waitForDeployment();
console.log("Upgradeable ERC721 deployed to:", await token.getAddress());
}
main().catch(console.error);
deployProxy
function: Deploys the logic contract behind a proxy, calling the initializer function (initialize
) automatically.initializer: "initialize"
: Explicitly specifies which function initializes the contract.
Deploy your contract:
npx hardhat run scripts/deploy-upgradeable.js --network testnet
Make sure to copy the smart contract address for your ERC-721 token.
// output
Compiled 32 Solidity files successfully (evm target: paris).
Upgradeable ERC721 deployed to: 0xb54c97235A7a90004fEb89dDccd68f36066fea8c
Step 4: Upgrade Your ERC-721 Contract
Let's upgrade your contract by adding a new version
function. Create erc-721-upgrade-v2.sol
in your contracts
folder:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "./erc-721-upgrade.sol";
contract MyTokenUpgradeableV2 is MyTokenUpgradeable {
// New function for demonstration
function version() public pure returns (string memory) {
return "v2";
}
}
Adds a simple
version
method to demonstrate the upgrade. Note that we are extending the "MyTokenUpgradeable" contract.
Compile the upgraded version:
npx hardhat compile
Step 5: Deploy the Upgrade and Verify
Create upgrade.js
script to upgrade and verify the new functionality:
const { ethers, upgrades } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Upgrading contract with the account:", deployer.address);
const MyTokenUpgradeableV2 = await ethers.getContractFactory(
"MyTokenUpgradeableV2"
);
// REPLACE with your deployed proxy contract address
const proxyAddress = "<YOUR-PROXY-CONTRACT-ADDRESS>";
const upgraded = await upgrades.upgradeProxy(
proxyAddress,
MyTokenUpgradeableV2
);
await upgraded.waitForDeployment();
console.log(
"Contract successfully upgraded at:",
await upgraded.getAddress()
);
// Verify the upgrade by calling the new version() function
const contractVersion = await upgraded.version();
console.log("Contract version after upgrade:", contractVersion);
}
main().catch(console.error);
upgradeProxy
: Replaces the logic contract behind your existing proxy with the new version.proxyAddress
: Points to the proxy contract that manages storage and delegates calls to logic contracts. Upgrading involves replacing the logic without altering the stored data, since all contract state resides in the proxy, anddelegatecall
ensures the new logic runs in that same storage context. Make sure to replace the proxy contract address with the address you've copied.Verification step: Calls the new
version
method to ensure the upgrade succeeded.
Run this upgrade script:
npx hardhat run scripts/upgrade.js --network testnet
Output confirms the upgrade:
// output
Upgrading contract with the account: 0x7203b2B56CD700e4Df7C2868216e82bCCA225423
Contract successfully upgraded at: 0xb54c97235A7a90004fEb89dDccd68f36066fea8c
Contract version after upgrade: v2
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.
Congratulations! 🎉 You've successfully implemented and upgraded an ERC-721 smart contract using OpenZeppelin’s UUPS proxy pattern with Hardhat.
Additional Resources
Last updated
Was this helpful?