> For the complete documentation index, see [llms.txt](https://docs.kairosswap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kairosswap.com/protocol/core-concepts.md).

# Core Concepts

The [reference pages](/protocol/overview.md) document what each function does. This page covers the mechanics that span multiple functions and aren't obvious from any single one: how the utilization fee is priced, the shape of the fee curve, how leverage sizes collateral, how the floating rate is derived per oracle convention, how open swaps are cohorted into buckets, and how index snapshots drive settlement.

Source: `SwapCore.sol`, `lib/SwapFormulas.sol`, `lib/Utils.sol`, `lib/RateIndexLib.sol`, `Admin.sol`.

***

### Seasoned utilization fee

The utilization fee a buyer pays at `buySwap` is **not** priced against the pool's live collateral. It's priced against a **seasoned exponential moving average** (EMA) of `pool.totalCollateral`, tracked per market in `utilAcc[marketId]` (`Types.UtilAccumulator = { avgTotalCollateral, lastUpdate }`) over a fixed `UTIL_AVG_WINDOW = 1 hour`.

The EMA is advanced at **every point `pool.totalCollateral` changes, and on every buy**: LP `supplyCollateral` and `withdrawCollateral`, each swap settled inside `makePayment`, `liquidateSwap`, and `buySwap` itself. Each advancement moves the average toward the pre-mutation spot collateral, weighted by how much of the window has elapsed:

```
newAvg = avg + (spot − avg) × elapsed / window     // spot ≥ avg
newAvg = avg − (avg − spot) × elapsed / window     // spot < avg
```

The fee is then charged against `min(EMA, spot)` — never more than the live balance, but lagging recent increases.

**Why it works this way — the JIT-liquidity defense.** The accumulator is advanced with the *pre-deposit* collateral, so a just-in-time LP deposit made in the same block earns `elapsed = 0` → **zero weight** in the EMA that block. That means a JIT deposit **immediately expands capacity** but **cannot suppress the util fee**, so an LP can't deposit → cheapen a buyer's fee → withdraw after the 1-block lock. Capacity and pricing are deliberately decoupled:

|                                                  | Basis                                             | Rationale                                      |
| ------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------- |
| **Capacity gate** (`validateSwapParams`, `E508`) | **Spot** liquidity (`totalLPAvailableCollateral`) | Fresh deposits enable volume immediately       |
| **Utilization fee**                              | **Seasoned** `min(EMA, spot)`                     | Fresh deposits can't front-run fee compression |

Withdrawals are asymmetric on purpose: after a withdrawal the **stored EMA is hard-clamped down** to the post-withdrawal pool (never raised). Withdrawn liquidity is recognized instantly, and a withdraw → redeposit cycle can't re-enter fresh liquidity as "already seasoned" — the read-time `min(EMA, spot)` alone wouldn't catch that, because the redeposit restores spot back up to the stale average.

The EMA seeds to spot on a market's first deposit and snaps back to spot once a full window elapses with no activity, so a quiet market doesn't ramp from a stale value.

> **For quoting:** a buyer's util fee reflects the pool's liquidity over the last \~hour, not the instantaneous balance. Right after a large LP deposit the fee stays elevated until the EMA catches up; right after a large withdrawal it's charged against the lower balance immediately (the stored EMA is clamped down, not just capped at read time).

***

### The utilization fee curve

The fee is a **kinked curve** in utilization `u`, defined by three market params: `utilFeeSlopeWad` (linear slope), `kinkUtilization` (where the curve steepens), and `maxKinkFeeWad` (the extra fee added between the kink and 100%). The marginal rate `f(u)` is:

```
u ≤ kink:   f(u) = slope × u
u > kink:   f(u) = slope × kink + maxKinkFee × ((u − kink) / (1 − kink))²
```

so it rises linearly to `slope × kink` at the kink, then adds a quadratic term reaching `f(100%) = slope × kink + maxKinkFee` at full utilization. The marginal rate is **continuous at the kink, but not smooth**: its derivative drops from `slope` to zero there (the quadratic term starts flat). The *total fee paid* — the integral of the curve — is continuously differentiable, so there's no fee cliff crossing the kink.

**Path-independence.** A swap consuming capacity from `uPre` to `uPost` is charged the **definite integral of the curve over `[uPre, uPost]`, averaged over the trade size** — not the endpoint rate. The direct consequence: splitting one trade into N smaller trades produces the **exact same total fee** as doing it in one shot. There's no gaming the fee by slicing orders.

A trade that exceeds the seasoned in-band capacity pays a notional-weighted blend: the in-capacity slice at its integrated band rate, and the excess at the `f(100%)` ceiling rate.

***

### Leverage

`leverageMultiplier` is a per-market constant (WAD; `1e18 = 1×`, capped at `12e18 = 12×`, immutable after creation). Higher leverage lets the same collateral support proportionally more notional: **at 5× leverage, backing a given notional takes one-fifth the collateral it would at 1×** — for both the buyer and the LP side. Equivalently, a pool of a given size can back 5× the notional.

Concretely, the two sides each post:

```
buyerCollateral = notional × (rc + utilFee + riskPremium) × term / (YEAR × leverage)
lpBacking       = notional ×  r                           × term / (YEAR × leverage)
```

where `r = max(|baseRate|, MIN_RATE)` is this market's own base-rate magnitude and `rc` is the same floor applied to the **pair-level collateral base rate** (below). A pool's capacity is the inverse relationship, on the market's own rate:

```
availableLiquidity = collateral × leverage × YEAR / (r × term)
```

Things to note:

* **Buyer collateral includes the fees; LP backing doesn't.** The buyer posts enough to cover their maximum payment (base rate + util fee + risk premium); the LP side backs only the base-rate exposure. Both are sized off the **magnitude** of the rate, which is why a *favorable* rate can still inflate the collateral pulled — see `maxTotalIn` on [`buySwap`](/protocol/swaps.md#buyswap).
* **Buyer collateral is sized off the pair, not just this market.** The two sides of a pair are priced off separate base-rate oracles, whose quotes can differ. Buyer collateral uses the **larger-magnitude of this market's and its twin's base quotes** (`collateralBaseRate`): the losing side's collateral is the winning side's payout cap in a paired liquidation, so sizing off the lower own-rate quote would let an equal-notional hedge across both sides pocket the quote spread risk-free once both legs saturate their caps. LP backing and the capacity gate stay on the market's own rate, so LP capital efficiency is untouched.
* **Every rate in these formulas is floored at `MIN_RATE = 1e14` (0.01%).** A near-zero base rate would otherwise collapse required collateral toward zero and blow available liquidity toward infinity; the floor keeps both bounded and the divisions well-defined.
* **Higher leverage → thinner buffer → faster liquidation.** A smaller collateral cushion per notional means a position crosses its liquidation threshold sooner. Leverage is fixed at creation and shared by both sides of the pair.

***

### Rate conventions

Every reference-rate oracle reports under one of three conventions, chosen at the market that first uses it and **immutable per oracle thereafter** (a later market trying to register a different convention on the same oracle reverts `E607`). The convention determines how the on-chain index is maintained and how a BUY\_FLOATING swap's realized floating rate is derived at settlement from the entry and expiry index.

| Convention           | Oracle reports                                | Index maintenance                                                                     | Realized rate from `entryIdx → expiryIdx`        |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ |
| **Cumulative**       | a monotone index (Aave/Morpho/Compound style) | stored as-is                                                                          | `(expiryIdx / entryIdx − 1) × YEAR / elapsed`    |
| **SpotRate**         | an instantaneous rate                         | accumulates `Σ rate × Δt`; `index = INITIAL × (1 + crt/(WAD·YEAR))` (simple interest) | growth rate, corrected by `× entryIdx / INITIAL` |
| **SpotCompoundRate** | an instantaneous rate                         | accumulates `Σ rate × Δt`; `index = INITIAL × exp(crt/YEAR)` (continuous compounding) | `ln(expiryIdx / entryIdx) × YEAR / elapsed`      |

**Cumulative clamps downward moves.** A Cumulative index is held to a monotonic non-decreasing floor: if the source reports a value below the last recorded index, it's raised back to the floor and an `IndexClamped` event is emitted. A genuinely decreasing rate therefore reads as flat under this convention, and the realized floating leg can never go negative — which is why markets on a Cumulative oracle reject a negative base rate at swap entry (`E512`). The gate is keyed on the oracle convention and applies to **both sides of the pair**, before the rate-type branch: on BUY\_FIXED a negative fixed leg would hand the buyer a one-sided claim on LP backing (floating ≥ 0 > fixed at every settlement), and on BUY\_FLOATING it would open a position every remaining-tenor consumer (mark-to-market share pricing, early exit, NAV views) immediately rejects with the same error — unpriceable and un-exitable until maturity. SpotRate / SpotCompoundRate are **not** clamped and can represent negative rates (e.g. perp funding), subject to the `MIN_INDEX` floor.

***

### Buckets and rate bands

Open swaps aren't valued one by one for pool accounting — each is folded into a **bucket** of weighted aggregates, and share pricing / pool NAV are computed over buckets. A swap's bucket is keyed by **two coordinates**: its entry-time window *and* its entry-rate band:

```
bucketId = ((entryTimestamp / bucketInterval) % numBuckets) × NUM_RATE_BANDS + rateBand
```

with `NUM_RATE_BANDS = 9`. The bands ladder the LP-leg base rate geometrically: seven interior bands covering `(1.5%, 28%]` whose width grows in proportion to the rate, plus two open end caps (`≤ 1.5%` including negatives, and `> 28%`). The banding key is the swap's `baseRate` for **both** rate types, and the ladder is identical across all three oracle conventions.

The reason for the second coordinate: within one time window, the entry rate is what determines whether positions win or lose together, and the per-bucket collateral clamp used in conservative pricing is only exact for cohorts that do. Sub-bucketing by rate band means opposite-rate positions never share a clamp, and the proportional band widths keep the clamp's behavior uniform across rate regimes. An occupancy bitmap tracks which of the `numBuckets × 9` sub-buckets are live, so valuation sweeps only occupied cohorts.

***

### The settlement / snapshot model

SwapCore keeps an append-only history of index snapshots per oracle, written whenever a state-changing function runs or when anyone calls the permissionless `updateMarketRateIndex`. Two writes within `MIN_SNAPSHOT_GAP` (12s) collapse into one slot.

**`makePayment` snapshots the index itself.** On the normal-expiry path, while inside the settlement grace window (below), `makePayment` makes a best-effort `updateMarketRateIndex` call *before* resolving the expiry index. Because a swap can only be settled once `block.timestamp ≥ entryTimestamp + swapTerm`, that self-update writes a snapshot at or after maturity — so in the normal case (a healthy oracle) settlement satisfies its own precondition and just proceeds. Integrators do **not** need to pre-snapshot before calling `makePayment`.

**Inside the grace window, settlement fails closed — `E450`.** After the best-effort update, settlement requires a stored snapshot **at or after** `entryTimestamp + swapTerm`; if none exists it reverts `E450` rather than projecting past the last known data. Given that `makePayment` just tried to write that snapshot, `E450` only surfaces when the internal update **couldn't** write one — i.e. the reference oracle is **unhealthy at settlement time** (the update reverts and is swallowed by the surrounding `try/catch`) **and** no earlier at-or-after-expiry snapshot was captured while the oracle was still healthy. This fail-closed behavior holds for a bounded window: `settlementGracePeriodSec` past maturity — an admin parameter (default **14 days**, hard bounds **\[7 days, 90 days]**, changes timelocked).

**Past the grace window, settlement always completes.** Once `block.timestamp ≥ expiry + settlementGracePeriodSec`, the oracle gets one last chance and then loses its veto:

1. The uncapped best-effort self-update is deliberately **skipped** — a gas-burning oracle dependency could otherwise starve the transaction before the fallback could run.
2. Instead, the real index update is retried once under a **fixed gas budget**. If the transaction can't fund that minimum budget, it reverts `E451` (retry with more gas). If the retry succeeds — the earlier failures were transient — settlement proceeds on the fresh, real snapshot: no extrapolation, no termination.
3. If the retry fails, settlement **proceeds anyway at a deterministic extrapolation** of the frozen snapshot history: the slope is anchored **1 day behind the final snapshot** (through the final snapshot's value), pricing the missing interval at the oracle's trailing average rate. Younger histories anchor at their oldest snapshot; a single-snapshot history extrapolates flat. The settlement emits `ExpiryIndexExtrapolated`.
4. Engaging the fallback **permanently terminates both markets in the pair** for new swaps — the twin prices against the same reference oracle and the same extrapolation-scarred history, so the dead-oracle verdict applies to it equally. Existing swaps still settle normally as they mature.

The consequence: a dead reference oracle cannot brick a market forever. Every open swap becomes settleable within `swapTerm + grace` of the oracle's death, so the expired-unsettled block on LP deposits and withdrawals is bounded, and the market winds itself down on a known schedule.

**Where a keeper snapshot actually helps.** Because a post-expiry snapshot captured during a healthy period satisfies the guard even if the oracle later fails, permissionlessly calling `updateMarketRateIndex` shortly after a swap matures is **insurance against a subsequent oracle outage** — it lets settlement complete on real data during the grace window instead of being blocked on `E450`, and avoids the extrapolated fallback (and pair termination) after it. It's a resilience measure, not a step every settlement requires.

**Views project for active swaps; expired swaps are different.** For an **active** swap, `getSwapNetAmount` and friends project a virtual index forward (so an off-chain reader sees the value settlement would realize, and a permissionless `makePayment` can't move a number they already cached in-transaction), while settlement itself only ever reads real stored snapshots. For an **expired** swap with no maturity-bracketing snapshot, the views revert `E450` — and they keep doing so even past the grace window, where `makePayment` itself would fallback-settle. The views never guess at the extrapolated outcome; settle the position to realize its value in that state.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kairosswap.com/protocol/core-concepts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
