After a sale completes, the seller calls collectUsdcFromSelling to receive the sale proceeds minus fees, plus their minting collateral. The function should only pay out once per sale.
collectUsdcFromSelling never marks the listing as collected. It checks !listing.isActive but never mutates listing.price, listing.seller, or collateralForMinting[tokenId]. A seller can call it repeatedly, extracting the full payout each time until the contract is drained of all USDC — including other users' collateral.
Likelihood:
Every time a seller calls collectUsdcFromSelling after a completed sale, the function pays out the full amount again. There is no state mutation to prevent re-entry on subsequent calls.
The self-transfer on line 195 (usdc.safeTransfer(address(this), fees)) is a no-op, so each repeated call also inflates totalFeesCollected without new USDC entering the contract. When the owner calls withdrawFees, it drains from the same shared pool.
Impact:
Attacker invests 20 USDC (minting collateral), lists at 25 USDC, gets one buyer, then calls collectUsdcFromSelling 5 times — extracting 223.75 USDC and draining 10 other users' collateral from 200 USDC down to 21.25 USDC.
The contract becomes insolvent. Other sellers cannot collect their sale proceeds and minters cannot recover their collateral — all calls revert with ERC20InsufficientBalance.
A marketplace with 10 minters holds 200 USDC in collateral. The attacker mints one NFT (20 USDC), lists it at 25 USDC, and gets a single buyer. After the sale, the attacker calls collectUsdcFromSelling in a loop. Each call pays out 44.75 USDC (25 - 0.25 fee + 20 collateral) because the listing data is never cleared. After 5 collections, the attacker has extracted 223.75 USDC from a 20 USDC investment, and the contract is drained from 245 to 21.25 USDC — the other 10 users' collateral is gone.
Delete the listing after collection. This zeros seller, price, and all other struct fields in one operation. The onlySeller modifier blocks any future call since seller becomes address(0). The collateral mapping is also zeroed to prevent re-extraction on resale. The self-transfer is removed — fees stay in the contract balance and the owner withdraws them via withdrawFees.
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.