Skip to content

On-chain withdrawal checks balance before submit and decrements after — concurrent double-spend #349

Description

@3m1n3nc3

Description

In app/app/api/wallet/withdraw/route.ts (on-chain path), the balance check and the decrement are separated by an un-held on-chain submission:

if (user.walletBalance < amount) return apiError("Insufficient wallet balance", 400);
// ... submitStellarWithdrawal(...) — network call, no balance hold ...
prisma.user.update({ data: { walletBalance: { decrement: amount } } });

Two concurrent withdrawal requests can both pass the < amount check, both submit a payment to Stellar, and both decrement afterward — driving walletBalance negative and disbursing more than the user holds. The decrement also lacks a walletBalance: { gte: amount } guard, so it cannot fail safely. (The simulated path is correctly atomic; only the on-chain path is vulnerable.)

More info

  • File: app/app/api/wallet/withdraw/route.ts (approx. lines 46-99)
  • Reserve the balance atomically before submitting: updateMany({ where: { id, walletBalance: { gte: amount } }, data: { decrement: { walletBalance: amount } } }) and bail if count === 0.
  • Refund the reserved amount if submitStellarWithdrawal fails.
  • Add a concurrency test issuing two simultaneous withdrawals that together exceed the balance and assert only one succeeds.

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions