After an NFT is sold via buy(), the seller calls collectUsdcFromSelling() to claim their sale proceeds minus fees plus their original minting collateral. This function should only allow a single collection per completed sale, sending the seller their rightful payout exactly once.
The function never resets the listing data, never zeroes collateralForMinting[tokenId], and never sets any "claimed" flag. After buy() sets isActive = false, every guard in collectUsdcFromSelling continues to pass on repeated calls: the onlySeller modifier succeeds because seller is never cleared, and require(!listing.isActive) succeeds because it's already false. Each call sends the full (price - fees + collateral) to the seller again, draining USDC belonging to other users.
Likelihood:
Any seller who completes a sale discovers this immediately — calling collectUsdcFromSelling a second time succeeds with zero friction, no special setup, and no prerequisites beyond having sold an NFT once.
Bots and sophisticated actors will automate repeated calls in a single transaction via a simple attack contract loop, draining the entire contract balance before anyone can react.
Impact:
Direct theft of all USDC in the contract — a single malicious seller extracts minting collateral and sale proceeds belonging to every other user in the protocol.
totalFeesCollected inflates with each call, meaning the owner's withdrawFees() will attempt to withdraw more USDC than actually exists as fees, either reverting (DoS) or consuming remaining user deposits.
Other users' cancelListing() calls revert because their collateral has been stolen from the shared pool, permanently locking their NFTs in unbuyable listings with no way to recover funds.
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];+ // Prevent repeated claims+ delete s_listings[_listingId];+ collateralForMinting[listing.tokenId] = 0;totalFeesCollected += fees;amountToSeller += collateralToReturn;- usdc.safeTransfer(address(this), fees);usdc.safeTransfer(msg.sender, amountToSeller);}The fix deletes the listing struct and zeroes the collateral mapping before transferring funds. This ensures any subsequent call to collectUsdcFromSelling will have listing.seller == address(0), causing the onlySeller modifier to revert since msg.sender cannot match address(0). The self-transfer of fees is also removed since it was a no-op — the fee amount is naturally retained in the contract after sending only the seller's share.
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.