Skip to content

EVM Smart Contract Development: Deploying an NFT with Upgradeable Metadata

EVM NFT PART 1 (1)

Upgradeable NFT metadata is a powerful feature that allows NFT creators and projects to modify the metadata associated with their tokens after minting. This metadata might be an image, video, gif, or object linking to any number of variable attributes. If this metadata can change over time, it can open up a world of new possibilities for dynamic, evolving NFTs that can evolve intelligently in response to specific events. 

 
From https://leftasexercise.com - Using NFT metadata to safely store digital assets

From https://leftasexercise.com - Using NFT metadata to safely store digital assets

Where to Apply Upgradeable Metadata
  • Evolving Artwork: NFTs that change appearance based on time, owner interactions, or external events
  • Dynamic Game Assets: In-game items that level up or change properties as players progress
  • Real-World Asset Tracking: NFTs representing physical assets with updatable information (e.g., property details, maintenance records)
  • Interactive Storytelling: NFTs that reveal new chapters or content over time or based on community decisions
  • Achievements and Badges: NFTs that display earned accomplishments or status, updated as users complete tasks
  • Event Tickets: NFTs that serve as access tokens, with metadata updates reflecting event changes or post-event memorabilia
  • Digital Identity: NFTs representing user profiles or credentials that can be updated with new information or certifications
  • Collectible Sets: NFTs that gain new attributes or appearances when collected as part of a set
  • Environmental Impact Tracking: NFTs tied to real-world sustainability projects, updated with progress reports and impact metrics
  • Music and Media Releases: NFTs that grant access to evolving content libraries or exclusive releases over time
The Development Plan

We’re going to guide you through step-by-step the process for setting up your development environment, then writing and deploying a simple ERC721 smart contract that will allow its owner to modify the tokenURI and subsequent metadata associated with the token. The NFT use case here will be a series of powerup NFTs that have attributes that can be modified. The resulting image will be different, reflecting the updated metadata attributes.

1. Set Up Your Environment

We'll need the following dependencies to get started:

1. Create a new TypeScript project

Ensure Yarn Package Manager is installed globally:

Command Line:


 
npm install --global yarn
    

Now lets install some of the core dependencies via the command line:


mkdir superpower-nft && cd superpower-nft
yarn init -y
yarn add typescript @types/node ts-node ethers @openzeppelin/contracts@4.3.2    

2. Install Foundry and Run the Setup

Follow the CLI prompts carefully. If you run into any installation issues, see Foundry’s FAQs.


 
curl -L https://foundry.paradigm.xyz | bash
 
foundryup
   

Now, Foundry is installed. We’ll also need to configure the foundry.toml file to connect to the Sepolia testnet and add OpenZeppelin dependencies. Create a "foundry.toml" file in the root of the directory if it wasn't created already, and specify our RPC endpoint.

foundry.toml:


 
[default]
 
solc_version = "0.8.0"  # Specify the Solidity version you’re using
 
optimizer = true          # Enable optimizer for the compiler
 
optimizer_runs = 200      # Default optimization runs

sepolia = "https://sepolia.ethereum.validationcloud.io/v1/Hk4H8....."   

We'll use OpenZeppelin's upgradeable contracts library for this example.

3. Metadata Hosting: Pinata Cloud

Before we create the metadata for the NFTs, let's align on what the collection is thematically for the tutorial:

Each NFT in the collection represents a unique superpower. The metadata includes traits like speed, altitude, transparency, etc., that describe the superpower's characteristics.

Upgrading is simply a matter of updating the metadata on the storage provider and using the upgradeSuperPower() function to update the TokenURI on-chain to redirect the NFT to a new image.

For this tutorial, the quickest free solution for storing basic images (and metadata files) was Pinata Cloud. Depending on your use case, you may opt to go with other solutions. We have uploaded our pre-generated images AND metadata-JSON files (or see Step 3 more guided instruction on how to do it). If you’ve already uploaded your metadata, take note of the CIDs - we will assign the token for these two different values later on in the tutorial. 

4. TypeScript Configuration (tsconfig.json)


{
   "compilerOptions": {
     "target": "ES6",                          // Target modern JavaScript
     "module": "commonjs",                     // Use CommonJS for Node.js compatibility
     "rootDir": "./",                          // Set the root directory (adjust if needed)
     "outDir": "./dist",                       // Output compiled JS to 'dist' folder
     "strict": true,                           // Enable strict type checking
     "esModuleInterop": true,                  // Allow ES6 module interop
     "skipLibCheck": true,                     // Skip type checking for libraries
     "forceConsistentCasingInFileNames": true  // Ensure consistent casing in imports
   },
   "include": [
     "scripts/**/*.ts"                         // Include all .ts files in the 'scripts' directory
   ],
   "exclude": [
     "node_modules"                            // Exclude dependencies
   ]
 }

