Real-Time NFTs: Dynamic Smart Contracts Building Workshop

gmchad.eth
ETH San Diego
Published in
9 min readFeb 24, 2022

In this tutorial, we’re going to build a dynamic NFT. Hopefully, you’re familiar with NFTs and have maybe even looked under the hood of some popular projects like BAYC, Cool Cats, and others. If not, you may want to check out this article first on launching an NFT from scratch.

Background

Traditionally, there are two types of ways NFT smart contracts like the ERC721 and ERC1155 variants can store image information:

  1. The contract stores a URI that’s accessible via the tokenURI() function. This URI points to a decentralized storage location like IPFS or Arvweave and applications like Opensea can query this data directly from the source.
  2. The contract generates the image directly via SVG layering and constructs the metadata on-chain. This is more complex than simply pointing to some metadata already generated off-chain. The Noun Protocol from NounsDAO is an excellent resource for this type of approach.
Figure 0. We Love the Nouns

For this tutorial, we’re going to use a hybrid approach for generating metadata. The image that the metadata contains will be stored on IPFS, however, the generation of the JSON will be done on-chain. This makes it easier to dynamically create the metadata on the fly while giving us the flexibility to change the image wrapped by the metadata with little effort.

Before we go on, it’s important to make note of the effect this can have on an NFT project. Imagine changing the metadata of a blue-chip collection like BAYC by manipulating the images or even changing the attributes to make some items more rare. If all it takes is for a developer to update the URI of an NFT to completely change the look then collectors are really putting all their trust into the teams delivering.

This is where the concept of provenance comes in and it’s something that isn’t mentioned enough with NFTs. If NFTs solely use IPFS, developers should publish an immutable checksum (hash) of the metadata folder after they reveal. That way, users can always verify off-chain that the metadata has not been tampered with. If the metadata is solely generated on-chain then there’s less of a risk.

In the case of dynamic NFTs where metadata and images can change, end-users should be made aware of how and when things will change. In the case of off-chain metadata (IPFS), a new provenance hash can be updated to the contract each time metadata changes that way there is a full provable audit trail.

Real-Time NFTs ⏳

Now that we understand how NFTs can be updated, what if we could use real-world data, events, or even true randomness to dynamically update the metadata in our smart contracts? Welcome to the world of Chainlink.

Chainlink is a decentralized oracle network that can feed off-chain data into smart contracts essentially giving them the ability to request and respond to any off-chain API.

Figure 1. ChainLink Network

This tutorial assumes you already have MetaMask and are familiar with the ability to change networks. We will be using the Rinkeby Testnet for everything moving forward.

Note: If you’ve never used test networks before you may need to enable this feature. My Accounts > Settings > Advanced > Show test networks.

Next, we need to fund our wallet with some Rinkeby ETH and LINK. Every call to an Oracle will cost some LINK which goes to the node operators as a service fee. Head over to https://faucets.chain.link/rinkeby and fill up your account with some test ETH and LINK.

Figure 2. Chainlink Faucet

If you haven’t imported tokens before, you’ll also want to import the LINK token address so it shows up in MetaMask. Look for the “Don’t see your token? Import Tokens” link under the Assets tab.

For Rinkeby the address is: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709

Figure 3. Importing Tokens

Time to Build 👷‍♂️

At this point, you’re probably wondering what we’re building and how we will be using Chainlink Oracles to update our NFT dynamically. There are tons of Data Providers out on the chainlink marketplace, but for this tutorial, we are going to use Chainlink’s VRF (verifiable random function) Oracle to randomly assign NFTs during mint. There are two key components that make VRF unique and interesting:

  1. The randomness comes from a truly random source. The Ethereum blockchain is completely deterministic*, meaning any method of trying to create randomness within a smart contract is not actually random, but pseudorandom, and can be calculated ahead of time.
  2. The randomness is verifiable, meaning anyone can validate how the randomness was generated via cryptographic proof. For more information on how this works, check out this detailed post by Chainlink.

* The blockchain needs to be deterministic so that all nodes can reach a consensus on each block and agree on the next state of the chain.

With these two characteristics, we can create truly random NFTs and verify the source of randomness. Neat!

Figure 4. Chainlink VRF

Metadata 💾

Before we hop into the code, as with any NFT, you need some metadata that describes the NFT and contains the pointer to the image of the NFT on IPFS. Since the NFTs we want to create will point to random images, we need a way to preserve the metadata such as tokenIdwhile still being able to randomize the image pointer.

To accomplish this we will dynamically generate the metadata on-chain and return it as a base64 encoded string. If you’re not familiar with base64, it looks something like this:

data:application/json;base64,eyJuYW1lIjoidG9rZW4gIyAwIiwgImRlc2NyaXB0aW9uIjoiQSBkeW5hbWljIE5GVCIsICJpbWFnZSI6ICJodHRwczovL2dhdGV3YXkucGluYXRhLmNsb3VkL2lwZnMvUW1YSDJlTmtIUzNXOFZveG5TcWhxWDVFSzE4cjkyNjg2a3VtS3VXM0dEeThVdi82LnBuZyJ9

Browsers are able to automatically parse this, hence why OpenSea will still be able to render the NFT. Throw that in your search bar and observe what happens…

You should get some metadata like:

{"name":"token # 0", "description":"A dynamic NFT", "image": "https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/6.png"}

For this tutorial, I’ve pulled 10 images from https://unsplash.com/ and stored them to IPFS via piñata so we can get straight to the code. The _baseTokenURI we will be using is:

