Understanding the Meebits Exploit

Hi,

i’m trying to understand the Meebits exploit from a few days ago.

The problem was, that the metadata was available before minting, and the attacker was able to “reroll” cryptopunk community mints until he got a tokenId he wanted.

In my understanding, this “reroll” is just cancelling his own transactions by sending a transaction with the same nonce + higher gas prices.

If that is correct, now my question: Couldn’t just anyone do that with any publicly mintable ERC721 collection? How is it special to the meebits cryptopunk community mint?

There are many NFT sales where the metadata api for the tokenIds is publicly accessible before/during sales and even where the tokenIds are not randomly minted but one by one (+1 increments).
Someone could send a bunch of transactions just to try to get a desirable token but cancel the transaction if the simulated transaction in mempool returns an undesirable token, right?

How can one make the minting process of their NFT secure so the above doesn’t work / doesn’t make sense?
Randomised tokenId assignment helps. What am i missing?

Thanks a lot for your insights on this!

3 Likes

Hi @jalil welcome to Open Zeppelin Forums!

Reading material for anyone interested on the hack: https://cointelegraph.com/news/85-million-meebits-nft-project-exploited-attacker-nabs-700-000-collectible

Couldn’t just anyone do that with any publicly mintable ERC721 collection? How is it special to the meebits cryptopunk community mint?

It's not "exactly". It's specific only to mintable ERC721s that rely on randomness to generate which metadata they get. Most NFTs don't rely on random things.

A similar exploit might be done for God's Unchained. I'd be interested to see how they handle an exploit of this nature, if they have one at all.

Let's say for example I have an NFT creator that uses Chainlink VRF.

Edit: I was wrong about Chainlink VRF - it can help solve these exploits, check Patrick's response below.

NOTE: I could be wrong about how Chainlink VRF works, but I think anyone can see its result after processing the VRF function.

I'm pinging @PatrickAlphaC to make sure I don't misspeak about Chainlink VRF. And maybe he can shed some light on how these exploits can be prevented.

I make a bot that reads my rolls for my NFT metadata, and sees my VRF result in the transaction. My bot sees that this VRF roll, albeit truly random, is not what I want.

So my bot sends a transaction to cancel the minting and I can keep rerolling.

Someone could send a bunch of transactions just to try to get a desirable token but cancel the transaction if the simulated transaction in mempool returns an undesirable token, right?

Yes. That is correct.

How can one make the minting process of their NFT secure so the above doesn’t work / doesn’t make sense?

This is a complex question and a philosophical one.

Before we get into the weeds, it's important to understand that the exploiter was burning gas at a rate of ~$20,000 an hour in gas fees This means that it only really works for expensive NFTs. What that really means, is that for God's Unchained, it would be pointless because their NFTs there don't cost thousands upon thousands of dollars so the exploiter would be losing his profit motive.

So a simple solution to this is to just not have your NFTs be worth the cost of gas required to exploit the results.

This isn't really a "true" solution, but if you're making a game, then make your game relatively inexpensive so that you aren't exploited in this manner.

Another way to do this would be to have a blacklist. As the NFT factory owner, check a wallet's failed transactions. If he's failing transactions on purpose, then blacklist him from using your NFT factory. This is somewhat problematic because now someone has the power to stop a specific wallet from making NFTs. It also doesn't stop the exploiter from doing the same thing with other wallets. It also costs you gas to add into a blacklist.

So this isn't really a good solution either, but I think it's a great control mechanism if you want to be more in control of your NFT factory.

Another way is to have an auxiliary controller program, such as a server running a database. If you roll an NFT with metadata - then the server updates that your wallet owns a specific NFT with the metadata. If the exploiter tries to cancel transactions to get a better roll, the server will know, and will invalidate him out of the database. So your front end program will "know" that his NFT isn't correct and that he exploited it.

This isn't really a solution either because it's relying on something outside the blockchain and not a decentralized solution. But if it's for a game, it might be usable.

A true solution might be an NFT validator contract. Somehow this validator could contain the rolls that "should" happen. I think this would require a lot of brain power, but it might be one of the ways that can work.

3 Likes

Thanks for tagging me.

Hopefully, I can shed some light here on the Meebits exploit, as randomness is a crucial piece of our ecosystem, and has received a lot of abuse with exploits just like this. Chainlink VRF doesn’t work in the way you described, so I’ll highlight the differences there, and explain why the Chainlink VRF was created for exactly these situations. Please note that I’ve only taken a shallow dive into the Meebits contracts, but I think it’s given me more than enough information.

Problem

After looking at their contract, we can see this line:

uint index = uint(keccak256(abi.encodePacked(nonce, msg.sender, block.difficulty, block.timestamp))) % totalSize;

