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

Overview

Kairos Protocol Documentation

Kairos is a decentralized interest rate swap AMM. Traders open positions that pay or receive a stream of interest payments indexed to an on-chain rate oracle; LPs supply the collateral that backs the other side of those payments.

This documentation is written for the people who interact with the protocol on-chain:

  • Traders / buyers — how to open a swap, manage it, settle it at expiry, or exit early.

  • LPs — how the shared pool works, how shares are priced, and what gates sit in front of deposits and withdrawals.

  • Market creators — how to create a BUY_FIXED / BUY_FLOATING market pair and manage ownership over time.

  • Integrators — how the onBehalfOf authorization pattern lets bundlers, routers, wrappers, and vault adapters act on behalf of end users.

  • Keepers / liquidators — what to watch for and how to settle or liquidate positions.

  • Indexers — the events emitted across every lifecycle transition.

SwapCore

SwapCore.sol is the heart of Kairos. It owns the full lifecycle of an interest rate swap: market creation, liquidity provisioning, opening positions, settlement, early exit, and liquidation. Everything a user, LP, keeper, or integrator does on-chain flows through one of its roughly two dozen external functions.

This chapter is a function-by-function reference. Start here for the mental model, then jump to the page that matches your role.

What SwapCore does

Kairos is a decentralized interest rate swap AMM. Traders enter positions that pay or receive a stream of interest payments indexed to an on-chain rate oracle, and LPs supply the collateral that backs the other side of those payments.

Every rate market that SwapCore creates is actually a pair:

  • BUY_FIXED — the buyer pays a locked-in fixed rate and receives the floating rate (from the reference rate oracle). Useful for hedging variable-rate exposure or speculating that rates will rise.

  • BUY_FLOATING — the buyer pays the floating rate and receives a locked-in fixed rate. Useful for locking in a yield or speculating that rates will fall.

Both sides of the pair share the same reference rate oracle, base rate oracle, collateral token, swap term, and leverage multiplier. LPs supply collateral to a single, share-based pool per market; buyers post their own collateral when they open a swap. The pool uses time buckets (numBuckets × bucketInterval) to aggregate mark-to-market P&L so share prices stay tractable even with many open positions.

Three inputs determine the all-in rate a buyer locks in at buySwap:

  1. Base rate from the base swap rate oracle (tenor-dependent)

  2. Utilization fee — a kinked curve integrated over the range [uPre, uPost] as the swap consumes pool capacity, priced against seasoned (EMA) liquidity rather than the instantaneous pool balance (see Core concepts → Seasoned utilization fee)

  3. Risk premium — an optional oracle-supplied markup, different per side of the pair

Actors

Actor
What they do
Key functions

Market owner

Anyone who calls createMarket (paying the optional creation fee) becomes the owner of the resulting pair of markets. The owner can optionally manage an LP whitelist, can terminate the market (stops new swaps), and receives a share of protocol fees if the creator fee is configured in Admin.

Liquidity Provider (LP)

Deposits the market's collateral token into the pool, receives shares priced at the pool's current mark-to-market value, earns yield from swap fees and settlement inflows.

Buyer

Opens a swap by posting collateral, holds the position until expiry, and receives the net payment (or pays it) at settlement. Can transfer or (if enabled) exit early.

Keeper

Permissionless role. Settles swaps at expiry in batches to keep the pool's accounting current. Anyone can do this — there is no keeper whitelist.

Liquidator

Permissionless role. Calls liquidateSwap on an underwater position to recover the shortfall and collect the market's liquidationIncentive.

Integrator / bundler

Contracts that act on behalf of end users (vault adapters, wrappers, routers). Must be pre-authorized by the end user via setAuthorization for any onBehalfOf call.

setAuthorization, all onBehalfOf entry points

End-to-end lifecycle

A swap's life runs through five phases. The function names below link to their reference entries.

1. Market setup

The market owner calls createMarket with oracle addresses, collateral token, leverage, swap term, fee-curve parameters, bucket configuration, and optional whitelist flag. SwapCore deploys both sides of the market pair (BUY_FIXED + BUY_FLOATING) atomically, charges the protocol's market creation fee (if configured in Admin), and initializes the pool, buckets, and rate index for each side.

Ownership can be handed off later via transferMarketOwnership + acceptMarketOwnership (2-step), or the market can be permanently closed to new swaps via terminateMarket. If terminateMarket is called, existing swaps continue to expiry as normal.

2. LP provisioning

LPs call supplyCollateral to deposit the market's collateral token. Shares are minted at the current mark-to-market share price (floor WAD for an empty pool, getPoolSharePrice(marketId) otherwise).

