Summary
In cross mode, one shared PartyB allocation backs positions against multiple PartyAs. When one of those PartyAs is liquidated,
the liquidation path can release balances locked for its positions before that PartyA's positive PnL owed by the PartyB is
actually paid. Core records the unpaid amount in partyBLiquidationSettlementReserve[partyB].
That reserve must behave like unavailable capital until settlePartyALiquidation consumes it or the Clearing House
takeover path clears it. v0.8.6 moves the reserve check into the shared PartyB availability helpers so it is enforced
consistently across cross-bucket operations.
The bug
The risky window existed after liquidatePositionsPartyA freed the cross PartyB locked balances but before the
liquidator completed settlePartyALiquidation. During that window, raw
partyBAllocatedBalances[partyB][address(0)] had not yet been debited for the pending settlement.
Affected paths
The reserve now applies to the shared cross-mode PartyB availability used by:
lockQuote, throughpartyBAvailableForQuote.deallocateForPartyBin cross mode, through the same quote-availability helper.safeDeallocateForPartyBin cross mode, through that same helper before its additional safe-path floors.- Open/close/funding/settlement solvency checks, through
partyBAvailableBalanceForLiquidation. liquidateCrossPartyBeligibility, so a PartyB can be liquidatable once reserved debt is counted.
Core tracks each PartyA liquidation's positive pending contribution in every PartyB margin mode. The availability helper
returns zero for an isolated PartyB, but if that PartyB switches to cross mode before settlement completes, the recorded
contribution becomes unavailable in the shared bucket. resolveLiquidationDispute resynchronizes the contribution
when it overrides a pending amount. Normal PartyA settlement consumes it, and Clearing House takeover clears it. The aggregate
reserve is subtracted only while the PartyB is currently in cross-mode accounting.
Fix
The fix centralizes reserve enforcement in LibAccount. Both quote availability and liquidation-style availability
subtract partyBLiquidationSettlementReserve for cross-mode PartyBs.
return available - int256(partyBLiquidationSettlementReserve(accountLayout, partyB));
function partyBAvailableBalanceForLiquidation(
int256 upnl,
address partyB,
address partyA
) internal view returns (int256) {
return partyBAvailableBalanceForLiquidation(upnl, partyB, partyA, true);
}
The old one-off subtraction was removed from PartyBAccountFacetImpl.deallocateForPartyB. Deallocation now uses
the same reserve-aware helper as quote locking, which avoids double-counting and keeps all cross-bucket availability checks on
one rule.
Settlement top-up exception
One path intentionally bypasses the reserve check: settlePartyBUpnlForLiquidation. That function exists so a
liquidator can realize a cross PartyB's positive uPNL from other solvent PartyAs before paying the pending liquidation
settlement. If the reserve were applied to that pre-settlement solvency check, the top-up path could be blocked by the same
reserve it is trying to fund.
To keep the bypass narrow, LibSettlement.settleUpnlUnified now accepts an explicit
applyPartyBLiquidationReserve flag. Normal settlement and force-close settlement pass true.
settlePartyBUpnlForLiquidation passes false and then requires the PartyB's cross allocated balance
to be non-decreasing.
uint256 partyBBalanceBefore = accountLayout.partyBAllocatedBalances[sig.partyB][address(0)];
LibSettlement.settleUpnlUnified(sig, updatedPrices, true, false, false);
require(
accountLayout.partyBAllocatedBalances[sig.partyB][address(0)] >= partyBBalanceBefore,
"SettlementFacet: PartyB balance decreased"
);
Resulting invariant
- Quote locking uses reserve-adjusted cross allocation, including the exact pass/fail boundary.
- Cross-PartyB liquidation eligibility counts pending settlement debt even when raw allocation alone still appears solvent.
- The liquidation top-up path alone bypasses the reserve, and it may not reduce the PartyB cross-allocation bucket.