James Bachini

Dynamic Evolving NFTs | How To Create Dynamic NFTs & dApps

Dynamic NFTs

Dynamic NFTs include logic that evolves the underlying data that the NFT contract holds. In this example we will build a picture profile NFT that is upgradeable to an alien 👽

  1. Dynamic NFT Smart Contract
  2. Uploading Artwork To IPFS
  3. Building A NFT dApp In React
  4. Ideas For Experimentation

All the source code for this project is available at: https://github.com/jamesbachini/WomenWhoCode

Dynamic NFT Smart Contract

The Solidity smart contracts will comprise of two tokens:

  • The ERC721 NFT Contract
  • An ERC20 Fungible Token

The user will have to earn enough ERC20 tokens to be able to upgrade their NFT to an alien.

OpenZeppelin has an excellent token wizard on their site which can be used to create the ERC20 NFT

https://www.openzeppelin.com/contracts

Creating tokens with Openzeppelin

We can then open this up in Remix and start building our main NFT contract. The NFT contract will deploy the ERC20 token and hold the entire supply within the contract.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

contract Token is ERC20 {
    constructor() ERC20("WomenWhoCode Token", "WWC") {
        _mint(msg.sender, 1_000_000 * 10 ** decimals());
    }
}

contract WomenWhoCode is ERC721 {
    address public tokenAddress;
    uint public tokenId;
    mapping(uint => bool) public isAlien;

    constructor() ERC721("WomenWhoCode NFT", "WWC") {
        Token token = new Token();
        tokenAddress = address(token);
    }

    function earnTokens() public {
        IERC20(tokenAddress).transfer(msg.sender,100e18);
    }

    function mint() public {
        require(tokenId < 1000, "All 1000 tokens have been minted");
        _safeMint(msg.sender, tokenId);
        tokenId = tokenId + 1;
    }

    function makeAlien(uint _tokenId) public {
        uint balance = IERC20(tokenAddress).balanceOf(msg.sender);
        require(balance >= 100e18, "You don't have enough tokens");
        require(ownerOf(_tokenId) ==  msg.sender, "You do not own this NFT");
        isAlien[_tokenId] = true;
    }

    function tokenURI(uint256 _tokenId) override public view returns (string memory) {
        string memory json1 = '{"name": "WomenWhoCode", "description": "A dynamic NFT for WWC", "image": "https://cloudflare-ipfs.com/ipfs/';
        string memory json2;
        // Upload images to IPFS at https://nft.storage
        if (isAlien[_tokenId] == false) json2 = 'bafybeidrcvkt63y4ananxegwleuo43dddt5zhcvtuaiv2bfv2ge7o73ibi"}'; // avatar.png
        if (isAlien[_tokenId] == true) json2 = 'bafybeibyecyp6lxb3p4g27gwsijr5txx72bnfigetwoswhfnrbejla7gcm"}'; // alien.png
        string memory json = string.concat(json1, json2);
        string memory encoded = Base64.encode(bytes(json));
        return string(abi.encodePacked('data:application/json;base64,', encoded));
    }

}

Note we are using the latest string.concat(); function which is only available from solc 0.8.12 so you may need to update your compiler.

earnTokens() – is a public function that lets the user earn the ERC20 tokens. There’s no additional logic here but you could quite easily add complexities of gamification to this.

mint() – is a public function for users to be able to mint a NFT

makeAlien() – is a public function that allows a user to upgrade their NFT to an alien.

tokenURI() – upgrades the default function to parse back base64 encoded JSON data including the image file which is uploaded to IPFS. The output string looks like this:

data:application/json;base64,eyJuYW1lIjogIldvbWVuV2hvQ29kZSIsICJkZXNjcmlwdGlvbiI6ICJBIGR5bmFtaWMgTkZUIGZvciBXV0MiLCAiaW1hZ2UiOiAiaHR0cHM6Ly9jbG91ZGZsYXJlLWlwZnMuY29tL2lwZnMvYmFmeWJlaWRyY3ZrdDYzeTRhbmFueGVnd2xldW80M2RkZHQ1emhjdnR1YWl2MmJmdjJnZTdvNzNpYmkifQ==

If you paste that into a web browsers URL bar you’ll see the decoded data as:

{"name": "WomenWhoCode", "description": "A dynamic NFT for WWC", "image": "https://cloudflare-ipfs.com/ipfs/bafybeidrcvkt63y4ananxegwleuo43dddt5zhcvtuaiv2bfv2ge7o73ibi"}

Now let’s look at how that image was generated and uploaded to IPFS.


