Case Study: Exploiting Irregular NFT Distributions — aka How We Could Have Won CryptoShack

CryptoSec_Group
7 min readMar 7, 2022

--

That’s right, we finally have a security write-up to share that DOES NOT have to do with an incident happening in real-time.

This post also serves as our first (slightly late, thanks again) submission to the HonestNFT Bounty Program.

Exploiting Irregular NFT Distributions - Introduction

We want to cover an irregular minting and distribution pattern that we saw recently, as well as a few ways that it could have been (and was) exploited.

Initially, I wanted to cover a case study using some of the HonestNFT Shenaningans tools, but hopefully, this is FAR more interesting.

We will be looking at CryptoShack, a p2e game based on golf in NFT Worlds.

CryptoShack - The Contract(s)

Like I normally do with most collections that I haven’t seen before, I started to take a look at the CryptoShack contracts. Note: I wanted to use the Rinkeby link, but it seems to be different than it was during my testing.

This was a pretty standard ERC721A mint, but there was still a bit of fun to be had in the main contract.

cryptoshack.sol source code

Irregular NFT Distributions - The Vulnerability

For those of you that are familiar with reading contracts, have spoken to me, or looked a bit further into the collection, you might already know what the vulnerability is. If not, let’s go through it!

I’ll try to keep this as accessible as possible, but we are going to delve a bit into Solidity code. If you’re interested in learning more then stay tuned for future CSG releases!

While there is plenty of interesting code, the most important thing right now is the tokenURI() method. This is how a user (or OpenSea) can look up the NFT’s metadata (including image) using the token ID number.

The most interesting part of this section is the second return statement. If you are familiar with math (especially integer division used by many programming languages), then you might already see the issue.

Before I explain the exact issue, I also wanted to include the typesToNames[] array, as that will be relevant as well.

So, if you haven’t realized it yet, it’s possible to determine the NFT type (aka the ‘rarity’ in this collection) based ONLY on the tokenID, whether or not the set has been revealed yet!

The reason this happens (and was designed this way), is that computers round down by default. So, if you have a token ID number of 8,755, if you divide it by 10,000, you will get a 0.

This means that the following tokenID ranges line up perfectly with the following NFT types (you’ll be able to verify after they reveal)!

How these are minted is up to the mintPublic() method (this is where you mint your NFT) which calls mintRandomInternal().

There’s plenty of stuff in mintRandomInternal() (including a problem with randomness on the blockchain, but that’s a different post). That said, the most important section is this one right here.

So, unlike most NFT collections, this mint method mints a TYPE, not a tokenID. The _mintInternal() method is what converts this type into a tokenID, which is how those tokenID ranges can be guaranteed.

This means that it is 100% guaranteed to know the rarity of your NFT pre-reveal, which leads to the distribution concerns AND possible exploits!

CryptoShack Exploit #1 - Sniping

Now that we know which IDs are the rarest (in this case, the Golden Gophers, so 10,000 to 19,999), we can snipe them off of the secondary market!

This started happening SHORTLY after my discoveries, so I was far from the only one who knew what was going on.

Here is just one example, ID #10,001.

As you can see, there are plenty of offers WELL over the floor that are days old, even though the collection hasn’t been revealed yet.

CryptoShack #10001 with high offers

This is the most straightforward example of what to do with metadata (or in this case, rarity/ID) leaks, so there isn’t that much more to cover.

That said, if you see a collection where the IDs are strange or multiple offers are coming in higher than the floor, then it might be worth checking out.

But, what if I told you that it was possible to get EVEN MORE profit from this vulnerability?

Exploiting Irregular NFT Distributions - The Mint

Being the high-brow hackers that we are, what if we are not satisfied from just sniping pre-reveals off of the secondary? In that case, we can take the exploit straight to the source, which is the mint!

