Skip to content

fix(maintenance-pool): guard assignReward on (pool escrow, issueId) (… - #715

Open
MrOmale wants to merge 1 commit into
MergeFi:mainfrom
MrOmale:fix/458-assign-reward-double-payout-guard
Open

MrOmale wants to merge 1 commit into
MergeFi:mainfrom
MrOmale:fix/458-assign-reward-double-payout-guard

Conversation

@MrOmale

@MrOmale MrOmale commented Oct 21, 2026

Copy link
Copy Markdown

closes #458
Related: #273, #274, #276

Fixes a regression in MaintenancePoolService.assignReward() where the double-payout guard was mistakenly keyed on (pool, recipient) rather than (pool, issue).

Problem

In the previous implementation (#273):

  1. Repeated payouts for the same issue: Calling assignReward(pool, issue#1, ..., recipientA) followed by assignReward(pool, issue#1, ..., recipientB) both succeeded because the query only filtered on payment.recipientId.
  2. Anonymous payouts bypassed: When recipientId was omitted, the condition became payment.recipientId = NULL (always false in SQL), allowing unlimited payouts for the same issue.
  3. False 409 rejections: A maintainer who resolved two distinct maintenance issues received a 409 on the second issue.
  4. Dead code: const existingPayment = await this.paymentRepo.findOne(...) ran an unscoped query whose return value was discarded.
  5. No DB-level constraint: No database-level unique constraint existed on Payment to close concurrent-call races.

Solution

  1. Schema & Entity (Payment):

    • Added maintenanceIssueId: string | null and relation maintenanceIssue: Issue | null (ON DELETE SET NULL) to Payment.
    • Added unique index UQ_payment_escrow_maintenance_issue on (escrowId, maintenanceIssueId). In PostgreSQL, standard unique indexes allow multiple rows where maintenanceIssueId IS NULL (bounty/milestone payments) while strictly enforcing uniqueness for non-null maintenance issues per escrow.
    • Added migration 1785400000000-AddPaymentMaintenanceIssueIdAndIndex.ts.
  2. Escrow Service (EscrowService.poolWithdraw):

    • Accepts optional maintenanceIssueId?: string and persists it on the created Payment row.
  3. Maintenance Pool Service (MaintenancePoolService.assignReward):

    • Removed the dead existingPayment query.
    • Replaced the guard query with:
      const existingPoolPayment = await this.paymentRepo
        .createQueryBuilder('payment')
        .innerJoin('payment.escrow', 'escrow')
        .where('escrow.maintenancePoolId = :poolId', { poolId: id })
        .andWhere('payment.maintenanceIssueId = :issueId', { issueId })
        .getOne();
      if (existingPoolPayment) {
        throw new ConflictException(
          `Issue ${issueId} has already received a reward from pool ${id}`,
        );
      }
    • Passed issueId into this.escrowService.poolWithdraw().
    • Handled concurrent call races by catching database unique constraint violations (23505), rolling back the balance decrement via this.poolRepo.increment, and throwing ConflictException.

Tests Added / Updated

  • src/maintenance-pool/maintenance-pool.service.spec.ts:
    • rejects when the same issue is rewarded a second time with a different recipient (409) (#458)
    • allows two different issues to be rewarded to the same recipient (#458)
    • rejects anonymous payout when issue was already rewarded (#458)
    • catches unique constraint violation on concurrent insert and rolls back balance (#458)
    • Updated #273 test to assert payment.maintenanceIssueId = :issueId.
    • Updated concurrent test #51 to operate on distinct issues.
  • src/common/entities/payment.entity.spec.ts:
    • Verified UQ_payment_escrow_maintenance_issue index metadata on Payment.
  • src/escrow/escrow.service.spec.ts:
    • Verified poolWithdraw persists maintenanceIssueId on Payment.

Verification

npm test -- payment.entity.spec.ts escrow.service.spec.ts maintenance-pool.service.spec.ts

@vercel

vercel Bot commented Oct 21, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@MrOmale Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

This branch has not been deployed

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

Labels

None yet

Projects

None yet

1 participant