Uploading Artwork To IPFS

The artwork was designed with more than a little help from OpenAI’s Dall-E.

OpenAI DallE Girls Holding Laptop

From there I cut out the main character in GIMP and traced the bitmap in Inkscape.

This created a layered image which needed a bit of tweeking but was ideal for creating variations on a theme. The variation I went for was an alien by simply adjusting the colour of the base skin tone.

Dynamic NFT Orginal
Evolving NFT Alien

I then used nft.storage to drag and drop these files in to IPFS which is a pseudo-decentralized file system.

Uploading dynamic nfts to IPFS with nft.storage

Building A Dynamic NFT dApp In React

For the frontend dApp I used react and ethers.js This will require you have Node.js installed.

npm init
npm install -s create-react-app
npm install -s ethers
npx create-react-app frontend
cd frontend
npm start dev

From there we can open up a text editor and start modifying the frontend/src/App.js file

import { useState } from "react";
import './App.css';

import tokenABI from './tokenABI.json';
import nftABI from './nftABI.json';
import { ethers } from "ethers";

let provider, signer, address, token, nft, tokenId;

function App() {
  const [msg, setMsg] = useState('');

  const tokenContractAddress = '0x82cF3a0a631bD601139D89063a0eBc4dA8aC15AF';
  const nftContractAddress = '0xE50366F0534C70D923503142eeB7e298ceD9F06f';
  
  const connect = async () => {
    provider = new ethers.providers.Web3Provider(window.ethereum);
    await provider.send("eth_requestAccounts", []);
    signer = provider.getSigner();
    token = new ethers.Contract(tokenContractAddress, tokenABI, signer);
    nft = new ethers.Contract(nftContractAddress, nftABI, signer);
    address = await signer.getAddress();
    setMsg(`Connected: ${address}`);  
  }
  
  const mint = async () => {
    const tx = await nft.mint();
    setMsg(`Processing transaction...`);
    await tx.wait();
    tokenId = await nft.tokenId();
    tokenId -= 1; // previous tokenId is ours
    const b64 = await nft.tokenURI(tokenId);
    const json = atob(b64.split(',')[1]);
    const imageURL = JSON.parse(json).image;
    setMsg(`You own WomenWhoCode NFT #${tokenId}`);
    document.getElementById('image').innerHTML = `<img src="${imageURL}" />`;
  }
  
  const earn = async () => {
    const tx = await nft.earnTokens();
    setMsg(`Sending you some tokens...`);
    await tx.wait();
    const balance = await token.balanceOf(address);
    setMsg(`Your balance is now: ${balance}`);
  }
  
  const alien = async () => {
    const tx = await nft.makeAlien(tokenId);
    setMsg(`Something strange is happening...`);
    await tx.wait();
    const b64 = await nft.tokenURI(tokenId);
    const json = atob(b64.split(',')[1]);
    const imageURL = JSON.parse(json).image;
    setMsg(`You own WomenWhoCode Alien NFT #${tokenId}`);
    document.getElementById('image').innerHTML = `<img src="${imageURL}" />`;
    
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>WomenWhoCode</h1>
        <h2>Dynamic NFT</h2>
        <div id="image"></div>
        <div id="buttons">
          <button onClick={connect}>Connect</button>
          <button onClick={mint}>Mint</button>
          <button onClick={earn}>Earn</button>
          <button onClick={alien}>Alien</button>
        </div>
        <div id="msg">{msg}</div>
      </header>
    </div>
  );
}

export default App;

This is all pretty standard. Connecting the wallet to Ethers.js and then using the ABI’s from remix to create contract instances for the ERC20 token and ERC721 NFT. We then have buttons to mint an NFT, earn ERC20 tokens and upgrade to an alien.

WomenWhoCode Dynamic NFTs Screenshot

Ideas For Experimentation

You could definitely take this further in a number of different ways.

  • In this contract you only need a balance of ERC20 tokens to upgrade your contract but you could potentially require users to stake assets in the contract to upgrade their NFT so the NFT acts as a vault of digital assets.
  • You could use something like Jang.js. to create 10,000 different images from layers and pass through the tokenId to the URL so that every image is unique.
  • You could add some form of gamification to make users complete levels to upgrade their NFT or dynamically evolve the character inline with a in-game story.
  • You could change the isAlien variable to isJamesExGf

I hope this walkthrough and demo has been of interest. Thank you to WomenWhoCode for inviting me to do the workshop for their community. All the code is on Github and bare in mind that it is for demonstration purposes and none of it has been tested in the wild.



Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.