If it was possible to mint a specific ID (or in this case, in a specific range), then we could guarantee that we minted a rare NFT every single time. Unfortunately, this isn’t a collection where you get to specify your ID during the mint. That said, we could just keep minting NFTs and hold on to any that don’t have an ID in our desired range. We could even write an exploit contract that outright rejects NFTs that don’t fit into our range, so we don’t have to bother with selling them, etc.

Well, that’s just what I did! I’m not going to share the entire exploit code at the moment since we’ll use it for future tutorials, but I’ll cover the most important parts here.

First, we have a very basic Solidity method that just creates an interface to the CryptoShack contract and calls the mintPublic() method to mint us an NFT.

This looks pretty simple, and there are only really two lines in the mintShack() method. The rest is all creating an interface to the contract or a contract in general.

In addition to that, we can add an onERC721Received() implementation to our contract. This method tells a contract what to do when it receives an ERC721 token (aka our NFT).

This is also fairly straightforward, and the required method just reverts a transaction if it doesn’t meet the requirements. In this case, the transfer will get canceled if the tokenID isn’t in our Golden Gopher range. AND, since that call will occur during our call to mintShack(), then the entire transaction will get reverted!

This means that, for just the cost of gas each time, we can guarantee that we only mint golden gophers. This would be like if you knew WHICH packs of Pokemon cards had a holographic Charizard AND you could pay a small fee to the guy stocking the store to keep pulling out new packs off the truck! At that point, why wouldn’t you?

Well, unfortunately, that’s a little on the expensive side, plus we’re still fighting RNG and a dropping floor.

Enter Flashbots

If you are not familiar with Flashbots, they are, “a research and development organization working on mitigating the negative externalities of Maximal Extractable Value (MEV) extraction techniques and avoiding the existential risks MEV could cause to state-rich blockchains like Ethereum.”

What this means to us (for now) is that it’s possible to submit transactions directly to miners without them having to enter the mempool first.

Flashbots

Flashbots ALSO allows you to bundle transactions, so your bundle will only succeed if all the transactions succeed.

This means that we can call our mintShack() function, which calls our onERC71Received() function. If any of those calls fail (aka we don’t get a Golden Gopher), then the transaction is never sent to the blockchain. Now we can attempt to mint Golden Gophers for FREE, without even having to pay gas.

Unfortunately, the mainnet contract differed from the Rinkeby contract in one KEY way, which at first made this seem hopeless.

As you can see, the mainnet contract does not allow the mint method to be called by another contract.

While this seemed hopeless at first, here was another not-so-obvious workaround.

If we know that the current max Golden Gopher is ID #10,012, then we know that the next one (by reading the mint methods above) will be ID #10,013.

In THAT case, instead of calling our exploit contract, we could attempt to bundle the following transactions using Flashbots:

  • mintPublic() for CryptoShack
  • transferFrom(attacker, anotherAttackerWallet, 10,013)

If any of these transactions fail (which they will if we don’t own ID #10,013 or it does not yet exist), then we are back to paying no gas for our exploit!

I’ll leave the actual implementation up to the reader, as I don’t want to give away all of the secret sauce yet.

Unfortunately, my Flashbots code (even the ones that won RNG) kept failing with a “BlockPassedWithoutInclusion” error and I didn’t care enough to keep fighting. Plus this is a simple implementation that requires the attacker to increment the next ID every time one is minted.

That said, let us know if you are more familiar with Flashbots and want to help us figure out what went wrong!

(Almost) Winning CryptoShack - Conclusion

While we were never able to mint “free” money from this collection, hopefully, I was able to demonstrate the dangers of ANY metadata being leaked, and how it can lead to an unfair distribution.

Let us know if you have any questions regarding this exploit or web3 security in general. Hopefully, this is accepted into the HonestNFT bounty program, but stay tuned for more content and exploits regardless!

--

--

CryptoSec_Group

Bringing security to the crypto and web3 masses. NFTs, education, tools, tutorials, and more!