Summary (CRITICAL — unfunded bounty triggers a real payout)
POST /api/bounties/[id]/claim lets a bounty be claimed while its status is open, and the claim path then broadcasts a real payout from the merchant web wallet — even though open means the creator's funding was never confirmed.
Details
- A bounty is created with
status = 'open' in POST /api/bounties, and only flips to funded after CoinPay confirms the creator's funding payment (webhooks/coinpay/route.ts sets status='funded' WHERE status='open').
- The claim guard accepts both:
if (!['open', 'funded'].includes(bounty.status)) { ... 409 ... } // line 23
...
WHERE id = ${bountyId} AND status IN ('open', 'funded') // line 38 (atomic UPDATE)
- So a bounty whose funding was never completed (creator abandoned the CoinPay checkout, or the payment failed) is still claimable, and the route then calls
/api/web-wallet/{MERCHANT_ID}/prepare-tx + broadcast to pay bounty.reward_usd to the claimer.
Impact
The merchant pays out reward_usd for bounties nobody funded. An attacker can create a bounty, never pay, have a second account claim it, and drain the merchant wallet reward_usd at a time.
Fix (PR opened)
Gate claims on status = 'funded' in both the pre-check (line 23) and the atomic UPDATE ... WHERE (line 38, closing the concurrent-flip TOCTOU) so a payout can only fire after the funding webhook has confirmed payment.
Summary (CRITICAL — unfunded bounty triggers a real payout)
POST /api/bounties/[id]/claimlets a bounty be claimed while its status isopen, and the claim path then broadcasts a real payout from the merchant web wallet — even thoughopenmeans the creator's funding was never confirmed.Details
status = 'open'inPOST /api/bounties, and only flips tofundedafter CoinPay confirms the creator's funding payment (webhooks/coinpay/route.tssetsstatus='funded' WHERE status='open')./api/web-wallet/{MERCHANT_ID}/prepare-tx+ broadcast to paybounty.reward_usdto the claimer.Impact
The merchant pays out
reward_usdfor bounties nobody funded. An attacker can create a bounty, never pay, have a second account claim it, and drain the merchant walletreward_usdat a time.Fix (PR opened)
Gate claims on
status = 'funded'in both the pre-check (line 23) and the atomicUPDATE ... WHERE(line 38, closing the concurrent-flip TOCTOU) so a payout can only fire after the funding webhook has confirmed payment.