5. Environment File (.env)


 
PRIVATE_KEY=<YOUR EXPORTED EVM PRIVATE KEY>
 
RPC_URL=https://sepolia.ethereum.validationcloud.io/v1/Hk4H82VinPWoRLLIr_aw....
   

 

💡 Note: it is STRONGLY ADVISED to never put production private keys into plain text. Be sure to have all .env files ignored with any github repository uploads, and avoid using production keys carrying significant value in this manner.

Once everything is installed and set up, we'll be ready to move onto the contract setup steps.

2. NFT Contract Setup

We're going to make an ERC721 NFT that contains special superpowers that can be upgraded over time.

First, Create a folder called contracts/ in your root project directory.

Then, add a new file called SuperPowerNFT.sol


 
// SPDX-License-Identifier: MIT
 
pragma solidity ^0.8.0;
 
import "./../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol";
 
import "./../node_modules/@openzeppelin/contracts/access/Ownable.sol";
 
import "./../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";




/**
 
* @title SuperPowerNFT
 
* @dev This contract is a simple ERC721 token that allows creating NFTs with metadata stored externally.
 
* The first NFT is minted in the constructor with a predefined token URI.
 
*/
 
contract SuperPowerNFT is ERC721URIStorage, Ownable {
 
   uint256 public tokenCounter;
 
   
 
   /**
 
    * @dev Constructor that mints the first NFT with a predefined tokenURI.
 
    * The constructor also sets the token counter to 1.
 
    * @param tokenURI The URI pointing to the metadata of the initial NFT (e.g., a JSON file).
 
    */
 
    constructor(string memory name, string memory symbol, string memory tokenURI) 
 
       ERC721(name, symbol)
 
       Ownable()
 
   {   
 
       tokenCounter = 1; // Start token counter at 1
 
       // Mint the first NFT to the owner (deployer) of the contract
 
       _safeMint(msg.sender, tokenCounter);
 
       _setTokenURI(tokenCounter, tokenURI);
 
   }
   /**
 
    * @dev Creates a new NFT with a given tokenURI.
 
    * @return The ID of the newly minted token.
 
    */
 
   function createSuperPower() public onlyOwner returns (uint256) {
 
       tokenCounter += 1; // Increment token counter
 
       uint256 newItemId = tokenCounter;
 
       // Mint the new token to the owner's address
 
       _safeMint(msg.sender, newItemId);
 
       return newItemId;
 
   }
 
   /**
 
    * @dev Updates the tokenURI for a specific token ID.
 
    * Only the owner can call this function.
 
    * @param tokenId The ID of the token whose URI is being updated.
 
    * @param newTokenURI The new URI pointing to the updated metadata.
 
    */
 
   function updateTokenURI(uint256 tokenId, string memory newTokenURI) public onlyOwner {
 
       require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
 
       _setTokenURI(tokenId, newTokenURI); // Update the token's metadata URI
 
   }
 
}
   

Let's wait before deploying this contract...

Key Elements:

  • Minting: When you mint a new NFT with the createSuperPower() function, you provide the tokenURI (which points to the metadata you stored earlier)
  • ERC721URIStorageUpgradeable: This is the upgradeable version of ERC721 with URI storage for metadata
  • Initializable: Instead of using constructors, this contract uses an initialize() function due to the upgradeable pattern
  • createSuperPower: This function allows the owner to mint new NFTs with metadata representing a superpowerupgradeSuperPower: This function allows the owner to update the metadata (or "upgrade" the superpower traits) by changing the URI
3. Metadata JSON File Upload

Create a directory called "metadata" in the root directory.

Move a number of desired images (or other type of unique data) that you would like to demonstrate the capabilities of upgradeable metadata. in our case, we used 2 1024x1024 images generated by ChatGPT. Those images have been placed in the "/metadata" directory. You can create a script yourself to bulk upload large numbers images to your hosting provider from this directory - OR for small numbers of file uploads (such as this tutorial), we can manually upload them to Pinata Cloud. Take note of the CID - with Pinata uploads, the image can be hyperlinked simply via ipfs.io/ipfs/<CID>

For example: 

