Per-Quote Close Settlement Netting

Before v0.8.6, a close moved funding and realized PnL one after the other. That could reject a valid close. The new flow nets those two components inside each quote, then finalizes and reports that quote before processing the next one.

Before v0.8.6

A quote close settled accumulated funding first and realized PnL second. Each transfer independently checked whether its payer had enough allocated balance at that moment.

  1. Calculate and transfer accumulated funding.
  2. Calculate and transfer realized PnL.
  3. Charge the close fee, finalize the quote, and call its hooks.

The economic result could be solvent even though the first gross transfer was not.

Why gross ordering failed

Funding and PnL from the same quote can move in opposite directions. Whichever component moved first could demand liquidity that the opposite component would immediately return.

The 70 / 80 / 90 case

PartyB begins with 70 allocated, owes PartyA 80 in funding, and receives 90 because PartyA realizes a loss on that same quote:

funding receipt    +80
realized PnL       -90
----------------------
net to PartyA      -10

The old funding-first flow tried to debit 80 from PartyB's 70 and reverted. The correct final movement is PartyA paying PartyB 10, leaving PartyB with 80. Reversing funding and PnL would make the opposite-direction case fail instead.

How v0.8.6 fixes it

The contract now prepares both settlement components for one quote before checking either payer. The check uses that quote's bilateral difference rather than funding or PnL in isolation.

The signs below are written from PartyA's point of view. Positive funding means PartyA pays; negative funding means PartyA receives.

partyAReceives =
    (PartyA profit ? pnl : 0)
  + (fundingFee < 0 ? abs(fundingFee) : 0)

partyAPays =
    (PartyA loss ? pnl : 0)
  + (fundingFee > 0 ? fundingFee : 0)

quoteNetToPartyA = partyAReceives - partyAPays
  • If the quote net is positive, PartyB must cover only that difference.
  • If the quote net is negative, PartyA must cover only its absolute value.
  • If the two totals match, neither party needs additional balance for funding and PnL.

After validating the difference, the contract posts that quote's receiver-side funding and PnL entries before its payer-side entries. Funding and PnL remain separately attributed in events even though solvency uses their combined result.

How a close is processed

Each quote completes the following pipeline before the next batch entry begins:

  1. Validate. Check quote state, deadline, fill amount, close price, and symbol restrictions.
  2. Prepare. Release the filled share of locks, record accumulated funding, and calculate realized PnL.
  3. Check the quote net. Require only the remaining bilateral funding-plus-PnL obligation.
  4. Post settlement. Apply that quote's funding and PnL credits, followed by its funding and PnL debits.
  5. Finalize. Emit funding attribution, update the average close price, charge the close fee, update the closed amount, reduce aggregate positions, release connections when appropriate, and set the resulting quote status.
  6. Notify. Call that quote's affiliate and system hooks and record its trade volume.

Batch behavior

Quotes do not share settlement liquidity

A later quote cannot fund an earlier quote's obligation. For example, if quote one requires PartyB to pay 80 and quote two would later pay PartyB 90, PartyB must still cover quote one's own net when quote one executes.

This boundary is deliberate. Cross-quote netting would require preparing and finalizing the entire batch before callbacks, changing what existing hooks observe and requiring a separate batch-context protocol. The v0.8.6 fix stays scoped to the funding and PnL generated by one quote.

Repeated quote IDs remain sequential

The same quote ID may appear more than once when each entry is valid against the state produced by the previous entry. A 40% fill followed by the remaining 60% therefore recalculates the open amount and lock share after the first fill. A complete close releases the quote's remaining CVA, LF, PartyA MM, and PartyB MM without residue.

Input order is observable

Because quotes execute sequentially, order can affect whether an intermediate quote has enough allocated balance. It also determines quote finalization, event, and hook order. Every quote in the batch must still belong to the same PartyA and PartyB.

Events and hooks

For each quote, observable settlement follows its own pipeline:

  1. Non-zero FUNDING_FEE_IN and REALIZED_PNL_IN balance changes.
  2. Non-zero FUNDING_FEE_OUT and REALIZED_PNL_OUT balance changes.
  3. QuoteFundingSettled when accumulated funding is enabled.
  4. Any close-fee balance change and TradingFeeCharged(CLOSE).
  5. Affiliate close and fee hooks, then system close and fee hooks.
  6. TradeVolumeRecorded(CLOSE).

For fillCloseRequests, the two overloaded FillCloseRequest result events are different: the implementation finishes every quote first, then the external facet emits those result events in input order.

Fees and unchanged behavior

The platform close fee is separate

The per-quote net check covers accumulated funding and realized PnL. It does not include PartyA's platform close fee. The fee remains filledAmount * closedPrice * closeFee / 1e36 and is charged after settlement. If PartyA cannot pay it, the transaction reverts.

What stays the same

This per-quote netting change does not alter Muon verification, signed UPNL solvency checks, the funding amount recorded for a close, partial-close rules, lock calculations, quote statuses, connection cleanup, hook selectors, or fee formulas. The dedicated accumulated-funding batch function also continues to settle its quote IDs sequentially.