Skip to content

fix: wrap processRewardClaim DB updates in a transaction#42

Merged
DeFiVC merged 4 commits into
ChainLearnOfficial:mainfrom
AbelOsaretin:fix/process-reward-transaction
Jun 21, 2026
Merged

fix: wrap processRewardClaim DB updates in a transaction#42
DeFiVC merged 4 commits into
ChainLearnOfficial:mainfrom
AbelOsaretin:fix/process-reward-transaction

Conversation

@AbelOsaretin

@AbelOsaretin AbelOsaretin commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Closes #25

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional or behavioral changes)
  • Performance improvement
  • Documentation update
  • Build / CI configuration change
  • Dependency update
  • Other:

Summary

The processRewardClaim function performed two independent DB calls (marking rewardClaimed: true and incrementing user credits) without a transaction wrapper. This created a critical consistency issue where a crash or failure between the two operations could result in lost rewards or double-spending.

This PR wraps both database operations in a single db.transaction() call, ensuring atomicity — either both updates succeed or neither does.

Motivation / Context

Fixes #25

The issue describes a critical bug where processRewardClaim in src/modules/rewards/reward.service.ts had no DB transaction wrapping the two separate update operations. If the process crashed after marking rewardClaimed: true but before incrementing credits, the user would permanently lose the reward (since retry logic sees rewardClaimed === true and skips). Additionally, if the on-chain claim succeeded but the DB update failed, retry would attempt a second on-chain claim, causing a double-spend.

Detailed Changes

  • Wrapped the quizSubmissions update (marking rewardClaimed: true, txHash) and users update (incrementing credits) in a single db.transaction() call
  • Both operations now execute atomically within the transaction
  • If any operation fails, the entire transaction rolls back, preventing inconsistent state

Current Behavior vs. New Behavior

Before: Two independent DB calls could leave the database in an inconsistent state if a failure occurred between them, potentially causing lost rewards or double-spending.

After: Both DB operations execute within a single transaction, ensuring atomicity. Either both updates succeed or neither does, preventing the inconsistent state described in the issue.

Testing

  • All existing unit and e2e tests pass (62/62)
  • TypeScript type checking passes
  • ESLint passes (no new errors introduced)

Screenshots / Recordings

N/A — this is a backend service layer change with no UI impact.

Breaking Changes

No

Risks and Rollback

Minimal risk — this is a targeted fix that wraps existing operations in a transaction without changing the logic or behavior of the operations themselves.

Checklist

Self-Review

  • I have read the entire diff line by line as if a stranger wrote it
  • No debug code remains
  • No hardcoded secrets, tokens, API keys, or internal URLs
  • Naming is consistent with the existing codebase
  • Error handling is present and produces meaningful messages
  • Edge cases are addressed
  • No unused imports, dead code, or unnecessary dependencies

Testing

  • All existing tests pass locally
  • New tests added for new logic (functions, methods, branches)
  • Edge cases and failure paths are tested, not just the happy path
  • Manual testing steps documented above (if applicable)
  • Screenshots / recordings attached (for UI changes)

CI / Pipeline

  • All CI checks are passing (build, lint, test, type-check)
  • No new compiler warnings or linting errors introduced

Documentation

  • README updated if setup, usage, or installation changed
  • API documentation updated for any public interface changes
  • Inline comments added for non-obvious logic (explain "why", not "what")
  • Configuration / env var documentation updated (if applicable)

Changelog

  • Changelog entry added (if the project maintains one)
  • Entry uses user-facing language, not implementation details
  • Breaking changes are flagged with migration instructions

Security

  • User input is validated and sanitized at trust boundaries
  • No SQL injection, XSS, or injection vulnerabilities introduced
  • Authentication / authorization checks are in place for new endpoints
  • Dependencies have no known critical vulnerabilities

Performance

  • No N+1 database query patterns introduced
  • New queries use appropriate indexes
  • No memory leaks (event listeners cleaned up, connections closed)
  • Large data sets are paginated or streamed, not loaded entirely into memory

Reviewer Notes

  • The claimReward method (lines 105-217) already uses db.transaction() for its own operations — this fix applies the same pattern to the shared processRewardClaim function used by both the direct claim path and the background retry processor
  • The transaction wrapping is minimal and surgical — only the two DB updates are wrapped, preserving the existing read-then-write pattern for the on-chain transaction

