Skip to content

Double-credit vulnerability in reward claim retry path #66

Description

@DeFiVC

What

The processRewardClaim function (used by the retry processor) reads a quiz submission without a row-level lock, checks rewardClaimed, invokes the Stellar contract (slow external call), then sets rewardClaimed = true. Two concurrent retry ticks or a retry + direct claim can both read rewardClaimed = false and both succeed, crediting the user twice.

Why

This is a direct financial vulnerability. A user could receive double (or more) token rewards for a single quiz submission. Under load or during Stellar network delays, the window for concurrent execution widens.

Scope

In scope:

  • Add FOR UPDATE row lock inside a transaction in processRewardClaim, identical to the pattern in claimReward (lines 161-171)
  • Re-check rewardClaimed after acquiring the lock before proceeding
  • Alternatively, wrap the call in withLock (the distributed lock utility already used by claimReward)

Out of scope:

  • Changes to the retry queue logic itself
  • Changes to the Stellar contract interaction
  • Frontend changes

Acceptance Criteria

  • processRewardClaim acquires a row-level lock (FOR UPDATE) before checking rewardClaimed
  • The rewardClaimed check happens INSIDE the lock, after acquisition
  • Concurrent calls for the same submissionId are serialized — only one succeeds
  • The retry processor still functions correctly for legitimate retries
  • Existing tests pass
  • New test: concurrent processRewardClaim calls for the same submission only credit once

Technical Context

Files to modify:

  • src/modules/rewards/reward.service.tsprocessRewardClaim function (lines 68-146)

Pattern to follow:
The claimReward method (lines 155-171) already uses the correct pattern:

const [submission] = await tx
  .select()
  .from(quizSubmissions)
  .where(eq(quizSubmissions.id, submissionId))
  .for("update");  // <-- row lock

The retry processor at src/server.ts:44 calls processRewardClaim directly without any locking wrapper.

Related:

  • src/services/retry-queue.ts — retry processor that calls processRewardClaim
  • src/utils/lock.tswithLock distributed lock utility

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26bugSomething isn't workingcriticalsecuritySecurity concern

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions