Eggstravaganza

First Flight #37
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

The `mintEgg()` Uses `_mint()` Instead of `_safeMint()`, Allowing Minting to Contracts That Don’t Support ERC721

Summary

The mintEgg() function in EggstravaganzaNFT.sol uses the low-level _mint() function, which does not verify whether the recipient can handle ERC721 tokens. This may lead to NFTs being irreversibly locked in contracts that do not support or reject them.

Vulnerability Details

ERC721::_mint() does not check if the recipient is a contract or whether it implements the IERC721Receiver interface. As a result, tokens can be minted to non-compliant contracts and become permanently inaccessible.

The ERC721.sol file in the OpenZeppelin library, has the following warning in the function natspec:

WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible.”

The ERC721::_safeMint() function includes an additional step to call onERC721Received() on the recipient if it’s a contract, ensuring compatibility and preventing token loss.

function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
_mint(to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), address(0), to, tokenId, data);
}
function checkOnERC721Received(
address operator,
address from,
address to,
uint256 tokenId,
bytes memory data
) internal {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
if (retval != IERC721Receiver.onERC721Received.selector) {
// Token rejected
revert IERC721Errors.ERC721InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-IERC721Receiver implementer
revert IERC721Errors.ERC721InvalidReceiver(to);
} else {
assembly ("memory-safe") {
revert(add(32, reason), mload(reason))
}
}
}
}
}

In the current implementation, if the mintEgg() function is called with an unsafe address (e.g., a contract without onERC721Received()), the mint will succeed and totalSupply will be incremented, even though the token may be permanently stuck.

Impact

  • Token Loss: NFTs can be sent to contracts that cannot manage or return them, resulting in permanent loss.

  • Silent Failure: totalSupply is incremented, misleading off-chain or on-chain tracking.

Tools Used

  • Manual Code Review

  • Foundry Test Suite

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
import "../src/EggVault.sol";
import "../src/EggHuntGame.sol";
contract BlackHoleContract {
// This contract does NOT implement onERC721Received,
// so it can't safely receive ERC721 tokens.
}
contract EggGameTest is Test {
EggstravaganzaNFT nft;
EggVault vault;
EggHuntGame game;
function setUp() public {
// Deploy the contracts.
nft = new EggstravaganzaNFT("Eggstravaganza", "EGG");
vault = new EggVault();
game = new EggHuntGame(address(nft), address(vault));
// Set the allowed game contract for minting eggs in the NFT.
nft.setGameContract(address(game));
// Configure the vault with the NFT contract.
vault.setEggNFT(address(nft));
}
/// @notice This test proves that minting to a non-ERC721Receiver contract succeeds,
/// causing the token to be permanently locked in the contract.
function test_MintToNonERC721Receiver_SucceedsButTokenIsStuck() public {
// Deploy a contract that does not implement ERC721 standard
BlackHoleContract nonCompliant = new BlackHoleContract();
// Simulate a user successfully finding an egg — mintEgg is triggered from the game contract
vm.prank(address(game));
nft.mintEgg(address(nonCompliant), 1);
// Verify that the NFT was minted to the smart contract address
assertEq(nft.ownerOf(1), address(nonCompliant));
// Confirm that totalSupply has increased — misleading for any token tracking tools
assertEq(nft.totalSupply(), 1);
// Since the recipient contract doesn't support ERC721, the token is permanently stuck.
// There is no way to transfer it out, retrieve it, or interact with it.
}
}

Recommendations

Replace the use of _mint() in mintEgg() with _safeMint():

function mintEgg(address to, uint256 tokenId) external returns (bool) {
require(msg.sender == gameContract, "Unauthorized minter");
- _mint(to, tokenId);
+ _safeMint(to, tokenId);
totalSupply += 1;
return true;
}
Updates

Lead Judging Commences

m3dython Lead Judge 8 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Unsafe ERC721 Minting

Protocol doesn't check if recipient contracts can handle ERC721 tokens

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!