cards-on-a-table

BadReveal – NFT Exploit

A New Family of Vulnerabilities

A new exploit found by Sayfer researchers and @rugpullfinder team enables attackers to know what is the rarest NFT before the reveal of the project. This allows an attacker an uneven advantage amongst investors to buy the rarest and most expensive piece. In some projects, the rarest can be priced 50X than the standard piece price.

The vulnerability occurs because of bad coding practices. We found the vulnerability in dozens of projects. Possibly existing in thousands that we haven’t manually tested yet.

There is no way to automatically test it so If you want to know if a project you want to invest in or already invested in, is vulnerable to BadReveal please tag us on Twitter @SayferSecurity with #BadReveal and our researchers will check it out.

Affecting Multiple Active Projects

To understand the magnitude of the problem we analyzed over 100 different projects, and we found that 12 were vulnerable to the attack. These are some of the vulnerable projects: 

CatBloxPUMACapsule (Exploit time: 2 days 18 hours, Market Cap: 628.19 ETH)

Everai Heroes: Duo (Exploit time: 4 days 2 hours, Market Cap: 1,905.37 ETH )

Degen Toonz (Exploit time: 3 days 13 hours, Market Cap: 6,221.6 ETH )

More details like specific transaction times are in the appendix.

Technical Details of the Vulnerability

Background 

As NFTs are ERC721 tokens, every token is unique with its own metadata. Usually, in an NFT project with a reveal step, tokens get minted blindly, and once everything has been minted, NFTs are revealed, and their metadata gets public. NFTs with rare metadata could value 50X from other NFTs in the projects. The rarest of them all is called the “Legend”.

But what happens if an attacker somehow manages to get the metadata before it was revealed and uses this data to buy the legend? The attacker gets an unfair benefit and gains more profit than all the other buyers by exploiting this data to purchase the legend.

What is a token URI?

The token URI of an NFT is a unique identifier of what the token “looks” like. A URI could be an HTTP address, an IPFS hash, or even a JSON blob that is kept on-chain. It points to a JSON file that contains the metadata of the token.

When creating your NFT project, you must ensure that token URIs are not accessible or guessable before the NFT reveal. Otherwise, this could potentially let attackers gain control of the most valuable NFTs in the project. Indeed, they could use these URIs to check for some rare properties in the metadata before anyone can see them.

The Vulnerability 

We found that many projects set the token URI in one transaction and then reveal it in a different transaction. In the time between those two transactions, which sometimes can be hours, an attacker can scan all NFTs in the project and find which one is the rarest, and then buy it based on its tokenID.

Exploit Example

Here we have an example of a function that returns the token URI if the reveal flag is true (it’s false by default) or a URI that points to a hidden image common for every token if the flag is false.

function tokenURI(uint256 tokenId) public view virtual override
       returns (string memory)
   {
       require(_exists(tokenId), "Nonexistent token");
       if (reveal == false) {
           return hideURI;
       }
       string memory URI = baseURI;
       uint256 randomId = ((randomNumber + tokenId) % supplyMax) + 1;
       return
           bytes(URI).length > 0
               ? string(abi.encodePacked(URI, randomId.toString(), ".json"))
               : "";
   }

The URI is created with four elements: baseURI, tokenId, supplyMax, and randomNumber. These four elements are public in our code, and we easily know the first three elements but we don’t know when randomNumber is initialized.

Now let’s see the function where reveal becomes true.

function revealNFT() external onlyOwner {
    require(randomNumber != 0);
       reveal = true;
   }

We understand that every URI is accessible once the NFTs are revealed. Before that, we just had one generic image for every NFT. But before calling revealNFT(), the owner has to make sure that randomNumber has been initialized.

So let’s see where the code assigns a value to randomNumber.

function requestRandomNumbers() external onlyOwner {
       requestId = COORDINATOR.requestRandomWords(
           keyHash,
           s_subscriptionId,
           requestConfirmations,
           callbackGasLimit,
           numWords
       );
   }
 
   function fulfillRandomWords(
       uint256, 
       uint256[] memory randomNumbers
   ) internal override {
       randomNumber = randomNumbers[0];
   }

