Dissecting Courtyard NFT
Gotta Catch 'Em All!
A majority of the NFT projects out there follow the pattern of pre-reveal → revealed through the update of the contract’s base token URI. What this means is the whole collection has to be revealed at once. What if I am not buying Interesting Kangaroo Flying Car Club, but something like this…?
People trade cards, both revealed and sealed in a pack. Some buyers like to buy a sealed pack to get that adrenaline rush from opening the pack. Some sellers are afraid that the cards inside the sealed pack are just not so rare ones, lowering its cash value. Watch this video from Vice to see how people trade these cards. It’s pretty interesting.
Courtyard recently did a Pokemon card pack drop and it gives owners the option to reveal what’s inside. How is it able to do it?
The basic idea is this: Courtyard is able to control the data returned for any token IDs via a centralized server, without compromising immutability. I bet you think this sounds counterintuitive right now, centralized and immutable?
This is a sample metadata that I downloaded from one of the sealed packs.
Something immediately caught my eyes. There is a field called
proof_of_integrity, which is a hash. We can probably guess that the NFT’s data is used to generate this hash. The first 4 and last characters of the hash is also included in the token’s
Now let’s look at the metadata of a revealed Pokemon card.
As expected, we are able to see a
fingerprint that is used to generate the
hex_proof. But what is the
The token’s fingerprint is “Graded Pokemon TCG | PSA 55990818 | 1996 Pokemon Japanese Base Set Holo Gyarados #130 | PSA 9 MINT”.
TCG = Trading Card Game
PSA = Professional Sports Authenticator, a trading card authentication and grading company
Gyarados = The Pokemon
The data included in this fingerprint is predictable, I can probably brute force the hashes by randomizing the year manufactured, the Pokemon’s name and the card’s language?
Not so soon anon…if you have brought any experience from coding in web2, you will immediately realize the
salt is used to prevent a rainbow table attack.
What is a rainbow table attack?
In a web application, passwords are (supposedly) never stored in plain-texts. They are stored in a one-way hash so not even the database administrator has access. However, hackers can curate a long list of passwords and create a key value mapping of passwords and their respective cryptographic hashes.
A rainbow table is a precomputed table for caching the output of cryptographic hash functions, usually for cracking password hashes.1
What if the database access is compromised and the hacker has all the hashed passwords that can be compared using the rainbow table? Then the passwords are practically revealed.
So, the purpose of having a salt (a random number) is to combine it with the password before hashing it, so that the same password will turn into a different hash in different databases, rendering the rainbow table useless.
(Now back to Courtyard)
Proof of integrity
Okay, now that we have the
salt and the
hex_proof, where do we prove the metadata has not been tampered with by Courtyard’s malicious developers??? In the
proof_of_integrity JSON, there is a field called
proof_contract. Looks promising, let’s try that.
In the ProofOfIntegrity contract, there is a function
generateProof and another function
verifyProof. Let’s input the data from the Gyarados metadata and see if the proof is valid.
The proof is valid! :-)
The contract is very simple, it just checks the
keccak256 hash of the concatenation of the fingerprint and the salt is equal to the proof.
From hex proof to token ID
Another interesting design is that all Courtyard’s current and future NFTs live in the same smart contract called the CourtyardRegistry. Unlike most generic NFT collections, Courtyard’s token IDs are not in sequence from 1 to 10,000. The token ID is actually the
bytes32 hex proof converted into an
uint256! This is nothing short of genius.
No matter how many NFTs Courtyard will launch in the future, there will practically never be a hash/token ID collision,
2 ** 256 - 1 is simply astronomical.
Now, where does the proof of integrity come from when a user mints the Pokemon?
What happens during the drop
CourtyardRegistry contract is access controlled and only allows addresses with the minter role to mint NFTs.
It trusts whichever minter to provide the correct proof and does no further validations before minting the token.
In Courtyard’s Back To School Pokemon drop, the privileged minter is this contract. It inherits from 2 contracts called the
TokenMintingPool. The contract owner is able to add precomputed token hashes to an array called
_availableTokenHashes, which is later used to take the next available token hashes to be minted.
Before we move on to the actual mint function, we need to take a small detour to look at how randomness is done in Courtyard.
Just by looking at the contract it inherits from and the use of these 3 private variables, it is obvious that Courtyard uses Chainlink’s Verifiable Random Function to generate a random seed to be used for the mint. The process of requesting a Chainlink random seed is as follows:
Make sure the contract has enough LINK to pay for VRF and then request for randomness. The VRF key hash is provided by the contract deployer.
Randomness cannot be generated on-chain within the same transaction. It must be generated off-chain and is returned in a separate transaction.
With the random seed being set, based on each minter’s mint amount, the contract will pick available token hashes using the remainder of the random seed divided by the remaining token supply. Isn’t this quite slick!
I don’t see any functions on-chain and I believe it happens off-chain through an API call.
Courtyard’s protocol is impressive because it is able to prove metadata immutability while having a centralized metadata API. That said, the base token URI is still a centralized API and there is a risk of server outage.
Also, I am curious to why Courtyard decided not to use a merkle root and instead whitelists addresses one by one.
It looks like the team thinks that by not saving the addresses in plain-texts, user privacy can be protected before the presale. (Using a merkle tree will normally expose the whitelist in the frontend, as the frontend needs the list to generate merkle proofs. But maybe they can also wait until the very last moment to publish the list?)
There is also an incident I noticed on Discord where someone made a bid to a sealed pack on OpenSea, then the buyer opened his pack, saw that it was not Charizard, and accepted the bid.
If the token has an on-chain flag that tells the bidder and the marketplace whether the pack is revealed, the marketplace’s order matching smart contract might be able to take it into consideration to prevent the malicious act above from happening.