This is in their randomIndex function and is how they generate the random Meebits. We can look at the minting function, and see that’s exactly what they are using to generate the id.

    function _mint(address _to, uint createdVia) internal returns (uint) {
        require(_to != address(0), "Cannot mint to 0x0.");
        require(numTokens < TOKEN_LIMIT, "Token limit reached.");
        uint id = randomIndex();

        numTokens = numTokens + 1;
        _addNFToken(_to, id);

        emit Mint(id, _to, createdVia);
        emit Transfer(address(0), _to, id);
        return id;
    }

There are a few issues with doing this:

  1. As we saw, you can actually just keep “rerolling” with canceled transactions until you get a meebit that you like. This is really easy for anyone to do.
  2. Using hashed things like block.difficulty (or really, anything else on-chain) as RNG actually gives the miners massive influence of the number, as they can do a similar “rerolling” strategy themselves by throwing out winning blocks that would result in them not getting the NFT if the so desired.
  3. Using things like block.timestamp provides 0 randomness, since the timestamp is predictable by anyone.

This has been pretty public knowledge for sometime, and most protocols that use randomness that have some sort of high-worth protocol, need a solution that doesn’t involve these 2 glaring issues.

So what can we do? As @Yoshiko mentions, we could 100% make sure all our collectibles are either:

  1. Not random
  2. Not high value

This doesn’t seem like a proper long-term solution for the industry as a whole, as we are striving for a world with superior digital agreements. A world without randomness would be a major hurdle for adoption, and artificially trying to deflate the value of high-worth assets seems difficult as well.

Solution

The solution here, is we need a way to create randomness that is verifiable and tamper-proof from miners and rerollers. We also have to do this using an oracle. Actual randomness in deterministic systems like a blockchain is nearly impossible without one.

Chainlink VRF is this exact solution. It looks off-chain for a random number, and is checked for it’s randomness on-chain by what’s called the VRF Coordinator. It works like so:

  1. A user requests a random number by calling some function inputting their own RNG seed. This function emits a log that an off-chain chainlink node reads.
  2. The off-chain oracle reads this log, and creates a random number based on the nodes keyhash and the users seed. It then returns the number in a second transaction back on-chain, going through the VRF Coordinator which verifies that the number is actually random.

How does this solve the 2 issues above?

You can’t reroll attack

Since this process is in 2 transactions, and the 2nd transaction is where the random number is created, you can’t see the random number and cancel your transaction.

The miners don’t have influence

Since you’re not using values that the miners have control over like block.difficulty or values that are predictable like block.timestamp, the miners can’t control the random number.

You can read more about it here and see the math and science behind the world-class group of researchers who developed it.

disclaimer: I work on the Chainlink project

3 Likes

Thank you both for these super insightful responses! Definitely sorted some stuff out for me.

And thanks Patrick - i will read up more on chainlink. The solution with the separate transaction makes complete sense :pray:.

2 Likes

I have been studying approaches for random allocation of NFT minting. Depending on the value of your assets and your minting timeframe, there are still reasonable approaches for getting entropy directly from the chain in a decentralized way to pick an NFT.

2 Likes

Yes i agree - Nietzsche (the exploiter) didn’t exploit the randomness in the Meebits contract but the fact that you can basically simulate the tokenId you’d get while transaction is in mempool and then cancel the transaction if you get one you don’t like (he knew which tokenIds had rare attributes from the IPFS leak).

All this only made sense because of the option to trade one in for free with a Cryptopunk, only paying gas. And for most other projects that wouldn’t make sense as the NFTs are not as valuable as the Meebits.

1 Like

@PatrickAlphaC’s approach with a second transaction would prevent the exploit for this case (until someone would crack the randomness factor of a contract like Meebits, which i personally think still is a very very hard thing to do).

But just having a separate ‘Minting’ contract, which then calls the the ERC721 contract (no public minting of the ERC721) upon a successful transaction would have prevented this particular exploit. Me thinks :see_no_evil:.

1 Like

It’s actually not hard at all for miners to do.

“Cracking the randomness” is as simple as tossing out blocks that don’t have a random value that you want. If a miner were to mine the winning block, but notice that the meebit that they get isn’t what they want, they could simply throw it out and try again. This removes all fairness and randomness from a system.

If a group of miners were to “farm” in this sense for the winning meebit, then it would essentially remove any randomness and fairness from the contract. Even 1 miner doing this technically makes the random number not actually random, but heavily influenced by the miners.

They do have to forgo the rewards since they would be not publishing a block, but especially in this case where the meebit was many times over worth much more than their block reward, miners are actually heavily incentivized to farm for the meebit that gives them the most value instead of being honest.

It’s not a question of how easy it is for miners, it’s pretty trivial for a miner to influence the fairness and randomness of using this method. The real question is whether or not they are economically incentivized to. And in this case, they are incentivized to act unfairly.

This is why we can’t rely on randomness in a deterministic system. Even with 2 transactions, the miners can still pick (or heavily influence) the winners based on selfish values. We need to look outside the blockchain to achieve true randomness.

2 Likes