For the complete documentation index, see llms.txt. This page is also available as Markdown.

Core Concepts

The reference pages 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 = 0zero 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:

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:

and a pool's capacity is the inverse relationship:

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.

  • 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: 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.

Last updated