collectUsdcFromSelling() calls usdc.safeTransfer(address(this), fees) — a self-transfer that moves zero net value — then increments totalFeesCollected. The fees are never actually separated from the shared pool. When the owner calls withdrawFees(), it sends totalFeesCollected USDC from the contract's unsegregated balance, which contains other users' collateral and pending sale proceeds.
This is a distinct root cause from the repeated-collect bug. The repeated-collect bug exploits missing state reset allowing multiple calls. This bug exploits the fact that even a single, legitimate collect call creates phantom fee accounting that lets the owner drain from the shared pool. Both exist independently in collectUsdcFromSelling — each alone is sufficient for fund theft.
Likelihood:
Occurs on every single sale — not an edge case. Every call to collectUsdcFromSelling produces a phantom fee entry
The owner calling withdrawFees() is intended normal behavior, making this a silent corruption that executes through the standard admin path
Impact:
Owner withdraws USDC backed by other users' collateral deposits — creating a balance shortfall
Late-claiming users (collateral reclaims via cancelListing, sellers calling collectUsdcFromSelling) find insufficient balance and their transactions revert
The shortfall grows proportionally with marketplace volume — 100 sales at 1,000 USDC average creates a 10 USDC × 100 = 1,000 USDC shortfall in the shared pool
Scenario: Alice mints (20 collateral), Carol mints (20 collateral), Alice lists at 1,000 USDC, Bob buys. Contract = 1,040 USDC.
| Step | Action | Contract Balance | totalFeesCollected |
|---|---|---|---|
| 1 | Alice + Carol mint | 40 | 0 |
| 2 | Bob buys Alice's listing for 1,000 USDC | 1,040 | 0 |
| 3 | Alice calls collectUsdcFromSelling(1) |
30 | 10 |
| 4 | Owner calls withdrawFees() |
20 | 0 |
At step 3, the self-transfer of 10 USDC fee is a noop — contract still has 30 USDC (20 = Carol's collateral + 10 = "fee" that was never moved). At step 4, owner withdraws 10 USDC of "fees" which are backed by Carol's collateral — there is no separate fee bucket.
Root cause — self-transfer is a noop, totalFeesCollected is a phantom counter:
Fix — remove the self-transfer entirely and use accounting-only tracking:
Additionally, withdrawFees should verify the contract has sufficient balance above user obligations before sending:
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.