ERC721R: A new ERC721 contract for random minting so people don’t snipe all the rares!

ERC721R

ERC721R代币标准正式发布,它在NFT智能合约中增加了去信任的退款设计,允许铸造者在给定的期限内退还按成本铸造的NFT,并获得相应退款。

Or: how to snipe all the rares if ERC721R isn’t being used!

TLDR: Here’s a link to the ERC721R repository.

Introduction: Blessed and Lucky

Mphers was the first mfers derivative and, because it is also a Phunks derivative, I was quite interested in owning one.

Specifically, I wanted an alien. They look the coolest and there are only 8 in the 6,969 collection. And I got one!

Though it might not have been clear from the Tweet, what I meant was that I was lucky to have figured out how to 100% guarantee I would get an alien without needing any additional luck.

Read on for how I did it, how you can do it too, and, if you’re a dev, how you can prevent it from happening!

How to mint rare NFTs without needing luck

The key to minting a rare NFT is knowing the id of every rare token in advance.

For example, once I knew my alien was #4002, all I had to do was refresh the mint page until I saw #3992 had been minted and then immediately mint 10 mphers.

How did I know #4002 was an alien? Let’s retrace my steps.

First, go to the Etherscan page for the mpher contract and look up the tokenURI of a token that has already been minted, token #1:

The template for mpher metadata

As you can see, mphers, like many contracts, constructs metadata URIs by combining the token id with an IPFS hash.

The benefit of this approach is that it gives you the provenance of the entire collection in every URI and, while that URI can be changed, doing so affects everyone and is public.

By contrast, imagine if the token URI contained no provenance hash, for example https://mphers.art/api?tokenId=1. As a collector you could never be certain that the devs weren’t silently changing #1’s metadata whenever they wanted.

However, if you have an API, you can say “if #4002 has not been minted, do not show any information about it” and you can’t do this if you go the IPFS route.

Once the metadata has been revealed (which in mpfers case was instantly), you can look up the metadata of any token, whether or not it has been minted.

Just replace the trailing “1” in the URI above with the id you want.

These metadata files give us all the attributes of the mpher with the specified id. For example, in the case of my alien:

Mpher #4002

To find the aliens, we just need to search all the metadata files for the string “alien mpher.”

Next, download the 6,969 metadata files. Here I’m using OpenSea’s IPFS gateway but you can try ipfs.io or something else if that works better.

This snippet uses curl to download the files 10 at a time. Downloading thousands of files quickly can be finicky so you might end up with some duplicates or errors. But if you fiddle with it you should be able to get everything (and for our purposes dupes aren’t a problem).

Now that you have the files in one directory, just grep for the aliens:

The numbers that appear are the names of the files that contain “alien mpher” and therefore the ids of the aliens themselves.

The whole process takes less than 10 minutes. And you can use this technique on many NFTs that are minting right now.

In practice, it is not completely trivial to manually mint at the exact right moment to get the alien, especially when tokens are minting quickly. If you really want to “go big” with this approach, you should write a bot to poll totalSupply() every second and automatically submit the mint transaction at the exact right moment.

And if you want to go “absolutely huge,” you could look for the token you need to see in the mempool before it is even minted and get your mint into the same block!

However, in my experience, the “big” approach is enough to win 99% of the time—though, interestingly, not 100% of the time.

“Have I been getting played this entire time?”

Is one question you might be asking yourself if you’re just learning about this now.

The idea that you had zero chance at minting anything that someone using this technique also wanted is distressing.

But, did you have no chance? In a way, you had the same chance as everyone else!

Take me for example: I figured this out on my own using public information and I put it to work using free open-source tools. Anyone can do this, and in general, if you don’t investigate how a contract works before minting you are going to run into much worse issues than this.

The mpher mint was 100% fair.

Still, while it was a fair game, “snipe the alien” might not have been the game everyone wanted to play.

Instead, people might have had more fun overall playing the game of “mint lottery” where tokens were distributed by chance and it was impossible to gain an advantage over someone who was just clicking the “mint” button.

How might we do this?

Fair Random Minting

For Fashion Hat Punks, my goal was to create a random minting experience without sacrificing fairness. In my view, a predictable mint is far better than an unfair one. Above all else, participants must be kept on equal footing.

Unfortunately, the most common way to create a random experience—the so-called a post-mint “reveal”— is deeply unfair. It works like this:

  • Token metadata is inaccessible during the mint. Instead, tokenURI() points to the same blank JSON file for all ids.
  • Once all the tokens are minted, the contract owner updates the IPFS hash to the real metadata.
  • There is no way to verify how the contract owner chose which token ids got which metadata, and the results appears to be random.

Here, the person setting the metadata obviously has a tremendous unfair advantage over the people who are minting because they alone determine who gets what! Unlike the mpher mint, here is a situation where you actually have no chance to compete.

But what if it is a well-known, trusted, doxxed dev team with a long track record. Are reveals okay in this case?

No! No one can be trusted with this kind of power. Even if someone isn’t consciously trying to cheat, they bring unconscious biases to the table just like everyone else. Beyond this, they might simply make a mistake and not realize it until it was too late.

You should not trust yourself either. Imagine, you do a reveal, you feel pretty good you did it correctly (nothing is 100%!), and somehow you end up with the rarest NFT. Would that not feel a tiny bit weird? Are you sure you deserve it? Personally, as an NFT developer, I would not want to be in this situation.

The bottom line is this: reveals are bad*

*UNLESS: they are done trustlessly, meaning everyone can verify their fairness without having to trust the devs (which you should never do).

To achieve a trustless reveal, you need a way of proving that the reveal was fair—typically by having the reveal happen on-chain and be powered by randomness that is verifiably outside of anyone’s control (e.g., through Chainlink).

