# Perp Funding & Liquidation — marginX vs Hyperliquid / Extended / Pacifica / Lighter

**Scope:** how funding is computed, what interval it applies on, *how it is accounted/settled* (the core question), and how liquidation works — compared across marginX (realtime.exchange) and four competitors.

**Sources:** marginX from the **contract code** (`clob-dex/src/perpetual/*`, line refs below); competitors **strictly from their official docs** (URLs at the end). Date: 2026‑05‑25.

---

## 0. TL;DR — the answer to "history-and-sum vs last-rate-×-interval"

There are really **three** funding-accounting patterns, not two:

| Pattern | Who uses it | How a held position's funding is computed |
|---|---|---|
| **A. Cumulative funding index** (dYdX/GMX style) | *Nobody in this set, literally* (HL exposes it only as an API read-out) | Per-market running index `F` accrues each interval. Position stores `F_entry`. On close: `size × (F_now − F_entry)`. Sums varying rates "for free", O(1), no keeper-per-position. |
| **B. Eager per-hour snapshot** | **Hyperliquid, Extended, Pacifica, Lighter** | Every hour the protocol charges *that hour's* rate to every open position and realizes it to balance: `size × mark × rate_thishour`. Over a 10h hold = 10 separate charges, each at its own rate ⇒ varying rates summed correctly. No entry-index, no close-time summation. |
| **C. Lazy snapshot at touch** | **marginX (us)** | Funding is **only** computed when the position is touched (increase / close / liquidation). At that moment: `size × rate_now × hours_since_last_touch`. Uses **one** rate — the rate at settlement time — for the *entire* elapsed interval. |

**Direct answer to your question:**
- *"Do they keep per-hour history and sum across different rates at close?"* → That is pattern **A** (literal index) or **B** (repeated hourly settlement). The four competitors achieve per-hour accuracy via **B** — they settle each hour at that hour's rate. None literally store per-position history and sum at close; the canonical cheap way to do that is the index (**A**), which none of them use on-chain in this set.
- *"Or do they calculate the last funding and multiply by the interval?"* → **That is exactly us (pattern C).** marginX takes the **current** funding rate and multiplies by the whole-hours elapsed since the last position modification. **Intra-period rate changes are ignored.**

So marginX is the only one of the five that does *not* charge each hour at that hour's rate. (More below, including a low-cost upgrade path that reuses scaffolding already in our code.)

---

## 1. marginX funding — exactly what the code does

**Charging point:** `PositionHandler.updateFees` (`src/perpetual/PositionHandler.sol:489`), the single place fees are charged. It runs **only** from `increasePositionFromMatch` (a fill that opens/increases) and `_decreasePositionInternal` (close / partial / liquidation). A resting order charges nothing. There is **no hourly keeper** that pokes open positions.

**Rate basis = open-interest imbalance, NOT price premium** (lines 547‑571):
```
totalOI   = longOI + shortOI
imbalance = longOI − shortOI
pif       = imbalance * 1e18 / totalOI            //  −1e18 … 1e18
adjRate   = BASE_FUNDING_RATE * (1 ± |pif|)        //  clamped to [MIN, MAX]
```
- `BASE_FUNDING_RATE = 1e14` = **0.01%/hr**; `MIN = 1e13` (0.001%/hr); `MAX = 1e15` = **0.1%/hr cap**.
- Dominant OI side **pays**, minority side **receives**; **balanced book ⇒ funding = 0** (live-verified: EUR‑PERP with longOI=shortOI shows `cumulativeFundingFee = 0`).
- This is fundamentally different from every competitor, which derives the rate from the **premium** (mark / impact price vs oracle/index) plus an interest component. Ours uses *who is more crowded*, not *how far perp price is from spot*.

**Amount = current rate × whole hours elapsed** (lines 585‑589):
```
hoursElapsed     = (block.timestamp − position.increasedAtTime) / 3600   // integer; <1h ⇒ 0
periodFundingFee = sizeInUsd * adjRate * hoursElapsed / 1e18 / 10^(18−collDecimals)
```
- `increasedAtTime` is **reset on every modification** (Security Rules 18‑19), so funding settles at each touch and the clock restarts.
- `adjRate` is computed from the OI imbalance **at the moment of this call** — i.e. one snapshot rate applied across the full `hoursElapsed`.

**Settlement:** deducted from (or credited to) `position.collateralAmount` immediately in `updateFees` (lines 618‑635). Health checks use the same formula via `MarketHandler.estimatePendingFundingFee` so liquidation accounts for not-yet-realized funding.

**Dead scaffolding worth noting:** `globalCumulativeFundingFee` is **read at line 496 and written back unchanged at line 657** — it is never accumulated. Someone stubbed a per-market funding index and never wired it. (See §5 — this is the natural home for a real index.)

