Root Cause: The collectUsdcFromSelling function never resets listing.price, listing.seller, or collateralForMinting[listing.tokenId] after paying out. The only guard is require(!listing.isActive, ...) which checks that the listing is inactive -- a condition that remains true permanently after a sale or cancellation. The onlySeller modifier checks s_listings[_listingId].seller == msg.sender, and seller is never zeroed out. This allows the seller to call collectUsdcFromSelling an unlimited number of times on the same listing, draining the contract's entire USDC balance.
Exploit Path / Impact:
Alice mints an NFT (20 USDC collateral locked).
Alice lists the NFT for 1000 USDC.
Bob buys the NFT for 1000 USDC (contract now holds 1020 USDC).
Alice calls collectUsdcFromSelling(tokenId) -- receives (1000 - fees) + 20 USDC collateral.
Alice calls collectUsdcFromSelling(tokenId) AGAIN -- the function passes all checks (listing is still inactive, seller is still Alice) and sends the same amounts again.
Alice repeats until the contract is completely drained of ALL USDC (including other users' collateral and pending sale proceeds).
This is a total fund drain. Any seller who has ever completed a sale can drain every single USDC token held by the contract, stealing from all other users.
PoC:
Recommendation:
Confidence: High
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.