Eggstravaganza

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

[M-2] `mintEgg()` Allows Duplicate Token IDs Without `_exists()` Check

Summary

The mintEgg() function in the EggstravaganzaNFT contract allows the authorized game contract to mint NFTs with arbitrary token IDs. However, the function lacks a check to prevent duplicate token IDs. If the game contract is buggy or compromised, it could mint a token ID that already exists, violating ERC721 invariants and breaking core assumptions in wallets, marketplaces, and dapps.

Vulnerability Details

The mintEgg() function is restricted to be callable only by the gameContract:

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

However, there is no check to ensure the tokenId is unique before minting. While the current game implementation (EggHuntGame) increments the tokenId with each mint, nothing in the NFT contract enforces this.

If a malicious or misconfigured gameContract bypasses eggCounter and uses arbitrary token IDs, it may succeed in minting overlapping or unordered tokens unless _exists() is manually checked.

Impact

Token ID collisions can:

  • Cause ownerOf() and tokenURI() to behave inconsistently.

  • Break compatibility with ERC721 indexers and wallets.

  • Lead to unexpected reverts, loss of access, or game logic inconsistencies.

Attackers (or bugs) could attempt to re-mint an already distributed token, undermining uniqueness guarantees.

Tools Used

  • Manual code review

  • ERC721 standard invariants

Recommendations

Add an _exists(tokenId) check to ensure token ID uniqueness:

function mintEgg(address to, uint256 tokenId) external returns (bool) {
require(msg.sender == gameContract, "Unauthorized minter");
require(!_exists(tokenId), "Token already minted");
_mint(to, tokenId);
totalSupply += 1;
return true;
}

This ensures any contract calling mintEgg() cannot accidentally or maliciously re-mint an existing NFT.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;
import "forge-std/Test.sol";
import "../src/EggstravaganzaNFT.sol";
contract MaliciousGame {
EggstravaganzaNFT public nft;
constructor(address _nft) {
nft = EggstravaganzaNFT(_nft);
}
function reMint(uint256 tokenId, address recipient) external {
nft.mintEgg(recipient, tokenId);
}
}
contract MintEggDuplicateTest is Test {
EggstravaganzaNFT nft;
MaliciousGame malicious;
address deployer = address(this);
address alice = address(0xA11CE);
function setUp() public {
nft = new EggstravaganzaNFT("Egg", "EGG");
nft.setGameContract(deployer);
nft.mintEgg(alice, 1); // Mint tokenId 1
malicious = new MaliciousGame(address(nft));
nft.setGameContract(address(malicious));
}
function test_RevertsOnDuplicateMint() public {
vm.expectRevert(); // Will revert inside _mint() due to duplicate tokenId
malicious.reMint(1, alice);
}
}

Setup Steps

  1. Deploy EggstravaganzaNFT, mint a token via the legitimate game contract with token ID 1.

  2. Use setGameContract(...) to assign MaliciousGame as the new game contract.

  3. Call reMintExistingEgg(...) with token ID 1.

Result
Without a _exists() check, mintEgg() reverts internally. This can:

  • Cause game or frontend errors

  • Be exploited for denial-of-service scenarios

  • Create ambiguity in token lifecycle guarantees

Updates

Lead Judging Commences

m3dython Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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