Three gates sit in front of a deposit:

  • 1-block deposit locklpPosition.lastDepositBlock is stamped on every deposit; a withdrawal in the same block reverts. Prevents same-block deposit+withdraw flash-loan attacks.

  • Underwater floor — deposits are blocked when the pool is at MIN_SHARE_PRICE with active swaps (lockedCollateral > 0), so new LPs aren't instantly diluted into an underwater pool.

  • Oracle floor — for SpotRate / SpotCompoundRate markets, deposits revert if the reference oracle index is at its floor (MIN_INDEX). Cumulative oracles are monotonic and unaffected.

Profit vest (LP share-price cap)

Every deposit records a profit-vest anchor on the LP's position: entryPrice (the share price at deposit) and vestEndTimestamp (block.timestamp + admin.lpProfitVestSeconds(), default 12h, bounded to [1h, 7d] and timelocked). While the vest window is active, withdrawCollateral burns the LP's shares at min(currentSharePrice, entryPrice) instead of the live share price. This neutralizes the canonical deposit → oracle-update → withdraw NAV-sandwich attack on LP shares.

The cap has three defining properties:

  • One-directional. It binds only when the live share price is above the anchor. Below-anchor exits realize the full loss — the vest is not a put option.

  • Uniform across the position. Every share in a single LP position prices at the capped rate while the window is active; there is no per-tranche accounting. The anchor never ratchets upward, so a mid-vest top-up at a price at or above the anchor leaves it unchanged. A top-up below the anchor blends it downward to a share-weighted average of the old anchor and the current price, so the new capital can't shelter under a stale high mark. The anchor is only fully reset to the current price when the prior window has expired or the position has been completely exited. A consequence: topping up at a price above the anchor realizes a haircut on the new shares if withdrawn before vest expiry — holding past vestEndTimestamp restores full value.

  • Always extended. Each new deposit pushes vestEndTimestamp out by the current lpProfitVestSeconds, giving the latest capital a full protection window. Existing positions keep their original window across admin config changes.

Off-chain surfaces should read getLpVestState(marketId, lp)(entryPrice, vestEndTimestamp, capActive) on the Views contract (a separate read-only helper deployed alongside SwapCore — not a SwapCore method) and warn an LP before they supply or withdraw while capActive is true.

Withdrawals

