PartyA Liquidation Snapshot Flow

The v0.8.6 snapshot liquidation path makes settlement use the same funding state that Muon signed when PartyA became liquidatable, so later PartyB funding updates cannot change the liquidation debt.

Summary

PartyA liquidation is supposed to settle from the accounting snapshot that made PartyA liquidatable. That snapshot includes PartyA UPNL, liquidation prices, and the funding debt implied by those prices and funding accumulators.

Before v0.8.6, the price-only liquidation path froze symbol prices from the Muon liquidation signature, but accumulated funding could still be derived from funding storage later, when liquidatePositionsPartyA was called. If an epoch passed and funding was rolled, or if epoch duration changed after funding accrued, settlement could mix frozen liquidation prices with the wrong funding value.

v0.8.6 adds a Muon-signed snapshot liquidation path. Muon now signs PartyA historical insolvency data plus the price and cumulative funding values needed for each (PartyB, symbolId) pair.

Affected scenario

The risky flow was:

  1. PartyA becomes liquidatable.
  2. Liquidator calls liquidatePartyA(partyA, liquidationSig).
  3. Liquidator calls setSymbolsPrice(partyA, liquidationSig).
  4. An epoch passes and funding is rolled, or epoch duration changes after funding has accrued.
  5. Liquidator later calls liquidatePositionsPartyA(partyA, quoteIds).
  6. Settlement is calculated with frozen liquidation prices, but funding may be reconstructed from storage that has moved past the liquidation timestamp.

Same-epoch funding updates are not the issue. The issue is losing historical reconstruction: live storage no longer returns the funding value Muon saw at liquidation time.

Impact

The issue could affect liquidation accounting when live funding storage no longer reconstructs the funding value at the liquidation timestamp.

Actual outcomes:

  • When the changed funding made partyAAccumulatedUpnl differ from the signed liquidation UPNL, the liquidation was marked disputed by the existing equality check.
  • In rare cases, wrong funding debts across positions or PartyBs could cancel out so PartyA's total UPNL stayed unchanged, allowing liquidation to pass with wrong settlement accounting.

New Muon payload

The snapshot path uses LiquidationSnapshotSig:

struct LiquidationPartyBSymbolState {
    address partyB;
    uint256 symbolId;
    uint256 price;
    int256 cumulativeLongFee;
    int256 cumulativeShortFee;
}

struct LiquidationSnapshotSig {
    bytes reqId;
    uint256 timestamp;
    bytes liquidationId;
    int256 upnl;
    int256 totalUnrealizedLoss;
    LiquidationPartyBSymbolState[] states;
    uint256 liquidationBlockNumber;
    uint256 liquidationTimestamp;
    uint256 liquidationAllocatedBalance;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}

Each states entry freezes one PartyB-symbol pair:

  • partyB: the solver side of the position.
  • symbolId: the market.
  • price: liquidation price for that PartyB-symbol pair.
  • cumulativeLongFee: cumulative long funding value at the liquidation timestamp.
  • cumulativeShortFee: cumulative short funding value at the liquidation timestamp.

When the snapshot flow was added, the liquidation start path was refactored around historical insolvency data. That removed the need for a separate snapshot-specific deferred signature. LiquidationSnapshotSig carries liquidationBlockNumber, liquidationTimestamp, and liquidationAllocatedBalance directly.

The same change also reorganized the liquidation code so the price-only and snapshot paths reuse the same core liquidation process logic. The refactor split path-specific setup into smaller libraries and did not change the existing price-only liquidation rules.

In the multi-step path, liquidatePartyAWithSnapshot starts liquidation with no PartyB-symbol states. A later setSymbolsPriceWithSnapshot call submits those states for the same liquidation and stores the signed prices and funding values.

Snapshot storage model

The snapshot path adds two storage concepts to AccountStorage:

mapping(address => mapping(bytes => bool)) liquidationUsesPartyBSymbolSnapshots;
mapping(address => mapping(bytes => mapping(address => mapping(uint256 => LiquidationPartyBSymbolSnapshot)))) liquidationPartyBSymbolSnapshots;

liquidationUsesPartyBSymbolSnapshots is needed because the same open-position liquidation code supports both paths. For a normal price-only liquidation, it reads symbolsPrices and reconstructs funding with getAccumulatedFundingFee from the live funding state when positions are processed. For a snapshot-backed liquidation, it must read signed PartyB-symbol data instead. The marker tells the liquidation code which source is allowed.