https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/

Note this only points to the images and the actual metadata will be generated on-chain. You can always update this by bringing your own 10 images.

Remix 💻

Let’s open up a new instance of Remix in the browser and create a new file called dynamicNFT.sol . Next copy and paste the following code into the file:

Figure 5. dynamicNFT.sol

A portion of this code should look similar to the simpleNFT.sol from the Launching an NFT from Scratch tutorial. A lot should be new and related to the Chainlink VRF functionality. Let’s dive in:

Line 57: Initializes the VRFConsumerBase with the _coordinator address and _link token address. The consumer base is what will drive the chainlink requests and callbacks.

Lines 103–105: Requests a new random number from getRandomNumber() and maps the requestId to the mintIndex (tokenId). The requestId is used because calls to the VRF coordinator are asynchronous and could come back in any order so we need a way to track which token made the specific request

Lines 156–162: fulfillRandomnessis the callback function that Chainlink will call once it has a random number to provide. The following lines constrain the random number from 1–10 and store it into another mapping randomMap that maps the tokenId to the modRandom number.

The next functions and lines of code are responsible for the on-chain generation of the metadata.

Lines 110–114: tokenURIis the method marketplaces like OpenSea use to pull information about the NFT. On line 114, the metadata is returned from the helper method constructTokenURIwhich takes in the tokenId as a parameter.

Lines 117–142: constructTokenURIis where the magic happens in. Line 123 uses the tokenId to grab the randomNumber stored in the randomMap mapping. Line 125 builds the full image URI by concatenating _baseTokenURI and the randomNumber pulled just above.

Pro tip: In solidity, you can perform string interpolation by using the very useful abi.encodePacked() method.

Lines 128–129: These lines generate the name and symbol that will be used in the metadata. Note on line 128, the name is dynamically generated each time the method is called.

Lines 132–142: This is where the metadata standard is constructed and converted into base64. Note base64 is very efficient when encoding SVG information, but in our case we just have an IPFS URI so the storage gain isn’t as great.

Deployment 🏗️

Hopefully, you have a better understanding of how this contract works and are ready to deploy it.

To compile your contract, head to the second tab on the lefthand side of Remix and ensure you’re using version solidity version 0.8.1 . Under Compiler Configurations you will also want to Enable optimization and set the number of runs to 200 . Optimizations reduce the bytecode but have some tradeoffs, check out this link for more information.

Lastly, make sure you’re compiling the correct contract: DynamicNFT (dynamicNFT.sol)

Figure 6. Remix Compilation

With the contract compiled and ready to deploy, we need to pass in a few constructor arguments before we transact.

Figure 7. Remix Deployment

Make sure you’re connected to the Rinkeby (4) network via the Injected Web3 environment. The following constructor parameters are:

  1. _link: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 — link token address
  2. _coordinator: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B — VRF oracle address
  3. _keyHash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 — used to generate randomness
  4. _fee: 100000000000000000– 0.1 LINK
  5. name: My Dynamic NFT — have fun with this one
  6. symbol: DNFT — have fun with this one too
  7. baseURI: https://gateway.pinata.cloud/ipfs/QmXH2eNkHS3W8VoxnSqhqX5EK18r92686kumKuW3GDy8Uv/ — remember that forward slash is important
  8. tokenPrice: 0 — unless you’re Rinkeby rich
  9. maxTokens: 10 — this is based on the number of images stored in the baseURI folder
  10. maxMints: 1 — how many you can mint at a time

Interaction 🖱️

Once you have that very long list of parameters copied in Remix, it’s time to deploy to the Rinkeby Testnet. Go ahead and hit transact and wait until the contract is mined. Keep track of the contract address, you can find this under the deployed contracts section of remix.

Remember, every call to a Chainlink Oracle costs LINK and in the case of the VRF Oracle, this amount is 0.1 LINK. You can even see this requirement in line 148:

require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");

Funding the Contract with Link 💸

We can send LINK directly to the contract using MetaMask. Open up the assets tab, find LINK, and send 1 LINK to the contract address you wrote down above.

Figure 8. Sending LINK

Mint 🔮

At this point, we should be ready to mint some dynamic NFTs. This part is pretty straightforward. The mintTokens the method is under the contract you deployed, type in 1 , and then transact!

Figure 9. Minting Dynamic NFTs

Go ahead and do this a few more times if you want so you can get a variety of images on OpenSea.

Keep in mind that with each mint a request is being sent out to the Chainlink VRF Oracle and the response is handled asynchronously. That means that it might take some time for random numbers to affect the metadata and hence the NFT image may not show up right away.

OpenSea 🖼️

Let’s see what shows up on OpenSea. You’ll want to use https://testnets.opensea.io/ which is the Rinkeby equivalent of OpenSea and type in your ETH Address.

Figure 10. Dynamic NFTs on OpenSea

Yay 🎉

Congrats on making it to the end! Throughout this tutorial you learned:

  1. How to use Chainlink VRF to generate provable randomness
  2. How to interpret the randomness to mint NFTs
  3. How to use LINK tokens to fund Chainlink Contracts
  4. How the Chainlink request/response architecture works
  5. How to dynamically generate metadata on chain

Services Used 🔨

Reach Out 📱

If you have any questions feel free to leave comments or follow on Twitter at https://twitter.com/ultrasoundchad.

This post is also part of the Chainlink X ETHSD meetup. Check us out at:

  1. https://www.meetup.com/eth-sd/
  2. https://twitter.com/EthSanDiego

--

--