|
| 1 | +# How to Upgrade an ERC-721 Token with OpenZeppelin UUPS Proxies and Hardhat (Part 3) |
| 2 | + |
| 3 | +In this tutorial, you'll learn how to upgrade your ERC-721 smart contract using the [OpenZeppelin UUPS](https://docs.openzeppelin.com/upgrades-plugins/proxies) (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. |
| 4 | + |
| 5 | +{% hint style="info" %} |
| 6 | +You can take a look at the **complete code** in the [**Hedera-Code-Snippets repository**](https://github.com/hedera-dev/hedera-code-snippets/tree/main/hardhat-erc-721-mint-burn). |
| 7 | +{% endhint %} |
| 8 | + |
| 9 | +## Understanding the Upgradeable Proxy Pattern (Simplified) |
| 10 | + |
| 11 | +In traditional smart contracts, you can't change the logic once deployed, which can be risky if you find bugs or want to add new features. The upgradeable proxy pattern solves this by separating your contract into two parts: |
| 12 | + |
| 13 | +1. **Proxy Contract**: Stores the state (data) and delegates all calls to the logic contract. |
| 14 | +2. **Logic Contract**: Contains the actual business logic and can be replaced or upgraded. |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +**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. |
| 19 | + |
| 20 | +*** |
| 21 | + |
| 22 | +## Prerequisites |
| 23 | + |
| 24 | +* ⚠️ **Complete** [**tutorial part 1**](how-to-mint-and-burn-an-erc-721-token-using-hardhat-and-ethers-part-1.md) **as we continue from this example. Part 2 is optional.** |
| 25 | +* Basic understanding of smart contracts. |
| 26 | +* Basic understanding of [Node.js](https://nodejs.org/en/download) and JavaScript. |
| 27 | +* Basic understanding of [Hardhat EVM Development Tool](https://hardhat.org/hardhat-runner/docs/guides/project-setup) and [Ethers](https://docs.ethers.org/v5/). |
| 28 | +* ECDSA account from the [Hedera Portal](https://portal.hedera.com/). |
| 29 | + |
| 30 | +*** |
| 31 | + |
| 32 | +## Table of Contents |
| 33 | + |
| 34 | +1. [Step 1: Set Up Your Project](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#step-1-set-up-your-project) |
| 35 | +2. [Step 2: Create Your Initial Upgradeable ERC-721 Contract](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#step-2-create-your-initial-upgradeable-erc-721-contract) |
| 36 | +3. [Step 3: Deploy Your Upgradeable Contract](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#step-3-deploy-your-upgradeable-contract) |
| 37 | +4. [Step 4: Upgrade Your ERC-721 Contract](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#step-4-upgrade-your-erc-721-contract) |
| 38 | +5. [Step 5: Deploy the Upgrade and Verify](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#step-5-deploy-the-upgrade-and-verify) |
| 39 | +6. [Why Use the UUPS Pattern?](how-to-upgrade-an-erc-721-token-with-openzeppelin-uups-proxies-and-hardhat-part-3.md#why-use-the-uups-pattern) |
| 40 | + |
| 41 | +*** |
| 42 | + |
| 43 | +## Step 1: Set Up Your Project |
| 44 | + |
| 45 | +Install necessary dependencies if you haven't done so. For part 3 of this tutorial series, we're adding two extra dependencies: |
| 46 | + |
| 47 | +* `@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. |
| 48 | +* `@openzeppelin/hardhat-upgrades` : This Hardhat plugin simplifies deploying and managing upgradeable contracts. It provides utilities like `deployProxy` and `upgradeProxy` and automatically manages the underlying proxy contracts. This plugin is imported in the `hardhat.config.js` file, so we can use it. |
| 49 | + |
| 50 | +<pre class="language-javascript"><code class="lang-javascript"><strong>// hardhat.config.js |
| 51 | +</strong><strong>require("dotenv").config(); |
| 52 | +</strong>require("@nomicfoundation/hardhat-toolbox"); |
| 53 | +require("@openzeppelin/hardhat-upgrades"); // Plugin for upgradeable contracts |
| 54 | +require("@nomicfoundation/hardhat-ethers"); |
| 55 | +</code></pre> |
| 56 | + |
| 57 | +## Step 2: Create Your Initial Upgradeable ERC-721 Contract |
| 58 | + |
| 59 | +Create `erc-721-upgrade.sol` in the `contracts` directory: |
| 60 | + |
| 61 | +```solidity |
| 62 | +// SPDX-License-Identifier: MIT |
| 63 | +// Compatible with OpenZeppelin Contracts ^5.0.0 |
| 64 | +pragma solidity ^0.8.22; |
| 65 | +
|
| 66 | +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; |
| 67 | +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; |
| 68 | +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
| 69 | +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; |
| 70 | +
|
| 71 | +contract MyTokenUpgradeable is Initializable, ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable { |
| 72 | + uint256 private _nextTokenId; |
| 73 | +
|
| 74 | + /// @custom:oz-upgrades-unsafe-allow constructor |
| 75 | + constructor() { |
| 76 | + _disableInitializers(); |
| 77 | + } |
| 78 | +
|
| 79 | + function initialize(address initialOwner) public initializer { |
| 80 | + __ERC721_init("MyTokenUpgradeable", "MTU"); |
| 81 | + __Ownable_init(initialOwner); |
| 82 | + __UUPSUpgradeable_init(); |
| 83 | + } |
| 84 | +
|
| 85 | + function safeMint(address to) public onlyOwner returns (uint256) { |
| 86 | + uint256 tokenId = _nextTokenId++; |
| 87 | + _safeMint(to, tokenId); |
| 88 | + return tokenId; |
| 89 | + } |
| 90 | +
|
| 91 | + function _authorizeUpgrade(address newImplementation) |
| 92 | + internal |
| 93 | + override |
| 94 | + onlyOwner |
| 95 | + {} |
| 96 | +} |
| 97 | +
|
| 98 | +``` |
| 99 | + |
| 100 | +* **`initialize` function**: Replaces the constructor in upgradeable contracts, setting initial values and calling necessary initializers. |
| 101 | +* **`initializer` modifier**: Ensures the initialize function is only called once. |
| 102 | +* **`_authorizeUpgrade`**: Ensures only the owner can authorize upgrades. |
| 103 | + |
| 104 | +Compile the contract: |
| 105 | + |
| 106 | +```bash |
| 107 | +npx hardhat compile |
| 108 | +``` |
| 109 | + |
| 110 | +## Step 3: Deploy Your Upgradeable Contract |
| 111 | + |
| 112 | +Create `deploy-upgradeable.js` under the `scripts` directory: |
| 113 | + |
| 114 | +```javascript |
| 115 | +const { ethers, upgrades } = require("hardhat"); |
| 116 | + |
| 117 | +async function main() { |
| 118 | + const [deployer] = await ethers.getSigners(); |
| 119 | + |
| 120 | + const Token = await ethers.getContractFactory("MyTokenUpgradeable"); |
| 121 | + const token = await upgrades.deployProxy(Token, [deployer.address], { initializer: "initialize" }); |
| 122 | + await token.waitForDeployment(); |
| 123 | + |
| 124 | + console.log("Upgradeable ERC721 deployed to:", await token.getAddress()); |
| 125 | +} |
| 126 | + |
| 127 | +main().catch(console.error); |
| 128 | +``` |
| 129 | + |
| 130 | +* **`deployProxy` function**: Deploys the logic contract behind a proxy, calling the initializer function (`initialize`) automatically. |
| 131 | +* **`initializer: "initialize"`**: Explicitly specifies which function initializes the contract. |
| 132 | +* **`kind: "uups"`**: Specifies using the UUPS proxy pattern. |
| 133 | + |
| 134 | +Deploy your contract: |
| 135 | + |
| 136 | +```bash |
| 137 | +npx hardhat run scripts/deploy-upgradeable.js --network testnet |
| 138 | +``` |
| 139 | + |
| 140 | +**Make sure to copy the smart contract address for your ERC-721 token.** |
| 141 | + |
| 142 | +<pre><code><strong>// output |
| 143 | +</strong><strong>Compiled 32 Solidity files successfully (evm target: paris). |
| 144 | +</strong>Upgradeable ERC721 deployed to: 0xb54c97235A7a90004fEb89dDccd68f36066fea8c |
| 145 | +</code></pre> |
| 146 | + |
| 147 | +## Step 4: Upgrade Your ERC-721 Contract |
| 148 | + |
| 149 | +Let's upgrade your contract by adding a new `version` function. Create `erc-721-upgrade-v2.sol` in your `contracts` folder: |
| 150 | + |
| 151 | +```solidity |
| 152 | +// SPDX-License-Identifier: MIT |
| 153 | +pragma solidity ^0.8.22; |
| 154 | +
|
| 155 | +import "./erc-721-upgrade.sol"; |
| 156 | +
|
| 157 | +contract MyTokenUpgradeableV2 is MyTokenUpgradeable { |
| 158 | +
|
| 159 | + // New function for demonstration |
| 160 | + function version() public pure returns (string memory) { |
| 161 | + return "v2"; |
| 162 | + } |
| 163 | +} |
| 164 | +
|
| 165 | +``` |
| 166 | + |
| 167 | +* Adds a simple `version` method to demonstrate the upgrade. Note that we are extending the "MyTokenUpgradeable" contract. |
| 168 | + |
| 169 | +Compile the upgraded version: |
| 170 | + |
| 171 | +```bash |
| 172 | +npx hardhat compile |
| 173 | +``` |
| 174 | + |
| 175 | +## Step 5: Deploy the Upgrade and Verify |
| 176 | + |
| 177 | +Create `upgrade.js` script to upgrade and verify the new functionality: |
| 178 | + |
| 179 | +```javascript |
| 180 | +const { ethers, upgrades } = require("hardhat"); |
| 181 | + |
| 182 | +async function main() { |
| 183 | + const [deployer] = await ethers.getSigners(); |
| 184 | + |
| 185 | + console.log("Upgrading contract with the account:", deployer.address); |
| 186 | + |
| 187 | + const MyTokenUpgradeableV2 = await ethers.getContractFactory( |
| 188 | + "MyTokenUpgradeableV2" |
| 189 | + ); |
| 190 | + |
| 191 | + // REPLACE with your deployed proxy contract address |
| 192 | + const proxyAddress = "<YOUR-PROXY-CONTRACT-ADDRESS>"; |
| 193 | + |
| 194 | + const upgraded = await upgrades.upgradeProxy( |
| 195 | + proxyAddress, |
| 196 | + MyTokenUpgradeableV2 |
| 197 | + ); |
| 198 | + await upgraded.waitForDeployment(); |
| 199 | + |
| 200 | + console.log( |
| 201 | + "Contract successfully upgraded at:", |
| 202 | + await upgraded.getAddress() |
| 203 | + ); |
| 204 | + |
| 205 | + // Verify the upgrade by calling the new version() function |
| 206 | + const contractVersion = await upgraded.version(); |
| 207 | + console.log("Contract version after upgrade:", contractVersion); |
| 208 | +} |
| 209 | + |
| 210 | +main().catch(console.error); |
| 211 | +``` |
| 212 | + |
| 213 | +* **`upgradeProxy`**: Replaces the logic contract behind your existing proxy with the new version. |
| 214 | +* **`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. **Make sure to replace the proxy contract address with the address you've copied.** |
| 215 | +* **Verification step**: Calls the new `version` method to ensure the upgrade succeeded. |
| 216 | + |
| 217 | +Run this upgrade script: |
| 218 | + |
| 219 | +```bash |
| 220 | +npx hardhat run scripts/upgrade.js --network testnet |
| 221 | +``` |
| 222 | + |
| 223 | +Output confirms the upgrade: |
| 224 | + |
| 225 | +```bash |
| 226 | +// output |
| 227 | +Upgrading contract with the account: 0x7203b2B56CD700e4Df7C2868216e82bCCA225423 |
| 228 | +Contract successfully upgraded at: 0xb54c97235A7a90004fEb89dDccd68f36066fea8c |
| 229 | +Contract version after upgrade: v2 |
| 230 | +``` |
| 231 | + |
| 232 | +## Why Use the UUPS Pattern? |
| 233 | + |
| 234 | +* **Security**: Upgrade functions can be restricted, ensuring only authorized roles can perform upgrades. |
| 235 | +* **Data Retention**: Maintains all token balances and stored data during upgrades. |
| 236 | +* **Flexibility**: Enables easy updates for new features, improvements, or critical fixes without redeploying a completely new contract. |
| 237 | + |
| 238 | +Congratulations! 🎉 You've successfully implemented and upgraded an ERC-721 smart contract using OpenZeppelin’s UUPS proxy pattern with Hardhat. |
| 239 | + |
| 240 | +*** |
| 241 | + |
| 242 | +## Additional Resources |
| 243 | + |
| 244 | +* [Proxy Upgrade Pattern (OpenZeppelin)](https://docs.openzeppelin.com/upgrades-plugins/proxies) |
| 245 | + |
| 246 | +<table data-card-size="large" data-view="cards"><thead><tr><th align="center"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><p>Writer: Michiel, Developer Relations Engineer</p><p><a href="https://github.com/michielmulders">GitHub</a> | <a href="https://www.linkedin.com/in/michielmulders/">LinkedIn</a></p></td><td><a href="https://www.linkedin.com/in/michielmulders/">https://www.linkedin.com/in/michielmulders/</a></td></tr><tr><td align="center"><p>Editor: Luis, Sr Software Developer</p><p><a href="https://github.com/acuarica">GitHub</a></p></td><td><a href="https://github.com/acuarica">https://github.com/acuarica</a></td></tr><tr><td align="center"><p>Editor: Krystal, Technical Writer</p><p><a href="https://github.com/theekrystallee">GitHub</a> | <a href="https://x.com/theekrystallee">X</a></p></td><td><a href="https://x.com/theekrystallee">https://x.com/theekrystallee</a></td></tr></tbody></table> |
0 commit comments