Hi,
There is a pattern in many PFP projects where the tokenURI does something like:
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
Where the baseURI
is some IPFS directory.
This is the standard implementation from OpenZeppelin v4 ERC721.sol
and it's fine.
However, relying on this implementation along with random startingIndex
generation and (inevitably) a setBaseURI
method - negates the entire point of a fair and random distribution:
Since the startingIndex
is not known beforehand, it is not possible to generate the mapping between the tokenId
and the initialSequenceId
.
This leads the devs to upload the metadata only after the startingIndex
has been set.
The mapping formula for reference :
F: {0...MAX_TOKENS-1} -> {0...MAX_TOKENS-1}
F(tokenId) = initialSequenceId = (tokenId + startingIndex) % MAX_TOKENS
(depends on the constant startingIndex
)
This presents us with some problems:
Primo, the devs and team are free to determine (aka Man-in-the-Middle) the order and allocation of the metadata (items in the collection) - the so-called "Original"/"Initial" sequence ID is meaningless.
Secundo, the metadata of the entire collection is not known before the sale ends which usually means the art itself will not be visible, unless otherwise exposed by the team - which requires additional and unnecessary efforts (see solution below). Take note that it is OK to expose the collection before and during sale - it only becomes a "metadata-leak" if the order in relation to the tokenId (aka mapping) is known as well.
Tertio, the devs are forced to implement a setBaseURI
method - which allows them to defacto change the allocation of items in the collection - unless a locking mechanism is implemented.
Proposed Solution:
Follow these steps:
-
Generate the metadata (and art if this is a generative art project).
-
Establish the collection order using a computer RNG and enumerate the metadata (and art) files from
0
toMAX_TOKENS - 1
. If you want to do a late reveal and display a mystery image, create an additional metadata file named-1
which will point to an IPFS link to a mystery image/video/file. -
Create the
provenanceHash
(sha256 string of concatenation of sha256 strings of ordered art files/images). -
Upload the art directory to IPFS and update the metadata files to point to the correct IPFS directory link (or Arweave path manifest, whatever you use).
-
Upload the metadata directory to IPFS.
-
Deploy your ERC721 contract with the
baseURI
variable set to the IPFS dir of the metadata
and theprovenanceHash
variable set to the calculated sha256 string (you can use aconst
variable in Solidity or pass a constructor argument).
In your contract, implement tokenURI
as follows:
function tokenURI(uint256 tokenId) public view virtual override(ERC721) returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
string memory baseURI = _baseURI();
string memory sequenceId;
if (startingIndex > 0) {
sequenceId = ( (tokenId + startingIndex) % MAX_TOKENS ).toString();
} else {
sequenceId = "-1";
}
return string(abi.encodePacked(baseURI, sequenceId));
}
Make sure your startingIndex
cannot be set to 0!
In this manner, the original sequence order as well as the provenance hash are well-known at contract deployment time (enforced by IPFS/Arweave immutability), they have actual meaning and there is no need to implement setBaseURI
(although you could if you want).
Or am I wrong?