In this example, they use Chainlink’s VRF, an off-chain random number generator, to create an offset in URIs with randomNumber. These two functions above show how it works. The owner will have to call requestRandomNumbers() which will assign a random number to randomNumber with an internal call of fulfillRandomWords() by Chainlink.

What is happening in real life when the owner wants to reveal the NFTs?

The owner will start by calling requestRandomNumbers() to assign a random value to randomNumber. Because randomNumber has now a non-zero value, the owner can call revealNFT() to turn the reveal flag into true, and then all the URIs are public with tokenURI().

But remember that to predict the URIs, we only need four elements including three already known and randomNumber which is also public and has its value assigned when requestRandomNumbers() is called.

So in the meantime between requestRandomNumbers() and revealNFT() are called, URIs are not public yet but we have all elements to predict them.

Another version of this vulnerability exists. Although more common, it is more difficult to exploit. If there is no reveal step, the baseURI is often set at the beginning or in the middle of the mint phase. So as soon as an NFT is minted, it is revealed, but the remaining tokens remain unknown. We can then retrieve the URI and use it to know the rarest NFTs among the remaining ones. Then you have to try to mint at the right time to find them.

As we said above, the result is that attackers can gain control of the most valuable NFTs in the project as they could use these URIs to check for some rare properties in the metadata before anyone can see them.

To avoid having the same kind of vulnerabilities, you can check these Randomization strategies for NFT drops By William Entrike. It will help you randomize token URIs securely.

Mitigation

So what should you do if you own an NFT project and you want to mitigate risks and protect the codebase from BadReveal?

Since the essence of the BadReveal vulnerability relies on the advantage the attacker has between setting the token URI transaction to the reveal (or mint) transaction, the mitigation would be to combine the two into one transaction.

If you plan on revealing the NFTs, set the base token URI at the same time:

 function reveal(string memory baseURI) public onlyOwner {
     revealed = true;
     baseTokenURI = baseURI;
 }

In the example we saw earlier the solution could also be to move the reveal to be automatically set by Chianlink’s transaction:

function fulfillRandomness(bytes32, uint256 _randomness) internal override {
     revealed = true;
     randomNumber = randomNumbers[0];
}

Leveraging this approach, you might want to consider discarding the revealed boolean from your code and replacing it with on-the-fly verification of the Chainlink randomNumber assignment:

function someMintingFunction(uint256 _quantity) internal {
     require(!randomNumber, "Not revealed yet");
     //…
}

Using this mechanism is the easiest way to mitigate BadReveal as it does not introduce the gap between transactions that allows an attacker to buy early before other buyers get the metadata information.

Summary

Cyber risks of NFT projects can take many forms and shapes, it is a common belief that if your NFT wasn’t stolen you didn’t get hacked, but what if you were supposed to get the rarest NFT in the deck, but an attacker was smart enough to buy it before you? You got hacked without even knowing it.

It is important to set requirements for NFT projects to invest in high-quality audits and use well security practices. 

We have a strong belief that this vulnerability is commonly exploited in the wild under the radar of the NFTs investors and communities. Please share this information with as many projects as possible so they ensure their security.

We would like to thank rugpullfinder @rugpullfinder the help in investigating and verifying the vulnerability with us. We used their expertise and knowledge in the NFT space to go deeper the rabbit hole of BarReveal

Appendix – Affected Project

CatBloxPUMACapsule’s Details

https://etherscan.io/address/0x8b8D1225bB21CA07812FF3c2ee9358f7b5d90EcA

Everai Heroes: Duo’s Details

https://etherscan.io/address/0x9a38dec0590abc8c883d72e52391090e948ddf12

Degen Toonz Details

https://etherscan.io/address/0x19b86299c21505cdf59ce63740b240a9c822b5e4

Written By
Avigdor Sason Cohen

Avigdor is a web3 security researcher at Sayfer. He’s passionate about new blockchain technologies and how we could make sure we develop them in a secure way.