Skip to content

Real liquidation price + off-chain liquidation keeper#168

Merged
ozpool merged 1 commit into
mainfrom
feat/liquidations
May 29, 2026
Merged

Real liquidation price + off-chain liquidation keeper#168
ozpool merged 1 commit into
mainfrom
feat/liquidations

Conversation

@ozpool
Copy link
Copy Markdown
Owner

@ozpool ozpool commented May 29, 2026

What

Builds out point-4 liquidations in two halves.

Liquidation price. account_summary served a hardcoded "0" for every position's liquidation price. It now computes the real value via the shared margin math (perplex-core::margin, the same code the on-chain engine mirrors), so the position read path / UI gets a true liq price. Cross-margin approximation: each position is priced against the whole account's collateral — exact for the common single-position case, conservative otherwise.

Keeper. Nothing automatically closed underwater positions, so a losing trader could sit below maintenance margin and leave the protocol holding bad debt. Added:

  • AppState::scan_liquidatable — every open position with health factor < 1.0.
  • AppState::liquidate_position — force-close at mark, realise PnL into the vault (bad debt clamps collateral to 0; the on-chain InsuranceFund absorbs it once trading is on-chain), remove the position, record the close on the tape + the user's fills. Returns None for a healthy position so a racing keeper can't close it.
  • Admin routes GET /v1/admin/liquidatable and POST /v1/admin/liquidate, gated by x-admin-secret against PERPLEX_ADMIN_SECRET (routes 401 when the secret is unset).
  • crates/perplex-liquidator — the keeper binary; polls the scan endpoint and liquidates each target on an interval.

Test

  • 32 edge tests green (incl. a new test asserting account_summary fills a real, sub-entry liq price for a long). fmt + clippy clean across the workspace.
  • Manual end-to-end against local Postgres: opened a 500-BTC long held deep underwater, ran the perplex-liquidator binary — it found the position, force-closed it, the position vanished from memory and Postgres, the liquidation fill was recorded, and a healthy counterparty short was left untouched.

Notes

  • This keeper acts on the current off-chain edge (the source of truth for positions today). When the trade flow moves on-chain it swaps its two HTTP calls for a PositionRegistry read + LiquidationEngine.liquidate(); the health math is already shared via perplex-core.
  • Bad debt is clamped to zero collateral for now rather than drawn from the InsuranceFund — that hook lands with on-chain settlement.

Two halves of the liquidation pipeline the perp DEX was missing.

Liquidation price: account_summary served a hardcoded "0" for every
position's liquidation price. Wire it to the shared margin math
(perplex-core::margin, the same code the on-chain engine mirrors) so the
position read path returns a real liq price. Cross-margin approximation:
each position is priced against the whole account's collateral — exact
for the common single-position case, conservative otherwise.

Keeper: nothing automatically closed underwater positions, so a losing
trader's position could sit below maintenance margin and leave the
protocol holding bad debt. Add:
  - AppState::scan_liquidatable — every open position with health < 1.0.
  - AppState::liquidate_position — force-close at mark, realise PnL into
    the vault (bad debt clamps collateral to 0; the on-chain InsuranceFund
    absorbs it once the trade flow is on-chain), remove the position, and
    record the close on the public tape + the user's fills. Returns None
    when the position is healthy, so a racing keeper can't close it.
  - Admin routes GET /v1/admin/liquidatable and POST /v1/admin/liquidate,
    gated by the x-admin-secret header against PERPLEX_ADMIN_SECRET
    (routes 401 when the secret is unset, so a misconfigured deploy can't
    expose force-close).
  - crates/perplex-liquidator: the keeper binary. Polls the scan endpoint
    and liquidates each target on an interval. When trading moves on-chain
    it swaps its two HTTP calls for a PositionRegistry read +
    LiquidationEngine.liquidate(); the health math already lives in
    perplex-core and is shared by both sides.
@ozpool ozpool merged commit 2c447ef into main May 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant