When a seller lists an NFT, the NFT_Dealers_Listed event should emit the correct identifier that a buyer needs to pass to buy(). Frontends and indexers rely on this event to show available listings and route purchases.
list() stores the listing at s_listings[_tokenId] but emits listingsCounter in the event. buy() uses its _listingId parameter to index s_listings. Whenever listingsCounter diverges from tokenId — which happens as soon as any minted NFT is not immediately listed — a buyer using the event ID calls buy() with the wrong key and silently purchases a different listing at a different price.
Likelihood:
Every time a user mints an NFT without immediately listing it, tokenIdCounter advances but listingsCounter does not. From that point on, every event ID is offset from the actual storage key.
This is normal marketplace usage — users mint and list at different times. The divergence grows with every unlisted mint.
Impact:
A buyer who uses the event ID to call buy() silently purchases a different NFT at a different price. With a broad USDC approval, the buyer pays up to 50x more than expected with no revert or warning.
The buyer receives the wrong NFT. The intended seller's listing remains unsold. Both parties are harmed — the buyer overpays for an unwanted asset, and the intended seller misses the sale.
Alice mints tokenId 1 but doesn't list. Bob mints tokenId 2 and lists at 500 USDC — the event emits listingsCounter = 1. Charlie mints tokenId 3 and lists at 10 USDC — the event emits listingsCounter = 2. A buyer sees Charlie's event (ID 2, 10 USDC) and calls buy(2) expecting the cheap NFT. But s_listings[2] is Bob's 500 USDC listing. The buyer pays 500 USDC instead of 10 (50x overpayment) and receives Bob's tokenId 2 instead of Charlie's tokenId 3. No revert, no warning — a silent wrong purchase.
Use listingsCounter as the actual mapping key instead of tokenId. This makes the storage key match the emitted event ID, and each listing gets a unique key that doesn't collide when tokens are re-listed (also fixes F-003). This requires updating buy(), cancelListing(), collectUsdcFromSelling(), and updatePrice() to use the same key — but it's the correct structural fix.
Alternatively, as a minimal fix, emit _tokenId instead of listingsCounter in the event so the emitted ID matches the current storage key:
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.