Summary
When an account does not have enough funds to keep its positions open, liquidation closes them and settles what each party owes. Muon, the external service that reports profit and loss, calculates a total from groups of positions. The contract calculates each position separately and adds the results.
Both calculations use tiny whole-number accounting units. If division leaves a fraction of a unit, that fraction is dropped. This is rounding. Dropping fractions before adding the results can give a slightly different total from adding first. Even with the same prices and positions, the totals can differ by a few units. Previously, even a one-unit difference could stop the liquidation and require manual resolution.
v0.8.6 accepts only a tiny difference, limited by how many positions the account had when liquidation started. When both calculations agree on who owes money, settlement uses the smaller amount: for example, 100 instead of 101 accounting units, not whole tokens. If they disagree on who owes the net payment, that payment is zero. Larger differences still require a dispute to be resolved.
The update also fixes how saved funding totals change when part of a position closes, provides a one-time repair for totals that are already wrong, and gives Muon exact position values to calculate from.
Why liquidation totals can differ
Integer division truncates toward zero: it removes the fractional part, so cut(4.8) = 4 and
cut(−4.8) = −4. Consider two values with decimals that the contract cannot store:
Quote-by-quote: cut(4.8) + cut(5.8) = 4 + 5 = 9
Group total: cut(4.8 + 5.8) = cut(10.6) = 10
The difference comes from when each calculation truncates; it does not imply incorrect price or funding data. Price PnL can lose less than one unit per quote, and funding can lose less than two. This gives a maximum of three raw units for each position, provided the stored totals and Muon inputs are exact.
The position count is saved at liquidation start, so closing positions in several batches cannot shrink or grow the limit. The final settlement amount is the smaller magnitude when both calculations agree on its direction. For example, losses of 100 and 101 settle at 100. No separate payment is created for the difference or assigned to another account.
Applying the cap at settlement
Closing a position removes its PartyB connection as soon as that pair has no open positions or pending quotes. Pending-quote cleanup follows the same rule. A saved settlement does not keep an empty connection alive. Once the final position closes, the contract checks the rounding bound and records the target UPNL and the signed reduction still needed. PartyB settlement amounts are not changed in the position-closing transaction.
settlePartyALiquidation and its snapshot variant use the PartyB addresses supplied by the caller. Before paying
each pending settlement, the contract reduces its expected amount toward zero and reduces its actual amount in the same
direction without crossing zero. Existing OVERDUE haircuts and funding classification still apply. Only buckets with the
needed sign consume the reduction, in caller-supplied order; it is not a proportional split between PartyBs.
Muon loss = 100; quote loss = 102; reduction needed = 2
First settlement: quote payment 1 becomes 0; reduction left = 1
Second settlement: quote payment 101 becomes 100; reduction left = 0
Total paid = 100; liquidation can finish
Partial batches carry only the remaining reduction in partyALiquidationRoundingReduction. Empty batches do not
consume it; unknown, duplicate, or already-settled PartyBs revert the transaction. Normal finalization requires every PartyB
settlement and the whole reduction to be consumed. Positive PartyB reserves remain conservative until payment, then their
contributions are released as usual. No new rounding event, vault transfer, or permanent counter is introduced.
If positive signed UPNL overstated PartyA's deferred credit, that credit is corrected once when the target is recorded after
the final position closes. The deduction is min(deferred balance, signed UPNL - target UPNL): a zero balance
stays zero, and an insufficient balance is reduced only to zero without reverting liquidation. A dispute-role override cancels
the unconsumed automatic reduction and uses the supplied amounts; Clearing House takeover also cancels the unconsumed
reduction. Neither path reverses payments already made. A new liquidation starts with no pending reduction, and normal
completion clears it.
Why partial closes left dust
A funding group stores the sum of each open quote's contribution, truncated toward zero. The old close path truncated each closed slice independently. The truncated slices did not always add up to the original contribution: a 60% close followed by a 40% close could leave one raw unit after the position reached zero.
Old behavior: remove cut(closed amount × funding)
New behavior: remove cut(old open amount × funding)
− cut(new open amount × funding)
The new calculation makes the stored value exactly match the remaining quote after every partial close. When the last position in a pair or global PartyB group closes, its funding total is also set to zero. Reopening the same group then starts without old dust.
Exact Muon PnL input
The original UPNL views return an average open price. Dividing notional by amount to calculate that average can drop a remainder. Multiplying the truncated average by the amount later cannot restore it.
Old LONG price PnL: (price − truncated average) × amount / 1e18
New LONG price PnL: (price × amount − exact notional) / 1e18
v0.8.6 adds three views: getPartyAExactNotionalUpnlData, getPartyBExactNotionalUpnlData, and
getPartyBGlobalExactNotionalUpnlData. They return the stored aggregatedNotional. The old views
remain unchanged for existing readers. The corresponding Muon implementation must enable the exact-notional path for contract
addresses listed in MUON_EXACT_NOTIONAL_CONTRACTS.
Muon must calculate each funded group once and add that result to total UPNL and, when negative, to total unrealized loss. Using the same result prevents the two signed fields from disagreeing because of different truncation.
Repairing existing groups
Fixing future partial closes does not correct funding totals that are already wrong. We therefore make a one-time repair of the affected groups, so their saved totals match the positions that are still open.
While the protocol is paused, a script reads the open quotes in manageable pages and recalculates the totals using the same arithmetic as the contract. Doing this work off chain avoids running out of transaction gas when a group contains many quotes.
An authorized operator reviews the results and submits them through resyncAggregateFunding. This corrects the
affected PartyA/PartyB funding totals and PartyB's overall funding total; it does not change positions or transfer money. The
contract rejects the repair if the checked values changed in the meantime. After verifying the repaired totals, activity can
resume with the new rules keeping them accurate.
Safe rollout
Plan the upgrade against the target diamond's deployed state. Deployments already running the v0.8.6 rounding release have the three-unit allowance and liquidation-start position count; this update does not introduce or enable them.
The settlement-time cap appends one mapping to AccountStorage.Layout without changing public function signatures
or existing storage offsets. Finish all active PartyA liquidations, including takeovers, on the previous implementation before
upgrading this flow. An in-progress liquidation may contain retained connections or already-adjusted buckets from the old
implementation; this update does not migrate that intermediate state. The shared liquidation-start helper also changes, so
rebuild and relink both legacy and snapshot setup paths together with settlement and Clearing House callers.
The deployment scope includes MigrationFacet, ViewFacetAggregate, and ViewFacetQuote,
plus the funding and liquidation changes in PartyBBatchActionsFacet, PartyBLiquidationFacet,
PartyBPositionActionsFacetImpl, SymbolAdjustmentFacet, ClearingHouseFacetImpl,
LibQuoteClose, LibForceActions, and LibPartyALiquidationProcess. Changes to internal
funding helpers are compiled into their callers, so a source-file list alone is not a complete deployment list.
Build the final deployment list from the compiled bytecode and artifact linkReferences. If a linked library gets
a new address, relink and redeploy every caller, including callers of those callers. This includes
ClearingHouseFacet, ForceActionsFacet, ForceCloseStepsFacet, PartyAFacet,
both PartyA liquidation facets, PartyBEmergencyActionsFacet, PartyBExecutionFacet,
PartyBPositionActionsFacet, and PartyBQuoteActionsFacet. Deploy library dependencies before their
callers, then update the diamond's facet mappings.
- Record the target chain, diamond address, deployed facets, and library addresses. Prepare the complete deployment and selector plan above, then rehearse the upgrade and repair against a recent fork of that deployment.
- Identify the funding groups that need repair and finish active PartyA and PartyB liquidations involving them before the global pause. The repair rejects liquidating parties, and a global pause also blocks liquidation settlement. If a liquidation cannot finish, resolve it through the existing dispute or Clearing House flow before repairing its groups.
- Globally pause the protocol and confirm that the affected accounts are no longer liquidating. Deploy the updated libraries and dependent facets, refresh the target's selector inventory, and apply the reviewed diamond cut. Verify its facet mappings, the repair function, and the exact-notional getters before calculating repairs.
- Use the repair script to calculate the correct totals without submitting changes. Review the proposed corrections and keep the protocol paused throughout the calculation and repair.
- Apply the approved repairs in manageable batches, then verify the corrected totals before unpausing.
-
Activate the corresponding Muon implementation, add the deployed chain and diamond address to
MUON_EXACT_NOTIONAL_CONTRACTS, then restart Muon. - Verify repaired funding totals and exact-notional Muon calculations, then resume affected actions.
When the fixed allowance accepts a difference, both parties settle using the smaller same-direction amount. The discarded difference is not a payment and is not emitted or assigned to a vault or account. Only the unapplied reduction is stored temporarily while settlement batches are in progress.