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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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