https://ipfs.io/ipfs/QmdcHnUu7ViWdzXzj4X8CfzLksX29nSzmqrgFSfZRysHkU 

Next, In your metadata/ directory created in Step 1 above, let’s next create a new file named initialMetadata.json. This JSON file is what OpenSea reads and interprets to redirect users to the creators’ images. 


 
{
 
 "name": "SuperPower: Flight",
 
 "description": "This NFT grants the superpower of flight. The superpower's attributes can evolve over time.",
 
 "attributes": [
 
   {
 
     "trait_type": "Superpower",
 
     "value": "Flight"
 
   },
 
   {
 
     "trait_type": "Speed",
 
     "value": "500 mph"
 
   },
 
   {
 
     "trait_type": "Max_Altitude",
 
     "value": "1,000 ft"
 
   },
 
   {
 
     "trait_type": "Heat Factor",
 
     "value": "Low"
 
   }
 
 ],
 
 "image": "https://ipfs.io/ipfs/QmdcHnUu7ViWdzXzj4X8CfzLksX29nSzmqrgFSfZRysHkU"
 
}
    

Now upload this metadata file to Pinata as well, and note the IPFS URL of this metadata file as well. This new metadata URL will contain an image URL within it. Save this URL  for the constructor argument in our deployment command. For example it may look like this:

https://ipfs.io/ipfs/3CiqtD8C2e8fycQs1VRzwmBeuqHGmltpDRC4Vo8hRqs 

4. Deploying the NFT Contract

We're now going to compile the contract and deploy it on the Sepolia testnet!

Note: The steps and code deployed are very similar if you wish to deploy on other EVM blockchains such as Polygon, Arbitrum, Optimism, or Base (to name a few!).

Back in the Environment Setup steps, we asked you to have an Ethereum wallet on hand funded with some faucet Sepolia Ethereum tokens. This will allow us to deploy the contract to Sepolia so we can get some live testing going. Remember that it costs some tokens to perform any amount of state changes on the network

1. Compile the Contract via the CLI and resolve errors, if any


 
forge build
    

2. Deploy from the Command Line

Be sure that your constructor arguments align with the solidity code itself:


forge create  contracts/SuperPowerNFT.sol:SuperPowerNFT \
--rpc-url https://sepolia.ethereum.validationcloud.io/v1/Hk4H82V.... \
--private-key <your_raw_private_key> \
--constructor-args "SuperPowerNFT" "SPNFT" "https://ipfs.io/ipfs/3CiqtD8C2e8fycQs1VRzwmBeuqHGmltpDRC4Vo8hRqs"
 \ 
--priority-gas-price 100000000000

 

💡You can run "foundry create -h" to get a list of all the possible arguments. If you run into gas fee issues, you can check the current gas price and adjust your priority gas price accordingly: https://sepolia.beaconcha.in/gasnow

3. Verifying the Contract

Verifying the contract allows you and the public to view the functions and code that make up the contract. This is important for building trust and assuring the community that you are more legitimate. We can do this with the forge cli as well!

Again, stay vigilant of the constructor requirements from the contract. Staying organized at all times is key when developing in web3!


forge verify-contract 0x20BEa03DdD82C7EE63f0B2AcAee655f8248d7b09 \   contracts/SuperPowerNFT.sol:SuperPowerNFT \   --chain sepolia \   --constructor-args $(cast abi-encode "constructor(string,string,string)" "SuperPowerNFT" "SPNFT" "https://ipfs.io/ipfs/3CiqtD8C2e8fycQs1VRzwmBeuqHGmltpDRC4Vo8hRqs") \   --etherscan-api-key 5Y4H......DU2T5 \   --compiler-version 0.8.0 \
   --watch

 

5. Wrapping Up Part 1

How was the verification and deployment? If you were able to deploy and verify the contract, congratulations! It isn’t easy deploying contracts to networks with such volatile gas prices at times. You now have all your metadata set and the NFT contract deployed and initialized, and it should even be eventually viewable on OpenSea after a few minutes. Be sure to navigate to OpenSea’s testnet dashboard as we did not deploy to mainnet in this tutorial.

The next part of the tutorial will demonstrate the process of upgrading the existing NFT’s metadata to represent the new trait.

About Validation Cloud

Validation Cloud is a Web3 data streaming and infrastructure company that connects organizations into Web3 through a fast, scalable, and intelligent platform. Headquartered in Zug, Switzerland, Validation Cloud offers highly performant and customizable products in staking, node, and data-as-a-service. Learn more at Validationcloud.io | LinkedIn | X