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
LIMITorMARKET_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
0unless it has an explicit override. - A symbol-specific rate of zero explicitly disables a nonzero PartyB default for that symbol.
clearPartyBLiquidationOvershootRateOverriderestores inheritance from symbol0.getPartyBLiquidationOvershootRatereturns 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_EFFORTas enum value2. Values0and1keep 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
2without a new selector or template. Opening operations still reject it. MARKET_BEST_EFFORTadds no new close-request selector, storage field, or dedicated event.- Force close remains restricted to
LIMITclose requests. -
SetPartyBLiquidationOvershootRaterecords the PartyB, symbol, old and new effective rates, and override state. -
PartyALiquidationOvershootUsedis emitted only when the final PartyA balance is negative. It reports the effective rate, allowed shortfall, and actual shortfall. -
FillCloseRequestreports the actual fill andcloseId. 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.