Close-to-Liquidation Execution

How a solver closes the largest valid part of a request near PartyA's solvency boundary, how a one-shot best-effort request handles the remainder, and why a manager may permit a controlled overshoot.

Why a close can make PartyA insolvent

v0.8.5 introduced fillCloseRequestToLiquidation for a close request that cannot be filled completely without crossing PartyA's solvency boundary. v0.8.6 builds on that path in two ways. It adds a close-only order type that cancels an unfilled remainder, and it lets a manager configure a bounded below-zero target for the handoff into liquidation.

Closing a position does more than reduce its quantity. The close releases part of the quote's locked CVA and LF, realizes PnL at the actual fill price, and charges the protocol close fee. A fee-aware fill can also charge a solver fee. The actual fill price can differ from the signed market price used in the solvency snapshot. A large close may execute away from that market reference because of price impact, especially in thin, lower-cap markets. The combined balance change can therefore be negative even though PartyA, the trader, was solvent before the close.

This becomes dangerous when PartyA is close to liquidation. An unfavorable fill and the fees can consume more balance than the close releases. Outside the existing oracle-less bindable mode, the ordinary fillCloseRequest path reverts if the full request would leave PartyA below the permitted balance boundary. A normal Muon-verified fill cannot create an unapproved shortfall.

Closing to the liquidation boundary

fillCloseRequestToLiquidation exists for the case where PartyA is solvent now but the requested close is too large to execute safely. For Muon-verified execution, PartyB, the quote's solver, supplies the close price and a price and uPnL snapshot. The protocol first simulates the requested amount, including released collateral, realized PnL, protocol fees, and any quoted solver fee.

If the whole bounded amount leaves PartyA nonnegative, the method fills it. A harmful close that starts from a positive balance may also fill the whole amount when its shortfall fits the configured overshoot. Otherwise, the planner calculates the largest amount inside the boundary. The method then executes that amount through the normal close pipeline, including bound-mode handling, the applicable Muon and solvency checks, settlement, uPnL counters, hooks, and fill events.

flowchart TD
    A[Validate pending close and signed snapshot] --> B[Simulate the bounded amount]
    B --> C{PartyA remains nonnegative?}
    C -->|Yes| D[Fill the bounded amount]
    C -->|No| E{Harmful close and PartyA starts positive?}
    E -->|No| F[Revert without changing the request]
    E -->|Yes| G{Shortfall fits configured overshoot?}
    G -->|Yes| D
    G -->|No| H[Calculate the largest amount inside the boundary]
    H --> I{Positive amount and valid remainder?}
    I -->|No| F
    I -->|Yes| J[Execute the normal close pipeline]

With no solver cap, the bounded amount is the pending quantityToClose. The fee-aware method can apply a smaller maxFillAmount to a LIMIT request. The calculation never fills more than either bound.

When the helper fills, partially fills, or reverts

Situation Result
The full bounded amount leaves PartyA nonnegative Fill the full bounded amount
PartyA starts positive and a harmful full close leaves a shortfall inside the configured overshoot Fill the full bounded amount
PartyA starts positive, but a harmful full close would exceed the configured overshoot Fill the calculated boundary amount
PartyA is already at or below zero before a harmful close Revert because this helper cannot deepen an existing shortfall
The close is beneficial or neutral, but the full close still leaves PartyA insolvent Revert because a smaller close cannot restore solvency
The calculation returns zero or the remaining position is invalid Revert and preserve the pending request
Muon-verified execution would leave PartyB insolvent Revert regardless of PartyA's result

Consider a simplified harmful close:

PartyA available balance before close       +25
Net effect of filling the full request       -76
PartyA balance after the full request        -51

ordinary fillCloseRequest                 reverts
zero-rate fillCloseRequestToLiquidation    chooses a smaller amount
PartyA balance after that partial fill      approximately 0

The helper uses the exact on-chain components and integer rounding rather than this simplified net rate. The example shows the decision: PartyA has positive headroom, but not enough to execute the entire harmful close.

Execution guardrails

  • The quote must have a live or cancellation-pending close request and an unexpired deadline.
  • The close price must satisfy PartyA's requested threshold. The signed snapshot must be valid when Muon verification applies.
  • The order type must be LIMIT or MARKET_BEST_EFFORT.
  • The calculated amount must be positive and cannot exceed the request or the caller's permitted cap.
  • A partial fill must preserve the minimum remaining quote value and nonzero proportional locked-value slices.
  • Fee-aware execution must stay within PartyA's close solver-fee rate cap.

