HANDS ON
Build a dynamic NFT

Create a dynamic NFT

In this tutorial, you will create a dynamic NFT using Irys' mutable references.

About

Dynamic NFTs are NFTs whose metadata evolves over time. They are commonly used in:

  • Gaming projects where in-game assets evolve as players progress.
  • Loyalty programs where NFTs evolve as users accumulate points.

This tutorial focuses on creating a SuperMon NFT that could evolve during gameplay. The NFT starts with a basic appearance and can be "upgraded" twice. We will use the Irys CLI to update the metadata, simulating the automatic changes that would occur through player interactions in an actual game.

Mutable references

Mutable references are a way to simulate "mutability".

  1. You start by creating a single static URL that is tied to a chain of transactions:
const irys = await getIrys();
const receiptOne = await irys.upload("First TX");
console.log(`TX 1 uploaded https://gateway.irys.xyz/mutable/${receiptOne.id}`);
  1. The chain can be updated at any time, and the original URL will always resolve to the most recent one:
const tags = [{ name: "Root-TX", value: receiptOne.id }];
const receiptTwo = await irys.upload("Second TX", { tags: tags });
console.log(`TX 2 uploaded https://gateway.irys.xyz/mutable/${receiptOne.id}`);

Setup

In this tutorial, we will build on the Base L2. Before starting, make sure you add Base Sepolia to your EVM wallet (opens in a new tab), pre-load some Base Sepolia (opens in a new tab) tokens, and export your private key.

ℹ️

While we focus on Base Sepolia, the principles outlined can be adapted for deployment on any blockchain.

Smart contract

We're building an NFT, which means we need a smart contract. Here's a simple one you can use to mint the NFT we'll create.

SuperMon.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
// Import OpenZeppelin's ERC721 and ERC721URIStorage contracts
// These URLs are compatible with Remix IDE
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
 
contract SuperMon is ERC721URIStorage {
    uint256 private _tokenIdCounter;
 
    // No arguments in the constructor, the owner will be the contract deployer
    constructor() ERC721("SuperMon", "SMON") {
        _tokenIdCounter = 0;
    }
 
    // Mint function to create a new NFT
    function mint(address to, string memory uri) public {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }
}
 

To deploy the smart contract using Remix IDE:

  1. Open Remix IDE (opens in a new tab).

  2. Create a new solidity file

  • In the File Explorers pane, click on the Create New File icon.
  • Name your file SuperMon.sol, and paste the smart contract in.
  1. Compile the contract
  • Click on the Solidity Compiler icon in the sidebar.
  • Select the compiler version that matches your contract's pragma (^0.8.0).
  • Click Compile SuperMon.sol.
  1. Deploy the Contract
  • Click on the Deploy & Run Transactions icon in the sidebar.
  • In the ENVIRONMENT dropdown, select Injected Web3.
  • MetaMask will prompt you to connect. Confirm the connection to your Remix session.
  • Ensure SuperMon is selected in the CONTRACT dropdown.
  • Click Deploy. MetaMask will ask for confirmation to proceed with the transaction.

Uploading the images

Rght-click on each of the above images and save them on your local drive.

Next, fund the Irys Devnet node with 0.1 Sepolia ETH to pay for your uploads.

ℹ️

In all of these CLI examples, make sure to replace the value of the -w parameter with your own private key.

irys fund 100000000000000000 \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --provider-url https://sepolia.base.org
ℹ️

The fund command accepts a value in atomic units, 0.1 ETH is equal to 100000000000000000 in atomic units.

Next, use the Irys CLI to upload each of the images to the Irys Devnet.

irys upload image-level-1.png \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --tags Content-Type image/png \
  --provider-url https://sepolia.base.org
 
irys upload image-level-2.png \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --tags Content-Type image/png \
  --provider-url https://sepolia.base.org
 
irys upload image-level-3.png \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --tags Content-Type image/png \
  --provider-url https://sepolia.base.org

Uploading the metadata

