Skip to content

fix: cooperative close signs spent amount instead of high-water mark#196

Merged
jxom merged 3 commits intomainfrom
fix/close-overcharge
Mar 18, 2026
Merged

fix: cooperative close signs spent amount instead of high-water mark#196
jxom merged 3 commits intomainfrom
fix/close-overcharge

Conversation

@brendanjryan
Copy link
Collaborator

@brendanjryan brendanjryan commented Mar 18, 2026

Problem

The client's close() was signing the cooperative close voucher with channel.cumulativeAmount (the high-water mark of all vouchers issued) instead of the actual spent amount reported by the server. This caused the server to settle on-chain for more than the client consumed.

For example, if a client pre-authorized 3,000,000 via a voucher but the server only charged 500,000, the cooperative close would settle 3,000,000 on-chain—overcharging the client by 2,500,000.

Fix

Server (Session.ts)

  • Validation: handleClose() now validates the close voucher against max(channel.spent, onChain.settled) instead of highestVoucherAmount. This allows closing at the actual spent amount rather than requiring the high-water mark.
  • Store update: Only overwrites highestVoucher/highestVoucherAmount when the close voucher exceeds the current highest. Always sets finalized: true.

Client (SessionManager.ts)

  • Tracks server-reported spent via monotonic max from receipt headers (both normal fetch responses and SSE payment-receipt events)
  • Resets spent on channel change to prevent cross-channel contamination
  • close() signs spent instead of cumulativeAmount

Tests (Session.test.ts)

  • "accepts close at spent amount (below highestVoucherAmount)": Verifies that closing at the spent amount succeeds and preserves the original highestVoucherAmount
  • "rejects close below spent amount": Verifies that closing below the spent amount is rejected

Deploy notes

Server-side changes are backwards compatible—existing clients closing at cumulativeAmount still pass the max(spent, settled) check since cumulativeAmount >= spent. Client changes require the server fix to be deployed first (closing at spent would be rejected by the old highestVoucherAmount check).

The client's close() was signing the cooperative close voucher with
channel.cumulativeAmount (the high-water mark of all vouchers issued)
instead of the actual spent amount reported by the server. This caused
the server to settle on-chain for more than the client consumed.

Server changes:
- handleClose() validates close voucher against max(channel.spent,
  onChain.settled) instead of highestVoucherAmount
- Store update only overwrites highestVoucher when close amount exceeds
  the current highest (always sets finalized: true)

Client changes:
- Track server-reported spent via monotonic max from receipt headers
  (both normal fetch responses and SSE payment-receipt events)
- Reset spent on channel change
- close() signs spent amount instead of cumulativeAmount
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/wevm/mppx@196

commit: 14ab022

brendanjryan and others added 2 commits March 17, 2026 20:35
…contamination

- Extract updateSpentFromReceipt() helper with channelId guard
- Call from toPaymentResponse() so non-SSE fetch() updates spent
- Reuse in SSE payment-receipt handler (replaces inline logic)
Amp-Thread-ID: https://ampcode.com/threads/T-019cff27-9be6-728d-a710-7dfe4a550072
Co-authored-by: Amp <amp@ampcode.com>
@jxom jxom merged commit 2a0b88e into main Mar 18, 2026
5 of 6 checks passed
@jxom jxom deleted the fix/close-overcharge branch March 18, 2026 04:18
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.

2 participants