Secure Smart Contract Development — Code Reentrancy in NFT Contracts
Introduction
Given the prosperity of NFT ecosystem on Ethereum and the increase of related security issues, we will post a series of blogs to introduce these security issues and give some suggestions for developers to secure contracts. In this blog, we will introduce the reentrancy issue in NFT contracts and how to mitigate the related vulnerability.
What is Reentrancy
According to wikipedia
In computing, a computer program or subroutine is called reentrant if multiple invocations can safely run concurrently on multiple processors, or on a single processor system, where a reentrant procedure can be interrupted in the middle of its execution and then safely be called again (“re-entered”) before its previous invocations complete execution.
In a smart contract, reentrancy can occur when a function executes an external call of another contract, which further invokes the original function (or other functions) before the original invocation returns. If the external call is controlled by an untrusted entity (e.g., a malicious contract), it can lead to an unexpected result. That’s because, in some functions, the result depends on the contract state. The contract state will be updated after the external call. However, the reentrant invocation of the function will use the old state instead of the updated one.
Reentrancy has been a prevalent issue in smart contracts, e.g., the MakerDAO attack, the ERC777 on Uniswap, and others (search Reentrancy on BlockSec Academy).
Reentrancy in NFT
For NFT contracts, there exist some implicit external function calls that could be neglected by developers. They include onERC721Received
and onERC1155Received
function. The onERC721Received
function was designed to check whether the receiver contract can handle NFTs (to prevent the NFTs from being locked forever.) This function is invoked in the safeTransferFrom
and _safeMint
of the ERC721 contract. The similar one exists in the ERC1155 contract. Due to these external function calls, the reentrancy could happen without being noticed by contract developers.
Single-Function Reentrancy
The single-function reentrancy is the simpler form of reentrancy attack. In this kind of reentrancy attack, the re-invoked function is same with the original function. An attacker can invoke the function repeatedly before the first invocation of the function is completed. In NFT contracts, this usually happens in the functions related to the mint
operation.
For instance, some NFT projects may give each user a chance to mint NFT freely, set the maximum supply of the whole project, or set the maximum amount of NFTs one user can hold. Usually, these constraints are checked before the actual mint operation. But if the states related to these constraints are updated after the safeMint
function, the attacker can reenter this mint function and bypass the constraints since the related states are the same as the first invocation of this function. The HypeBears reentrancy attack posted in our previous blog is an example.
A more complicated single-function reentrancy occurs when safeMint
function is used in a loop and the constraints are checked before the loop starts. In this scenario, even some states will be updated automatically before the external call, the remaining safeMint
function calls in the loop can still bypass the validation since the loop has already started and the validation happens before the loop starts.
For instance, in the example shown in another post, the mintNFT
function will check if the number of NFTs the user wants to mint plus the current supply can exceed the max supply. The safeMint
function updates the total supply before the onERC721Received
external call. An attacker can still exploit it since the safeMint
function only increases the total supply by 1 each time. So if the attacker re-enter the mintNFT
function in the first function call of safeMint
in the loop, the total supply becomes old supply plus 1, instead of old supply plus the amount of NFTs that will be minted by the first call of mintNFT
.
Cross-Function Reentrancy
Instead of entering the same function, the attacker can enter another function that shares or depends on the states with the origin function. We have detailed some cases in our previous blogs for the Revest case and the OMNI case.
Summary And Suggestions
Here are some suggestions for NFT smart contract developers to mitigate the reentrancy threat.
- Use the Checks-Effects-Interactions Pattern in your code.
- Be careful when you use any third-part library that will introduce external calls. For NFT contracts, be cautious with the implicit callback of
onERC721Received
andonERC1155Received
function.
About Us
The BlockSec Team focuses on the security of the blockchain ecosystem and the research of crypto hack monitoring and blocking, smart contract auditing.
Learn more about BlockSec:
Twiiter: https://twitter.com/BlockSecTeam
Github Academy: https://github.com/blocksecteam/blocksec_academy