When “SafeMint” Becomes Unsafe: Lessons from the HypeBears Security Incident
On the morning of Feb 3rd (+8 timezone), our system reported an attack transaction 0xfa97c3476aa8aeac662dae0cc3f0d3da48472ff4e7c55d0e305901ec37a2f704 towards the HypeBears NFT contract. After the investigation, we found it’s a re-entrancy attack caused by the
_safeMint function of ERC721.
The root cause
The project has a limitation of the NTFs that an account can mint. Basically, it has a map
addressMinted that logs whether an account has minted the NTFs.
When minting NFTs, the code uses
_safeMint function of the OZ reference implementation. This function is
safe because it checks whether the receiver can receive ERC721 tokens. The can prevent the case that a NFT will be minted to a contract that cannot handle ERC721 tokens. According to the document:
If to refers to a smart contract, it must implement IERC721Receiver.onERC721Received, which is called upon a safe transfer. The following code shows the OZ implementation of
However, this external function call creates a security loophole. Specifically, the attacker can perform a reentrant call inside the
onERC721Received callback. For instance, in the vulnerable HypeBears contract, the attacker can invoke the
mintNFT function again in the
onERC721Received callback (since 1addressMinted` has not been updated.)
The following screenshot shows the attack transaction.
The risk called by
SafeMint has been discussed by security researchers link1 link2. However, we can still see the vulnerable code and the attack in the wild. As shown in the
safeTransfer in QBridge security incident, using a safe function does not guarantee a safe contract 😃.