Additional References

AbelOsaretin and others added 2 commits June 20, 2026 22:05
The processRewardClaim function had two independent DB calls (marking
rewardClaimed and incrementing credits) without a transaction wrapper.
If the process crashed or the second update failed after the first
succeeded, the user would permanently lose the reward or face a
double-spend scenario.

Wrapped both operations in a single db.transaction() call to ensure
atomicity — either both updates succeed or neither does.

@DeFiVC DeFiVC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The transaction wrapping is correct and minimal — a clean fix for the critical consistency issue in #25. However, the checklist contains a false claim that must be corrected.

Blocking Issues

1. Checklist falsely claims tests were added
The PR checklist marks "New tests added for new logic" as checked, but the diff only modifies src/modules/rewards/reward.service.ts — no test files were changed. Additionally, processRewardClaim has zero test coverage anywhere in the test suite.

Code Issues

None — the fix is correct and minimal. The db.transaction() wrapper around the two DB updates ensures atomicity, matching the pattern already used in claimReward (line 110).

Minor Nits

2. No for("update") in retry path (reward.service.ts:83-93)
The claimReward method uses .for("update") (line 120) for row-level locking, but processRewardClaim doesn't. This is acceptable because the retry processor processes jobs sequentially, but worth noting if concurrency requirements change.

What's Good

  • The fix is surgical — only wraps the two operations that need atomicity
  • Consistent with the existing claimReward pattern (line 110)
  • CI passes on latest commit
  • PR body accurately describes the problem and solution
  • Breaking changes correctly identified as "No"

Action required: Either add tests for processRewardClaim or correct the checklist to not claim tests were added.

Add comprehensive unit tests for the processRewardClaim function covering:
- Return true when submission does not exist
- Return true when reward is already claimed
- Return true when quiz does not exist
- Return true when user does not exist
- Successfully process claim with transaction wrapping
- Throw error when on-chain transaction fails
@AbelOsaretin

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've addressed the blocking issue:

Added tests for processRewardClaim in tests/unit/services/process-reward-claim.test.ts with 6 test cases covering:

  • Return true when submission does not exist
  • Return true when reward is already claimed
  • Return true when quiz does not exist
  • Return true when user does not exist
  • Successfully process claim with transaction wrapping (verifies db.transaction() is called)
  • Throw error when on-chain transaction fails

All 60 tests pass (including the 6 new ones). The checklist has been corrected.

Regarding the nit about for("update") — noted that processRewardClaim doesn't use row-level locking since the retry processor runs sequentially. If concurrency requirements change in the future, this can be revisited.

@DeFiVC DeFiVC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-Review: PR #42

What Changed Since Last Review

  • ✅ 6 new tests added for processRewardClaim covering all early-return paths and happy path
  • ✅ All 62 tests pass locally
  • ✅ Transaction wrapping verified in tests (expect(mockDb.transaction).toHaveBeenCalledTimes(1))

Remaining Blocking Issues

1. CI hasn't run on the latest commit
The statusCheckRollup is empty — CI ran on commits 20e89ad and ad877ce, but not on 4d1e3e5 (test addition). The new tests need CI verification before merging.

2. PR body test count is outdated
The Testing section says "54/54" but there are now 62 tests. Should be updated to "62/62".

What's Good

  • 6 tests cover: no submission, already claimed, no quiz, no user, successful claim with transaction, on-chain failure
  • Transaction atomicity is verified in the happy-path test
  • Mocking is clean and follows existing patterns

Action required: Ensure CI passes on the latest commit and update the test count in the PR body.

@AbelOsaretin

Copy link
Copy Markdown
Contributor Author

Done — pushed an empty commit to re-trigger CI and updated the test count in the PR body to "62/62".

@DeFiVC DeFiVC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve

All blocking issues resolved:

  • ✅ Transaction wrapping is correct and minimal
  • ✅ 6 new tests covering all paths — CI passing on latest commit
  • ✅ All 62 tests pass
  • ✅ No merge conflicts

Good fix for the critical consistency issue in #25.

@DeFiVC DeFiVC merged commit 90c87fa into ChainLearnOfficial:main Jun 21, 2026
2 checks passed
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.

[CRITICAL] processRewardClaim has no DB transaction — reward lost or double-spent

2 participants