After a sale through buy(), the original seller is expected to call collectUsdcFromSelling() to receive their sale proceeds and recover their locked collateral. There is no deadline or urgency for this call — a seller may legitimately delay collecting.
list() allows a new owner to relist a token as soon as the previous listing is inactive. When they do, the entire s_listings[tokenId] entry is overwritten with the new seller's address and price. Because s_listings is keyed by tokenId (not by a unique listing ID), there is no separate slot for the original seller's pending claim.
Once the mapping is overwritten, the onlySeller modifier permanently blocks the original seller from calling collectUsdcFromSelling() or updatePrice(). Because collateralForMinting[tokenId] was never zeroed after the original sale, it is now accessible to whichever party next calls collectUsdcFromSelling() or cancelListing() — i.e., the new seller.
Likelihood: Medium
The trigger requires two conditions to align: (1) the new owner must be whitelisted (list() enforces onlyWhitelisted), and (2) they must relist before the original seller calls collectUsdcFromSelling(). Whitelisting is owner-controlled, so the re-lister is always a vetted participant.
In normal marketplace use, NFT flipping (buy then immediately relist) is a common pattern. Any whitelisted flipper will trigger this silently without realising the original seller has not yet collected — no malicious intent required.
Impact: High
The original seller permanently loses their entire sale proceeds (listing.price - fees) and their locked collateral (lockAmount). There is no recovery path — no admin function can reassign the frozen funds.
The new seller who eventually calls collectUsdcFromSelling() or cancelListing() receives the original minter's lockAmount as a windfall, draining funds they are not entitled to.
Setup: seller mints tokenId=1 and has 20 USDC locked as collateral.
Seller lists tokenId=1 at 100 USDC. s_listings[1].seller = seller.
Buyer purchases tokenId=1. Buyer pays 100 USDC. s_listings[1].isActive = false. collateralForMinting[1] = 20e6 (never zeroed by buy()).
Seller has not yet collected — this is normal; there is no deadline.
Buyer immediately relists tokenId=1 at 50 USDC. list(1, 50e6) executes without error and overwrites s_listings[1].seller = buyer.
Seller calls collectUsdcFromSelling(1). The onlySeller modifier checks s_listings[1].seller == seller but the stored seller is now buyer — the call reverts.
Buyer sells to a third party and calls collectUsdcFromSelling(1). Buyer receives 50 - fee + 20 (seller's collateral) = ~67 USDC instead of the ~47 USDC they are entitled to, pocketing the original seller's 20 USDC collateral.
The root cause is that s_listings uses tokenId as its key, so re-listing destroys the previous seller's claim. The fix requires decoupling the listing record from the token ID by keying it on the monotonic listingsCounter and preventing re-listing until the previous seller has collected.
In list(), store under listingsCounter and block re-listing while proceeds are uncollected:
Note: fully fixing this requires also resolving H-02 (keying all operations on listingsCounter rather than tokenId).
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.