Skip to content

#44 release() and splitRelease() never check for pre-existing Payment rows, so calling them after releasePartial causes a double payout of the full escrow amount FIXED - #87

Open
Kappa16 wants to merge 2 commits into
MergeFi:mainfrom
Kappa16:#44--release()-and-splitRelease()-never-check-for-pre-existing-Payment-rows,-so-calling-them-after-releasePartial-causes-a-double-payout-of-the-full-escrow-amount-FIX
Open

#44 release() and splitRelease() never check for pre-existing Payment rows, so calling them after releasePartial causes a double payout of the full escrow amount FIXED#87
Kappa16 wants to merge 2 commits into
MergeFi:mainfrom
Kappa16:#44--release()-and-splitRelease()-never-check-for-pre-existing-Payment-rows,-so-calling-them-after-releasePartial-causes-a-double-payout-of-the-full-escrow-amount-FIX

Conversation

@Kappa16

@Kappa16 Kappa16 commented Aug 21, 2026

Copy link
Copy Markdown

CLOSE #44

Findings

The root cause was in src/escrow/escrow.service.ts:

  • release() checked only whether the escrow was LOCKED.
  • splitRelease() also checked only whether the escrow was LOCKED.
  • releasePartial() creates Payment records but intentionally keeps the escrow LOCKED until the full amount is distributed.
  • Therefore, this sequence was possible:
await escrowService.releasePartial(escrowId, '50', recipientA);
await escrowService.release(escrowId, recipientB);

The second call could release the full escrow amount again, producing an overpayment.

The original code also had a read-then-write race condition: concurrent release requests could both read the same payment history before either request inserted a new Payment.

Fix Features

  1. Payment-history protection for release()

    release() now rejects when the escrow already has one or more Payment rows.

  2. Payment-history protection for splitRelease()

    splitRelease() now applies the same protection and rejects instead of paying the full escrow again.

  3. Protection for releasePartial() after full release

    A full release changes the escrow status to RELEASED. releasePartial() rejects non-LOCKED escrows, preventing partial release after a full or split release.

  4. Shared transaction lock

    All release-family operations now use a shared locking helper:

    withReleaseLock()
    

    It:

    • Starts a database transaction.
    • Locks the escrow row using pessimistic_write.
    • Performs the payment-history check while the lock is held.
    • Calls Soroban.
    • Saves the escrow and payment records before releasing the lock.
  5. TOCTOU protection

    The same locking mechanism is used consistently by:

    • release()
    • splitRelease()
    • releasePartial()

    This prevents concurrent requests from using the same stale payment-history snapshot.

  6. No duplicate chain invocation

    If a prior payment exists, the request is rejected before calling:

    soroban.invoke(...)
  7. Regression coverage

    Tests now cover:

    • Partial release followed by full release.
    • Partial release followed by split release.
    • Full release followed by partial release.
    • Ensuring rejected calls do not invoke Soroban again.

The issue is resolved using the documented behavior that full and split releases are rejected whenever an escrow has existing payment history.

release() and splitRelease() never check for pre-existing Payment rows, so calling them after releasePartial causes a double payout of the full escrow amount FIXED
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@chonilius

Copy link
Copy Markdown
Contributor

fix ci

@Kappa16

Kappa16 commented Aug 21, 2026

Copy link
Copy Markdown
Author

OK

@Kappa16

Kappa16 commented Aug 21, 2026

Copy link
Copy Markdown
Author

@chonilius DONE

@Kappa16

Kappa16 commented Aug 21, 2026

Copy link
Copy Markdown
Author

@chonilius PLS maintainers, after reviewing this contribution. If possible, please consider issuing payment through GrantFox, or otherwise consider me for a contributor payment. This support would be greatly appreciated and would help keep me motivated to continue contributing to the project. Thank you!

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

Labels

None yet

Projects

None yet

2 participants