Join our
Discord!
LogoLogo
SupraScan ExplorerStarKey WalletDiscord
  • Network
  • Oracles
  • Automation
  • SupraNova
  • Guides
  • Overview
  • Move
    • Building a Crypto Index Fund Smart Contract on Sui Blockchain // Move
    • Crowdfunding Smart Contracts on Sui Blockchain // Move
    • Lottery Smart Contract on Aptos Blockchain // Move
  • Solidity
    • Pull Oracle Walkthrough
    • Randomize ERC-721 NFT Traits with VRF
    • Simple Loot Box Contract with VRF
Powered by GitBook
On this page
  • Intro
  • Getting Started
  • Time to Code
  • Dependencies
  • Variables
  • Functions
  • Final Code
  • Conclusion
Edit on GitHub
  1. Solidity

Randomize ERC-721 NFT Traits with VRF

This guide uses our VRF service to randomize the trait values of each ERC-721 token that is minted.

PreviousPull Oracle WalkthroughNextSimple Loot Box Contract with VRF

Last updated 18 days ago

This guide assumes that you have successfully interacted with the to whitelist and fund the VRF requests for the use of VRF V2. This step is only applicable if the.

Intro

If you're interested in NFTs, you may have heard about randomizing traits/properties assigned to each token. This is a very common topic within the NFT space and is something that many developers (like you!) will face throughout their journey. There are a few different ways to do this. Today, we’ll be randomizing the traits for each token and then storing them on-chain within an NFT smart contract.

Getting Started

Imagine this: You’re creating an NFT project for a game where each token represents a character. When a new character is created (minted), we want to randomly assign their starting stats/levels. Of course, we’ll want to be able to level up and view the stats for each token after they're generated as well.

In this guide, we’ll be for our NFT contract, for tracking the next token ID to be minted, and the Supra VRF to generate verifiable random numbers for the token traits.

Time to Code

First, make a new solidity file randomTraits.sol and declare our contract randomTraits with an empty constructor. After that, we'll set up our dependencies, variables, and functions before finalizing our code.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
contract randomTraits {
 
    constructor(){
       
    }
 
}

Dependencies

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
 
contract randomTraits is ERC721 {
 
  constructor(address supraSC) ERC721("randomTraits", "RAT") {
  }
 
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
 
contract randomTraits is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;
 
    constructor(address supraSC) ERC721("randomTraits", "RAT") {
 
    }
 
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
 
interface ISupraRouter {
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed, address _clientWalletAddress) external returns(uint256);
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, address _clientWalletAddress) external returns(uint256);
}
 
contract randomTraits is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;
    address supraAddr;
 
    constructor(address supraSC) ERC721("randomTraits", "RAT") {
        supraAddr = supraSC;
 
    }
 
}

Variables

    struct playerStats{
        uint8 level;
        uint8 health;
        uint8 strength;
    }
mapping (uint256 => playerStats ) tokenIdToStats;

Now, we have one more important variable to create before we move on to our functions. When a user calls our mint() function to mint a token, a request to the Supra VRF service is made.

However, the response won’t be coming from that same user’s address. It will be coming from the Supra VRF service’s address. This begs the question of what to use as an identifier when we get our response.

As such, we can map this nonce value to the address of the original mint() caller to keep track of which responses belong to which requests. Remember that this means which random numbers belong to which users/requests.

mapping(uint256 => address) nonceToMinterAddress;

Up until this point, your code should look like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
 
interface ISupraRouter {
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed, address _clientWalletAddress) external returns(uint256);
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, address _clientWalletAddress) external returns(uint256);
}
 
contract randomTraits is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;
    address supraAddr;
 
    struct playerStats{
        uint8 level;
        uint8 health;
        uint8 strength;
    }
 
    mapping(uint256 => address) nonceToMinterAddress;
    mapping (uint256 => playerStats ) tokenIdToStats;
 
    constructor(address supraSC) ERC721("randomTraits", "RAT") {
        supraAddr = supraSC;
 
    }
 
}

Functions

We’re on the home stretch now. Our contract will consist of four (4) functions to make the request, receive the response (callback), view the token stats/traits, and "level up" the token stats.

Mint() - Request function

You’ll see that our parameters passed with the request are as follows:

_functionSig: "finishMint(uint256,uint256[])" _rngCount: 1 _numConfirmations: 1 _clientSeed: 123 _clientWalletAddress: msg.sender

    function mint() public {
        uint256 nonce =  ISupraRouter(supraAddr).generateRequest("finishMint(uint256,uint256[])", 1, 0, 123, msg.sender);
        nonceToMinterAddress[nonce] = msg.sender;
    }

finishMint() - Callback function

As you may have guessed, the callback function that we'll now define will is finishMint(). This is where we'll handle the response from the Supra VRF.

