Solidity – Best Way to Set Metadata in ERC721 Contract on Production

erc-721nftsolidity

What is the best method to set the metadata for each token in a ERC721 contract? Please take a PRODUCTION ENVIRONMENT in mind while answering the question. How do the industry giants achieve it?

Case 1: Hardcoding the IPFS URLs in the contract itself before minting begins.

This seems to be the easiest way. I can upload the metadata of each token to IPFS (using Pinata or something else) and get the URLs which I can then hardcode as a mapping in the contract itself. Then, I can internally call _setTokenURI() method to set the metadata of the token being minted in my mintNFT() function.

mapping(uint => string) public ipfsURLs;  // Hardcode URLs here
function mintNFT() public payable 
    {
        _tokenIds.increment();  // Counter
        uint256 newItemId = _tokenIds.current();
        _mint(msg.sender, newItemId);
        string memory tokenURI = ipfsURLs[newItemId];
        _setTokenURI(newItemId, tokenURI);
    }

Pros: I won't have to pay the gas fees while setting the metadata as it is set in the mintNFT() function, i.e. the gas is deducted from the wallet of the user who is minting the token.
Cons: The problem with this approach is that all the tokenURIs will be visible to everyone who reads the contract and people might take unfair advantage of that.

Case 2: Setting the metadata as the minting is going on

In the mintNFT() function, I can emit an event with tokenId as the parameter. Then, in my JS code, I can watch for the event and call a function that sets the tokenURI of the the minted token, which I'll get from my database.

event MintComplete(uint tokenId, address owner);
function mintNFT() public payable
    {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(msg.sender, newItemId);
        emit MintComplete(newItemId, msg.sender);
    }
function setURI(uint256 id, string memory ipfsURI)
        public onlyOwner // called from a script when above event emitted 
    {
        _setTokenURI(id, ipfsURI);
    }

Pros: The metadata IPFS URLs are not visible in the contract before the token is minted. This will act as a good security measure in maintaining randomness of minting.
Cons: User will pay the gas fees for mintNFT() method, and the contract owner will have to pay the gas fees for calling setURI() method. More the number of tokens (~1000s), more the contract owner will have to pay in Gas fees.

Case 3: Setting the metadata after the minting process is complete

After the minting process is over and all the tokens have been minted, I can send the mapping of tokenId to IPFS URLs to a contract function in a single transaction (or in batches of say 100 URLs per transaction).

function setURI(uint256 startIndex, string[] memory listOfIpfsURI)
            public onlyOwner // called after batch minting over
    {
           for (index = startIndex; index < listOfIpfsURI.length; index++) {
               _setTokenURI(index, listOfIpfsURI[index-startIndex]);
           }
    }

Pros: Same as Case 2.
Cons: Same as Case 2, but Gas fees is lower due to lower number of calls to setURI(). Also, people might have to wait for the reveal till the batch size is complete, which can take a lot of time in some cases.

Case 4: Any other method …

In case there exists a better method which I haven't listed above, please include it with an example code if possible. Also, if anyone has any idea how the big NFT collections (BoredApes etc.) do it, please shed some light on that as well.

Best Answer

There's no need to set individual tokenURI. ERC721 contract provides a method called _setBaseURI(baseURI_).

Internal function to set the base URI for all token IDs. It is automatically added as a prefix to the value returned in tokenURI, or to the token ID if tokenURI is empty.

So basically you can follow the following steps (works on production):

  • Upload all your NFT metadata as a Folder to IPFS (e.g. Pinata Cloud). Then the folder URL will act as your BaseURI.
  • Implement a method in your contract which will take the BaseURI as a string param and call _setBaseURI(baseURI_) internally. This function should ideally use onlyOwner modifier.
  • When you are about to start the sale (before any minting has happened), call the function from Javascript(Web3.js) and pass the url of the folder (Caution: Don't forget to append '/' after the IPFS folder path otherwise the URL formation will be wrong).

Expected tokenURIs for tokenIds starting from 1:
1: https://gateway.pinata.cloud/ipfs/QmZzLcYPYE3unDQ7x9PFtrfoWuDo/1
2: https://gateway.pinata.cloud/ipfs/QmZzLcYPYE3unDQ7x9PFtrfoWuDo/2
and so on...

Related Topic