PerpLandperpland

liquidation & funding

Two mechanisms force positions to eventually close: a health check on price drops, and a funding rate on time held. Together they remove the "hold a position forever" exit path that made the original Uniperp attack profitable.

the health check

Every external swap fires _scanAndLiquidate in the hook's afterSwap. The scan iterates open positions (capped per swap and per block) and checks each one's health:

holdingValueAtTwap = holding × P(TWAP) effectiveDebt      = pos.debtETH + accruedInterest(pos) health             = holdingValueAtTwap / effectiveDebt if health < 105% → liquidate

The price source is the 5-minute TWAP. This is deliberate: a flash crash in spot can't manufacture liquidations of healthy positions, and a flash pump can't hide an actually-underwater position.

liquidation flow

  • Sells the position's tokens at spot through the same pool.
  • Refills bands with the proceeds, nearest-active-first.
  • If sell yields more than debt, surplus goes to the user (minus 1% close fee → stakers).
  • If sell yields less than debt, the shortfall is realized as bad debt.

defenses around liquidation

Three guards layer onto the basic flow:

  • D4 — positions are immune to liquidation for LIQ_COOLDOWN_BLOCKS = 2 after open. Symmetric with the close cooldown — no asymmetric same-tx exit.
  • M-01 — if current spot has drifted more than 1500 ticks from TWAP at scan-time, the scan defers entirely. An MEV bot can't sandwich a liquidation by manipulating spot.
  • per-swap cap MAX_LIQS_PER_SWAP = 10, MAX_LIQS_PER_BLOCK = 5. Bounds liquidation cascades so one external swap can't domino-liquidate the entire book.

funding rate

Effective debt grows over time at 50% APR on the principal. Implementation is a Compound-style cumulative index:

cumulativeFundingIndexE18 += elapsed × FUNDING_RATE_PER_SECOND_E18 accrued(pos) = pos.debt × (currentIndex − pos.openIndex) / 1e18 effectiveDebt = pos.debt + accrued(pos)

No ETH actually moves — the funding rate just inflates effective debt in the health calculation. A lev-5 position with no price movement becomes liquidatable in roughly 1 year. Abandoned positions can't hide forever.

what happens if liquidation produces bad debt

totalBadDebtETH tracks realized bad debt protocol-wide. Once it crosses BAD_DEBT_PAUSE_THRESHOLD = 5 ETH, new opens auto-pause (D9). Closes and liquidations continue so users can always exit. The owner can heal bad debt via donateAndRefillBands() — ETH deposited refills the bands first, with the bad-debt counter reduced accordingly.