As per the Supra VRF docs, callback functions must accept two parameters of uint256 and uint256[]. The first parameter is the nonce that was returned to us when the original request was made. The second parameter is an array (uint256[]) consisting of the random values that the Supra VRF has generated for us. We’ll call this rngList.

The first step in our callback function is to check to make sure that the calling address is that of the Supra VRF/Router contract. This ensures that only Supra VRF can return values to your callback function.

require(msg.sender == supraAddr, "only supra router can call this function");

We'll then pass these two parameters through the _safeMint() function.

uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(nonceToMinterAddress[nonce], tokenId);

Next, we are going to use the randomly generated number to get a number between the range of 0-99 for each trait that we need to set within the playerStats. Luckily, we can use a single random number from the Supra VRF to generate multiple numbers within the range of 0-99.

To do this, we use a combination of modulus and division. Alternatively, we could also request 3 random numbers by changing our _rngCount value in the initial request and then assign the returned values accordingly.

 tokenIdToStats[tokenId].level = uint8((rngList[0] % 100));
 tokenIdToStats[tokenId].health = uint8((rngList[0] % 10000)/100);
 tokenIdToStats[tokenId].strength = uint8((rngList[0] % 1000000)/10000);

Here's the code for the complete callback function:

function finishMint(uint256 nonce, uint256[] calldata rngList) external {
        require(msg.sender == supraAddr, "only supra router can call this function");
 
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
 
        _safeMint(nonceToMinterAddress[nonce], tokenId);
 
        tokenIdToStats[tokenId].level = uint8((rngList[0] % 100));
        tokenIdToStats[tokenId].health = uint8((rngList[0] % 10000)/100);
        tokenIdToStats[tokenId].strength = uint8((rngList[0] % 1000000)/10000);
    }

getTokenStats() - View function to return token stats/trait values

The final step is to add our third and final function getTokenStats() , which will return the trait values for the passed token ID.

function getTokenStats(uint tokenId) external view returns (uint, uint, uint){
    return (tokenIdToStats[tokenId].level, tokenIdToStats[tokenId].health, tokenIdToStats[tokenId].strength);
  }

levelUpToken() - Function to increase token stats for "leveling up"

  function levelUpToken(uint tokenId) external{
    require(_exists(tokenId), 'Token does not exist.');
    tokenIdToStats[tokenId].level = tokenIdToStats[tokenId].level + 1;
    tokenIdToStats[tokenId].health = tokenIdToStats[tokenId].health + 10;
    tokenIdToStats[tokenId].strength = tokenIdToStats[tokenId].strength + 10;
  }

Final Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
 
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
 
interface ISupraRouter {
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed, address _clientWalletAddress) external returns(uint256);
   function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, address _clientWalletAddress) external returns(uint256);
}
 
contract randomTraits is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;
    address supraAddr;
 
    struct playerStats{
        uint8 level;
        uint8 health;
        uint8 strength;
    }
 
    mapping(uint256 => address) nonceToMinterAddress;
    mapping (uint256 => playerStats ) tokenIdToStats;
 
    constructor(address supraSC) ERC721("randomTraits", "RAT") {
        supraAddr = supraSC;
 
    }
 
    function mint() public {
        uint256 nonce =  ISupraRouter(supraAddr).generateRequest("finishMint(uint256,uint256[])", 1, 1, 123, msg.sender);
        nonceToMinterAddress[nonce] = msg.sender;
    }
 
    function finishMint(uint256 nonce, uint256[] calldata rngList) external {
        require(msg.sender == supraAddr, "only supra router can call this function");
 
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(nonceToMinterAddress[nonce], tokenId);
 
        tokenIdToStats[tokenId].level = uint8((rngList[0] % 100));
        tokenIdToStats[tokenId].health = uint8((rngList[0] % 10000)/100);
        tokenIdToStats[tokenId].strength = uint8((rngList[0] % 1000000)/10000);
    }
 
    function getTokenStats(uint tokenId) external view returns (uint, uint, uint){
        return (tokenIdToStats[tokenId].level, tokenIdToStats[tokenId].health, tokenIdToStats[tokenId].strength);
    }
    
    function levelUpToken(uint tokenId) external{
        require(_exists(tokenId), 'Token does not exist.');
        tokenIdToStats[tokenId].level = tokenIdToStats[tokenId].level + 1;
        tokenIdToStats[tokenId].health = tokenIdToStats[tokenId].health + 10;
        tokenIdToStats[tokenId].strength = tokenIdToStats[tokenId].strength + 10;
    }
 
}

Note: in this guide, the token isn't officially minted/created until the callback function finishMint() is hit. Alternatively, you could move the minting logic into the request function to ensure that the token is generated prior to the callback being received.

Conclusion

A request creates a response, which creates a world of opportunity.

With just a little bit of time, you were able to create the foundation for your own NFT project that utilizes the Supra VRF to randomize NFT trait values. In one of our next guides, we’ll show you how to generate a JSON metadata file with the randomized traits that we just stored on-chain in our NFT smart contract to set the token URI.

