Skip to content

Commit d5eec9e

Browse files
FuturMixclaude
andauthored
fix: prevent race condition in bounty claim with atomic status check (#24)
The bounty claim handler uses a SELECT to check status, then a separate UPDATE to set it. Two concurrent requests can both pass the status check and claim the same bounty, potentially triggering duplicate payouts. Add a WHERE status IN ('open', 'funded') clause to the UPDATE so only the first concurrent claim succeeds at the database level, and verify the claimer_did after the UPDATE to detect and reject losing races. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b93d28c commit d5eec9e

1 file changed

Lines changed: 8 additions & 2 deletions

File tree

  • apps/web/app/api/bounties/[id]/claim

apps/web/app/api/bounties/[id]/claim/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
3030
const coupons = await db.sql`SELECT * FROM coupons WHERE id = ${coupon_id}`;
3131
if (!coupons.length) return NextResponse.json({ error: 'Coupon not found' }, { status: 404 });
3232

33-
// Mark bounty as claimed
33+
// Mark bounty as claimed — atomic WHERE prevents race conditions
3434
await db.sql`
3535
UPDATE bounties
3636
SET status = 'claimed', coupon_id = ${coupon_id}, claimer_did = ${did},
3737
updated_at = ${new Date().toISOString()}
38-
WHERE id = ${bountyId}
38+
WHERE id = ${bountyId} AND status IN ('open', 'funded')
3939
`;
4040

41+
// Verify the claim succeeded (handles concurrent claim race)
42+
const verify = await db.sql`SELECT claimer_did FROM bounties WHERE id = ${bountyId}`;
43+
if (verify.length && verify[0].claimer_did !== did) {
44+
return NextResponse.json({ error: 'Bounty was already claimed by another user' }, { status: 409 });
45+
}
46+
4147
// Attempt payout to claimer via web wallet
4248
// Docs: POST /api/web-wallet/:id/prepare-tx then /broadcast
4349
let payoutOk = false;

0 commit comments

Comments
 (0)