collectUsdcFromSelling pays the seller their sale proceeds plus minting collateral but never zeros out s_listings[_listingId].price or collateralForMinting[listing.tokenId]. Since buy() only sets isActive = false, the seller can call collectUsdcFromSelling repeatedly — each call passes the require(!listing.isActive) guard and transfers the full amount again.
Three compounding bugs in one function:
No state reset — s_listings[_listingId].price and collateralForMinting[listing.tokenId] remain intact. Every subsequent call computes the same amountToSeller and transfers it again.
Fee self-transfer — usdc.safeTransfer(address(this), fees) moves tokens from the contract to itself, a no-op. Fees are never separated.
Phantom fee counter — totalFeesCollected increments on every call but the USDC backing those "fees" was never isolated. withdrawFees() competes with sellers for the same depleting pool.
Likelihood:
This will occur every time a seller calls collectUsdcFromSelling more than once after their NFT is sold, because there is zero state change preventing re-collection
No special setup required — any whitelisted user who sells an NFT can exploit this immediately
Impact:
A single malicious seller drains the entire USDC balance of the contract by calling collectUsdcFromSelling in a loop
Steals other sellers' uncollected sale proceeds, all minters' locked collateral (20 USDC each), and the owner's accumulated protocol fees
Victim sellers who call collectUsdcFromSelling afterward face a revert (insufficient balance)
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.