After a successful NFT sale, the seller should be able to call collectUsdcFromSelling() exactly once to retrieve the sale proceeds (minus fees) plus their minting collateral. After collection, the seller should not be able to claim funds again.
collectUsdcFromSelling() never resets collateralForMinting[listing.tokenId] to 0, nor marks the listing as "already claimed". Since the only guard is require(!listing.isActive) — which remains false after a sale — a malicious seller can call the function repeatedly, draining (listing.price - fees + lockAmount) USDC from the contract on each iteration until the contract balance is fully exhausted, stealing funds belonging to other users (their collateral and pending sale proceeds).
Likelihood:
Every sale followed by `collectUsdcFromSelling()` call. Once a listing transitions to `isActive = false` (after `buy()` or `cancelListing()`), the seller can repeatedly invoke `collectUsdcFromSelling()` without any state reset preventing re-entry.Reason 2
Impact:
Complete fund drainage. A malicious seller can drain the entire contract USDC balance by calling `collectUsdcFromSelling()` multiple times until the contract is empty.
Other users' minting collateral and pending sale proceeds become vulnerable. A single compromised seller can steal funds that belong to legitimate users who are awaiting their own payouts or have locked collateral for minting.
This test uses the same setup as the NFTDealersTest file, extended with an additional account: maliciousUser.
Pre-conditions setup: The contract is pre-funded with USDC to simulate a realistic scenario where the protocol has already accumulated a significant balance (e.g. from past fees and sales). The malicious user is whitelisted, mints an NFT, and lists it for sale — both required prerequisites to trigger the attack.
Attack prerequisites: A third-party buyer purchases the NFT from the malicious seller. This makes the listing inactive (isActive = false), which is the final prerequisite needed to call collectUsdcFromSelling.
The attack: Once the listing is inactive, the malicious seller repeatedly calls collectUsdcFromSelling with the same listingId.
Final assertions:
The contract is left with less than 100 USDC (exact zero is not reachable due to loop exit condition)
All drained funds are confirmed to have been transferred to maliciousUser, accounting for virtually the entire pre-existing contract balance
Add a claimed boolean field to the Listing struct and set it to true on the first successful call to collectUsdcFromSelling. A require check at the top of the function ensures subsequent calls revert immediately, preventing the seller from collecting the same funds multiple times. Additionally, collateralForMinting[listing.tokenId] should be reset to zero in the same transaction to eliminate any residual state that could be exploited.
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.