Skip to content

fix(maintenance-pool): restore balance on poolWithdraw failure - #475

Open
Fabulouz34 wants to merge 1 commit into
MergeFi:mainfrom
Fabulouz34:fix/maintenance-pool-balance-leak
Open

Fabulouz34 wants to merge 1 commit into
MergeFi:mainfrom
Fabulouz34:fix/maintenance-pool-balance-leak

Conversation

@Fabulouz34

Copy link
Copy Markdown

Summary

MaintenancePoolService.assignReward() atomically decremented pool.balance before calling
escrowService.poolWithdraw(). Any throw inside poolWithdraw — escrow not LOCKED, invalid
amount, recipient/user mismatch, Soroban simulate/send/poll failure, or a paymentRepo.save
error — left the balance permanently reduced even though no USDC ever left the contract. Over
time this caused the DB balance to drift below the real on-chain balance, eventually blocking
legitimate rewards with "exceeds pool balance" with no admin path to reconcile.

Changes

EscrowService (escrow.service.ts)

  • Added public assertPoolWithdrawPreconditions() that exposes the three cheap guards already
    inside poolWithdraw (assertLocked, assertValidAmount, assertRecipientsMatchUsers) so callers
    can invoke them before touching any state.
  • Fixed a pre-existing syntax error in invokeRelease() (duplicate return statement from an
    earlier partial edit that broke compilation).

MaintenancePoolService.assignReward() (maintenance-pool.service.ts)

  • Calls assertPoolWithdrawPreconditions() before the atomic UPDATE … SET balance = balance -
    :amount query. Bad input (malformed amount, escrow not LOCKED, recipient/user mismatch) is now
    rejected without ever touching the DB balance.
  • Wraps the poolWithdraw() call in try/catch: on any throw, `poolRepo.increment({ id },
    'balance', Number(amount)) atomically restores the reservation before rethrowing the original
    error. The escrow stays LOCKED in all failure paths — no USDC moved — so the increment
    faithfully returns the pool to its real on-chain state.

Tests (maintenance-pool.service.spec.ts)

Added a describe('balance restoration on poolWithdraw failure') block with 6 regression tests:

  • poolWithdraw throwing a Soroban error → increment is called with the correct amount
  • poolWithdraw throwing a DB save error → increment is called
  • The exact decremented amount (including decimals) is always restored
  • A successful poolWithdraw does not trigger increment
  • The original error object is rethrown unchanged (not wrapped)
  • A pre-decrement precondition failure neither decrements nor triggers a restore

All 22 unit tests pass.

Testing

npx jest src/maintenance-pool/maintenance-pool.service.spec.ts

22 passed, 0 failed

Closes #459

Move cheap preconditions (assertLocked, assertValidAmount,
assertRecipientsMatchUsers) before the atomic balance decrement in
assignReward() so bad-input errors never touch the pool balance.

Wrap the poolWithdraw() call in try/catch: on any throw, restore the
decremented amount via poolRepo.increment() before rethrowing. The
escrow stays LOCKED in all failure paths (no USDC moved), so the
increment returns the pool to its true on-chain state.

Also fix a pre-existing syntax error in EscrowService.invokeRelease()
(duplicate return statement left from an earlier partial edit).

Adds 6 regression tests in 'balance restoration on poolWithdraw failure'
asserting increment is called on failure, not called on success, the
exact amount is restored, the original error is rethrown, and that a
pre-decrement validation failure neither decrements nor restores.

Closes MergeFi#274. Related: MergeFi#51, MergeFi#163.
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Fabulouz34 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

@vercel

vercel Bot commented Sep 30, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

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

Development

Successfully merging this pull request may close these issues.

assignReward decrements MaintenancePool.balance before poolWithdraw() and never restores it when the withdrawal fails

1 participant