**Two extra costs competitors don't have** (funding on all four competitors is pure peer-to-peer, no protocol cut):
- **Borrowing fee** — always-on, `BASE_BORROWING_RATE = 100` = **1% APR** on notional, per whole hour, regardless of imbalance (lines 598‑600).
- **Position fee** — **0.1%** of notional on every increase/close *fill* (not first open, not liquidation) (lines 605‑607).

---

## 2. Funding comparison chart

| | **marginX** | **Hyperliquid** | **Extended** | **Pacifica** | **Lighter** |
|---|---|---|---|---|---|
| **Interval** | whole hours, **lazy** | hourly (1/8 of 8h rate) | hourly (8h realization) | hourly (5s‑sampled TWAP) | hourly (per‑min premium) |
| **Accounting model** | **C — lazy snapshot** | **B — eager per‑hr snapshot** (+API cumulative read-out) | **B — eager per‑hr snapshot** | **B — eager per‑hr snapshot** (TWAP then reset) | **B — eager per‑hr snapshot** |
| **Rate over a multi-hr hold** | one rate (at touch) × hours | each hour at its own rate | each hour at its own rate | each hour at its own rate | each hour at its own rate |
| **Rate driver** | **OI imbalance** (long vs short) | **premium** (mark vs oracle)+interest | premium (impact vs index)+interest | premium (impact/oracle)+interest | premium (impact vs index)+interest |
| **Interest component** | none | 0.01%/8h | 0.01%/8h | 0.01% | 0.01% |
| **Clamp / cap** | rate ∈ [0.001%, **0.1%**]/hr | ±0.05% clamp, **4%/hr cap** | ±0.05% clamp, **0.25–2%/hr** by group | ±0.05% clamp, **±4%/hr** | ±0.05% clamp, **4%/hr** |
| **Settled to** | position collateral, at touch | account balance, each hour | account, each hour | margin balance, each hour | P2P transfer, each hour |
| **Protocol cut on funding** | n/a (funding is P2P‑equivalent) but **+1% APR borrowing fee** always-on | none (pure P2P) | none | none | none (up to 15% rebate on RWA) |

**Practical impact of being pattern C:** because our base rate is tiny and is **zero on a balanced book**, the dollar error is usually small. But the model is structurally less accurate and **gameable**: you pay based on the imbalance at the instant you close, not on what prevailed while you held. It also under-charges sub-hour holds (integer hours) and resets the clock on every modification.

---

## 3. Liquidation comparison chart

| | **marginX** | **Hyperliquid** | **Extended** | **Pacifica** | **Lighter** |
|---|---|---|---|---|---|
| **Trigger** | isolated: `remainingCollateral < MM`; cross: account `equity·100/MM < 100` | account equity < MM | margin ratio (ΣMM/equity) > 100% | equity < MM | tiered: AV vs IMR/MMR/CMR |
| **Maint. margin** | **1%** synthetic / 5% default (CLAUDE.md says 3% — *discrepancy to reconcile*) | ½ initial @ max lev (e.g. 2.5% @20x) | per market group | **½ initial = 1/(2·maxLev)** | per tier (BTC/ETH 50x: MMR **1.2%**, CMR 0.8%) |
| **Cross + isolated** | both (cross via BalanceManagerVault) | both | unified margin-ratio | both | both (isolated = AllocatedMargin) |
| **Partial liq** | code supports (sizeRatio); **full-close observed** in tests | >100k USDC → 20% + 30s cooldown | **5 steps of 20%** (min $1k) | yes (restore MM) | **partial default**; full only at CMR |
| **Liq price** | `PositionLens.getLiquidationPrice` (isolated exact, 0 for cross) — verified to the tick | formula w/ margin_available | liq price + **bankruptcy price** | formula | **zero prices** (long/short) |
| **Executor** | liquidator reduce-only order vs book; `closePositionDirect` @ oracle | market orders to book; HLP backstop <⅔MM | Extended Vault (auto) | 3-tier: book → backstop <⅔MM → ADL | engine IoC @ zero price; LLP takeover |
| **Penalty / fee** | **0.5%** of returned collateral → liquidator/insurance | retained maintenance margin | **1%** → Extended Vault | **max(0.75%, MMR·0.4)** of liq'd value | **up to 1%** → LLP |
| **Backstop** | InsuranceFund + BackstopVault + UnifiedVault socialization | **HLP** liquidator vault | **Extended Vault** insurance + ADL + socialization | backstop liquidator vault + **ADL** | **LLP** insurance + **ADL** |
| **Explicit ADL** | **no** (insurance/backstop/socialization instead) | not in official docs | yes (within 5% of bankruptcy) | yes (equity < 0) | yes (LLP under-margined) |