Withdrawals happen through withdrawCollateral, which burns shares and transfers the proportional amount of underlying collateral (capped per the profit vest above). Passing a withdrawal amount at or above the position's full value is treated as a full exit. Several guards apply:

  • The withdrawal is blocked if the pool has expired unsettled swaps (the share price would be stale until they're settled).

  • When the pool is idle (lockedCollateral == 0), the share price is computed directly from totalCollateral / totalShares and the oracle is bypassed entirely — LP withdrawals stay available even if the reference oracle is reverting.

  • A last-LP guard prevents pool.totalShares from reaching zero while lockedCollateral > 0: a single share is retained so the pool's accounting stays alive for any remaining active swaps.

  • If a full last-LP exit on an idle pool would otherwise sweep cap-protected surplus, the call reverts — the LP can wait for vest expiry or make a partial withdrawal instead.

The CollateralTokenWithdrawn event carries a capApplied flag indicating whether the profit-vest cap bound on that withdrawal.

3. Opening a swap

A buyer calls buySwap with the target market, notional amount, an onBehalfOf owner, and three slippage guards:

  • rateBound — for BUY_FIXED, a ceiling on the locked-in swapRate (reverts if the rate would be higher); for BUY_FLOATING, a floor on the baseRate received (reverts if it would be lower). Pass type(int256).max to disable.

  • maxMarkup — ceiling on utilFee + riskPremium (in WAD). Pass 0 to disable.

  • maxTotalIn — ceiling on the total tokens pulled from the caller (requiredBuyerCollateral + protocolFee + liquidationBounty). Pass type(uint256).max to disable. This guard is necessary because buyer collateral is sized off abs(baseRate), so a favorable rateBound check (a BUY_FLOATING with a high base rate, or a BUY_FIXED with a negative base rate) can still inflate the collateral posted — rateBound alone does not bound the tokens spent.

SwapCore reads the base rate (tenor-aware) and reference-rate index from their oracles, computes the utilization fee by integrating the kinked fee curve across the capacity the new swap consumes, and reads the risk premium (0 if no oracle is set). For BUY_FIXED, these combine into the final locked-in swapRate (base + utilFee + riskPremium); for BUY_FLOATING, swapRate is left at 0 — the floating leg isn't known until settlement, so the rate is derived from the entry index versus the expiry index.

It then derives the required buyer collateral (sized off abs(baseRate) plus fees, to cover the buyer's maximum payment) and LP collateral backing (sized off baseRate, to cover the LP's maximum payment), and checks that both are at least minCollateral (dust guard) and that the pool has enough available collateral. After the slippage checks pass, it:

  1. Pulls requiredBuyerCollateral + protocolFee + liquidationBounty from msg.sender in a single transfer.

  2. Routes the protocol fee to the protocol multisig, splitting off the creator-fee share to the market owner if configured. Both transfers are best-effort so a bad recipient never bricks the swap: if the market owner can't receive, its share falls back to the multisig; if the multisig can't receive, the protocol share is refunded to the payer and a ProtocolFeeForegone event is emitted rather than the fee being held on the contract.

  3. Reserves the LP backing by moving it from the pool's available balance into pool.lockedCollateral.

  4. Prefunds the liquidationBounty (carved out of the buyer's posted amount and tracked separately, so bucket P&L stays accurate). It is returned to the buyer at normal expiry if the swap is never liquidated.

The new swap is assigned to a time bucket ((entryTimestamp / bucketInterval) % numBuckets) and appended to the market's expiryQueue, so future share-price calculations only need to iterate over buckets instead of all open swaps. A SwapCreated event is emitted with the full entry snapshot (rates, collateral both sides, util fee, risk premium, protocol fee, liquidation bounty, entry index).

4. Swap management (during the term)

5. Swap closure

Three paths take a swap to settled = true. All three release the locked collateral, zero out the bucket slot, and advance the market's expiryQueuePointer past the now-settled entry. All three emit SwapClosed (the unified indexer-friendly closure event); liquidation additionally emits SwapLiquidated with the liquidator-specific details.

  • Normal expiry — makePayment. Permissionless. Called after entryTimestamp + swapTerm. Computes the fixed and floating legs over the full term, nets them, and transfers the net to the winner. If the buyer's token transfer fails (e.g. blacklisted address), the payout is held in escrowedCollateral[swapId] and can be pulled later via claimEscrow. makePayment accepts an array of swap IDs — keepers batch.

  • Early exit — exitSwapEarly. Only the swap owner (or an authorized delegate). Requires market.earlyExitAllowed = true. Settles using the current index, applies the market's earlyExitFee, and enforces per-swap minExitAmount slippage protection.

  • Liquidation — liquidateSwap. Permissionless. Triggered when accrued (not projected) payments exceed the liquidatable side's available collateral — the buyer's collateral balance on the buyer side, or, on the pool side, the LP's pool backing net of the liquidation incentive (poolBacking × (1 − liquidationIncentive)), so the incentive is always coverable. Buyer-side liquidations pay the prefunded liquidationBounty to the liquidator; pool-side liquidations pay liquidationIncentive × the swap's pool backing.

After closure the buyer can call claimEscrow if their settlement transfer failed. Funds always go back to the original swap.userAddress — no admin can redirect them.

Happy-path sequence

Data structures you'll see in this chapter

All defined in Types.sol:

  • Market — full market config: oracles, pool, fee curve, risk settings, whitelist flag, market owner, termination flag

  • Pool(totalShares, totalCollateral, lockedCollateral)

  • SwapPosition — the swap record: marketId, userAddress, settled + isEarlyExit flags, collateralBalance, notionalAmount, rates (baseRate + swapRate, both signed; swapRate is 0 for BUY_FLOATING and derived at settlement), entryTimestamp, entryFloatingIndex (the entry index), poolCollateralBacking, utilFee, riskPremium, liquidationBounty

  • LpPosition(shares, lastDepositBlock, entryPrice, vestEndTimestamp) — the last two are the profit-vest anchor and window end (see LP provisioning)

  • Bucket — per-bucket aggregates, in struct order: lpNotional, weightedLpRate (signed), weightedEntryTime, weightedInverseIndex (harmonic-mean term for the floating rate), weightedUtilFee, weightedRiskPremium, totalBuyerCollateral, totalPoolBacking, and weightedFeeTime (the fee×entry-time term for exact MTM accrual)

  • ExitRequest(swapId, minExitAmount) input to exitSwapEarly

  • SettlementResult(settlementAmount, netRecipient) returned by makePayment / exitSwapEarly. settlementAmount is the net obligation in swap-token decimals; netRecipient is a uint8 direction flag — 0 means the LP pool receives, 1 means the buyer receives.

  • RateConvention — how the reference rate oracle reports data: Cumulative, SpotRate, or SpotCompoundRate

  • RateTypeBUY_FIXED = 0, BUY_FLOATING = 1

Chapter layout

Page
Covers

createMarket, transferMarketOwnership, acceptMarketOwnership, terminateMarket, setMarketLpWhitelist, getAllMarketIds

supplyCollateral, withdrawCollateral, and all LP/pool view functions

buySwap, makePayment, exitSwapEarly, liquidateSwap, transferSwapPosition, claimEscrow

setAuthorization, isAuthorized, and the onBehalfOf pattern

Read-only P&L, liquidity, oracle-index, and mapping getters

Integrator-facing events emitted across the lifecycle

Last updated