NFT Dealers

First Flight #58
Beginner FriendlyFoundry
100 EXP
Submission Details
Impact: low
Likelihood: medium

Reentrancy in mintNft()

Author Revealed upon completion

Root + Impact

Reentrancy in mintNft() allows unexpected double execution before mint state is updated

Risk

IMPACT: LOW
LIKELIHOOD: MEDIUM

Description

The mintNft() function performs an external call to usdc.transferFrom(msg.sender, address(this), lockAmount) before updating critical internal state such as tokenIdCounter and collateralForMinting[tokenIdCounter].

require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
tokenIdCounter++;
collateralForMinting[tokenIdCounter] = lockAmount;

Impact:

A malicious payment token can reenter mintNft() and trigger multiple executions before the original call updates internal accounting.

This may lead to:

  • unexpected multiple mints in a single transaction

  • inconsistent state transitions around tokenIdCounter

  • unsafe reliance on external token behavior

  • broader cross-function reentrancy risk if other protocol logic depends on partially updated mint state

Although the demonstrated PoC required paying the locked amount twice, the protocol still exposes a real reentrancy surface and relies on unsafe ordering of external interactions.

Proof of Concept:

A malicious ERC20 was used in place of USDC. During transferFrom(), it called back into the whitelisted attacker contract, which reentered mintNft() before tokenIdCounter was incremented in the original execution.

Observed result from the test:

the initial call to mintNft() invoked transferFrom()

transferFrom() triggered a reentrant call to mintNft()

mintNft() executed twice in the same transaction

totalMinted() became 2

PoC results:

assertEq(nftDealers.totalMinted(), 2);
assertEq(nftDealers.collateralForMinting(1), LOCK_AMOUNT);
assertEq(nftDealers.collateralForMinting(2), LOCK_AMOUNT);

This confirms that mintNft() is reentrable through the external token call.

Recommended Mitigation:

  1. Follow the Checks-Effects-Interactions pattern by updating internal mint state before performing external calls.

  2. Also consider adding nonReentrant protection and using SafeERC20.

function mintNft() external payable onlyWhenRevealed onlyWhitelisted nonReentrant {
if (msg.sender == address(0)) revert InvalidAddress();
require(tokenIdCounter < MAX_SUPPLY, "Max supply reached");
require(msg.sender != owner, "Owner can't mint NFTs");
uint256 newTokenId = tokenIdCounter + 1;
tokenIdCounter = newTokenId;
collateralForMinting[newTokenId] = lockAmount;
require(usdc.transferFrom(msg.sender, address(this), lockAmount), "USDC transfer failed");
_safeMint(msg.sender, newTokenId);
}

Support

FAQs

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

Give us feedback!