Understanding and exploiting reentrancy while safeMint()-ing NFTs.

safeMint() function expects that the recipient contract should implement onERC721Received which is basically a sign of the contract supporting ERC721 so that tokens won’t get stuck once someone sends it to that address.

And this is how it works…

It must return a solidity selector for onERC721Received to confirm the token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.

Example to understand (and to exploit):

NFTContract.sol

It's setting the price in the constructor and there are two functions:

  1. buyNFT(): which is a payable function and takes a native token e.g ETH/BNB after that it sets canClaim mapping to true for msg.sender.
  2. claim(): is for claiming the NFT which uses safeMint() to mint the NFT and sets canClaim to false for the msg.sender.

The way safeMint() checks that the to address can handle the NFT is how we discussed above — by calling onERC721Received on to address. so here control flow moves to to address. and this is where it creates/increases an attack surface.

Let's write an exploit for the above contract :

It has a constructor which takes the NFTContract address so it can be used later and two functions:

  1. buyAndClaimNftsWithTrick() is the function that carries the whole attack from buying NFT with buyNFT() and claiming it with the claim().
  2. A malicious onERC721Received() which reenters (claims NFT again) and returns 4 bytes selector.

Let's discuss the exploit first:

When the attacker calls AttackerContract : buyAndClaimNftsWithTrick() it first buys the NFT with buyNFT() now after that it calls claim() it safemints the NFT to msg.sender, Here the msg.sender is AttackerContract which implements malicious onERC721Received which will reenter to call claim() again. if you see the malicious onERC721Received it contains an if statement which will set the cnt to false and will call the claim() again to get an extra NFT.

The intention behind setting cnt to false is simple and that is to avoid a loop when claim calls onERC721Received while safeminting NFTs to AttackerContract.

Let’s test the exploit…

The test file contains some variables that we are using in some test functions.

  1. setUp(): is similar to Mocha’s beforeEach(), Here we are going to create instances with which we are interacting in test functions.
  2. testBuyAndHack(): is the function that tests the exploit. It transfers 1e18 native token to attacker contract and calls buyAndClaimNftsWithTrick() which performs a reentrancy attack and then on the last line we have an assertion to check the NFT balance of attacker contract is 2 (i.e greater than 1 and we got that extra 1 NFT by reentering )

Why it happened :

How to prevent it 💡:

Code used in this article is damn vulnerable, do not use in production.This article is for educational purpose only.

--

--

Smart contract security and Blockchain ⛓

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store