How has the metadata been added to the TokenID in this NFT contract?

Hi everyone.

I am studying NFTs contracts and I am analyzing the famous BORED APE YACHT CLUB contract

Here is:

Well, I check that the OpenZeppellin standard _setTokenURI method is not used in any place of conctract, and that when the token is created I don't see a way to associate any info to it.

However, when we search for a token by its ID, an associated tokenURI appears.

How could they do it?

The function is overwritten but is not called anywhere.

  
    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
        require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
        _tokenURIs[tokenId] = _tokenURI;
    }

They set the baseURI in this transaction. All the tokens have URI sequentially w.r.t. tokenIds. That works out as new tokens are minted sequentially. in the safeMint function. The _setTokenURI function is used for more customized or unorganized URIs.

1 Like

Thank you very much for answering me

What is a w.r.t id?

The safeMInt function:

   function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual {
        _mint(to, tokenId);
        require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

leads us to ...

    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _holderTokens[to].add(tokenId);

        _tokenOwners.set(tokenId, to);

        emit Transfer(address(0), to, tokenId);
    }

_beforeTokenTransfer
It is overwritten but empty of code

save in the mapping of token holders _holderTokens[to].add(tokenId);

and it uses an Enumerable mapping for me I imagine being able to know how many tokens a holder has. _tokenOwners.set(tokenId, to);

But..
Where is the metadata associated?

1 Like

w.r.t.: with respect to

Token URIs do not need to be set up deliberately if they are in a sequential order. The following function returns the URI given tokenId.

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = baseURI();

        // If there is no base URI, return the token URI.
        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }
        // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
        return string(abi.encodePacked(base, tokenId.toString()));
    }
1 Like

Yes, I have no problems with this function, which is the one that returns the metadata of each id. My question is how this metadata is associated with each id. Because in the creation of the token no information is associated.

I have only found this function to associate the metadata to each id:

   /**
     * @dev Sets `_tokenURI` as the tokenURI of `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
        require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
        _tokenURIs[tokenId] = _tokenURI;
    }

But it is not overwritten or called anywhere in the contract.

When mints NFTs only created IDs without metadata associated:

 /**
    * Mints Token
    */
    function mintToken(uint numberOfTokens) public payable {
        require(saleIsActive, "Sale must be active to mint Token");
        require(numberOfTokens <= maxTokenPurchase, "Can only mint 20 tokens at a time");
        require(totalSupply().add(numberOfTokens) <= MAX_TOKEN, "Purchase would exceed max supply of Token");
        require(tokenPrice.mul(numberOfTokens) <= msg.value, "BNB value sent is not correct");
        
        for(uint i = 0; i < numberOfTokens; i++) {
            uint mintIndex = totalSupply();
            if (totalSupply() < MAX_TOKEN) {
                _safeMint(msg.sender, mintIndex);
            }
        }

        // If we haven't set the starting index and this is either 1) the last saleable token or 2) the first token to be sold after
        // the end of pre-sale, set the starting index block
        if (startingIndexBlock == 0 && (totalSupply() == MAX_TOKEN || block.timestamp >= REVEAL_TIMESTAMP)) {
            startingIndexBlock = block.number;
        } 
    }

2 Likes

Take a look at the last 2 lines of this code snippet

     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory _tokenURI = _tokenURIs[tokenId];
        string memory base = baseURI();

        // If there is no base URI, return the token URI.
        if (bytes(base).length == 0) {
            return _tokenURI;
        }
        // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
        if (bytes(_tokenURI).length > 0) {
            return string(abi.encodePacked(base, _tokenURI));
        }
        // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
        return string(abi.encodePacked(base, tokenId.toString()));
    }

What @maxaero is saying is you don't need to explicitly call _setTokenURI() for each token created because the baseURI has already been set. Since the 2 if conditions are not triggered, when you call tokenURI function, it will return the output from evaluating return string(abi.encodePacked(base, tokenId.toString()));.

If you look at the contract, calling baseURI gets you ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ so by concatenating the tokenID, you can get the metadata stored at this link. For example, here is an example of me getting the metadata for token ID 1 on brave browser.

1 Like

Wow!, what a cool way to save storage. Thank you.

However, I have some doubts. The baseURI appears to be decentralized, but the concatenation is not. How do they do that? Is it an own decentralized server? How do we relate each id to a descentralized CID? It is not necessary to put the file extension?

1 Like

What do you mean by the concatenation is not decentralised?

I would beg to differ because the ID of the token is the only factor that determines which metadata goes to which token and this ID can only be incremented sequentially on the smart contract via 1 function call (mint). There are no callable functions in the smart contract that lets an owner or a privileged user modify the URI of each token so from this pov, it's def. decentralised.

1 Like

My experience with IPFS files is limited. And until now it was checking how a hash of the URItoken was generated. As for example Pinata works:

The CID is a hash that I imagine appears as a virtual stamp of its immutability. On the other hand, with a link type "2" or "3", I do not understand how a decentralized server processes it. However, to point the detail commented that if there is no possibility of changing the URI of a token we can determine that it is immutable. But this does not explain that it has a decentralized base and a decimal URItoken.

I want to thank you again for the possibility of this conversation. Thank you.

2 Likes

On the other hand, with a link type "2" or "3", I do not understand how a decentralized server processes it.

Ah! Okay I think I understand what you're trying to figure out. You're wondering how BAYC is able to use numbers instead of the CID to represent the location of the metadata on IPFS.

Here's an example... I uploaded a folder with 5 files in it. Each file has its own CID but what I did was to add a key value pair (you can do this via the CLI as well) so when you try to do

ipfs://QmaX1NnD73PByYd7TH64HQEJ4UKrD1qZJwHMhg2dXQX1JL/1, it is the same as doing ipfs://QmeNMzu557q14Jf5MhmfBBxALpVGdGp68qJ5fXkauoMWAU

Ok!!

However we must have a decentralized domain for this. Without hiring him, serialization would be impossible. not?