Create three metadata files similar to the ones below. Make sure to change the value of the image field to match the URLs generated in the previous step.

metadata-level-1.json
{
  "name": "SuperMon",
  "symbol": "SMON",
  "image": "https://gateway.irys.xyz/QH3rksVhbFg5L9vvjGzb4POUibCEG-TGPInmofp-O-o",
  "description": "Super dooper, changing shapes, changing power",
  "attributes": [
    {
      "trait_type": "supermon-level",
      "value": "1"
    }
  ]
}
metadata-level-2.json
{
  "name": "SuperMon",
  "symbol": "SMON",
  "image": "https://gateway.irys.xyz/QH3rksVhbFg5L9vvjGzb4POUibCEG-TGPInmofp-O-o",
  "description": "Super dooper, changing shapes, changing power",
  "attributes": [
    {
      "trait_type": "supermon-level",
      "value": "2"
    }
  ]

}
metadata-level-3.json
{
  "name": "SuperMon",
  "symbol": "SMON",
  "image": "https://gateway.irys.xyz/QH3rksVhbFg5L9vvjGzb4POUibCEG-TGPInmofp-O-o",
  "description": "Super dooper, changing shapes, changing power",
  "attributes": [
    {
      "trait_type": "supermon-level",
      "value": "3"
    }
  ]

}

And upload just the first file using the Irys CLI.

irys upload metadata-level-1.json \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --tags Content-Type application/json \
  --provider-url https://sepolia.base.org

The CLI will return a URL similar to https://gateway.irys.xyz/NDtKvjlmZL2iXUPmX6P-BuvtnvAEFkUiQWG8ToyK5FM. To convert that to a mutable references URL, interpolate it by adding /mutable/ after the domain and before the transaction ID.

Your final URL will be similar to https://gateway.irys.xyz/mutable/NDtKvjlmZL2iXUPmX6P-BuvtnvAEFkUiQWG8ToyK5FM.

Mint the NFT

To mint your NFT in Remix:

  1. Return to Remix.
  2. Under "Deployed Contracts", locate your contract and expand it to see its functions.
  3. Under the Mint function, enter the wallet address you want to mint the NFT to and the metadata URL (e.g. https://gateway.irys.xyz/mutable/NDtKvjlmZL2iXUPmX6P-BuvtnvAEFkUiQWG8ToyK5FM) from the previous step.
  4. Click Transact.

You can now view the NFT on the Opensea Testnet (opens in a new tab).

Mutating the metadata

To now "mutate" the NFT, upload a new version of the metadata tagging it as having a Root-TX equal to the transaction ID of your first transaction. In my example, I pass the value of NDtKvjlmZL2iXUPmX6P-BuvtnvAEFkUiQWG8ToyK5FM, however make sure to change this to match your unique transaction ID.

irys upload metadata-level-2.json \
  -n devnet \
  -t base-eth \
  -w 6dd5e....54a120201cb6a \
  --tags Content-Type application/json Root-TX NDtKvjlmZL2iXUPmX6P-BuvtnvAEFkUiQWG8ToyK5FM \
  --provider-url https://sepolia.base.org

Return to Opensea and request that it refresh your metadata.

Give it a few minutes and your updated NFT should be visible.

Free metadata uploads

This tutorial uses the Irys Devnet where uploads are kept for ~60 days and are paid for with free faucet currencies. When deploying production projects, you'll want to use either Node 1 or 2 where uploads are permanent.

On Irys' Node 2 uploads of less than 100 KiB are free, which is more than enough for most metadata files. This means projects can let users "evolve" their NFTs without having to pay gas fees.

Caching

Wallets and NFT platforms typically cache metadata to optimize performance, this can affect the visibility of updates to dynamic NFTs. While OpenSea offers a feature for users to manually request metadata refreshes, not all platforms provide this level of control. When building dynamic NFT projects, make sure to thoroughly test and understand the implications of caching on your platform.

Getting help

Any questions? Reach out to us in Discord (opens in a new tab).