liquidationPartyBSymbolSnapshots stores the signed data for:

partyA -> liquidationId -> partyB -> symbolId

The stored snapshot contains:

struct LiquidationPartyBSymbolSnapshot {
    bool isSet;
    uint256 price;
    int256 cumulativeLongFee;
    int256 cumulativeShortFee;
}

The active PartyA liquidation's signed liquidationId is the storage key.

In the multi-step path, these fields are populated in two calls:

  • liquidatePartyAWithSnapshot starts liquidation and sets liquidationUsesPartyBSymbolSnapshots[partyA][liquidationId] = true. This call requires an empty states array.
  • setSymbolsPriceWithSnapshot verifies that the signed liquidationId and timestamp match the active liquidation, then writes each states entry into liquidationPartyBSymbolSnapshots.

The snapshot facet emits SetPartyALiquidationSnapshot with partyBs, symbolIds, prices, and cumulative funding arrays. It also emits the existing SetSymbolsPrices event for compatibility with existing liquidation indexers.

Method order before and now

The liquidation phases stay the same. What changes in v0.8.6 is the setup and price/funding source.

Path Liquidator call order Price and funding source
Before v0.8.6, normal price-only
  1. liquidatePartyA(partyA, liquidationSig)
  2. setSymbolsPrice(partyA, liquidationSig)
  3. liquidatePendingPositionsPartyA(partyA)
  4. liquidatePositionsPartyA(partyA, quoteIds)
  5. settlePartyALiquidation(partyA, partyBs)
Prices came from setSymbolsPrice. Funding was reconstructed later from getAccumulatedFundingFee using the then-live funding state.
Before v0.8.6, deferred price-only
  1. deferredLiquidatePartyA(partyA, deferredLiquidationSig)
  2. deferredSetSymbolsPrice(partyA, deferredLiquidationSig)
  3. liquidatePendingPositionsPartyA(partyA)
  4. liquidatePositionsPartyA(partyA, quoteIds)
  5. settlePartyALiquidation(partyA, partyBs)
Prices came from deferredSetSymbolsPrice. Funding was still reconstructed later from getAccumulatedFundingFee using the then-live funding state.
v0.8.6 snapshot
  1. liquidatePartyAWithSnapshot(partyA, liquidationSnapshotSig) with empty states
  2. setSymbolsPriceWithSnapshot(partyA, liquidationSnapshotSig) with signed states
  3. liquidatePendingPositionsPartyAWithSnapshot(partyA)
  4. liquidatePositionsPartyAWithSnapshot(partyA, quoteIds)
  5. settlePartyALiquidationWithSnapshot(partyA, partyBs)
Prices and cumulative funding come from the signed PartyB-symbol snapshot stored by setSymbolsPriceWithSnapshot.

New settlement behavior

When liquidatePositionsPartyA processes a quote, it reads liquidationUsesPartyBSymbolSnapshots[partyA][liquidationId] to decide which accounting path to use.

If snapshot-backed pricing is active:

  • the quote must have a snapshot for (quote.partyB, quote.symbolId);
  • the close price comes from that snapshot;
  • accumulated funding is calculated from the signed cumulative long/short values;
  • pairs without accumulated funding active at the liquidation timestamp are represented by zero cumulative values; and
  • missing state reverts with LiquidationFacet: Missing signed state.

If snapshot-backed pricing is not active:

  • the non-snapshot path remains unchanged;
  • price comes from setSymbolsPrice; and
  • funding is calculated with getAccumulatedFundingFee(quote.id) when the position is processed.

Snapshot-backed pricing is all-or-nothing for an active liquidation. Once the snapshot marker is set, the liquidation cannot silently mix signed funding for some quotes with live-storage funding for others.

Price-only deprecation flag

An address with PROTOCOL_CONFIG_ROLE can require the new snapshot flow for future liquidation starts by calling:

setLegacyPartyALiquidationDeprecated(true)

When enabled, these price-only liquidation entrypoints revert with LiquidationFacet: Legacy liquidation deprecated:

  • liquidatePartyA
  • deferredLiquidatePartyA

The flag does not abort a legacy liquidation that was already started: its later price, position-processing, and settlement steps remain available so the in-flight liquidation can finish.