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% → liquidateThe 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 = 2after open. Symmetric with the close cooldown — no asymmetric same-tx exit. - M-01 — if current spot has drifted more than
1500 ticksfrom 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.