Skip to content

fix(withdraw): gate balance increment on event INSERT rowcount to pre… - #1410

Open
Martinsbab wants to merge 1 commit into
LabsCrypt:mainfrom
Martinsbab:fix/issue-1216-idempotent-withdraw
Open

fix(withdraw): gate balance increment on event INSERT rowcount to pre…#1410
Martinsbab wants to merge 1 commit into
LabsCrypt:mainfrom
Martinsbab:fix/issue-1216-idempotent-withdraw

Conversation

@Martinsbab

Copy link
Copy Markdown

Summary

Fixes an accounting bug where duplicate or retried withdrawal requests could increment withdrawnAmount more than once for the same claimable withdrawal.

Problem

The withdrawal flow uses a deterministic transaction hash for simulated withdrawals. The StreamEvent upsert correctly handles duplicate events by treating an existing event as a no-op.

However, the adjacent prisma.stream.update was not protected by the same idempotency check. As a result, retrying the same withdrawal request could increment withdrawnAmount repeatedly even though the withdrawal event had already been recorded.

Changes

  • Made the withdrawal accounting update idempotent.
  • Ensured duplicate withdrawal requests cannot double-count withdrawnAmount.
  • Kept the event recording and balance update consistent.
  • Added integration coverage for repeated withdrawal requests.
  • Verified the behavior against the existing withdrawal flow and database patterns.

Testing

  • Added/updated integration tests covering duplicate withdrawal requests.
  • Verified that calling withdraw twice for the same claimable window increases withdrawnAmount only once.
  • Ran the relevant project validation checks.

Issue

Closes #1216

…vent double-counting

The withdraw handler atomically incremented withdrawnAmount on every request,
regardless of whether the corresponding StreamEvent already existed. Because
sorobanWithdraw returns a deterministic txHash ('simulated-withdraw-' +
streamId), retries and concurrent duplicate requests would each re-increment
the balance — a real fund-accounting bug.

Move the StreamEvent creation inside the $transaction using a conditional
INSERT with WHERE NOT EXISTS + RETURNING. The INSERT's rowcount (1 = new,
0 = duplicate) gates the balance UPDATE, guaranteeing exactly-once accounting
per claimable window. This is race-condition-safe because the unique
constraint on (transactionHash, eventType) prevents duplicate events at the
database level, and the rowcount is evaluated within the same transaction.

Updates existing tests in eventRace.test.ts and withdraw.handler.test.ts,
and adds two new integration tests proving idempotent withdrawal behavior.

Closes LabsCrypt#1216

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] Duplicate/retried withdraw requests double-count withdrawnAmount

1 participant