Tubby Cats did a great job on a reveal like this and I recommend you check out their contract and launch reflections. Their reveal was also cool in that it was progressive—you didn’t have to wait for the end of the mint to learn what you got.

The downside to trustlessness in general is that it is extremely difficult to get right—@DefiLlama had this to say in his launch reflections:

When writing the contract I made it as trustless as possible, removing as much trust as possible into the team.

The reason for it is that I believe it’s important for every participant to know the rules of the game and know that they won’t be changed from under them (everyone should have complete information to make decisions, if processes are changed in the middle that creates groups of people with privileged information), while trust minimization is important because that’s the whole raison d’etre for smart contracts (and it makes it impossible to hack even if the team is compromised). However, this was a huge mistake, since it greatly reduced our flexbility and the actions we could take to correct things that happened.

And @DefiLlama is a top-tier dev. If maximizing trustlessness gave him this many headaches, imagine what it will do to you!

Therefore, my recommendation is to use a worse solution that still suffices in 99% of cases and that is much easier to implement: random token assignments.

Enter ERC721R: A fully-compliant implementation of IERC721 that selects token ids pseudo-randomly

ERC721R implements the converse of a reveal: instead of minting token ids deterministically and assigning metadata randomly, we mint token ids randomly and assign metadata deterministically.

This allows us to reveal all metadata before minting while still minimizing snipe opportunities.

To use it, copy the contract into your project directory (sorry, no NPM package yet), import it, and use this code:

How does ERC721R work?

First, a disclaimer: unlike a trustless reveal, ERC721R is not truly random. In this sense it creates the same “game” we saw in the mpher situation where minters can use publicly-available information to compete to exploit the mint. However, in the case of ERC721R, the game is significantly more difficult.

In order to game ERC721R, you would need to be able to predict the value of a hash with these inputs:

For a normal person, this is impossible because it requires knowing the timestamp of the block of your mint and you do not have access to this information.

A miner who has control over when blocks mine (and therefore can influence the timestamp) can theoretically do this, but even then they must set the timestamp to a value in the future and whatever they’re doing depends on the hash of the previous block which expires in about 10 seconds when the next block is mined.

I believe this pseudo-randomness is “good enough,” but if there is big money on the line, it will be gamed with 100% certainty, so be careful! Of course the system it is replacing—predictable minting—will also be gamed.

The token id selection itself happens in a very clever implementation of the modern version of the Fisher–Yates shuffle algorithm that I copied from CryptoPhunksV2.

To understand it, first consider the naive solution: (the below assumes a 10,000 item collection)

  1. Create an array containing the numbers 0–9999.
  2. When you want to mint a token, randomly select an item from the array and use that value as your token id.
  3. Remove that value from the array and reduce its length by 1 so that every index in the shortened array corresponds to an available token id.

This works, but it costs too much gas because changing the length of an array and storing a huge array filled with non-zero values are costly.

How can we avoid both? What if we instead started with an array containing 10,000 zeros, which is cheap to create. Now let’s use each index in that array to represent an id.

Suppose we choose index #6500 randomly—#6500 would be our token id and we would indicate index #6500 was already used by (for example) replacing the 0 in index #6500 with a 1.

But what would happen if we chose #6500 again? We would observe a 1, indicating #6500 was taken, but then what? We cannot simply “roll again” as this would make gas unpredictable and high, especially for later mints.

The genius of modern Fisher-Yates is that it gives us mechanism for selecting an available token id 100% of the time without the cost of maintaining a separate list. Here’s how it works:

  1. Create an array containing 10,000 zeros.
  2. Initialize a uint numAvailableTokens with the value 10,000.
  3. Pick a random number between 0 and numAvailableTokens — 1
  4. Suppose you chose #6500—look at index #6500. If the value is 0, #6500 is your next token id. If the value is non-zero, the value at index #6500 is your next token id (starting to get weird!)
  5. Now, look at the last value in the array, which is the value at index numAvailableTokens — 1. If that value is 0, update the value at index #6500 to the last index in the array (#9999 if it’s the first token). If the last value in the array is not zero, update index #6500 to store this final non-zero value.
  6. Decrement numAvailableTokens by 1.
  7. Repeat 3–6 to get the next available token id.

And there you have it! The array stays the same size and yet we are able to reliably choose an available id. Here is the Solidity code:

Unfortunately, this algorithm still uses significantly more gas than the leading sequential mint solution, ERC721A.

This is most pronounced when minting multiple tokens in one transaction—e.g., a 10 token mint costs about 5x more on ERC721R than ERC721A. That said, ERC721A has been optimized much further than ERC721R so there is probably room for improvement.

Conclusion

Here are your options:

  • ERC721A: Minters pay lower gas but must spend time and energy devising and executing a competitive minting strategy or be comfortable with worse minting results.
  • ERC721R: Higher gas, but the easy minting strategy of just clicking the button is optimal in all but the most extreme cases. If miners game ERC721R it’s the worst of both worlds: higher gas and a ton of work to compete.
  • ERC721A + standard reveal: Low gas, but not verifiably fair. Please do not do this!
  • ERC721A + trustless reveal: The best solution if done correctly, highly-challenging for dev, potential for difficult-to-correct errors.

Did I miss an option?! Leave a comment or hit me up @dumbnamenumbers on Twitter.

If you want to learn more, head over to the GitHub repository and check out the code! Pull requests more than welcomed—I’m sure I’ve overlooked many opportunities for gas savings.

Reference

1.https://erc721r.org/

2.https://medium.com/@dumbnamenumbers/erc721r-a-new-erc721-contract-for-random-minting-so-people-dont-snipe-all-the-rares-68dd06611e5

Subscribe to Renaissance Labs
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.