Any failed check reverts the whole transaction. No partial close, cancellation-state change, fee charge, or hook result remains after the revert.

Why MARKET_BEST_EFFORT was added

A partial close-to-liquidation fill on a LIMIT request leaves the unfilled request pending. That is useful when PartyA wants later fills at the same limit, but it does not express a one-shot instruction: close as much as the protocol can execute now, then clear the rest of this request.

The existing MARKET type cannot express that instruction. It is full-fill-only and is rejected by close-to-liquidation execution. v0.8.6 therefore appends MARKET_BEST_EFFORT as OrderType value 2. It is valid only for close requests. Opening a quote with value 2 reverts, including openings routed through AccountLayer or InstantLayer.

Order type Ordinary fill Close-to-liquidation Unfilled request
LIMIT (0) Full or partial Supported Remains pending
MARKET (1) Full only Rejected No remainder after its required full fill
MARKET_BEST_EFFORT (2) Full only while the request is live Supported Cancelled after a partial boundary fill

PartyA creates the request through the existing requestToClosePosition selector. If the full requested amount is valid, PartyB can fill it normally or through the close-to-liquidation method. If only part is valid, the protocol moves the request to CANCEL_CLOSE_PENDING, fills the calculated amount once, and uses the existing close finalizer to clear quantityToClose and requestedClosePrice. The quote returns to OPENED unless the position itself was fully closed.

PartyA may still request manual cancellation before execution. Once the request is in CANCEL_CLOSE_PENDING, PartyB may use the ordinary fill path with a smaller amount. The existing cancellation finalizer then clears the remainder, matching the cancellation PartyA already requested.

Why stopping at zero is fragile

For a harmful request that cannot be filled completely, the original zero-rate calculation stops PartyA at approximately zero available balance. This prepares the account for liquidation, but the close and the liquidation occur in separate transactions. A small favorable move in the remaining market exposure can make PartyA solvent again. The liquidation transaction then rejects the account, and operations may have to repeat the close attempt.

sequenceDiagram
    participant Solver
    participant Core
    participant Market
    participant Liquidator

    Solver->>Core: Close PartyA to the zero boundary
    Core-->>Solver: Partial close succeeds
    Market-->>Market: Price moves in PartyA's favor
    Liquidator->>Core: Start PartyA liquidation
    Core--xLiquidator: PartyA is solvent again

What liquidation overshoot changes

Liquidation overshoot lets the close-to-liquidation planner cross below zero by a manager-configured amount. The planner uses PartyA's remaining account-level locked CVA and LF after the candidate close as its threshold:

postCloseThreshold = remaining account locked CVA + remaining account locked LF
allowedShortfall   = floor(postCloseThreshold * effectiveRate / 1e18)

accepted when postCloseBalance >= -allowedShortfall

For example, if the post-close threshold is $10, a 5 bps rate is stored as 5e14 and permits at most $0.005 of PartyA shortfall. The planner can therefore close slightly more than the zero-rate amount, while the exact post-close check prevents the shortfall from exceeding that allowance.

The overshoot reduces the chance that a small favorable move makes PartyA solvent before liquidation starts. It does not guarantee liquidation. A favorable move larger than the overshoot can still restore solvency, and an adverse move can deepen the shortfall before the liquidator submits the next transaction.

The allowance applies only to these close-to-liquidation methods. Outside bindable mode, a normal fillCloseRequest still requires PartyA to finish solvent. Overshoot also does not relax the refusal to deepen a shortfall that existed before a harmful close, or the refusal of a non-harmful close that cannot restore solvency. The Muon-verified path still requires PartyB to finish solvent. The existing bindable-mode exemption is unchanged.

Configuration and inheritance

  • setPartyBLiquidationOvershootRate(partyB, 0, rate) sets a PartyB default.
  • A nonzero symbol inherits symbol 0 unless it has an explicit override.
  • A symbol-specific rate of zero explicitly disables a nonzero PartyB default for that symbol.
  • clearPartyBLiquidationOvershootRateOverride restores inheritance from symbol 0.
  • getPartyBLiquidationOvershootRate returns the effective rate and whether an override exists.