In the meantime, I need a coffee…

Now is a great time to set up our dependencies. We have three (3) things that we need to do. First, we'll import , it, and then update our constructor accordingly. The contract accepts two parameters. The first parameter is the name of the token, and the second parameter is the symbol. We’ll be naming it randomTraits with the symbol RAT, but you can name yours whatever you want.

This contract has become an industry standard that is compliant with the . Don’t be afraid to read through their documentation and explore the expansive functionality that this token standard offers.

Since we’ll be minting tokens in order by ID, we’ll need a way to keep track of which token ID is next in line to be minted. Rather than simply incrementing any uint/int that we declare within our contract, we'll follow the industry standard of using library. Import library and then include it with the : using Counters for Counters.Counter;. While we’re at it, we’ll also go ahead and instantiate a new counter with the name _tokenIdCounter.

The final step for our dependencies is declaring the that will allow us to interact with the Supra VRF service. We'll declare an address variable named supraAddr that will hold the , which will be passed through our contract constructor upon deployment. We’ll need this later to make our request for a random number. More information on available chains and the relative contract addresses are available in our . If you haven’t already, now is a great time to take a look at the .

Great! With that out of the way, it's time to declare all of the variables that we need. Let’s say that we want each character (token) to have a “level,” “health,” and “strength” trait assigned to it. We’ll need a way to conveniently store these values and a way to map each token to these values. This is a perfect use case for a and a .

We’ll declare a struct with the name characterStats. Within the , let’s add three (3) uint8 variables with the names level, health, and strength. Remember, these will be holding the randomly generated values from the Supra VRF.

For the , what better value to use than the token ID! As per the , the token ID is a uint256 value. As such, create a mapping of uint256 to the playerStats with the name tokenIdToStats.

Due to the request and response model of VRF services, we'll need a way to keep track of which responses belong to which requests. That is, we need a way to know which random numbers belong to which users. Since the user has called the mint() function using their wallet, we can easily access it through the msg.sender and store it for later. That means we’ll know which user address is responsible for the request.

Luckily, the Supra VRF developer guide provides us with all the relevant information. When we make a request using the and contract address that we previously discussed, we'll be returned a value named nonce that is of type uint256. The Supra VRF will then provide this nonce to our callback function when it delivers the randomly generated number.

To do this, create a of type uint256 to address with the name nonceToMinterAddress.

The first function will be our mint() function. For our situation, this function won’t actually hold any of the minting logic from the . Instead, its entire purpose is to make the request for a random number from the Supra VRF and map the returned nonce to the requesting user's address.

As per the Supra VRF docs and interface, we initiate the request by pairing the address stored within supraAddr and our declared to call the generateRequest() function of the Supra Router contract. Our request must consist of our callback function’s signature (_functionSig), the amount of random numbers that are requested (_rngCount), the number of confirmations needed before the Supra VRF can generate a number (_numConfirmations Minimum of 1 - Maximum of 20) , and an optional client-seed parameter that provides additional unpredictability (_clientSeed). An additional parameter (_clientWalletAddress) is used in dVRF v2 whitelisting. This is the client wallet address that is responsible for the whitelisting of your contract and will be used to deduct the appropriate funds from the deposit contract. Please refer to the dVRF developer guide v2 for more information on this step. We’ll then map the returned nonce value to the msg.sender value, as we previously discussed.

Now, it's important to note that our token doesn’t actually exist yet, as we haven’t called the function belonging to .

The function accepts two parameters. The first parameter is the address of the user to whom the token belongs/should be minted for. If you recall, we stored this address within the nonceToMinterAddress mapping.

The second is the tokenId that is next in line to be minted. Luckily, our trusty has been keeping track for us. We’ll grab that value and then it to keep track of the token IDs that have been minted.

This function will increase the level of the passed token ID by 1, health by 10, and strength by 10. In our example, the only condition we have added is that the token Id must exist. We do this by using the function included within the . &#xNAN;Note: It would be best practice to apply some form of to restrict who can call this function. As there is no added to this function, any user could call this function to increase their token stats as much as they desired.

Supra Deposit Contract
version of your destination chain is V2
inheriting
OpenZeppelin's ERC-721
OpenZeppelin’s Counters
OpenZeppelin’s ERC-721
inherit
constructor of the ERC-721
ERC-721 Non-Fungible Token Standard
OpenZeppelin’s Counters
OpenZeppelin’s Counters
Using For directive
ISupraRouter
interface
contract address of the SupraRouter contract
docs
dVRF litepaper
struct
mapping
struct
mapping
ERC-721 Non-Fungible Token Standard
struct
global variable
interface
mapping
ERC-721 standard
interface
_safeMint()
ERC-721
_safeMint(address _to, uint256 tokenId)
counter
increment
_exists(uint tokenId)
ERC-721 contract
access control
access control