**Where marginX is in line with the field:** maintenance-margin trigger, isolated + cross, a fixed liquidation penalty routed to insurance, and a backstop/insurance layer. The boundary is precise to one tick (verified live on Arc & Monad).

**Where marginX differs on liquidation:**
- **No auto-deleveraging (ADL).** All four competitors have an explicit ADL stage as the final backstop; we rely on insurance fund → backstop vault → bad-debt socialization. ADL is a more standard last-resort for solvency.
- **Liquidation needs book liquidity.** Our normal path closes via a reduce-only order matching the book (InsuranceFund/`closePositionDirect` @ oracle is the fallback). HL/Pacifica/Lighter codify a backstop-liquidator vault that *takes over* the position when the book can't absorb it — a cleaner guarantee.
- **Partial-first is explicit and tiered** at every competitor (20% steps, or IMR/MMR/CMR tiers). Ours supports partial in code but liquidates to zero in practice; no documented step schedule.
- **MM% ambiguity:** CLAUDE.md says 3% maintenance, but live `getMarginRequirements` returns 1% (synthetic) / 5% (default). Worth reconciling and documenting per-market tiers like the competitors do.

---

## 4. Two things that make marginX an outlier (summary)

1. **Funding rate is driven by OI imbalance, not price premium.** Everyone else anchors the perp to the index via a premium term (mark/impact vs oracle) + interest. OI imbalance is a *proxy* — a market can be OI-balanced yet trade at a premium (no funding pressure to correct it) or OI-imbalanced at fair price (funding charged with nothing to correct). For a pure CLOB this was a pragmatic choice, but it does not directly perform funding's main job: pulling perp price toward index.
2. **Funding is settled lazily with a single snapshot rate (pattern C).** Competitors settle each hour at that hour's rate (pattern B). Our model is the least accurate over multi-hour holds and is somewhat gameable around close timing and sub-hour modifications.

Plus a minor: we levy an **always-on 1% APR borrowing fee** that the competitors' pure-P2P funding does not.

---

## 5. Recommendation — turn the dead scaffold into a real cumulative index (pattern A)

We don't need an hourly per-position keeper to get per-hour accuracy. The cheap, standard fix is the **cumulative funding index (A)**, and we already have the storage stub:

- Maintain a per-market index `F` in `globalCumulativeFundingFee` (currently read+written unchanged at `PositionHandler.sol:496/657`). On any market touch (or a cheap per-market keeper poke), advance it: `F += rate_now × hoursElapsedSinceLastGlobalUpdate`, tracking a per-market `lastFundingUpdate`.
- Store `F_entry` on the position at open/increase.
- Charge `size × (F_now − F_entry)` at close. This sums varying rates correctly, is O(1), and removes the lazy-snapshot inaccuracy — without any per-position cron.
- Independently, consider switching the rate driver from OI imbalance to a **premium index** (mark/impact vs oracle) + interest, matching the field, so funding actually anchors price to index. (Bigger change; the index refactor above is the higher-value, lower-risk first step.)
- Reconcile the **maintenance-margin** docs (1%/5% live vs 3% in CLAUDE.md) and consider documenting per-market tiers; evaluate adding an explicit **ADL** stage and a **backstop-liquidator vault** that takes over positions the book can't absorb.

*(All of the above is observation/recommendation — no code was changed for this study.)*

---

## Source map

**marginX (code):** funding `PositionHandler.sol:489‑661` (rate 547‑571, amount 585‑589, settle 618‑635, dead index 496/657), constants `MarketHandler.sol:16‑21`; borrowing 598‑600; position fee 605‑607; liquidation penalty 809‑827; margin `DataStore.sol:719`; liq gate `PositionHandler.sol:700`; cross health `BalanceManagerVault.getCrossMarginPortfolioHealth/canLiquidateCrossMargin`; liq price `PositionLens.sol:81`. See also `docs/FUNDING_FEES_LIQUIDATION.md` and `docs/perp-edge-case-tests.csv`.

**Hyperliquid:** funding https://hyperliquid.gitbook.io/hyperliquid-docs/trading/funding · liquidations …/trading/liquidations · margining …/trading/margining · margin-tiers · protocol-vaults (HLP) · API cumFunding fields.

**Extended:** https://docs.extended.exchange/extended-resources/trading/funding-payments · …/trading/liquidation-logic · …/vault.

**Pacifica:** https://docs.pacifica.fi/trading-on-pacifica/funding-rates · …/liquidations · …/margin-and-leverage · …/contract-specifications/oracle-price-and-mark-price.

**Lighter:** https://docs.lighter.xyz/trading/funding (+/funding-rate-rebates) · …/liquidations-and-llp-insurance-fund · …/contract-specifications · whitepaper.pdf.
</content>
</invoke>
