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.
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.
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);
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()));
}
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;
}
}
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 @maxareo 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.
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?
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.
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.
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
I came here from a google search wondering the same thing as Cainuriel but then had the question, if the references are just to folders, what prevents the original owners from changing the files to point to some other images? The answer (after a little experimentation) is that if the files within that "folder" change, then the name of the folder changes too. It's all based on content addressing so, for example, that address QmaX1NnD73PByYd7TH64HQEJ4UKrD1qZJwHMhg2dXQX1JL/1 will always point to the same content. If the file 1 was changed from 1111 to 1112 then the folder name would also change.
Exactly. Nothing prevents the metadata file from being changed. In fact, it can happen on IPFS servers that get wiped out for not paying for hosting. Since custodians delete old files that are not paid.
As in a BEP20 contract, the owner's privileges in the BEP721 contract will determine the honesty of the project. If the contract can alter the tokenURI, nothing guarantees the buyer that their item cannot be changed. We have not eliminated mistrust between the parties. There are no guarantees that the buyer will dispose of his work under any circumstances. And I think this distorts the nature of smart contracts. Audits will always be necessary to estimate the value of one NFT over another.