Only PARTY_B_MANAGER_ROLE can set or clear a rate. The target must be a registered PartyB, the symbol must exist or be symbol 0, and the rate cannot exceed 1e18. Every PartyB starts with an effective rate of zero, so an upgrade does not enable overshoot by default. PartyB supplies neither the rate nor the resulting allowance during execution.

Remaining-value fallback

An overshoot can increase the fill enough to leave a nonzero position remainder below minAcceptableQuoteValue. The planner does not accept that dust remainder. It recalculates the capped amount with a zero overshoot rate and uses that smaller amount if the resulting remainder is valid. If neither amount is valid, the transaction reverts with PartyBFacet: Remaining quote value is low. A full close remains valid because its remainder is zero.

The preview applies the same fallback. Its canCloseAll result means the full pending close fits within the configured allowance. With a nonzero overshoot, it does not mean PartyA remains solvent after that close.

Entrypoints, previews, and solver fees

The legacy entrypoint preserves the v0.8.5 selector:

function fillCloseRequestToLiquidation(
    uint256 quoteId,
    uint256 closedPrice,
    PairUpnlAndPriceSig memory upnlSig
) external returns (uint256 filledAmount);

It accounts for the protocol close fee but does not reserve balance for a solver fee. Use the fee-aware PartyBExecutionFacet overload whenever execution charges one:

function fillCloseRequestToLiquidation(
    uint256 quoteId,
    uint256 maxFillAmount,
    uint256 closedPrice,
    PairUpnlAndPriceSig memory upnlSig,
    SolverFeeEntry[] calldata maxSolverFees
) external returns (uint256 filledAmount);

Each entry's amount is quoted for maxFillAmount and routes to the receiver resolved for its tag; the planner budgets the list total. If the boundary permits less, execution charges amount * filledAmount / maxFillAmount per entry and drops entries that floor to zero. A fee-bearing call should use a concrete quantity. Passing an effectively unbounded maxFillAmount can make every prorated entry round down to zero.

For LIMIT, maxFillAmount may cap the transaction below the protocol boundary and leave PartyA solvent. For MARKET_BEST_EFFORT, it must be at least the full pending quantityToClose. PartyB cannot use that argument to choose a smaller best-effort fill. See Solver Fees for the complete solver-fee model.

Integrators can preview the uncapped plan with:

function getMaxCloseAmountToLiquidation(
    uint256 quoteId,
    uint256 closedPrice,
    uint256 marketPrice,
    int256 upnlPartyA,
    uint256 maxSolverFee
) external view returns (uint256 maxCloseAmount, bool canCloseAll);

The preview uses the full pending close quantity as its bound and fee basis. Treat it as an estimate. The signed values and on-chain state can change before execution.

Integration and events

  • Core clients append MARKET_BEST_EFFORT as enum value 2. Values 0 and 1 keep their existing meanings.
  • AccountLayer mirrors value 2. A partial close leaves the position open, so its Virtual Account continues tracking the quote.
  • InstantLayer signed close operations can forward value 2 without a new selector or template. Opening operations still reject it.
  • MARKET_BEST_EFFORT adds no new close-request selector, storage field, or dedicated event.
  • Force close remains restricted to LIMIT close requests.
  • SetPartyBLiquidationOvershootRate records the PartyB, symbol, old and new effective rates, and override state.
  • PartyALiquidationOvershootUsed is emitted only when the final PartyA balance is negative. It reports the effective rate, allowed shortfall, and actual shortfall.
  • FillCloseRequest reports the actual fill and closeId. A partial best-effort fill adds no new cancellation event because the cancellation and close complete inside the same transaction.

After a partial MARKET_BEST_EFFORT fill returns the quote to OPENED, bots must stop retrying the completed request. PartyA must create a new close request and a new closeId for the remaining position.

On the fee-aware path, the SolverFeeCharged events precede PartyALiquidationOvershootUsed when an overshoot is used, and FillCloseRequest follows both. Indexers should still read the final quote status and actual filled amount instead of inferring them from the preview.

Risk boundary

Overshoot intentionally permits a PartyA shortfall. The allowance is based on the full remaining account CVA and LF, not only the closing quote's remaining LF. A large rate can move the later liquidation into LATE and change which balance absorbs the deficit. A further adverse market move can still push it into OVERDUE.

Managers should therefore configure overshoot as an explicit liquidation-risk decision. The zero default preserves the original solvency boundary. Nonzero values trade a bounded PartyA shortfall for a more reliable handoff between the close and the separate liquidation transaction.