> 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, and how index snapshots drive settlement.

Source: `SwapCore.sol`, `lib/SwapFormulas.sol`, `lib/Utils.sol`, `lib/RateIndexLib.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`.

On every LP `supplyCollateral` / `withdrawCollateral`, the EMA is advanced toward the current 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 |

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 spot immediately (because of the `min`).

***

### 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 curve is continuous and smooth at the kink by construction.

**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 `18e18 = 18×`, 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 × (|baseRate| + utilFee + riskPremium) × term / (YEAR × leverage)
lpBacking       = notional ×  |baseRate|                          × term / (YEAR × leverage)
```

and a pool's capacity is the inverse relationship:

```
availableLiquidity = collateral × leverage × YEAR / (|baseRate| × term)
```

Two 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 base rate (`|baseRate|`), which is why a *favorable* rate can still inflate the collateral pulled — see `maxTotalIn` on [`buySwap`](/protocol/swaps.md#buyswap).
* **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 BUY\_FIXED markets on a Cumulative oracle reject a negative base rate (`E512`). SpotRate / SpotCompoundRate are **not** clamped and can represent negative rates (e.g. perp funding), subject to the `MIN_INDEX` floor.

***

### 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, `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`.

**Settlement refuses to extrapolate — `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. There is no live-index fallback. 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.

**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 during the outage instead of being blocked on `E450`. It's a resilience measure, not a step every settlement requires.

**Views project; settlement doesn't.** This is the deliberate asymmetry behind the two P\&L reads on the [Views page](/protocol/views.md): `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 and fails closed with `E450` if one isn't there.


---

# 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.
