# The Kairos Protocol

Overview of the protocol and popular use cases

## Interest Rate Swap Markets: The Missing DeFi Primitive That Unlocks Long-Term Fixed Rate Lending

<figure><img src="/files/ZdFPvyhCFZfQKunvtSJb" alt=""><figcaption></figcaption></figure>

\
**The Current State of DeFi**

Today's DeFi lending markets are dominated by variable, and at times volatile, rates. Whether you're borrowing from Aave, lending to Morpho, or saving in Ethena, your interest rate can fluctuate in real time with market conditions. While variable rates are practical for certain use cases, the uncertainty is not ideal in many other instances where the user wants to know exactly what rate they will receive or pay over a period of time, such as a 2-year loan.

Additionally, there are limited options to bet on rate directions in a capital efficient manner onchain. If a trader expects Ethena's yield to rise, SOFR to drop, or Aave's borrow rate to spike, they have limited options in expressing that trade.

**Interest Rate Swaps in Traditional Finance**

In traditional finance, interest rate swaps are the critical piece that enables traders to bet on rate directions, borrowers to receive fixed-rate loans, and more. An interest rate swap is an agreement between two parties to trade fixed and floating payment obligations for a set period of time based on a reference rate. For example, a bank may agree to lend at a 5-year fixed rate to a borrower and then purchase a swap in which they receive a variable-rate payment. This allows them to match their assets and liabilities. Or a trader may purchase a floating rate swap for SOFR on the expectation the FED will raise rates. Because interest rate swaps are for the payment obligations, participants can get high leverage relative to the notional amount of the swap.

**Kairos**

Kairos is an onchain primitive that enables the creation of permissionless interest rate swap markets in which parties can exchange fixed and floating-rate payments based on a variety of reference rates and swap terms. The markets function using automated-market makers and pooled liquidity, ensuring aggregated liquidity and permissionless swaps for buyers. Each market is created with a set of parameters that determine how the market functions including a collateral token, reference rate, swap term, and swap type. For example, a market could be created that offered 3-month fixed-rate swaps based on Ethena’s staking yield (sUSDe) and using USDC as the collateral token. Buyers in this market could provide USDC collateral to purchase a swap which locks in a yield rate for Ethena, regardless of whether they hold USDe. And because the swap payments are based on the payment rate, buyers can benefit incredibly high ratios of leverage (over 5,000x in some instances) on their collateral.

Each market uses creator-determined oracles which offers two key benefits. The first is that it allows the market to decide which oracles provide the most accurate data. The second is that is allows markets to be created for any reference rates, off or onchain. Markets also incentivize liquidity through a utilization fee that rewards curators for allocating liquidity to markets when demand is high.

**Unlocking the Next Wave of Growth Onchain**

Interest rate swap markets aren't just another trading tool—they're the missing infrastructure that enables:

* **Long-duration, fixed-rate loans:** lenders can offer multi-year fixed rates without taking on excess interest rate risk
* **Sophisticated trading strategies:** traders can bet on interest rate movements and capitalize on market inefficiencies
* **Predictable corporate financing:** companies can issue cost-effective debt while locking in stable borrowing costs
* **Deeper credit markets:** expanding access beyond just over-collateralized loans

Without this primitive, DeFi lending stays limited to short-term, variable-rate, crypto-native use cases. With it, we unlock trillions in traditional credit market demand that's been waiting for the infrastructure to exist.

**Use Case 1: Capital Efficient Bets on Rates**

Suppose a trader expects s**USDe yields to rise from 7% to 10%** over the coming months. Instead of just holding USDe passively, they can use your Kairos to turn that view into profit.

Here’s how it plays out:

* **Notional swap Amount:** $1,000,000 — this is the size of the swap which determines the potential profit or loss.
* **Swap Term:** 30 days — this is the length of the swap.
* **Collateral posted:** The trader deposits, for example, $5,000 USDe as margin — sufficient to secure the swap position per protocol requirements and providing leverage of \~200x.
* **Swap Position:** They enter into a **pay-fixed / receive-floating** swap. The fixed leg is set at **7%** (the current market swap rate), while the floating leg tracks the realized USDe yield.
* **Scenario:**
  * If yields rise to **10%**, the floating payments they receive exceed the 7% fixed rate they pay. On a $1,000,000 notional, that’s roughly $2,500 in profit ($30,000 in annualized profit) (10% – 7% = 3% spread). This is in addition to the yield they are already earning by holding USDe.
  * If yields instead fall to **5%**, the trader owes more on the fixed leg than they earn on the floating leg, realizing a \~$1,667 loss on the same notional.

By doing this, the trader has leveraged exposure to yield movements without selling their USDe or taking on additional crypto price risk. They can scale the position by adjusting notional size, hedge it with offsetting swaps, or combine it with other DeFi strategies.

**Use Case 2: Enabling Fixed-Rate Loans Onchain**

Imagine you’re a lender considering offering a 2-year fixed-rate loan at 6% APY. Here’s the risk without hedging:

* **Month 1:** You originate a 2-year loan at 6% fixed.
* **Month 6:** Market borrowing rates on Aave spike to 12%. You’re now locked into earning 6%, while new borrowers pay 12%. The value of your fixed-rate loan has declined relative to the market.
* **Month 24:** Rates remain elevated around 12%. You’ve effectively lost \~6% per year for the remaining 18 months — a potential loss of millions on large exposures.

This interest rate risk is exactly why most DeFi protocols only offer variable rates or extremely short fixed-rate terms. Lenders simply won't commit capital for long periods without protection against rate movements.

Here's how a lender can use Kairos to mitigate this risk:

* **Notional swap amount:** $1,000,000 — the size of the swap, which determines potential profit or loss.
* **Swap term:** 760 days — the duration of the swap.
* **Swap position:** Pay-fixed / receive-floating. Fixed leg set at 7% (current market swap rate); floating leg tracks Aave USDC borrow rates.

**Scenario outcomes:**

* Aave rates rise to 12%: Floating payments exceed the 7% fixed payments owed on the swap, generating additional yield on top of the 6% fixed-rate loan income.

By using swaps, lenders can hedge their fixed-rate loans against rising market rates, enabling longer-term, predictable lending without losing out when rates spike on protocols like Aave.

With Karios, a variety of borrowing use cases are supported:

* **Business loans:** Companies can lock in predictable borrowing costs to plan operations and manage cash flow.
* **Leveraged strategies:** Traders can know their exact carrying costs for multi-month positions.
* **Real estate:** Property developers can secure fixed rates for construction loans.
* **Consumer credit:** Individuals can gain certainty on future payments when financing large purchases.


# Market Architecture

This section covers the high-level architecture for the Kairos V1 Protocol. Please view the protocol section for more details.

**Market Creation**

Markets are created in pairs via a public contract call, enabling the caller to create a set of corresponding markets (one sells fixed-rate swaps and the other floating-leg swaps). When a market pair is created, the market creator must pass values for the required markets parameters.

The required parameters are:

1. referenceRateOracle: oracle address used to read the floating-leg index for the market. Subject to the rateConvention semantics below.
2. baseSwapRateOracle: tenor-aware oracle (getRateByTenor) used to price the fixed leg at entry and on early exits.
3. swapToken: the ERC20 used as collateral and settlement currency for the market (both sides post in this token).
4. leverageMultiplier: multiplier that determines how much LP-side notional capacity is available per unit of pool collateral.
5. swapTerm: length of swaps in seconds (uint32). All buys in this market run for exactly this duration.
6. utilFeeSlopeWad: slope (WAD) of the linear portion of the utilization fee curve — fee per unit utilization below the kink.
7. kinkUtilization: utilization level (WAD, e.g. 0.7e18 = 70%) at which the fee curve transitions from linear to quadratic.
8. maxKinkFeeWad: maximum additional fee (WAD) contributed by the quadratic portion above the kink.
9. earlyExitAllowed: boolean toggle. If false, exitSwapEarly is disabled for the market.
10. earlyExitFee: convenience fee applied at early exit. Accrues to the LP pool.
11. liquidationIncentive: portion of collateral (WAD, e.g. 0.05e18 = 5%) prefunded as the liquidator bounty. Returned to the buyer at normal expiry if the swap is never liquidated.
12. numBuckets: number of time buckets (uint32) used to aggregate active swaps for MTM / share-price math. Bucket IDs cycle via modulo over (entryTimestamp / bucketInterval).
13. bucketInterval: width of each bucket in seconds (uint32). Must satisfy bucketInterval × MAX\_ACTIVE\_WINDOWS ≥ swapTerm, with MAX\_ACTIVE\_WINDOWS = 7 (E729) — a **floor** on bucketInterval that caps how many windows can be active at once, bounding the O(occupied buckets) withdraw scan. Combined with E704 (bucketInterval ≤ swapTerm, and ≥ 1200), the valid range is roughly \[max(1200, swapTerm ÷ 7), swapTerm].
14. fixedRiskPremiumOracle / floatingRiskPremiumOracle: optional per-side oracles (address(0) = no premium) that add a risk premium to the buyer's swap rate at entry. The two sides can have independent premium curves.
15. lpWhitelistEnabled: if true, only addresses on the per-market LP whitelist may supply collateral.
16. minCollateral: minimum collateral (uint88, in swap-token decimals) required from both the buyer and LP per swap. Must be > 0.
17. rateConvention: enum specifying how referenceRateOracle exposes its data — Cumulative (oracle returns a monotone index directly), SpotRate (oracle returns an instantaneous rate; library accumulates via simple interest), or SpotCompoundRate (oracle returns an instantaneous rate; library accumulates via continuous compounding). Set per-oracle on first use and immutable thereafter.

Once initialized, all market parameters are immutable. The only post-creation mutations are administrative: ownership transfer (transferMarketOwnership / acceptMarketOwnership), termination (terminateMarket, which blocks new buys but does not change any economic parameter), and per-address entries on the LP whitelist when lpWhitelistEnabled was set true at creation. The parameters are the same for each pair of corresponding markets, with two exceptions. The first is that one of the markets will have a rateType of 0 (buyers purchase fixed-rate swaps) and the other 1 (buyers purchase floating-rate swaps). The second is the RiskPremiumOracle which compensates LPs for supplying liquidity to each side of a trade.

**Market Liquidity**

LPs provide liquidity by supplying the market's collateral token. The notional liquidity an LP's deposit supports is poolCollateral × leverageMultiplier × SECONDS\_IN\_YEAR / (|baseRate| × swapTerm), where leverageMultiplier is set at market creation and baseRate is read from the market's baseRate oracle for the configured swapTerm. Because each market only allows one direction of swap (fixed-payer or floating-payer) and is paired with a corresponding market for the opposite side, LPs choose which direction of rate exposure to take, rather than supplying both sides equally. This lets LPs more precisely manage their own interest-rate risk.

**Fee Structure - Fixed-Rate Swaps (BUY\_FIXED)**

A buyer in a BUY\_FIXED markets pays a fixed rate of baseRate + utilizationFee + riskPremium for the swap term and receives the realized floating rate. All three components are locked in at entry; the buyer's collateral is sized accordingly.

* Base rate:
* Utilization fee: a piecewise function of pool utilization, with a utilFeeSlopeWad linear region up to kinkUtilization and a quadratic region adding up to maxKinkFeeWad above the kink. The fee for a trade is the path-independent integral of this curve from pre-trade utilization to post-trade utilization, so splitting a trade across multiple transactions does not reduce the total fee. The shape incentivizes LPs to supply markets that are near the kink or above it.
* Risk premium: an optional flat rate read from a per-side riskPremiumOracle (set to address(0) to disable). Allows market creators to price idiosyncratic risk (e.g. credit, oracle, or basis risk) on top of the base rate.

**Fee Structure - Floating-Rate Swaps**

Buyers pay the swap rate for each swap they purchase. The swap rate for floating-rate swaps is the sum of the reference rate, utilization fee, and fee spread for the markets. This swap rate is fixed for the term of the swap.

* **Reference Rate:** The current reference rate derived from the market's oracle. Since the reference rate can change over the course of a swap term, the final rate reference rate used in the swap rate is calculated as the average floating rate over the course of the swap as derived from the market's reference rate index.
* **Utilization Fee:** The purpose of the utilization fee is to calculate how much of the market's available liquidity will be consumed by the proposed trade and return a fee that accounts for the impact. This fee serves to incentivize LPs to supply additional liquidity to markets with low available liquidity.
* **Fee Spread:** This fee is set during market creation and is a static fee added to every swap. This incentive LPs to supply liquidity regardless of available liquidity or market conditions.


# Popular Use Cases

Here are some use cases that Kairos will be able to support

## Popular Use Cases

Kairos enables a wide range of interest rate strategies — from simple hedging to sophisticated structured products. Because Kairos markets are oracle-agnostic and support up to 5,000x capital efficiency, participants can express views on virtually any onchain or offchain rate with minimal collateral. Below are detailed use cases illustrating how different market participants can use Kairos interest rate swaps.

***

### Yield Farmer: Leveraged Yield Token Exposure via Pendle + Kairos

A Pendle user could purchase a yield token (YT) for a specific rate on Pendle and then use the YT as collateral in a Kairos market that references the same underlying rate to purchase a floating-rate swap. The user would then have leveraged exposure to the YT, receiving both the native yield from holding the token and the net difference between the current floating rate and the fixed rate of their swap.

**Example — sUSDe Yield Token + Kairos Floating Swap:**

A trader purchases Pendle YT-sUSDe expiring in 90 days at an implied yield of 8%. They deposit the YT as collateral into a Kairos market referencing Ethena's sUSDe staking rate and purchase a 90-day floating-rate swap on $500,000 notional, paying a fixed rate of 8% and receiving the floating sUSDe rate.

If sUSDe yields rise to 12% over the swap term, the trader earns the YT yield (which appreciates as realized yield exceeds the implied rate at purchase) *plus* the swap profit of roughly 4% annualized on $500,000 notional — approximately $4,932 over 90 days. The combined position creates a leveraged long bet on sUSDe yields rising, funded by minimal collateral.

If yields instead fall to 5%, the YT declines in value and the swap generates a loss (paying 8% fixed while receiving only 5% floating). The trader faces compounding downside, making this a high-conviction strategy best suited for participants with strong directional views on yield movements.

***

### ETH Staker: Locking In a Fixed Staking Yield

ETH staking currently yields roughly 2.8–3.5% APY, but this rate fluctuates with validator participation, network activity, and MEV dynamics. A staker who has committed significant capital to validators — or who holds liquid staking tokens like stETH, cbETH, or rETH — faces uncertainty about what they'll actually earn over the next year.

**Example — Solo Validator Operator Hedging Yield Decline:**

A validator operator running 10 validators (320 ETH staked, worth roughly $640,000 at $2,000/ETH) currently earns \~3.2% APY. They're concerned that increasing validator participation — the entry queue has surged, with over 1.3 million ETH pending activation — will compress base rewards further over the next 12 months.

The operator deposits collateral (ETH or a stablecoin, depending on the market) into a Kairos market referencing the ETH staking rate and enters a 12-month swap on $640,000 notional, receiving a fixed rate of 3.0% and paying the floating ETH staking rate.

If the staking rate declines to 2.2% as validator count grows, the operator's actual staking income drops — but the swap generates a profit of approximately 0.8% on $640,000, or roughly $5,120 over the year. This offsets the lost staking income. Conversely, if a spike in network activity or MEV pushes the staking rate to 4.0%, the operator earns more from staking but pays the difference on the swap. The net effect is a smoothed, predictable yield close to 3.0% regardless of rate movements.

**Example — Liquid Staking Token Holder Converting to Fixed:**

A DeFi participant holds 100 stETH (worth \~$200,000) earning a variable \~3.0% from Lido. Rather than accept rate volatility, they deposit a portion of their stETH as collateral into a Kairos market and enter a 6-month swap on $200,000 notional, receiving fixed at 2.9% and paying the floating stETH rate.

For the next six months, the holder knows they'll net approximately 2.9% annualized on their position, regardless of whether Lido's rate dips to 2.0% during a quiet period or spikes to 4.5% during a fee surge. The swap converts their variable staking income into predictable fixed income — a building block for treasuries, funds, or individuals who need certainty.

***

### Mortgage Holder: Profiting from Rate Declines

A homeowner holds a mortgage with a fixed rate of 6.5%. They believe interest rates will decline over the coming year — perhaps due to expected central bank rate cuts — and want to profit from that view without refinancing, which carries closing costs and paperwork.

**Example — Hedging Against a SOFR-Referenced Rate:**

The homeowner deposits $2,000 in collateral into a Kairos market referencing the SOFR (Secured Overnight Financing Rate) and purchases a 12-month swap on $400,000 notional, paying the floating SOFR rate and receiving a fixed rate of 5.0%.

Over the next year, SOFR declines from 5.0% to 3.5% as the Federal Reserve cuts rates. The homeowner is paying the declining floating rate (averaging roughly 4.2% over the year) while receiving the locked-in 5.0% fixed rate. On $400,000 notional, this \~0.8% average spread generates approximately $3,200 in profit over the year. The homeowner can apply these funds toward mortgage payments, effectively reducing their borrowing cost.

If rates instead rise, the homeowner pays more on the floating leg than they receive on the fixed leg, generating a loss on the swap. However, this loss is bounded by collateral, and the homeowner's underlying mortgage payment remains unchanged — making this a defined-risk bet on the rate direction.

***

### Lender: Hedging Fixed-Rate Loan Exposure

If a lender offers a 10% fixed rate for two years via Morpho V2 while variable rates trade around 7%, they face interest rate risk if market conditions change. Should rates rise to 11% one year later, the lender continues earning 10% while new loans price at the higher market rate, causing the market value of their fixed-rate position to decline. The lender is effectively locked into an underperforming asset.

Kairos enables lenders to hedge this exposure through interest rate swaps.

**Example — Morpho V2 Fixed-Rate Loan Hedge:**

Following loan origination of a $1,000,000 two-year fixed-rate loan at 10%, a lender enters a 2-year Kairos swap agreement, paying a fixed rate of \~8% while receiving the variable rate tied to Morpho Market A.

*If rates rise to 11%:* The loan continues generating 10% returns, but the swap produces a \~3% gain (receiving 11% floating, paying 8% fixed). This 3% swap profit on $1,000,000 notional — roughly $30,000 per year — compensates for the opportunity cost of being locked into a below-market 10% loan when new originations price at 11%.

*If rates decline to 5%:* The swap generates a loss (receiving 5% floating, paying 8% fixed), but the lender's 10% fixed-rate loan is now significantly above market. The increased market value of the loan offsets the swap loss. In either scenario, the lender's total return is smoothed and protected.

This mechanism allows lenders to capture rate increases without affecting borrower terms, and is the fundamental technique that enables fixed-rate lending at scale — the same approach banks have used in traditional finance for decades, now available permissionlessly onchain.

***

### Restaking Participant: Managing Layered Yield Exposure

The Ethereum restaking ecosystem — led by EigenLayer ($15.3B TVL) and Symbiotic ($897M TVL) — allows stakers to earn additional yield by securing Actively Validated Services (AVSs) on top of their base ETH staking rewards. However, restaking yields are highly variable: AVS demand fluctuates, reward token emissions can change, and the base ETH staking rate itself moves.

**Example — EigenLayer Restaker Locking In Combined Yield:**

A restaker has deposited 500 ETH (\~$1,000,000) into EigenLayer and is earning a combined yield of approximately 5.5% (3.0% base staking + 2.5% restaking rewards). They want to lock in this yield for six months to plan around a known capital deployment.

The restaker enters two Kairos swaps: one referencing the ETH staking rate (receiving fixed at 2.9%, paying floating) and a second referencing a restaking rewards rate oracle (receiving fixed at 2.2%, paying floating). Together, these two swaps convert the restaker's variable \~5.5% combined yield into a predictable \~5.1% fixed income stream for six months.

If either the base staking rate or restaking rewards decline — say, base staking drops to 2.2% and restaking rewards dry up to 1.0% — the restaker's actual income falls, but the swaps generate offsetting profits. The restaker nets approximately 5.1% regardless.

***

### Stablecoin Yield Trader: Speculating on Lending Rate Movements

A trader notices that Aave USDC supply rates have been compressing — currently at 4.5% — but believes an upcoming wave of leveraged trading activity will push borrow demand (and therefore supply rates) significantly higher.

**Example — Going Long on Aave USDC Rates:**

The trader deposits $3,000 USDC as collateral into a Kairos market referencing the Aave USDC supply rate and enters a 30-day swap on $1,500,000 notional (500x leverage), paying a fixed rate of 4.8% and receiving the floating Aave USDC rate.

Over the next 30 days, a surge in leveraged ETH positions drives Aave USDC borrow demand sharply higher. The average floating rate over the period is 9.2%. On $1,500,000 notional over 30 days, the 4.4% spread (9.2% received – 4.8% paid) generates roughly $5,425 in profit — a return of over 180% on the $3,000 collateral in a single month.

If rates instead decline to 3.0%, the trader pays 4.8% and receives 3.0%, losing approximately $2,219 over the 30-day term. The loss is bounded by the posted collateral, subject to liquidation thresholds.

This is the "perps-style UX for rates" that Kairos enables: leveraged directional bets on interest rate movements, with a familiar long/short interface and defined risk.

***

### DAO Treasury: Converting Variable Protocol Revenue to Fixed Income

A DAO earns protocol revenue denominated in a variable yield — for example, lending protocol fees that fluctuate with utilization, or staking rewards that vary with network conditions. The DAO's contributors and grant recipients need predictable budgets, but the treasury's income is anything but predictable.

**Example — Lending Protocol DAO Stabilizing Revenue:**

A lending protocol's treasury holds $5,000,000 in assets deployed across its own markets, earning a variable yield currently around 6%. The DAO needs to commit $250,000 to a 12-month developer grant program and wants certainty that the funds will be available regardless of rate movements.

The DAO enters a 12-month Kairos swap on $5,000,000 notional, receiving a fixed rate of 5.5% and paying the floating rate tied to its own lending market's supply rate. This guarantees approximately $275,000 in fixed income over the year — more than covering the grant commitment — even if a bear market crushes utilization and the floating rate drops to 2%.

The trade-off is that if rates spike to 10%, the DAO pays the difference on the swap and foregoes the upside. But for budget planning purposes, the certainty is worth more than the optionality.

***

### Digital Asset Treasury (DAT) Company: Issuing Fixed-Rate Bonds Backed by Staked ETH

A DAT (such as a publicly traded company following a digital asset treasury strategy) purchases a large amount of ETH and stakes it, receiving liquid staking tokens in return. The DAT then uses these liquid staking tokens as collateral in a Kairos market referencing the ETH staking rate and enters a long-duration swap — locking in a fixed yield payment for two years while paying the fluctuating staking rate.

**Example — Structured ETH Bond Issuance:**

A DAT acquires 5,000 ETH (\~$10,000,000) and stakes it via Lido, receiving stETH. The stETH earns a variable \~3.0% staking yield. The DAT enters a 2-year Kairos swap on $10,000,000 notional, receiving a fixed rate of 2.8% and paying the floating ETH staking rate.

The DAT now holds a fixed-income instrument: $10,000,000 in staked ETH producing a guaranteed 2.8% annual return ($280,000/year) regardless of staking rate volatility. Backed by this predictable cash flow, the DAT issues fixed-rate bonds to investors at 2.5%, pocketing the 0.3% spread. The bond proceeds fund additional ETH purchases, creating a flywheel.

Investors in these bonds receive predictable, ETH-denominated yield exposure without needing to manage staking infrastructure, validator risk, or rate volatility. The DAT has effectively created an onchain fixed-income product — a category that barely exists in DeFi today but represents a multi-trillion-dollar market in traditional finance.

***

### RWA Rate Speculator: Trading Offchain Benchmark Rates Onchain

Because Kairos is oracle-agnostic, markets can reference offchain rates like SOFR, EURIBOR, or other RWA (Real World Asset) benchmarks. This opens the door for crypto-native traders to take positions on traditional interest rate movements without ever touching the traditional financial system.

**Example — Trading the Fed Rate Decision:**

A trader expects the Federal Reserve to cut rates by 50 basis points at the next FOMC meeting. They deposit $5,000 in collateral into a Kairos market referencing SOFR and enter a 90-day swap on $2,000,000 notional, paying the floating SOFR rate and receiving the current fixed rate of 4.8%.

The Fed cuts by 50bps as expected, and SOFR drops from 4.8% to 4.3% over the following weeks. On $2,000,000 notional, the average 0.4% spread over 90 days generates roughly $1,973 in profit — a \~39% return on collateral.

This trade is analogous to a Fed Funds futures position in traditional finance, but executed entirely onchain with crypto collateral, instant settlement, and no counterparty credit risk beyond smart contract risk.

***

### Composability: ERC-721 Swap Positions as DeFi Building Blocks

Every Kairos swap position is represented as an ERC-721 NFT, making it composable with the broader DeFi ecosystem. This enables use cases that go beyond simple rate trading.

A profitable swap position can be used as collateral in lending protocols, allowing the holder to borrow against unrealized swap gains. Swap positions can be bundled into structured products — for example, a vault that aggregates multiple swap positions across different rate markets to create a diversified fixed-income portfolio. Swap NFTs can also be traded on secondary markets, enabling price discovery and liquidity for rate positions before their natural expiration.

This composability is what transforms Kairos from a standalone trading venue into an infrastructure layer for onchain fixed income — the same role that interest rate swaps play in traditional finance, but natively interoperable with every other protocol in DeFi.


# FAQ

Answers to the most common questions about Kairos — what interest rate swaps are, how trading works, fees, liquidation, liquidity provision, oracles, and security.

Everything below links out to the deeper reference pages. If your question isn't answered here, reach out on [X (@KairosSwap)](https://x.com/KairosSwap) or in the [Telegram](https://t.me/+IM8fDT0MJBBlNzUx).

***

## The Basics

### What is Kairos?

Kairos is a non-custodial, permissionless protocol for onchain **interest rate swaps**. It lets anyone create markets where two parties exchange fixed-rate and floating-rate payment obligations on a reference rate — an Aave supply rate, a staking yield, SOFR, or anything else an oracle can report.

Traders use it to take leveraged positions on rate movements. Lenders and borrowers use it to hedge rate risk. Treasuries and DATs use it to convert variable income into predictable fixed income. See [Popular Use Cases](/popular-use-cases) for worked examples of each.

### What is an interest rate swap, in plain terms?

It's an agreement to trade *payment streams*, not principal. One side pays a fixed rate on an agreed **notional** amount; the other pays a floating rate on the same notional. At the end of the term, only the **net difference** between the two legs changes hands.

Because nobody ever exchanges the notional itself, the capital required is tiny relative to the exposure — which is exactly why interest rate swaps are the largest derivatives market on earth, with over $500 trillion in notional outstanding.

### Do I need to hold the underlying asset to trade a rate?

No. Swaps on Kairos are fully synthetic. You can buy a swap on Ethena's sUSDe yield without holding a single USDe, or trade the ETH staking rate without running a validator. You post collateral in the market's collateral token and take a view on the rate.

### What does "notional" mean, and why is it so much larger than my deposit?

**Notional** is the size the interest payments are calculated against. **Collateral** is what you actually deposit. Your collateral only has to cover the *maximum payment you could owe over the term* — not the notional itself.

A worked example: a 30-day swap on a rate around 5%, on $1,000,000 notional, generates a maximum payment obligation of roughly $1,000,000 × 5% × (30/365) ≈ $4,100. That's the order of magnitude of collateral you post — not $1,000,000.

### What's the difference between "long" and "short" a rate?

| You think rates will… | Position    | On Kairos         | You pay                    | You receive                |
| --------------------- | ----------- | ----------------- | -------------------------- | -------------------------- |
| **Rise**              | Long rates  | **BUY\_FIXED**    | A locked-in fixed rate     | The realized floating rate |
| **Fall**              | Short rates | **BUY\_FLOATING** | The realized floating rate | A locked-in fixed rate     |

Every Kairos market is created as a **pair** — one BUY\_FIXED market and one BUY\_FLOATING market sharing the same oracles, collateral token, term, and leverage. See [Trade on Interest Rates](/how-tos/trade-on-interest-rates).

### What chains is Kairos deployed on?

Kairos launches on **Ethereum mainnet**, and mainnet is the only supported network at launch. Deployed contract addresses will be published in these docs once the launch deployment is live — always verify an address against this documentation before interacting with any contract.

### How do I get started?

1. An Ethereum wallet — MetaMask, Coinbase Wallet, Rabby, WalletConnect, or similar.
2. The market's collateral token, plus a little ETH for gas.
3. Head to the app, pick a direction, and follow the [Buy a Rate Swap](/how-tos/buy-a-rate-swap) walkthrough.

Start small. Swaps are leveraged instruments, and the fastest way to learn how a market behaves is a position sized so that being wrong is cheap.

### What's the difference between Degen Mode and Pro Mode?

**Degen Mode** is the simple interface: pick a direction, slide to the amount you want to wager, confirm. **Pro Mode** exposes the full market view — notional entry, the live rate chart, order widget, and per-market positions. Both open the same underlying swap. See [Buy a Rate Swap](/how-tos/buy-a-rate-swap) and [Read Kairos Charts](/how-tos/read-kairos-charts).

***

## How Kairos Compares

### How is Kairos different from Pendle?

They solve adjacent problems with fundamentally different mechanics. **Pendle tokenizes yield** — it wraps a yield-bearing asset and splits it into a principal token (PT) and a yield token (YT), which trade against each other on an AMM. **Kairos swaps payment obligations** — no principal ever moves, and no asset needs to be wrapped.

Four structural differences follow from that:

|                        | Pendle                                          | Kairos                                                                  |
| ---------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- |
| **What you hold**      | A tokenized claim on principal or yield         | A swap contract on a payment stream                                     |
| **Shorting yield**     | Buy PT — requires committing **full principal** | BUY\_FLOATING — collateral covers only the payment obligation           |
| **What can be traded** | Assets that have been tokenized into SY/PT/YT   | Any rate with an oracle feed, onchain or off                            |
| **Maturity**           | A fixed calendar date shared by all holders     | The market's full term, running from **your** entry                     |
| **Pricing**            | Implied yield from AMM trades                   | Oracle-anchored base rate + a deterministic, path-independent fee curve |

The comparison people usually mean is against **Boros**, Pendle's rate-trading product. Boros is narrowly scoped to **perpetual funding rates over short durations** — genuinely useful for funding-rate traders, but not a tool a lender can use to hedge a two-year fixed-rate loan book. Kairos is built for the full duration curve, from hours to years.

### Can you show me where that difference actually bites?

Three cases where the mechanics diverge materially.

**1. Shorting yield at size.** Suppose you believe sUSDe yields are going to fall, and you want $500,000 of exposure to that view.

* *On Pendle:* you buy PT-sUSDe. PT locks in the fixed yield, but you must deploy the **full $500,000 of principal** to get $500,000 of notional exposure. Your view on the rate is inseparable from a $500,000 capital commitment to the underlying asset.
* *On Kairos:* you buy a BUY\_FLOATING swap on $500,000 notional in a market referencing the sUSDe rate. On a 90-day term at an 8% base rate in a 10× market, your collateral is roughly `$500,000 × 9% × (90/365) / 10 ≈ $1,110`. You've expressed the same directional view with about 0.2% of the capital, and you've taken on no exposure to the price of USDe itself.

The long-yield side is less lopsided — YT is already a capital-efficient way to go long yield — but the asymmetry on the short side is structural, not a matter of degree.

**2. Rates that aren't tokenized.** A Morpho V2 lender originates a $1,000,000 two-year fixed-rate loan at 10% and wants to neutralize the duration risk.

There is no PT/YT for "the borrow rate on Morpho Market A over the next two years." Tokenization requires a wrappable yield-bearing token; a market-specific borrow rate isn't one. Kairos requires only an oracle, so anyone can permissionlessly create that exact market: a 2-year term, that specific reference rate, USDC collateral.

The lender enters a swap paying \~8% fixed and receiving the floating Morpho rate. If rates rise to 11%, the loan still earns 10% and the swap nets \~3% — roughly $30,000/year — covering the opportunity cost of the below-market loan. If rates fall to 5%, the swap loses but the above-market 10% loan gains value. Either way the rate exposure is neutralized. This is the trade that makes fixed-rate lending viable at scale, and it isn't expressible through yield tokenization.

The same argument extends further: **SOFR, EURIBOR, perp funding, restaking reward rates** — none of them have a token to wrap, all of them have feeds. A trader expecting a 50bp Fed cut can post $5,000 against $2,000,000 notional of 90-day SOFR exposure and net roughly $1,973 if they're right. There is no tokenization route to that trade at all.

**3. Constant tenor vs. a shrinking clock.** Pendle maturities are fixed calendar dates shared by everyone in the pool. Buy YT twelve days before expiry and you get twelve days of exposure — the tenor you get depends on when you show up relative to a date somebody else chose.

On Kairos, `swapTerm` is a market parameter and **every purchase runs that full term from your own entry timestamp**. A 90-day market gives you 90 days whether you buy on day one or day two hundred. For a hedger matching a swap to a loan, or a trader who wants stable duration exposure without rolling into an expiring pool, that difference compounds.

### When is Pendle the better tool?

Being straight about this matters more than the pitch:

* **You want to hold the asset and earn the yield.** PT is a direct fixed-yield instrument on capital you're deploying anyway. Kairos is an overlay on exposure you already have — it doesn't generate yield on its own.
* **You want zero liquidation risk.** PT and YT can't be liquidated. Kairos positions are collateralized and *can* be — if rates move far enough against you before expiry, you lose your collateral. That's the price of the capital efficiency.
* **You want convex upside on yield with a hard-capped downside.** A YT's worst case is the premium you paid, with non-linear upside. A Kairos swap is linear in both directions.
* **You're trading a major asset where Pendle already has deep liquidity.** Kairos markets are new; on the assets Pendle has spent years bootstrapping, its books are deeper today.

### Do Kairos and Pendle compete, or compose?

Mostly they compose — and the most interesting positions use both.

A trader can buy Pendle YT-sUSDe expiring in 90 days at an 8% implied yield, then **post that YT as collateral in a Kairos market** referencing the same sUSDe rate and buy a floating-rate swap on $500,000 notional. They then earn the YT's native yield *and* the swap's spread on rate movements — leveraged long exposure to the same view, funded by collateral that was already working.

That's more capital efficient than looping through a money market, because margin plus a small swap fee replaces a full borrow rate. The full worked example, including the downside scenario, is the first entry in [Popular Use Cases](/popular-use-cases).

More broadly, Kairos is designed to complement the existing stack rather than displace it. Money markets like Aave, Morpho, and Kamino gain a hedge their lenders previously couldn't access; tokenization platforms gain a rate-risk transfer layer their products can plug into. PTs and vault receipt tokens work as Kairos collateral precisely because the goal is to be connective tissue, not a destination.

***

## Trading Swaps

### How is the rate I get quoted determined?

Three components, all locked in at the moment you open the swap:

1. **Base rate** — read from the market's tenor-aware base swap rate oracle for that market's term.
2. **Utilization fee** — a kinked curve that rises as your trade consumes pool capacity. It compensates LPs for liquidity scarcity.
3. **Risk premium** — an optional oracle-supplied markup, configured independently for each side of the market pair.

A BUY\_FIXED buyer pays `baseRate + utilFee + riskPremium` and receives the realized floating rate. A BUY\_FLOATING buyer pays the realized floating rate and receives a fixed rate. The full mechanics are in [Core Concepts](/protocol/core-concepts).

### What leverage can I get?

Two different numbers get called "leverage," and they mean different things:

* **`leverageMultiplier`** is a per-market parameter, capped at **18×** and immutable after creation. It scales *down* how much collateral both the buyer and the LP side must post for a given notional.
* **Effective notional-to-collateral ratio** is what most people mean by leverage — and it can reach **5,000× or more**. It's high because your collateral is sized off `rate × term`, not off notional. Short terms and low reference rates produce dramatic ratios; long terms and high rates produce modest ones.

The formula:

```
buyerCollateral = notional × (|baseRate| + utilFee + riskPremium) × term / (YEAR × leverage)
```

So a 30-day swap on a 3% rate at 10× market leverage needs roughly `3% × (30/365) / 10 ≈ 0.025%` of notional in collateral — about 4,000× effective leverage. The same market at a 2-year term on a 10% rate needs far more.

**Higher leverage means a thinner buffer, which means liquidation comes sooner.** Read [Leverage](/protocol/core-concepts#leverage) before sizing up.

### What fees do I pay?

| Fee                    | When                                                  | Where it goes                                                   |
| ---------------------- | ----------------------------------------------------- | --------------------------------------------------------------- |
| **Utilization fee**    | Baked into your rate at entry                         | LP pool                                                         |
| **Risk premium**       | Baked into your rate at entry (if the market has one) | LP pool                                                         |
| **Protocol fee**       | Charged at entry: `notional × feeRate × term / YEAR`  | Protocol treasury, optionally split with the market creator     |
| **Liquidation bounty** | Prefunded at entry                                    | **Refunded to you at normal expiry** if you're never liquidated |
| **Early exit fee**     | Only if you close before expiry                       | LP pool                                                         |

### Can I reduce fees by splitting my trade into smaller pieces?

No. The utilization fee is the **definite integral of the fee curve** over the range of utilization your trade consumes, averaged over trade size — not the endpoint rate. Splitting one trade into ten produces the exact same total fee. This is a deliberate design property, not an accident.

### Why did my fee stay high right after a big LP deposit?

The utilization fee is priced against a **seasoned exponential moving average** of pool collateral over a rolling one-hour window — not the instantaneous balance. A fresh deposit expands capacity *immediately* (so you can trade larger size right away) but takes up to an hour to fully lower the fee.

This decoupling is the protocol's defense against just-in-time liquidity: an LP can't deposit, cheapen a buyer's fee, and withdraw a block later. Note the asymmetry — the fee is charged against `min(EMA, spot)`, so a large *withdrawal* raises fees immediately. See [Seasoned utilization fee](/protocol/core-concepts#seasoned-utilization-fee).

### Can I add more collateral to an open swap?

No. Collateral is sized and posted when the swap is opened and is fixed for the life of that position. There is no top-up to escape a liquidation. If you want a larger buffer, that decision has to be made at entry — choose a market with a lower `leverageMultiplier`, or size your notional down.

(The "Adjust Collateral" control on the Portfolio page manages your *liquidity provider* collateral by market, not the collateral backing an open swap position.)

### Can I close a swap before it expires?

Only if the market was created with `earlyExitAllowed = true` — it's an immutable per-market setting. Where it's enabled, `exitSwapEarly` settles the position using the same pipeline as normal settlement, but treats *now* as the expiry, re-reads the base rate for the remaining tenor to price the unrealized fixed leg, and charges the market's `earlyExitFee`.

You can also set a `minExitAmount` as a slippage guard on what you receive. See [`exitSwapEarly`](/protocol/swaps#exitswapearly).

### Can I sell or transfer my position?

Yes, two ways. A position can be moved directly with `transferSwapPosition`, or it can be wrapped as an **ERC-721 NFT** via `SwapPositionWrapper`.

Wrapping is **custodial**: the wrapper takes ownership of the underlying swap in SwapCore and mints you an NFT representing it. From then on the NFT transfers as pure ERC-721 — moving the token does *not* touch SwapCore, and no per-holder authorization is needed. Settlement funds flow to the wrapper and are redeemed by whoever holds the NFT at that time. The lifecycle is wrap → transfer freely → settle → redeem (which burns the token). Only the original wrapper needs to call `setAuthorization` before wrapping.

If you're indexing transfers, note the consequence: an NFT transfer emits no SwapCore event, because the underlying position never changes hands there.

That composability is what makes swap positions usable as building blocks elsewhere in DeFi: collateral in a lending market, an input to a structured product, or an asset on a secondary marketplace. See [Composability](/popular-use-cases#composability-erc-721-swap-positions-as-defi-building-blocks).

### What slippage protection do I have when opening a swap?

Three independent guards, all optional:

* **`rateBound`** — a ceiling on the fixed rate you'll pay (BUY\_FIXED) or a floor on the base rate you'll receive (BUY\_FLOATING).
* **`maxMarkup`** — a ceiling on `utilFee + riskPremium` combined.
* **`maxTotalIn`** — a ceiling on the **total tokens pulled from your wallet**, covering collateral + protocol fee + liquidation bounty.

Use `maxTotalIn` even when your rate bound looks safe. Collateral is sized off the *magnitude* of the base rate, so a rate move in your favor can still increase the tokens pulled. `rateBound` alone does not bound what you spend.

There's a second reason it matters, and it surprises integrators: your collateral is sized off the **larger-magnitude base rate of your market and its twin**, not off your own market's quote alone. This keeps the losing side's collateral sufficient to cover the winning side's backing across the pair. If you compute an expected total from the quote you see on your side, you can under-estimate what will actually be pulled. See [`buySwap`](/protocol/swaps#buyswap).

***

## Settlement & Liquidation

### How does settlement work?

Once the term elapses, the protocol computes each leg's payment as `notional × rate × term`, nets them against each other, caps each side at its posted collateral, and transfers only the difference to whichever side is owed. Both parties then get their remaining collateral back. If you were never liquidated, your prefunded liquidation bounty comes back too.

In the UI this is the **"Claim Payments"** button on an expired position — see [Close a Rate Swap](/how-tos/close-a-rate-swap).

### Who settles my swap? Do I have to do it myself?

Settlement is **permissionless**. Anyone can call `makePayment` on any expired swap — keepers, bots, the LP, or you. There's no whitelist and no privileged settler. Keepers typically batch many swaps into one call.

### What is a liquidation, and when does it happen?

Both sides of a swap post collateral as a guarantee of future payments. A position becomes liquidatable when one side's collateral can no longer cover what it **already owes** on accrued payments.

Two properties worth internalizing:

* **Liquidation is based on accrued P\&L, not projected P\&L.** Where the rate *might* go is irrelevant. A swap is only liquidatable when one side's collateral is mathematically insufficient right now.
* **It's symmetric.** The LP pool faces liquidation on exactly the same terms as the buyer. Only one side is liquidated per call; the other settles normally.

See [Liquidation](/how-tos/liquidation).

### Who can liquidate?

Anyone. `liquidateSwap` is permissionless and the caller is paid a reward. On a buyer-side liquidation, the liquidator receives the **prefunded bounty** banked from the buyer at entry. On a pool-side liquidation, they receive `liquidationIncentive × poolCollateralBacking`, drawn from the LP backing for that swap.

### What happens if I get liquidated?

The swap closes immediately and your remaining collateral, after the liquidator's reward is paid, transfers to the counterparty. The liquidator's reward is the **liquidation bounty you prefunded at entry** — a percentage of your required collateral set per market and capped at 5%. That bounty is refunded to you in full at normal expiry if you're never liquidated, so being liquidated costs you the bounty on top of settling what you owed.

**Your maximum loss on a swap is what you posted.** You cannot end up owing more.

### My settlement transaction reverted with E450. What does that mean?

Settlement needs a stored oracle snapshot at or after your swap's expiry timestamp. `makePayment` tries to write that snapshot itself, so in normal conditions this never surfaces. `E450` means the reference oracle was unhealthy at settlement time *and* no post-expiry snapshot was captured earlier.

**`E450` is temporary, not terminal.** It only fires inside a **settlement grace period** after expiry. Within that window the protocol refuses to guess at missing data and asks you to wait. Once the grace period elapses, settlement stops waiting and adjudicates:

1. It retries the oracle read with a guaranteed gas budget. If the oracle recovered, settlement proceeds normally on real data.
2. If the read still fails, settlement **extrapolates** the expiry index from the trailing rate implied by the final snapshots, emits `ExpiryIndexExtrapolated`, and settles the swap.
3. Because that index history is now compromised, the protocol **permanently terminates both halves of the market pair** to new swaps. Existing positions still settle; nothing new can be opened.

So a dead oracle does not strand your position forever — it delays settlement to the end of the grace period, then forces a resolution and closes the market to new business.

One related code you may hit: **`E451`** means the settlement call didn't carry enough gas to run that fallback adjudication safely. Retry with a higher gas limit.

The preventative measure — useful for integrators and keepers — is to call the permissionless `updateMarketRateIndex` shortly after a swap matures. A snapshot captured while the oracle is healthy settles the swap on real data and avoids the fallback path entirely. See the settlement / snapshot model in [Core Concepts](/protocol/core-concepts).

### My settlement succeeded but I never received the funds. What now?

If the transfer to you reverts at settlement or liquidation — for example, because your address became blocked on the collateral token between entry and expiry — the payout is moved to escrow and a `BuyerTransferFailed` event is emitted. Call [`claimEscrow`](/protocol/swaps#claimescrow) once the blocker is resolved.

Escrowed funds are always delivered to the original position owner, never to whoever calls the function. An authorized delegate can trigger the claim but cannot redirect the money.

***

## Providing Liquidity

### How do LPs make money?

LPs are the counterparty to every swap and earn on every one: the **risk premium**, which compensates for uncertainty in the base rate over the tenor, and the **utilization fee**, which rises as the pool fills up. LPs also take the other side of rate movements, so directional P\&L flows through the pool.

Because swap pricing is oracle-derived rather than bonding-curve-based, Kairos LPs aren't exposed to the arbitrage-driven adverse selection that afflicts traditional AMMs. See [How it Works](/liquidity-providers/how-it-works).

### What are the risks of providing liquidity?

The pool takes the opposite side of every swap it backs, so it loses when rates move in the buyer's favor. Margin requirements are deterministic and each swap is independently collateralized, so **the pool's maximum loss on any individual position is capped at its posted backing for that swap**. Beyond directional risk: oracle failure, smart contract risk, and thin-market risk all apply. Read the [Legal Disclaimers & Risk Disclosures](/liquidity-providers/legal-disclaimers-and-risk-disclosures) in full.

### Do I have to supply both sides of the market?

No — and this is a deliberate design choice. Each market is **single-sided**: it sells either fixed or floating swaps, and is paired with a corresponding market for the opposite side. LPs pick which direction of rate exposure they want rather than being forced into a 50/50 position. That's what lets an LP manage their own interest rate risk precisely.

### What is the profit vest, and why can't I withdraw at the current share price?

Every deposit records a **profit-vest anchor**: the share price at deposit, plus a vest window (12 hours by default, bounded to 1 hour–7 days and timelocked). While the window is active, withdrawals price your shares at `min(burnPrice, entryPrice)`.

Note which price that is. Withdrawals settle at the pool's **burn price**, which is deliberately at or below the headline share price — it takes the more conservative of the fair and accrued-only marks and deducts any early-exit vesting reserve still draining. If you quote your exit from the informational share price you'll over-estimate what you receive. The vest cap then applies on top of that burn price.

This neutralizes the deposit → oracle-update → withdraw NAV sandwich. Three things to know:

* **It's one-directional.** The cap binds only when the share price is *above* your anchor. Exits below the anchor realize the full loss — this is not a put option.
* **It applies uniformly across your position.** There's no per-tranche accounting. A mid-vest top-up *below* your anchor blends it downward; a top-up at or above leaves it unchanged.
* **Every deposit extends the window, and never shortens it.** A top-up pushes the vest end to the later of your existing end and a fresh full window, so topping up can only delay your uncapped exit.

Check `getLpVestState(marketId, lp)` on the `Views` contract before supplying or withdrawing — it returns your anchor, vest end, and whether the cap is currently active.

### Why is my withdrawal reverting?

Several guards can block a withdrawal:

* **Expired unsettled swaps in the pool.** The share price would be stale until they settle — settle them with `makePayment` first. If the pool's expiry queue has grown long enough that the bounded scan gives up before finding the unsettled entry, the fix is `advanceExpiryPointer(marketId)` instead.
* **Same-block deposit.** A one-block lock is stamped on every deposit to prevent flash-loan deposit-and-withdraw.
* **Last-LP guard.** The pool can't drop to zero shares while swaps are still open; one share is retained to keep accounting alive.
* **Profit-vest surplus.** A full last-LP exit on an idle pool that would sweep cap-protected surplus reverts. Wait for vest expiry or withdraw partially.
* **Undripped early-exit vesting.** A full last-LP exit on an idle pool also reverts while the market's early-exit vesting reserve is still draining. Wait for the drip to finish, or withdraw partially.

Note the one thing that *doesn't* block you: when the pool is idle, the share price is computed directly from collateral and shares with the oracle bypassed entirely — so LP withdrawals remain available even if the reference oracle is down. See [`withdrawCollateral`](/protocol/liquidity#withdrawcollateral).

### Can I supply through a vault instead of managing positions myself?

Yes. **Morpho Vaults V2** are supported through a specialized adapter, so a curator can allocate across Kairos markets and handle position management for depositors. Share pricing uses a bucketed mark-to-market system that updates continuously as swaps open and settle, with a share price floor and deposit block protecting existing LPs from dilution when a pool is underwater.

### Are there permissioned markets for institutional LPs?

Yes. A market creator can enable an **LP whitelist** at creation, restricting who may supply collateral. It's designed for institutional liquidity providers with compliance requirements. The flag is set at creation and can't be turned on later.

If you're looking to deploy meaningful size, the [KLP interest form](https://forms.gle/kK1LrqXQWbkzAkZr5) is the fastest route to a conversation — see [Opportunity](/liquidity-providers/providing-liquidity-on-kairos).

***

## Markets & Oracles

### Who can create a market?

Anyone. `createMarket` is a public call that deploys **both** sides of the pair — BUY\_FIXED and BUY\_FLOATING — atomically. The caller becomes the market owner and can earn a share of protocol fees generated by volume in their markets, if the creator fee is configured.

The creator sets the reference rate oracle, base rate oracle, collateral token, swap term, leverage multiplier, fee curve, bucket configuration, liquidation incentive, early-exit policy, and whitelist flag. See [Market administration](/protocol/market-admin).

### Can a market's parameters change after launch?

**No.** Every economic parameter is immutable once the market is initialized. The only post-creation actions are administrative: transferring ownership (two-step), adding addresses to the LP whitelist if it was enabled at creation, and terminating the market.

This is a deliberate reduction of governance risk — the terms you trade under can't be changed out from under you.

### What does it mean when a market is "terminated"?

Termination blocks **new** swaps only. Existing positions run to their natural expiry and settle normally, on unchanged economic terms. No parameter is altered.

### What oracles does Kairos use, and can any rate become a market?

Kairos is **oracle-agnostic by design** — that's the core architectural bet. Any rate a market creator can source a feed for can become a tradeable market: onchain lending rates, staking and restaking yields, perp funding, or offchain benchmarks like SOFR and EURIBOR. Chainlink and Pyth are used across live markets today.

Each market specifies two oracles: a **reference rate oracle** for the floating leg, and a tenor-aware **base swap rate oracle** that prices the fixed leg at entry and on early exits.

### What is a "rate convention" and why does it matter?

It's how an oracle reports its data, and it determines how the floating leg is computed at settlement. Every oracle is assigned one convention on first use and it's **immutable per oracle thereafter**.

| Convention           | Oracle reports                                | Notes                                                  |
| -------------------- | --------------------------------------------- | ------------------------------------------------------ |
| **Cumulative**       | A monotone index (Aave/Morpho/Compound style) | Clamped so it can never decrease                       |
| **SpotRate**         | An instantaneous rate                         | Accumulated as simple interest; can go negative        |
| **SpotCompoundRate** | An instantaneous rate                         | Accumulated as continuous compounding; can go negative |

The practical consequence: under a **Cumulative** convention, a genuinely falling rate reads as flat and the floating leg can never go negative — which is why BUY\_FIXED markets on a Cumulative oracle reject a negative base rate. SpotRate and SpotCompoundRate *are* able to represent negative rates, which is what makes markets on things like perp funding possible. See [Rate conventions](/protocol/core-concepts#rate-conventions).

***

## Security

### Has Kairos been audited?

Yes — by two independent firms. **Octane Security** ran an adversarial, proof-of-concept-driven engagement across the full swap lifecycle, LP pool accounting, oracle integration, the Morpho Vault V2 adapter, and the permissionless settlement and liquidation paths. It surfaced 20 findings; every one was reproduced with a runnable PoC and every one has been fixed and re-verified. A second audit from **Guardian** is forthcoming.

Reports are published on the [Audit Reports](/dev-docs/audit-reports) page. Integrators should read them before building on top of the protocol.

### Is there a bug bounty?

Yes — a **$100,000** Guardian Defender contest focused on critical-severity findings, running for 30 days. See [Bug Bounty](/dev-docs/bug-bounty) for details and the contest link once live.

### Is Kairos custodial? Can the team take my funds?

No. Kairos is non-custodial and permissionless. Collateral sits in the protocol contracts under the rules encoded at market creation, and those rules are immutable. Settlement and liquidation are permissionless — no privileged actor gates your exit.

### What are the main risks I should understand before trading?

In short: **liquidation risk** (rates move against you and your collateral is consumed), **interest rate risk** (you're exposed to rate movements for the full term), **smart contract risk**, **oracle risk** (stale, manipulated, or failed rate data), **counterparty liquidity risk** (thin markets), and **underpayment risk** (a counterparty's collateral doesn't fully cover what they owe).

Read the full [Risks](/how-tos/risks) page before trading. Swaps are leveraged instruments — you can lose all of your posted collateral.

### Can I lose more than I deposit?

No. Each swap is independently collateralized and settlement caps each side's payment at its posted collateral. Your maximum loss is what you put in.

***

## Token & Rewards

### Does Kairos have a token?

Kairos issues **Time Krystals**, earned by opening swap positions and holding them to maturity. Krystals accrue as:

```
Collateral Amount × Swap Term Hours × Klaim Bonus
```

So a $100 swap held 24 hours at a 2× bonus earns 4,800 Krystals. The bonus multiplier can change at any time or with each quest.

**Time Krystals have zero monetary value.** They are not a security, not an investment, carry no profit expectation, and cannot be sold or exchanged for anything of value. Read the [Token Terms](/kairos-quests/token-terms) in full — this is not boilerplate.

### What are Kairos Quests?

Recurring campaigns with their own bonus structures and rewards — [Genesis](/kairos-quests/genesis-quest), [Chronos](/kairos-quests/chronos-quest), [Gravity](/kairos-quests/gravity-quest), and [Fellowship](/kairos-quests/fellowship-quest). Each has its own page under Kairos Quests.

***

## Building on Kairos

### Can a contract act on behalf of my account?

Yes, through the `onBehalfOf` pattern. Bundlers, routers, wrappers, and vault adapters can act for an end user — but only after that user has explicitly pre-authorized them via `setAuthorization`. Nothing acts on your account without your prior approval. See [Authorization](/protocol/authorization).

### Where do I find the ABI, events, and error codes?

* [Overview](/protocol/overview) — the mental model and end-to-end lifecycle
* [Swaps](/protocol/swaps), [Liquidity provision](/protocol/liquidity), [Market administration](/protocol/market-admin) — function-by-function reference
* [Views](/protocol/views) — read-only helpers for quoting and position monitoring
* [Events](/protocol/events) — every event emitted across the lifecycle, for indexers
* [Error Codes](/protocol/error-codes) — every revert code and what triggers it
* [Liquidation Bot](/dev-docs/liquidation-bot) — a reference keeper/liquidator implementation
* [Whitepaper](/dev-docs/kairos-protocol-whitepaper) — the full protocol design

### How do I quote a position off-chain?

Use the [Views](/protocol/views) contract. Note the deliberate asymmetry: view functions like `getSwapNetAmount` **project** a virtual index forward, so an off-chain reader sees the value settlement would realize. Settlement itself never projects — it only reads real stored snapshots, and fails closed rather than extrapolating.

***

## Still have questions?

* **X:** [@KairosSwap](https://x.com/KairosSwap)
* **Telegram:** [Join the community](https://t.me/+IM8fDT0MJBBlNzUx)
* **Liquidity providers:** [KLP interest form](https://forms.gle/kK1LrqXQWbkzAkZr5)

*DISCLAIMER: Interest rate swaps are leveraged instruments. You can lose all of your posted collateral if positions move significantly against you. Nothing in this documentation is financial advice. Always understand the risks before trading.*


# About us

Our mission: Enable traders, borrowers, and lenders to hedge interest rate risk and bet on interest rate movements though a new onchain primitive.

Kairos is built by Kairos Labs Inc., backed by 6MV, Lattice, Advancit, Alliance, and Compa Capital. Through personal experience and speaking with DeFi participants, we've identified the lack of effective interest-rate hedging tools as a blocker to continued onchain growth. The issue isn't the small number of fixed-rate lending protocols; it's that without hedging primitives, those products can't scale. In traditional markets, interest rate swaps let lenders hedge rate risk and traders express views on rate movements. That hedging layer is what makes fixed-rate loans and fixed-income products viable. With no effective way to hedge interest rate risk, onchain lenders and borrowers have been forced to rely on variable rates for every use case. We're setting out to fix that with an onchain primitive for interest rate swap markets that is permissionless, composable, and non-custodial.

**Founding Team:**

[Thomas Harrison](https://x.com/unofficial_thom) (CEO) and [Vince DePalma](https://x.com/0xVinnieJames) (CTO) are the founders of Kairos Labs Inc. Thomas is a 2nd-time founder who sold his previous company, Blockrize, to the Winklevoss-owned global cryptocurrency exchange Gemini. At Gemini, Thomas led the development and launch of the Gemini Credit Card and scaled the program from $0 to hundreds of millions in loan volume. Vince is an experienced developer who has led engineering teams at major financial services companies including Goldman Sachs, Gemini, and Nifty Gateway. Vince also served as an engineering advisor for Ondo Finance.

<br>


# 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](/protocol/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`. | [`createMarket`](/protocol/market-admin#createmarket), [`transferMarketOwnership`](/protocol/market-admin#transfermarketownership), [`terminateMarket`](/protocol/market-admin#terminatemarket), [`setMarketLpWhitelist`](/protocol/market-admin#setmarketlpwhitelist) |
| **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.                                                                                                                            | [`supplyCollateral`](/protocol/liquidity#supplycollateral), [`withdrawCollateral`](/protocol/liquidity#withdrawcollateral)                                                                                                                                             |
| **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.                                                                                                                                 | [`buySwap`](/protocol/swaps#buyswap), [`transferSwapPosition`](/protocol/swaps#transferswapposition), [`exitSwapEarly`](/protocol/swaps#exitswapearly), [`claimEscrow`](/protocol/swaps#claimescrow)                                                                   |
| **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.                                                                                                                                                      | [`makePayment`](/protocol/swaps#makepayment), [`updateMarketRateIndex`](/protocol/views#updatemarketrateindex)                                                                                                                                                         |
| **Liquidator**              | Permissionless role. Calls `liquidateSwap` on an underwater position to recover the shortfall and collect the market's `liquidationIncentive`.                                                                                                                                                         | [`liquidateSwap`](/protocol/swaps#liquidateswap)                                                                                                                                                                                                                       |
| **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`](/protocol/authorization#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`](/protocol/market-admin#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`](/protocol/market-admin#transfermarketownership) + [`acceptMarketOwnership`](/protocol/market-admin#acceptmarketownership) (2-step), or the market can be permanently closed to new swaps via [`terminateMarket`](/protocol/market-admin#terminatemarket). If [`terminateMarket`](/protocol/market-admin#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 **mint price** (`WAD` for an empty pool, `VirtualShareLib.poolMintAndAnchorPrices` otherwise) — not at `getPoolSharePrice`, which is an informational mark that nothing transacts at. Withdrawals burn at a deliberately different, lower mark. See [The three share prices](/protocol/liquidity#the-three-share-prices).

Three gates sit in front of a deposit:

* **1-block deposit lock** — `lpPosition.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)

* [`getSwapNetAmount`](/protocol/views#getswapnetamount) / [`getFreshSwapNetAmount`](/protocol/views#getfreshswapnetamount) — query the net payment obligation at any time.
* [`updateMarketRateIndex`](/protocol/views#updatemarketrateindex) — permissionlessly refresh the on-chain rate index. Anyone can call this before reading P\&L or settling.
* [`transferSwapPosition`](/protocol/swaps#transferswapposition) — reassign ownership of an open swap to another address.

#### 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`**](/protocol/swaps#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`](/protocol/swaps#claimescrow). `makePayment` accepts an array of swap IDs — keepers batch.
* **Early exit —** [**`exitSwapEarly`**](/protocol/swaps#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`](/protocol/swaps#claimescrow) if their settlement transfer failed. Funds always go back to the original `swap.userAddress` — no admin can redirect them.

### Happy-path sequence

```mermaid
sequenceDiagram
    participant Owner as Market Owner
    participant LP
    participant Buyer
    participant SC as SwapCore
    participant Keeper

    Owner->>SC: createMarket(...)
    SC-->>Owner: (fixedMarketId, floatingMarketId)

    LP->>SC: supplyCollateral(marketId, amount, lp, minShares)
    SC-->>LP: CollateralSupplied

    Buyer->>SC: buySwap(marketId, notional, buyer, rateBound, maxMarkup, maxTotalIn)
    SC-->>Buyer: SwapCreated (swapId)

    Note over SC: swap accrues P&L against oracle index

    Keeper->>SC: updateMarketRateIndex(marketId)
    Keeper->>SC: makePayment([swapId])
    SC-->>Buyer: net payment (or LP, depending on direction)
    SC-->>Keeper: SwapClosed
```

### 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`
* **`RateType`** — `BUY_FIXED = 0`, `BUY_FLOATING = 1`

### Chapter layout

| Page                                            | Covers                                                                                                                           |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [Market administration](/protocol/market-admin) | `createMarket`, `transferMarketOwnership`, `acceptMarketOwnership`, `terminateMarket`, `setMarketLpWhitelist`, `getAllMarketIds` |
| [Liquidity provision](/protocol/liquidity)      | `supplyCollateral`, `withdrawCollateral`, and all LP/pool view functions                                                         |
| [Swaps](/protocol/swaps)                        | `buySwap`, `makePayment`, `exitSwapEarly`, `liquidateSwap`, `transferSwapPosition`, `claimEscrow`                                |
| [Authorization](/protocol/authorization)        | `setAuthorization`, `isAuthorized`, and the `onBehalfOf` pattern                                                                 |
| [Views](/protocol/views)                        | Read-only P\&L, liquidity, oracle-index, and mapping getters                                                                     |
| [Events](/protocol/events)                      | Integrator-facing events emitted across the lifecycle                                                                            |


# Core Concepts

The [reference pages](/protocol/overview) 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#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): `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.


# Liquidity provision

This page covers everything an **LP** (or an integrator acting on an LP's behalf) touches on SwapCore. Deposits mint shares against the pool's mark-to-market value, withdrawals burn them at the current share price, and a handful of view functions expose the pool's state.

Source: `SwapCore.sol`

## How the pool works

Each market has a single collateral pool (`Types.Pool`) with three numbers:

* `totalShares` — total LP shares outstanding.
* `totalCollateral` — total underlying `swapToken` the pool holds (LP-supplied + retained swap payments, minus paid-out losses).
* `lockedCollateral` — the portion of `totalCollateral` reserved against open swaps. `totalCollateral − lockedCollateral` is what can actually be withdrawn or used to back new swaps.

### The three share prices

There is no single "share price." SwapCore maintains **three** distinct marks, and using the wrong one is the most common integration error on this page. Deposits and withdrawals price at **different** marks by design — the spread between them is what defeats the deposit → oracle-update → withdraw NAV sandwich.

| Mark                   | Function                                             | Used for                                                                                                                                                                                                               |
| ---------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Mint price**         | `VirtualShareLib.poolMintAndAnchorPrices` (mint leg) | Minting shares in `supplyCollateral`. The accrued-only **upper** bound, no vesting deduction.                                                                                                                          |
| **Burn price**         | `getPoolBurnPrice(marketId)`                         | Burning shares in `withdrawCollateral`, and valuing a position via `getLpValue`. `min(fair, accrued)` **lower** bounds, vesting-deducted, liquidation-threshold-aware. Also seeds the profit-vest `entryPrice` anchor. |
| **Fair / netted mark** | `getPoolSharePrice(marketId)`                        | **Informational only — non-transactable.** The netted-fair midpoint between the two. No transfer in SwapCore prices at this value.                                                                                     |

Mint prices at the upper bound and burn at the lower, so a deposit-then-withdraw round trip cannot extract value from the spread between them. (This is complementary to the per-LP profit-vest cap below, which is what blocks the slower deposit → oracle-update → withdraw sandwich.) All three are floored at `Utils.MIN_SHARE_PRICE` — an underwater pool reports the floor rather than zero so the math stays well-defined.

**If you want the number an LP would actually receive, use `getPoolBurnPrice` (or `getLpValue`, which applies the per-LP cap on top). `getPoolSharePrice` will over-state it.**

**Mint flow (`supplyCollateral`):** shares minted = `collateralAmount × WAD / mintPrice`, rounded down, where `mintPrice` comes from `VirtualShareLib.poolMintAndAnchorPrices` — **not** from `getPoolSharePrice`. For the first deposit (empty pool) the price defaults to `WAD` and one token unit mints one share. Shares and `lastDepositBlock` are credited to `onBehalfOf`. The same call returns the anchor price (the burn mark) that is stored as `entryPrice`; both are reported on the `CollateralSupplied` event, as `mintPrice` and `entryPrice` respectively.

**Burn flow (`withdrawCollateral`):** on a partial withdrawal, shares burned = `amount × WAD / burnPrice`, rounded **up in favor of the pool**, where `burnPrice` is `getPoolBurnPrice(marketId)` further capped at `entryPrice` while the LP's profit-vest window is active; a full exit (`amount ≥` the position's full value) burns the LP's entire share balance directly instead. Shares are burned from `onBehalfOf`'s balance (caller must be `onBehalfOf` or authorized); tokens are delivered to `receiver`.

**Two gates LPs should know about:**

1. **1-block deposit lock.** `supplyCollateral` stamps `lpPos.lastDepositBlock = block.number`, and `withdrawCollateral` reverts with `E413` if you try to withdraw in the same block. Plus, `supplyCollateral` requires auth for `onBehalfOf` even though deposits are nominally beneficial — this prevents an attacker from front-running your withdrawal with a dust deposit that would push your `lastDepositBlock` forward and DoS you for a block.
2. **Expired-unsettled gate.** Both `supplyCollateral` and `withdrawCollateral` call `_revertIfExpiredUnsettled(marketId, market.swapTerm)`. If the market has any expired swaps that haven't been run through `makePayment` yet, LP operations revert with `E415`. The share price is unreliable until those swaps settle — call `makePayment(expiredSwapIds)` first (anyone can).

**MIN\_SHARE\_PRICE guard.** If the pool is at the underwater floor **and** has active swaps (`lockedCollateral > 0`), deposits revert with `E414`. At the floor, shares are massively inflated (1M:1), so a fresh deposit would capture any eventual settlement recovery at the expense of existing LPs. If there are no active swaps, deposits are allowed — there's no pending inflow to capture.

**Last-LP guard.** `withdrawCollateral` will not let `pool.totalShares` drop to zero while `lockedCollateral > 0`. When a withdrawal would redeem every outstanding share, the guard retains one share so accounting stays alive through settlement — the payout is reduced by that one share's worth. It only reverts with `E416` in the degenerate case where the pool holds exactly one share to begin with (there's nothing left to keep the pool alive). Once all swaps have settled (`lockedCollateral == 0`), a last LP can sweep the pool clean.

**Profit-vest 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 `[1h, 7d]`, timelocked). While the window is active, `withdrawCollateral` burns the LP's shares at `min(currentSharePrice, entryPrice)` instead of the live price, neutralizing the deposit → oracle-update → withdraw NAV-sandwich. The cap is one-directional (binds only above the anchor; below-anchor exits take the full loss) and uniform across the position (every share, including mid-vest top-ups, prices at the capped rate — there is no per-tranche accounting). The anchor holds steady on a mid-vest top-up of a non-zero position and is only (re-)set once the prior window expires or the position is fully exited; each deposit extends `vestEndTimestamp`. Off-chain surfaces should read `getLpVestState` and warn an LP before supplying or withdrawing while `capActive` is `true`.

***

## supplyCollateral

```solidity
function supplyCollateral(
    bytes32 marketId,
    uint256 collateralAmount,
    address onBehalfOf,
    uint256 minSharesOut
) external nonReentrant;
```

**Who calls:** an LP, or an authorized delegate. Shares are credited to `onBehalfOf`, but tokens are pulled from `msg.sender` (so bundlers and routers work via Permit2-style flows).

**What it does:** refreshes the rate index, computes the mint and anchor prices in one call (`VirtualShareLib.poolMintAndAnchorPrices`), mints `collateralAmount × WAD / mintPrice` shares to `onBehalfOf`, stamps `lastDepositBlock`, sets the profit-vest anchor (`entryPrice` — the **anchor/burn** leg of that same call, held steady on a mid-vest top-up of a non-zero position; refreshed otherwise) and extends `vestEndTimestamp`, and pulls `collateralAmount` of `market.swapToken` from `msg.sender` into SwapCore.

Note that minting and anchoring use **different** prices from the one call: shares mint at the upper (accrued-only) bound while the anchor records the lower (burn) bound. See [The three share prices](#the-three-share-prices).

**Parameters:**

| Parameter          | Meaning                                                                                                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `marketId`         | Target market.                                                                                                                                                           |
| `collateralAmount` | Tokens to deposit, in `swapToken` decimals. Must be `> 0`.                                                                                                               |
| `onBehalfOf`       | The address that receives the shares. `msg.sender` must be authorized via [`setAuthorization`](/protocol/authorization#setauthorization) if different from `msg.sender`. |
| `minSharesOut`     | Slippage guard: if `> 0` and fewer shares would be minted, revert with `E411`. Pass `0` to disable.                                                                      |

**State changes / events:**

* `lpPositions[marketId][onBehalfOf].shares += sharesToMint`
* `lpPositions[marketId][onBehalfOf].lastDepositBlock = block.number`
* `lpPositions[marketId][onBehalfOf].entryPrice` — (re)set to the **anchor/burn** price when the prior vest expired or the position was empty; held steady on a mid-vest top-up
* `lpPositions[marketId][onBehalfOf].vestEndTimestamp = block.timestamp + admin.lpProfitVestSeconds()`
* `pool.totalShares += sharesToMint`
* `pool.totalCollateral += collateralAmount`
* `IERC20(swapToken).transferFrom(msg.sender, address(this), collateralAmount)`
* Emits `CollateralSupplied(marketId, onBehalfOf, caller, amount, sharesMinted, sharePrice, mintPrice, entryPrice, vestEndTimestamp)` — **nine** arguments; see [the event reference](/protocol/events#collateralsupplied) for the `sharePrice` vs. `mintPrice` distinction.

**Reverts:**

| Code   | Reason                                                                                                                                                      |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `E206` | `msg.sender` is not authorized to act for `onBehalfOf`.                                                                                                     |
| `E300` | Market doesn't exist.                                                                                                                                       |
| `E415` | Market has expired swaps that haven't been settled — call `makePayment` first.                                                                              |
| `E201` | `lpWhitelistEnabled` and `onBehalfOf` is not on `marketLpWhitelist[marketId]`.                                                                              |
| `E400` | `collateralAmount == 0`, or share calculation rounds down to `sharesToMint == 0` (deposit too small for the current share price).                           |
| `E414` | Pool is at `MIN_SHARE_PRICE` floor and has active swaps — deposits are blocked to prevent dilution capture of settlement recovery.                          |
| `E411` | Slippage — fewer shares minted than `minSharesOut`.                                                                                                         |
| `E728` | `oracleIndex <= SwapFormulas.MIN_INDEX`.Refuses LP deposits when the oracle index is at the floor; only hits SpotRate/SpotCompoundRate markets in practice. |

**See also:** [`withdrawCollateral`](#withdrawcollateral), [`getPoolSharePrice`](#getpoolshareprice), [`setAuthorization`](/protocol/authorization#setauthorization).

***

## withdrawCollateral

```solidity
function withdrawCollateral(
    bytes32 marketId,
    uint256 amount,
    address onBehalfOf,
    address receiver,
    uint256 minCollateralOut,
    uint256 maxSharesToRedeem
) external nonReentrant returns (uint256);
```

**Who calls:** an LP, or an authorized delegate of `onBehalfOf`. Shares are burned from `onBehalfOf`'s balance; the redeemed `swapToken` is delivered to `receiver` (any non-zero address — typically `onBehalfOf`, or a bundler/router that passes itself as `receiver` and forwards).

**What it does:** refreshes the rate index, burns enough of `onBehalfOf`'s shares to cover the requested withdrawal, and sends the underlying `swapToken` out. While the position's profit-vest window is active, shares burn at `min(currentSharePrice, entryPrice)` rather than the live price (the `CollateralTokenWithdrawn` event's `capApplied` flag records whether the cap bound). Passing `amount >= the LP's current position value` (idiomatically `type(uint256).max`) triggers a full withdrawal. On an exact-`amount` partial withdrawal the token payout is pinned to `amount`, so the share-burn count — not the payout — absorbs any adverse share-price move; `maxSharesToRedeem` bounds it.

**Parameters:**

| Parameter           | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `marketId`          | Market to withdraw from.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `amount`            | Collateral to withdraw in `swapToken` decimals. Pass a value `≥` the position's full value (idiomatically `type(uint256).max`) to trigger a full withdrawal.                                                                                                                                                                                                                                                                                        |
| `onBehalfOf`        | Owner of the shares. `msg.sender` must be authorized via `setAuthorization` unless they are `onBehalfOf`.                                                                                                                                                                                                                                                                                                                                           |
| `receiver`          | Address that receives the withdrawn `swapToken`. Must be non-zero.                                                                                                                                                                                                                                                                                                                                                                                  |
| `minCollateralOut`  | Slippage guard: if `> 0` and the computed `withdrawalAmount` is lower, revert with `E412`. Pass `0` to disable. Especially useful when passing `type(uint256).max` for `amount`, where you can't predict the exact payout upfront.                                                                                                                                                                                                                  |
| `maxSharesToRedeem` | Share-burn slippage guard — the max shares the call may burn. If `> 0` and the burn would exceed it, revert with `E418`. Pass `0` to disable. This is the share-side dual of `minCollateralOut`: on an exact-`amount` partial withdrawal the payout is pinned to `amount`, so `minCollateralOut` can't catch an adverse share-price move that burns extra shares — `maxSharesToRedeem` can. On a full withdrawal it bounds the total shares burned. |

**Returns:** `withdrawalAmount` — the actual amount of `swapToken` delivered.

**State changes / events:**

* `lpPositions[marketId][onBehalfOf].shares -= sharesToRedeem`
* `pool.totalShares -= sharesToRedeem`
* `pool.totalCollateral -= withdrawalAmount`
* `IERC20(swapToken).safeTransfer(receiver, withdrawalAmount)`
* Emits `CollateralTokenWithdrawn(marketId, onBehalfOf, caller, receiver, amount, sharesRedeemed, sharePrice, capApplied)`.

**Reverts:**

| Code   | Reason                                                                                                                                                                                                                                                                             |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `E103` | `receiver == address(0)`                                                                                                                                                                                                                                                           |
| `E206` | `msg.sender` not authorized for `onBehalfOf`.                                                                                                                                                                                                                                      |
| `E300` | Market doesn't exist.                                                                                                                                                                                                                                                              |
| `E400` | `amount == 0`, or `sharesToRedeem` rounds to 0.                                                                                                                                                                                                                                    |
| `E413` | Same-block deposit + withdraw from `onBehalfOf` (flash-loan guard).                                                                                                                                                                                                                |
| `E407` | Pool has zero total shares.                                                                                                                                                                                                                                                        |
| `E405` | Computed `withdrawalAmount` exceeds `totalLPAvailableCollateral(marketId)` — the pool doesn't have enough unlocked collateral.                                                                                                                                                     |
| `E412` | Slippage — `withdrawalAmount < minCollateralOut`.                                                                                                                                                                                                                                  |
| `E415` | Expired swaps haven't been settled yet — call `makePayment` first.                                                                                                                                                                                                                 |
| `E416` | You'd redeem the pool's only share (`pool.totalShares == 1`) while `lockedCollateral > 0` — there's no share left to retain, so accounting couldn't stay alive through settlement. (When more than one share is redeemed, the guard silently keeps one back instead of reverting.) |
| `E417` | Cap is active and this would be a full last-LP exit on an idle pool — would bypass the vest cap. Wait for vest expiry or do a partial withdrawal.                                                                                                                                  |
| `E418` | Share-burn slippage — `maxSharesToRedeem > 0` and the burn would redeem more shares than that (adverse share-price move). Checked after the last-LP guard, so a guard-reduced burn is never rejected.                                                                              |

**See also:** [`supplyCollateral`](#supplycollateral), [`totalLPAvailableCollateral`](#totallpavailablecollateral), [`getLpValue`](#getlpvalue).

***

## getLpPosition

```solidity
function getLpPosition(bytes32 marketId, address account)
    external view returns (Types.LpPosition memory);
```

**Who calls:** anyone — UIs, indexers, adapters.

**What it does:** returns the full `LpPosition` struct for `account`:

```solidity
struct LpPosition {
    uint256 shares;            // pool shares held
    uint256 entryPrice;        // profit-vest anchor (burn price at deposit), WAD
    uint64  lastDepositBlock;  // block of most recent supplyCollateral
    uint64  vestEndTimestamp;  // profit-vest window end (unix seconds)
}
```

**Field order matters if you decode positionally.** `entryPrice` is the second field and `lastDepositBlock` the third, and `lastDepositBlock` is a `uint64` packed into one slot with `vestEndTimestamp`. Getting the order wrong returns a WAD share price where a block number is expected — a value large enough to look like a far-future block rather than to fail loudly. Named/ABI decoding is unaffected.

`entryPrice` anchors to the **burn** price at deposit (see [`getPoolBurnPrice`](#getpoolburnprice)), which is what makes `min(currentBurnPrice, entryPrice)` a meaningful cap.

Reverts with `E300` if the market doesn't exist.

***

## getLpShares

```solidity
function getLpShares(bytes32 marketId, address lpAddress)
    external view returns (uint256);
```

Thin accessor — returns just `lpPositions[marketId][lpAddress].shares`. Does not revert on a non-existent market; it will simply return `0`. Use this when you only need the share count and want to avoid the `getLpPosition` tuple.

***

## getLpValue

```solidity
function getLpValue(bytes32 marketId, address lpAddress)
    external view returns (uint256);
```

Returns the redeemable value of `lpAddress`'s shares, denominated in `swapToken` decimals:

```
value = shares × min(getPoolBurnPrice(marketId), entryPrice if vest active) / WAD
```

It prices through **`getPoolBurnPrice`** — the same mark `withdrawCollateral` burns at — **not** `getPoolSharePrice`. That is deliberate: valuation and settlement stay consistent, so the number a UI shows matches the number the LP receives.

It projects the oracle index forward and reflects unrealized swap P\&L, floored at `MIN_SHARE_PRICE` (so an underwater pool still returns a positive number), and it **also applies the LP's profit-vest cap** when active. The result is therefore what the LP would actually receive on `withdrawCollateral`, not the uncapped pool NAV.

To show an uncapped mark-to-market figure instead — for analytics rather than for a withdrawal quote — compute `shares × getPoolSharePrice(marketId) / WAD` off-chain, and label it as such.

***

## getPoolSharePrice

```solidity
function getPoolSharePrice(bytes32 marketId) public view returns (uint256);
```

Returns the **informational** netted-fair share price in WAD (`1e18 = 1.0`) — the midpoint between the mint and burn marks. If the pool has `totalShares == 0`, returns `WAD` as a safe default (prevents manipulation of the first-deposit ratio). Otherwise it projects a fresh oracle index without mutating state and iterates over the market's buckets via `BucketBoundsLib.calculateLpSharePrice` to compute the fair value.

> **This price is non-transactable, and nothing in SwapCore trades at it.** It has **no internal callers**. `supplyCollateral` mints at `VirtualShareLib.poolMintAndAnchorPrices`, `withdrawCollateral` burns at [`getPoolBurnPrice`](#getpoolburnprice), and `getLpValue` values at `getPoolBurnPrice` too. Quoting an LP's exit from `getPoolSharePrice` **over-states the proceeds**, because the burn mark sits at the lower bound and this one sits above it. Use it for analytics and NAV display, never for a withdrawal quote.

The result is floored at `Utils.MIN_SHARE_PRICE`. If the pool is deeply underwater, `getPoolSharePrice` will report the floor — consumers should treat a floor reading as "pool is distressed, deposits will be blocked if `lockedCollateral > 0`" rather than as an accurate valuation.

Safe to call off-chain.

***

## getPoolBurnPrice

```solidity
function getPoolBurnPrice(bytes32 marketId) public view returns (uint256);
```

**The price an immediate withdrawal actually burns at** — the mark to use for any redemption quote.

Returns the fair mark floored at the accrued-only lower bound, vesting-deducted and liquidation-threshold-aware, in WAD. Delegates to `VirtualShareLib.poolBurnPrice`, which shares one body with the anchor leg of `poolMintAndAnchorPrices` so the burn price and the profit-vest anchor can never diverge.

This is a **pool-level** price. The per-LP profit-vest cap applies **on top** of it: an LP inside an active vest window burns at `min(getPoolBurnPrice, entryPrice)`. For the true redeemable figure for a specific LP, use [`getLpValue`](#getlpvalue), which applies both. `Views.getLpVestState` reports `capActive` against this same price.

Called internally by `withdrawCollateral` and `getLpValue`. Floored at `Utils.MIN_SHARE_PRICE`. Safe to call off-chain; does not mutate state.

***

## getPoolSharePriceVirtual

```solidity
function getPoolSharePriceVirtual(bytes32 marketId) external view returns (uint256);
```

**Deployed on the `Views` helper contract, not SwapCore.** Call it against the `Views` address. It is a thin wrapper over `SwapCore.getVirtualSharePrice(marketId, Types.PriceMode.Fair)`.

Returns the LP share price in WAD (1e18 = 1.0) after virtually settling all expired-unsettled swaps at the queue head. Unlike `getPoolSharePrice`, which can read stale when expired swaps haven't been run through `makePayment` yet, this walks the `expiryQueue` from the current pointer forward, simulates the settlement of each expired-but-unsettled swap (rolling its P\&L into the pool), and prices shares against the resulting projected pool value.

If there are no expired-unsettled swaps, it transparently falls back to `getPoolSharePrice(marketId)`.

This is the value UIs should display as **"current NAV per share"** — an unrealized mark that stays meaningful during the **E415** expired-unsettled window, when `getPoolSharePrice` cannot be trusted.

> **It is not a withdrawal quote.** Being Fair mode, it equals the non-virtual `getPoolSharePrice` whenever a withdrawal is actually possible — the two diverge only *inside* the E415 window, during which LP operations revert anyway. It is neither vesting-deducted nor liquidation-threshold-aware, and it applies no per-LP profit-vest cap. For what an LP would receive, use [`getLpValue`](#getlpvalue); for the pool-level burn mark, [`getPoolBurnPrice`](#getpoolburnprice).

The result is floored at `Utils.MIN_SHARE_PRICE`, same as `getPoolSharePrice`. Safe to call off-chain; does not mutate state.

***

## getVirtualSharePrice

```solidity
function getVirtualSharePrice(bytes32 marketId, Types.PriceMode mode)
    external view returns (uint256);
```

On **SwapCore**. The full-fidelity form of the above — same virtual settlement of the expired queue head, but with the valuation basis selectable. `getPoolSharePriceVirtual` is exactly this with `mode = Fair`.

| `Types.PriceMode` | Basis                                                                                                           | Use for                                                                                                |
| ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `Fair`            | Accrued + projected P\&L per active bucket.                                                                     | NAV display. Equals `getPoolSharePrice` whenever a withdrawal is possible.                             |
| `AccruedOnly`     | Drops each active bucket's projected leg — the basis the liquidation trigger uses (`Utils.isSwapLiquidatable`). | Guard thresholds **only**.                                                                             |
| `Conservative`    | `min(Fair, AccruedOnly)` — projected losses count, projected gains don't.                                       | Solvency-sensitive valuation; never exceeds what a live pool-side liquidation would leave in the pool. |

Two traps worth stating plainly:

* `AccruedOnly` is **not** a `min` with `Fair` and **can exceed it**. `AccruedOnly > Fair` is a meaningful signal — it says a forced exit would burn below the immediately-executable value. It is bucket-averaged and reward-slack approximate, so it is safe as a guard threshold and **never** as a mint or burn price.
* `Conservative` is what `KairosMarketAdapter._realAssets` reads, so a Morpho-style vault cannot redeem idle cash against an optimistic mark before a liquidation crystallizes the loss. Direct-LP withdrawals reach the same `min` via `getPoolBurnPrice` — without the virtual settlement, which the E415 gate rules out on that path.

***

## getPoolMetrics

```solidity
function getPoolMetrics(bytes32 marketId)
    external view returns (uint256 totalShares, uint256 totalCollateral, uint256 lockedCollateral);
```

One-shot read of the three numbers that make up `Types.Pool`. `totalCollateral − lockedCollateral` is the unlocked balance available for new swaps or LP withdrawals; the dedicated [`totalLPAvailableCollateral`](#totallpavailablecollateral) helper returns the same subtraction.

***

## totalLPAvailableCollateral

```solidity
function totalLPAvailableCollateral(bytes32 marketId) public view returns (uint256);
```

Returns `pool.totalCollateral − pool.lockedCollateral` — the amount of `swapToken` in the pool that is **not** currently backing an open swap. This is the number that both `withdrawCollateral` and `buySwap` check against before committing state. Pair it with `Views.getCalculatedAvailableLiquidity(marketId)`, which lives on the read-only `Views` contract, to convert "unlocked collateral" into "maximum notional a new swap can use" (it combines this value with the live base rate, swap term, and leverage multiplier via `SwapFormulas.calculateAvailableLiquidity`).


# Swaps

This page covers the full **buyer lifecycle**: opening a swap, moving it, closing it (three ways), and retrieving escrowed settlement if a direct transfer ever fails. Every function lives in `SwapCore.sol`.

## Lifecycle at a glance

```
        ┌─────────────┐
        │   buySwap   │
        └──────┬──────┘
               │
   (optionally transferSwapPosition)
               │
     ┌─────────┼─────────────────────┐
     │         │                     │
     ▼         ▼                     ▼
 makePayment  exitSwapEarly     liquidateSwap
 (expiry)     (before expiry,   (underwater,
               owner only)       anyone)
     │         │                     │
     └─────────┼─────────────────────┘
               │
       (if transfer failed)
               │
               ▼
         claimEscrow
```

Three things to keep in mind across all of these:

* **Collateral cap.** The net payment a swap can produce at settlement is capped at each side's posted collateral — the buyer can never lose more than `collateralBalance`, the pool can never lose more than `poolCollateralBacking`. `getSwapNetAmount` applies the same cap.
* **Accrued-only liquidation.** Liquidation triggers when the **accrued** P\&L exceeds a side's posted collateral. There is no "projected" liquidation that closes a swap based on where it's trending. This keeps the rule simple and predictable.
* **Failed transfers escrow.** If SwapCore can't deliver settlement to the buyer (blacklisted address, blocked receiver, etc.) the payout is held in `escrowedCollateral[swapId]` and emits `BuyerTransferFailed`. The buyer (or their authorized delegate) retrieves it later via [`claimEscrow`](#claimescrow). Funds always go back to the original `swap.userAddress` — there is no admin redirect path.

***

## buySwap

```solidity
function buySwap(
    bytes32 marketId,
    uint256 notionalAmount,
    address onBehalfOf,
    int256  rateBound,
    uint256 maxMarkup,
    uint256 maxTotalIn
) external nonReentrant returns (bytes32 swapId);
```

**Who calls:** anyone. `buySwap` is beneficial to `onBehalfOf` (they get the position), so unlike most other `onBehalfOf` entry points it does **not** require `setAuthorization`. The caller pays the collateral and fees.

**What it does:**

1. Loads the market, checks it's live (`exists && !terminated`), and reads `baseRate` from `baseSwapRateOracle` (tenor-dependent).
2. Computes `availableLiquidity` from the pool's unlocked collateral — `totalLPAvailableCollateral(marketId)` (i.e. `totalCollateral − lockedCollateral`) — via `SwapFormulas.calculateAvailableLiquidity(availableCollateral, baseRate, swapTerm, leverageMultiplier)`, and asserts `notionalAmount > 0 && ≤ availableLiquidity` (reverting `E509` / `E508` respectively).
3. Integrates the kinked utilization fee curve over `[uPre, uPost]` using `market.utilFeeSlopeWad`, `kinkUtilization`, and `maxKinkFeeWad`. The utilization is measured against seasoned (EMA) liquidity, not the spot pool balance — the capacity gate in step 2 uses spot, but the fee is priced on the smoothed average (see [Core concepts → Seasoned utilization fee](/protocol/core-concepts#seasoned-utilization-fee)). This is a JIT-deposit defense: a same-block LP deposit expands capacity but can't cheapen the fee.
4. Reads `riskPremium` from the market's risk premium oracle (returns `0` if the address is zero).
5. Builds `swapRate`: for **BUY\_FIXED**, `swapRate = baseRate + utilFee + riskPremium`; for BUY\_FLOATING, `swapRate` is left at the sentinel value `0` and the floating leg is derived at settlement from the entry/expiry cumulative index. The buyer's pre-funded collateral still covers `utilFee + riskPremium` on top of `baseRate` — those accrue to LPs through the floating leg at settlement.
6. Applies slippage guards: `rateBound` and `maxMarkup` (see below), then — after sizing collateral, fee, and bounty — checks `requiredBuyerCollateral + protocolFee + liquidationBounty ≤ maxTotalIn`, reverting `E513` if exceeded.
7. Sizes `requiredBuyerCollateral` and `totalLpCollateralRequired` using the fee-inclusive and base-only formulas respectively; rejects if either side is below `market.minCollateral` or if the pool lacks enough unlocked collateral.
8. Reads protocol fee config from Admin in a single `admin.getFeeConfig()` call — returning `feeRate`, `creatorFeeShare`, and `protocolMultisig` together — and computes `protocolFee = notionalAmount × feeRate × swapTerm / (SECONDS_IN_YEAR × WAD)`.
9. Computes `liquidationBounty = requiredBuyerCollateral × market.liquidationIncentive / WAD`. This is prefunded by the buyer at entry, held outside `collateralBalance`, and either paid to a liquidator on liquidation or returned to the buyer at normal expiry.
10. Snapshots the current cumulative index from `referenceRateOracle` via `rateIndex.update` (reverts `E728` if it's at the `MIN_INDEX` floor). The reference rate itself isn't materialized until settlement, when the entry/expiry index ratio is converted via the market's `rateConvention`.
11. Generates `swapId` via `Utils.generateSwapId`, writes the `SwapPosition` to storage (embedding the just-snapshotted index as `entryFloatingIndex`), assigns it to a bucket (`Utils.addSwapToBucket`), bumps `pool.lockedCollateral += totalLpCollateralRequired`, and appends the `swapId` to the market's `expiryQueue` so settlement/LP-gate logic can find it later.
12. Pulls `requiredBuyerCollateral + protocolFee + liquidationBounty` from `msg.sender`. The protocol fee is split: if `creatorFeeShare > 0` and the market has an owner, the creator's share is sent to `market.marketOwner` via a graceful low-level call (on failure the full fee goes to the multisig); the remainder goes to the `protocolMultisig` returned by `getFeeConfig`.
13. Emits `SwapCreated`, plus `ProtocolFeeCollected` and `CreatorFeeCollected` when applicable.

**Parameters:**

| Parameter        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `marketId`       | Which side of the pair to open on.                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `notionalAmount` | Swap notional in `swapToken` decimals. Must be `> 0` and `≤ getCalculatedAvailableLiquidity(marketId)`.                                                                                                                                                                                                                                                                                                                                                             |
| `onBehalfOf`     | The address that will own the new swap. Must not be `address(0)`.                                                                                                                                                                                                                                                                                                                                                                                                   |
| `rateBound`      | Slippage on the rate. For **BUY\_FIXED**, this is a **ceiling on `swapRate`** — revert if `swapRate > rateBound`. For **BUY\_FLOATING**, it's a **floor on `baseRate`** — revert if `baseRate < rateBound`. Pass `type(int256).max` to disable.                                                                                                                                                                                                                     |
| `maxMarkup`      | Slippage on the fee markup: revert if `utilFee + riskPremium > maxMarkup`. Pass `0` to disable.                                                                                                                                                                                                                                                                                                                                                                     |
| `maxTotalIn`     | Ceiling on the total tokens pulled from **`msg.sender`** (`requiredBuyerCollateral + protocolFee + liquidationBounty`). Reverts `E513` if exceeded. Pass `type(uint256).max` to disable. Necessary because buyer collateral is sized off `abs(baseRate)`, so a *favorable* `rateBound` check (BUY\_FLOATING with a high base rate, or BUY\_FIXED with a negative base rate) can still inflate the posted collateral — `rateBound` alone doesn't bound tokens spent. |

**Returns:** `swapId` — a unique `bytes32` produced by `Utils.generateSwapId(marketId, onBehalfOf, block.timestamp, ++globalNonce)`

**Payment from caller:** `requiredBuyerCollateral + protocolFee + liquidationBounty`. Approve this amount on `swapToken` before calling. `msg.sender` is the token source; `onBehalfOf` is the position owner. This decoupling lets bundlers and routers pay on behalf of the end user.

**State changes / events:**

* `swapPositions[swapId]` written with all fields from `Types.SwapPosition`.
* `buckets[marketId][bucketId]` updated with the new aggregates.
* `pool.lockedCollateral += totalLpCollateralRequired`.
* `expiryQueue[marketId].push(swapId)` — the swap ID is appended to the market's expiry queue. The separate `expiryQueuePointer[marketId]` (which tracks the earliest unsettled position) is advanced opportunistically by `Utils.tryAdvanceExpiryPointer` on later settlements, not on entry.
* `IERC20(swapToken).transferFrom(msg.sender, address(this), requiredBuyerCollateral + protocolFee + liquidationBounty)`.
* `IERC20(swapToken).transfer` (or low-level call for creator split) to the multisig and/or market owner.
* Emits `SwapCreated`, `ProtocolFeeCollected` (if `protocolFee > 0`), `CreatorFeeCollected` (if the creator split transfer succeeded).

**Reverts:**

| Code         | Reason                                                                                                      |
| ------------ | ----------------------------------------------------------------------------------------------------------- |
| `E103`       | `onBehalfOf == address(0)`.                                                                                 |
| `E300`       | Market doesn't exist.                                                                                       |
| `E304`       | Market is terminated — no new swaps.                                                                        |
| `E611`       | Base rate oracle returned an invalid reading.                                                               |
| `E509`       | `notionalAmount == 0`.                                                                                      |
| `E508`       | `notionalAmount` exceeds the pool's available liquidity.                                                    |
| `E512`       | BUY\_FIXED only — `baseRate < 0` on a Cumulative reference oracle (floating leg can't go negative).         |
| `E510`       | Slippage — rate bound exceeded (BUY\_FIXED: `swapRate > rateBound`; BUY\_FLOATING: `baseRate < rateBound`). |
| `E511`       | Slippage — `utilFee + riskPremium > maxMarkup`.                                                             |
| `E503`       | `requiredBuyerCollateral < minCollateral` or `totalLpCollateralRequired < minCollateral`.                   |
| `E404`       | Pool has insufficient available collateral to back the LP side.                                             |
| `E513`       | `requiredBuyerCollateral + protocolFee + liquidationBounty > maxTotalIn` — total-in slippage guard.         |
| `E606`       | Reference rate oracle returned invalid or zero data during the index update.                                |
| `E728`       | `oracleIndex <= SwapFormulas.MIN_INDEX` — reference index at the floor.                                     |
| ERC20 revert | `swapToken.transferFrom` failed (allowance or balance).                                                     |

**Choosing slippage bounds.**

* **BUY\_FIXED:** you're locking in what you pay, so the risk is rates move down between your quote and the tx landing. Use `rateBound = quotedSwapRate × (1 + tolerance)` to cap your worst case.
* **BUY\_FLOATING:** you're locking in what you receive, so the risk is the base rate drops. Use `rateBound = quotedBaseRate × (1 − tolerance)` as a floor.
* `maxMarkup` is useful in both directions — it caps the fee portion of the all-in rate so a utilization spike after quote time doesn't burn you. Set it to your quoted `utilFee + riskPremium` plus a small buffer.
* Set `maxTotalIn` to your expected total cost plus a small buffer to bound worst-case tokens pulled, especially for BUY\_FLOATING

**See also:** [`getCalculatedAvailableLiquidity`](/protocol/views#getcalculatedavailableliquidity), [`getSwapNetAmount`](/protocol/views#getswapnetamount), [`transferSwapPosition`](#transferswapposition).

***

## makePayment

```solidity
function makePayment(bytes32[] calldata swapIds)
    public nonReentrant returns (Types.SettlementResult[] memory results);
```

**Who calls:** anyone. Settlement is permissionless — keepers, bots, adapters, even the buyer or LP themselves. There's no keeper whitelist.

**What it does:** iterates the `swapIds` array and, for each one that's reached expiry (`block.timestamp ≥ swap.entryTimestamp + market.swapTerm`), runs the settlement pipeline:

1. Validates `swap.entryTimestamp != 0` and `!swap.settled`.
2. Updates the market's oracle index to the latest read.
3. Resolves the historical oracle index at expiry: after best-effort calling `updateMarketRateIndex` to densify snapshots, if no snapshot exists at or after `entryTimestamp + swapTerm` it reverts with **E450** (refusing to extrapolate past the last known data); otherwise reads `rateIndex.getIndexAt(oracle, expiryTimestamp)`.
4. Calls `Utils.settleSwap` to compute fixed and floating payments over the full term, net them, cap at each side's collateral, and determine `netRecipient` (`0` = LP receives, `1` = buyer receives).
5. Transfers the net to the winning side and releases the unused collateral back to its original owner (or to the pool). If the buyer-side transfer reverts, the payout is moved to `escrowedCollateral[swapId]` and `BuyerTransferFailed` is emitted.
6. Marks `swap.settled = true`, records `settlementPayouts[swapId] = buyerCollateralReleased`, and advances `expiryQueuePointer[marketId]` past now-settled queue entries via `Utils.tryAdvanceExpiryPointer` (the queue itself is append-only — settled entries are zeroed in place but the array does not shrink).
7. Emits `SwapClosed` with `closureType = 0` and, if pool collateral hit zero, `PoolCollateralZeroed`.

**Returns:** one `Types.SettlementResult` per input ID:

```solidity
struct SettlementResult {
    uint256 settlementAmount; // net obligation transferred
    uint8   netRecipient;     // 0 = LP receives, 1 = buyer receives
}
```

**Batching.** Pass an array because keepers typically sweep many swaps at once — the oracle index update per-swap is cheap, and batching amortizes the `rateIndex.update` call.

**Reverts (per swap, the whole batch aborts):**

| Code   | Reason                                                                                                                                                                                          |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `E500` | `swap.entryTimestamp == 0` (swap doesn't exist).                                                                                                                                                |
| `E506` | `block.timestamp < expiry` — too early to settle this swap.                                                                                                                                     |
| `E450` | No oracle snapshot at or after `swap.entryTimestamp + swapTerm` — settlement refuses to extrapolate. Keepers should call `updateMarketRateIndex` post-maturity to advance the snapshot horizon. |

> Already-settled IDs in the batch are **skipped** (not reverted), so a partially pre-settled batch still succeeds for the rest.

**See also:** [`exitSwapEarly`](#exitswapearly), [`liquidateSwap`](#liquidateswap), [`claimEscrow`](#claimescrow), [`updateMarketRateIndex`](/protocol/views#updatemarketrateindex).

***

## exitSwapEarly

```solidity
function exitSwapEarly(
    Types.ExitRequest[] calldata requests,
    address onBehalfOf
) external nonReentrant returns (Types.SettlementResult[] memory results);
```

**Who calls:** the owner of every swap in `requests`, or an authorized delegate of that owner. All `requests[i].swapId` must belong to `onBehalfOf`.

**What it does:** marks each swap `isEarlyExit = true` and settles them using the same settlement pipeline as `makePayment` — but with the `earlyExit` branch taken, which (a) uses `block.timestamp` as the effective expiry, (b) re-reads the base rate for the remaining tenor to project the unrealized fixed leg, and (c) applies `market.earlyExitFee` to the buyer. After settlement, each request's `minExitAmount` is checked against the buyer's total entitlement (`settlementPayouts[swapId] + escrowedCollateral[swapId]` — both are summed so an escrowed payout still satisfies the slippage check).

Requires the market to have been created with `earlyExitAllowed = true`, and the swap must still be open and already have at least one second of elapsed time.

**Parameters:**

| Parameter    | Meaning                                                                                                                             |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `requests[]` | Array of `ExitRequest { bytes32 swapId; uint256 minExitAmount; }`. `minExitAmount == 0` disables the slippage check for that entry. |
| `onBehalfOf` | Swap owner. All `requests[i]` must reference swaps owned by this address.                                                           |

**Returns:** one `SettlementResult` per request, same shape as `makePayment`.

**State changes / events:**

* Each swap: `swap.isEarlyExit = true`, then full settlement (see `makePayment`).
* Emits `SwapClosed` with `closureType = 1` per swap.

**Reverts:**

| Code   | Reason                                                                                                              |
| ------ | ------------------------------------------------------------------------------------------------------------------- |
| `E206` | `msg.sender` not authorized for `onBehalfOf`.                                                                       |
| `E500` | Swap doesn't exist.                                                                                                 |
| `E203` | Swap is not owned by `onBehalfOf`.                                                                                  |
| `E501` | Swap already settled.                                                                                               |
| `E506` | `swap.entryTimestamp >= block.timestamp` — can't exit in the same second you opened.                                |
| `E550` | Market was created with `earlyExitAllowed = false`.                                                                 |
| `E502` | Swap is already past expiry — use `makePayment` instead.                                                            |
| `E611` | Base rate oracle returned an invalid reading for the remaining tenor (fetched to project the unrealized fixed leg). |
| `E606` | Reference rate oracle returned invalid or zero data during the index update.                                        |
| `E552` | Slippage — buyer payout below `minExitAmount` for some request.                                                     |

**See also:** [`buySwap`](#buyswap), [`makePayment`](#makepayment).

***

## liquidateSwap

```solidity
function liquidateSwap(bytes32 swapId) external nonReentrant;
```

**Who calls:** anyone. Liquidation is permissionless — `msg.sender` becomes the liquidator and is paid a reward whose source depends on which side is being liquidated. Buyer-side liquidations pay the **prefunded `liquidationBounty`** that was banked from the buyer at `buySwap` entry (held separately from `collateralBalance` and refunded to the buyer at normal expiry if liquidation never happens). Pool-side liquidations pay `market.liquidationIncentive × poolCollateralBacking / WAD`, drawn from the LP backing for this swap.

**What it does:** updates the market's oracle index, delegates to `Utils.liquidateSwap`, and emits `SwapLiquidated` + `SwapClosed` (with `closureType = 2`). `Utils.liquidateSwap` computes the accrued P\&L and decides whether the **buyer** is liquidatable (their `collateralBalance` can't cover the payment they owe), whether the **pool** is liquidatable (its `poolCollateralBacking` can't cover the payment it owes), or neither.

Key properties:

* **Accrued, not projected.** Liquidation is based on payments that have already been earned against the oracle index up to `block.timestamp` — not on where the rate might go. A swap is only liquidatable when one side's collateral is mathematically insufficient right now.
* **Liquidator incentive.** The payout source depends on which side is liquidated:
  * **Buyer-side**: the liquidator receives the prefunded `swap.liquidationBounty` — a fixed amount banked at swap entry as `requiredBuyerCollateral × market.liquidationIncentive / WAD`. It is held outside `collateralBalance` and is excluded from bucket aggregates; if the swap is *never* liquidated, the bounty is returned to the buyer at normal settlement.
  * **Pool-side**: the liquidator receives `swap.poolCollateralBacking × market.liquidationIncentive / WAD`, drawn from the LP backing reserved for this swap. The trigger threshold is set to align with this payout, so the liquidator's reward and the liquidatable boundary stay consistent.
* **Single side.** At most one side is liquidated in a call; the other receives its normal settlement.
* **Escrow fallback.** If the post-liquidation transfer to the buyer reverts, the payout is escrowed for later [`claimEscrow`](#claimescrow).

**Parameters:** `swapId` — the position to liquidate.

**State changes / events:**

* `swap.settled = true`; `settlementPayouts[swapId]` recorded.
* `pool.lockedCollateral -= lpCollateralReleased`; `pool.totalCollateral` adjusted for any pool-side loss.
* `expiryQueuePointer[marketId]` advanced past now-settled entries via `Utils.tryAdvanceExpiryPointer` (the queue itself is append-only — settled entries are zeroed in place but the array does not shrink).
* Emits `SwapLiquidated(marketId, swapId, liquidatedParty, buyerLiquidated, poolLiquidated, collateralTransferred, liquidator)`.
* Emits `SwapClosed` with `closureType = 2`.

**Reverts:**

| Code   | Reason                                                                                                                                                       |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `E608` | `swapId` doesn't exist — its zero-address reference oracle is uninitialized, so the index update reverts before liquidation runs.                            |
| `E606` | Reference rate oracle returned invalid or zero data during the index update.                                                                                 |
| `E507` | Not liquidatable — neither buyer nor pool is underwater on accrued P\&L. Also covers already-settled and past-expiry swaps (settle those via `makePayment`). |

**See also:** [`getSwapNetAmount`](/protocol/views#getswapnetamount) to check a swap's P\&L before attempting liquidation, [`claimEscrow`](#claimescrow) for the escrow fallback.

***

## transferSwapPosition

```solidity
function transferSwapPosition(bytes32 swapId, address newOwner, address onBehalfOf) external;
```

**Who calls:** the current swap owner, or an authorized delegate of that owner.

**What it does:** flips `swap.userAddress` from `onBehalfOf` to `newOwner`. After this call, the new owner is the one who will receive any positive net settlement and who can call `exitSwapEarly` / `claimEscrow` on this swap. The swap's economic terms are unchanged.

**Parameters:**

| Parameter    | Meaning                                                                                                                                                                                          |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `swapId`     | The swap to transfer. Must exist and be unsettled.                                                                                                                                               |
| `newOwner`   | The new owner. Must not be `address(0)`. No authorization check is performed on `newOwner` — the transfer is one-sided, so only send it to an address you control or a contract that expects it. |
| `onBehalfOf` | The current owner. `msg.sender` must be authorized for this address unless they are `onBehalfOf`.                                                                                                |

**State changes / events:**

* `swap.userAddress = newOwner`.
* Emits `SwapTransferred(swapId, previousOwner, newOwner, caller)`.

**Reverts:**

| Code   | Reason                                        |
| ------ | --------------------------------------------- |
| `E206` | `msg.sender` not authorized for `onBehalfOf`. |
| `E500` | Swap doesn't exist.                           |
| `E203` | `swap.userAddress != onBehalfOf`.             |
| `E501` | Swap already settled.                         |
| `E103` | `newOwner == address(0)`.                     |

This is the hook that `SwapPositionWrapper` (the ERC-721 wrapper contract) uses under the hood — transferring the NFT calls `transferSwapPosition` to move the underlying position to whoever holds the token.

**See also:** [`setAuthorization`](/protocol/authorization#setauthorization).

***

## claimEscrow

```solidity
function claimEscrow(bytes32 swapId) external nonReentrant returns (uint256 amount);
```

**Who calls:** the original swap owner (`swap.userAddress`) or an authorized delegate.

**What it does:** retrieves collateral that SwapCore couldn't deliver at settlement or liquidation. When the buyer-side transfer in `_settleSwaps` or `liquidateSwap` reverts (e.g., the recipient became blacklisted between `buySwap` and settlement), the payout is moved to `escrowedCollateral[swapId]` and `BuyerTransferFailed` is emitted. `claimEscrow` pulls that balance, zeroes the entry, and sends it to `swap.userAddress`.

**Crucially, funds are always delivered to the original `swap.userAddress`, never to `msg.sender`**. An authorized delegate can trigger the claim but cannot redirect the money. If the recipient is still blocked, `safeTransfer` will revert and the escrow stays in place — try again later.

**Parameters:** `swapId`.

**Returns:** `amount` — the amount delivered.

**State changes / events:**

* `escrowedCollateral[swapId] = 0` (before the transfer, CEI pattern).
* `IERC20(swapToken).safeTransfer(swap.userAddress, amount)`.
* Emits `EscrowClaimed(swapId, recipient, caller, amount)`.

**Reverts:**

| Code         | Reason                                                |
| ------------ | ----------------------------------------------------- |
| `E206`       | `msg.sender` not authorized for `swap.userAddress`.   |
| `E400`       | `escrowedCollateral[swapId] == 0` — nothing to claim. |
| ERC20 revert | Target is still blocked from receiving the token.     |

**See also:** [`makePayment`](#makepayment), [`exitSwapEarly`](#exitswapearly), [`liquidateSwap`](#liquidateswap), [`setAuthorization`](/protocol/authorization#setauthorization).


# Market administration

Everything on this page is called by a **market owner** (or someone stepping into the role). A market is created by `createMarket` — whoever calls it becomes the owner of the resulting pair and can later transfer that role, update the LP whitelist, or permanently stop new swaps.

Source: `SwapCore.sol`

## Market lifecycle

```
createMarket ──► (market is live) ──► transferMarketOwnership ──► acceptMarketOwnership
                       │                                               │
                       │                                               ▼
                       │                                       (new owner active)
                       │
                       └──► setMarketLpWhitelist (if lpWhitelistEnabled)
                       │
                       └──► terminateMarket ──► (no new swaps; existing swaps still settle)
```

Note that **every call to `createMarket` produces two market IDs** — one BUY\_FIXED and one BUY\_FLOATING — that share the same oracles, collateral token, term, and config. The two sides are independent markets from an ownership and accounting standpoint, but `transferMarketOwnership` / `terminateMarket` / `setMarketLpWhitelist` each operate on a single `marketId`, so if you want to change both sides you call each function twice.

***

## createMarket

```solidity
function createMarket(
    address referenceRateOracle,
    address baseSwapRateOracle,
    address swapToken,
    uint64  leverageMultiplier,
    uint32  swapTerm,
    uint256 utilFeeSlopeWad,
    uint256 kinkUtilization,
    uint256 maxKinkFeeWad,
    bool    earlyExitAllowed,
    uint256 earlyExitFee,
    uint256 liquidationIncentive,
    uint32  numBuckets,
    uint32  bucketInterval,
    address fixedRiskPremiumOracle,
    address floatingRiskPremiumOracle,
    bool    lpWhitelistEnabled,
    uint88  minCollateral,
    Types.RateConvention rateConvention
) external payable nonReentrant returns (bytes32 fixedMarketId, bytes32 floatingMarketId);
```

**Who calls:** anyone. No prior permission required — but if the protocol has a market creation fee configured in `Admin`, `msg.sender` has to pay it (ETH or ERC20, depending on `CREATE_MARKET_FEE_TOKEN`).

**What it does:** atomically creates a BUY\_FIXED / BUY\_FLOATING market pair. For each side, it validates oracles, checks parameter bounds, generates a unique market ID (hashing the params with `creator`, `timestamp`, and a nonce), initializes the pool and rate index, and emits `MarketCreated` + `MarketConfigured`. The caller is recorded as `marketOwner` on both sides.

**Parameters:**

| Parameter                   | Meaning                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `referenceRateOracle`       | Oracle that provides the floating rate used by the swap's floating leg. Shared between the two sides of the pair.                                                                                                                                                                                                                                                                                                     |
| `baseSwapRateOracle`        | Oracle that provides the tenor-dependent base rate used in swap pricing and collateral sizing. Shared between sides.                                                                                                                                                                                                                                                                                                  |
| `swapToken`                 | ERC20 used for collateral and settlement. Must be a standard ERC20 (no rebasing, no fee-on-transfer).                                                                                                                                                                                                                                                                                                                 |
| `leverageMultiplier`        | WAD-scaled leverage multiplier. `1e18` = 1x, `2e18` = 2x. Capped at `18e18`.                                                                                                                                                                                                                                                                                                                                          |
| `swapTerm`                  | Duration of each swap in seconds. Must be `> 0`.                                                                                                                                                                                                                                                                                                                                                                      |
| `utilFeeSlopeWad`           | Slope (WAD) of the linear region of the utilization fee curve. Capped at `10e18`.                                                                                                                                                                                                                                                                                                                                     |
| `kinkUtilization`           | Utilization at which the quadratic region kicks in (WAD, must be `< 1e18`).                                                                                                                                                                                                                                                                                                                                           |
| `maxKinkFeeWad`             | Maximum additional fee from the quadratic region (WAD, capped at `10e18`).                                                                                                                                                                                                                                                                                                                                            |
| `earlyExitAllowed`          | Whether buyers can close a swap before expiry via `exitSwapEarly`.                                                                                                                                                                                                                                                                                                                                                    |
| `earlyExitFee`              | WAD early-exit fee. Capped at `1e18` (100%).                                                                                                                                                                                                                                                                                                                                                                          |
| `liquidationIncentive`      | WAD fraction that drives the liquidator reward. **Buyer-side** liquidations pay a prefunded `liquidationBounty = requiredBuyerCollateral × liquidationIncentive / WAD` (banked at swap entry, held outside `collateralBalance`, refunded to the buyer if the swap is never liquidated). **Pool-side** liquidations pay `poolCollateralBacking × liquidationIncentive / WAD` from the LP backing. Capped at 5e16 (5%). |
| `numBuckets`                | Number of time buckets. Must be in `[1, 365]`.                                                                                                                                                                                                                                                                                                                                                                        |
| `bucketInterval`            | Seconds per bucket. Must be `≥ 1200` and `≤ swapTerm`. `numBuckets × bucketInterval` must `≥ swapTerm + bucketInterval`.                                                                                                                                                                                                                                                                                              |
| `fixedRiskPremiumOracle`    | Optional risk premium oracle for the BUY\_FIXED side. Pass `address(0)` to disable.                                                                                                                                                                                                                                                                                                                                   |
| `floatingRiskPremiumOracle` | Optional risk premium oracle for the BUY\_FLOATING side.                                                                                                                                                                                                                                                                                                                                                              |
| `lpWhitelistEnabled`        | If true, only addresses on `marketLpWhitelist[marketId]` can `supplyCollateral`. The caller is auto-whitelisted.                                                                                                                                                                                                                                                                                                      |
| `minCollateral`             | Minimum per-swap collateral (both buyer and LP). Denominated in `swapToken` decimals. Must be `> 0`.                                                                                                                                                                                                                                                                                                                  |
| `rateConvention`            | How the reference rate oracle provides data. See `Types.RateConvention` (`Cumulative`, `SpotRate`, or `SpotCompoundRate`).                                                                                                                                                                                                                                                                                            |

**Returns:** `(fixedMarketId, floatingMarketId)`. These are also published in `MarketCreated` events.

**Payable:** if `Admin.CREATE_MARKET_FEE_AMOUNT() > 0` and `CREATE_MARKET_FEE_TOKEN() == address(0)`, send the fee as `msg.value`. Excess ETH is refunded to `msg.sender` at the end of the call. If the fee token is an ERC20, approve `SwapCore` for the fee amount before calling — no `msg.value` is required.

**State changes / events:**

* `markets[fixedMarketId]` and `markets[floatingMarketId]` populated, both marked `exists = true`, `marketOwner = msg.sender`.
* Both IDs appended to `allMarketIds`.
* Global `rateIndex` initialized for the reference rate oracle if it wasn't already.
* If `lpWhitelistEnabled`, `marketLpWhitelist[<each>][msg.sender] = true` (creator is auto-whitelisted so they can seed liquidity).
* Emits `MarketCreated` (×2) and `MarketConfigured` (×2), plus `MarketCreationFeeCollected` if a fee was charged (or `ProtocolFeeForegone` instead, if the multisig couldn't receive the fee — in which case the fee is refunded to the caller rather than collected).

**Reverts:**

| Code            | Reason                                                                                                                                                                                                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `E150`          | ETH fee required but `msg.value < feeAmount`.                                                                                                                                                                                                                                        |
| `E151`          | ETH refund to the caller failed (excess `msg.value` couldn't be returned). A failed send to the multisig does *not* revert — the fee is instead forgone and refunded to the caller.                                                                                                  |
| `E301`          | Market ID collision (should not happen in practice — nonces are unique).                                                                                                                                                                                                             |
| `E600` / `E603` | Base rate oracle call reverted or returned `isValid = false`.                                                                                                                                                                                                                        |
| `E602` / `E605` | Risk premium oracle (if set) call reverted or returned `isValid = false`.                                                                                                                                                                                                            |
| `E606`          | Reference rate oracle is unhealthy during `rateIndex.initialize`.                                                                                                                                                                                                                    |
| `E607`          | `rateConvention` doesn't match the convention already set for `referenceRateOracle` — it's fixed on first use and shared by every market on that oracle.                                                                                                                             |
| `E703`          | `numBuckets == 0` or `> MAX_NUM_BUCKETS` (**181**).                                                                                                                                                                                                                                  |
| `E704`          | `bucketInterval < 1200` or `> swapTerm`.                                                                                                                                                                                                                                             |
| `E705`          | `numBuckets × bucketInterval < swapTerm + bucketInterval`.                                                                                                                                                                                                                           |
| `E710`          | `referenceRateOracle == address(0)`.                                                                                                                                                                                                                                                 |
| `E711`          | `baseSwapRateOracle == address(0)`.                                                                                                                                                                                                                                                  |
| `E712`          | `swapToken == address(0)`.                                                                                                                                                                                                                                                           |
| `E713`          | `leverageMultiplier == 0` or `> MAX_LEVERAGE` (**12e18**).                                                                                                                                                                                                                           |
| `E714`          | `liquidationIncentive > 5e16`.                                                                                                                                                                                                                                                       |
| `E715`          | `swapTerm == 0` or `> MAX_SWAP_TERM` (**10 years**).                                                                                                                                                                                                                                 |
| `E716`          | `kinkUtilization >= 1e18`.                                                                                                                                                                                                                                                           |
| `E717`          | `earlyExitFee > 1e18`.                                                                                                                                                                                                                                                               |
| `E718`          | `utilFeeSlopeWad > 10e18`.                                                                                                                                                                                                                                                           |
| `E719`          | `maxKinkFeeWad > 10e18`.                                                                                                                                                                                                                                                             |
| `E720`          | Invalid `rateType` (unreachable from the enum but guarded).                                                                                                                                                                                                                          |
| `E721`          | `minCollateral == 0`.                                                                                                                                                                                                                                                                |
| `E729`          | `bucketInterval × MAX_ACTIVE_WINDOWS < swapTerm` (`MAX_ACTIVE_WINDOWS = 7`) — `bucketInterval` is too **fine**, which would leave too many active windows and expose the withdraw scan to DoS. Pick a **longer** `bucketInterval` (at least `swapTerm / 7`) or a shorter `swapTerm`. |

**See also:** [`getAllMarketIds`](#getallmarketids), [`transferMarketOwnership`](#transfermarketownership), [`terminateMarket`](#terminatemarket).

***

## transferMarketOwnership

```solidity
function transferMarketOwnership(bytes32 marketId, address newOwner) external;
```

**Who calls:** the current `marketOwner` of `marketId`.

**What it does:** step one of a two-step ownership handoff. Sets `pendingMarketOwner[marketId] = newOwner`. No state on the market itself changes yet — the actual owner only flips when `newOwner` accepts.

Pass `newOwner = address(0)` to cancel an in-flight transfer. There is no `renounceOwnership` — a market always has an owner once created.

**Modifiers:** `marketOwner(marketId)` (reverts with `E202` if not the current owner, `E300` if the market does not exist).

**State changes / events:**

* `pendingMarketOwner[marketId] = newOwner`.
* Emits `MarketOwnershipTransferStarted(marketId, currentOwner, newOwner)`.

**Reverts:**

* `E300` — market doesn't exist.
* `E202` — caller is not the current market owner.

**See also:** [`acceptMarketOwnership`](#acceptmarketownership).

***

## acceptMarketOwnership

```solidity
function acceptMarketOwnership(bytes32 marketId) external;
```

**Who calls:** the address previously set as `pendingMarketOwner[marketId]`.

**What it does:** step two of the handoff. Flips `markets[marketId].marketOwner` to `msg.sender` and clears the pending slot. After this call, the new owner has full control over `terminateMarket` and `setMarketLpWhitelist`, and will receive the creator fee split on new swaps (if configured in `Admin`).

**State changes / events:**

* `markets[marketId].marketOwner = msg.sender`.
* `pendingMarketOwner[marketId] = address(0)`.
* Emits `MarketOwnerChanged(marketId, msg.sender)`.

**Reverts:**

* `E300` — market doesn't exist.
* `E202` — caller is not the pending owner.

***

## terminateMarket

```solidity
function terminateMarket(bytes32 marketId) external;
```

**Who calls:** the `marketOwner` of `marketId`.

**What it does:** permanently flips `markets[marketId].terminated = true`. From that block forward, `buySwap` reverts on this market (see `E304`). **Existing swaps are unaffected** — LPs can still withdraw, keepers can still `makePayment`, liquidators can still `liquidateSwap`, and buyers can still `exitSwapEarly` if it was enabled at market creation. This is a one-way switch; there is no `unterminate`.

Terminate when you want to wind a market down cleanly — new positions can't be opened, but open ones still settle.

**Modifiers:** `marketOwner(marketId)`.

**State changes / events:**

* `markets[marketId].terminated = true`.
* Emits `MarketAdminChange(marketId, "terminateMarket", true)`.

**Reverts:**

* `E300` — market doesn't exist.
* `E202` — not market owner.
* `E304` — market is already terminated.

***

## setMarketLpWhitelist

```solidity
function setMarketLpWhitelist(bytes32 marketId, address lp, bool status) external;
```

**Who calls:** the `marketOwner` of `marketId`.

**What it does:** toggles an address's presence in `marketLpWhitelist[marketId]`. Only meaningful if the market was created with `lpWhitelistEnabled = true` — otherwise the call reverts. `supplyCollateral` checks this mapping inline and reverts with `E201` if the target LP isn't listed. `withdrawCollateral` is **not** gated on the whitelist — an LP who is removed can still withdraw their existing shares.

The market creator is auto-whitelisted at `createMarket` time, so there's always at least one LP who can seed the pool.

**Modifiers:** `marketOwner(marketId)`.

**State changes / events:**

* `marketLpWhitelist[marketId][lp] = status`.
* Emits `MarketLpWhitelistUpdated(marketId, lp, status)`.

**Reverts:**

* `E300` — market doesn't exist.
* `E202` — not market owner.
* `E205` — `lpWhitelistEnabled == false` on this market (you can't toggle entries on a market that doesn't use a whitelist).

**See also:** [`supplyCollateral`](/protocol/liquidity#supplycollateral).

***

## getAllMarketIds

```solidity
function getAllMarketIds() external view returns (bytes32[] memory);
```

**Who calls:** anyone — frontends, indexers, scripts.

**What it does:** returns every market ID that has ever been created, in creation order. BUY\_FIXED and BUY\_FLOATING sides appear consecutively for each `createMarket` call. Use the returned IDs as keys into the public `markets(bytes32)` getter to read full configs.

There is no pagination — if you expect thousands of markets, read this via `staticCall` off-chain rather than from on-chain code that would run out of gas.

**See also:** [`markets` mapping getter](/protocol/views#markets).


# Authorization

Several SwapCore functions take an `onBehalfOf` parameter — the address that owns the resulting state change — which is separate from `msg.sender`, the address that pays for the call. This is the integration hook for **bundlers, routers, vault adapters, and wrapper contracts**: the end user signs and funds the transaction (perhaps through a bundler), while a contract address takes the action on their behalf.

For every `onBehalfOf` entry point except `buySwap`, `msg.sender` must be pre-authorized by `onBehalfOf`. The check is a single mapping lookup (`isAuthorized`) plus a self-path fallback.

Source: `SwapCore.sol`

## The `onBehalfOf` pattern

Internally, SwapCore runs every guarded call through this helper:

```solidity
function _isSenderAuthorized(address onBehalfOf) internal view returns (bool) {
    return msg.sender == onBehalfOf || isAuthorized[onBehalfOf][msg.sender];
}
```

So the rule is simple: **you can always act on your own behalf, and you can delegate that right to any other address by calling `setAuthorization(delegate, true)`**. Revoke with `setAuthorization(delegate, false)`.

A failure on this check reverts with `E206`.

### Functions that require authorization

| Function                                                       | Who the caller can be                                                                                                               |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [`supplyCollateral`](/protocol/liquidity#supplycollateral)     | `onBehalfOf`, or a delegate they've authorized                                                                                      |
| [`withdrawCollateral`](/protocol/liquidity#withdrawcollateral) | `onBehalfOf`, or a delegate they've authorized                                                                                      |
| [`exitSwapEarly`](/protocol/swaps#exitswapearly)               | `onBehalfOf`, or a delegate they've authorized                                                                                      |
| [`transferSwapPosition`](/protocol/swaps#transferswapposition) | Current swap owner, or a delegate they've authorized                                                                                |
| [`claimEscrow`](/protocol/swaps#claimescrow)                   | Current `swap.userAddress` (i.e. the buyer, or whoever the swap was most recently transferred to), or a delegate they've authorized |

### Functions that do NOT require authorization

* [`buySwap`](/protocol/swaps#buyswap) — opening a new position is beneficial to `onBehalfOf`, and the caller is the one paying collateral and fees. Allowing anyone to open a swap for anyone else is safe and useful (bundlers, pre-funded relayers).
* [`makePayment`](/protocol/swaps#makepayment), [`liquidateSwap`](/protocol/swaps#liquidateswap), [`updateMarketRateIndex`](/protocol/views#updatemarketrateindex) — all permissionless, no per-user auth.
* Market admin functions (`createMarket`, `transferMarketOwnership`, etc.) — gated on `marketOwner` (the `marketOwner` modifier), which is a separate mechanism.

### Why `supplyCollateral` requires auth even though it "helps" `onBehalfOf`

A dust deposit from an attacker would stamp `lpPositions[marketId][victim].lastDepositBlock = block.number`, which makes the victim's own `withdrawCollateral` revert with `E413` in the same block. Without the auth check, an attacker could repeatedly front-run a victim's withdrawal and DoS it one block at a time. The auth gate closes that off at zero cost.

### What the `onBehalfOf` → `msg.sender` split enables

* **Bundlers with Permit2.** The bundler holds approvals from the end user, pulls tokens from its own balance (funded via permit), and calls `supplyCollateral(marketId, amount, endUser, minShares)`. Shares go to the end user; the bundler simply forwarded the call.
* **Wrapper NFTs.** `SwapPositionWrapper` holds actual swap ownership on-chain while issuing an ERC-721 to the end user. On transfer, the wrapper calls `transferSwapPosition(swapId, newOwner, wrapperAddress)` — it's authorized because it owns the underlying swap.
* **Vault adapters.** Morpho vault adapters use the same trick: the adapter is the on-chain LP, the vault's users get accounting via the vault's own share token.

***

## setAuthorization

```solidity
function setAuthorization(address authorized, bool status) external;
```

**Who calls:** anyone — the caller is authorizing (or revoking) `authorized` to act on **their own** behalf. There is no admin path; every account manages its own authorization list.

**What it does:** sets `isAuthorized[msg.sender][authorized] = status` — just a single idempotent storage write, no external calls and no checks.

**Parameters:**

| Parameter    | Meaning                                                    |
| ------------ | ---------------------------------------------------------- |
| `authorized` | The address you're granting or revoking authorization for. |
| `status`     | `true` to grant, `false` to revoke.                        |

**State changes / events:**

* `isAuthorized[msg.sender][authorized] = status`.
* Emits `AuthorizationSet(msg.sender, authorized, status)`.

**Reverts:** none directly. The function does not validate `authorized != address(0)` — authorizing the zero address is a no-op since `msg.sender == address(0)` is unreachable, so it simply produces a dead entry.

**Example — authorizing a bundler once, then letting it act repeatedly:**

```solidity
// Done once from the end-user's EOA:
swapCore.setAuthorization(bundler, true);

// From then on, the bundler can do things like:
swapCore.supplyCollateral(marketId, amount, endUser, minShares);
swapCore.withdrawCollateral(marketId, amount, endUser, bundler, minOut, maxShares);
swapCore.exitSwapEarly(requests, endUser);
```

When the bundler is retired or compromised, revoke:

```solidity
swapCore.setAuthorization(bundler, false);
```

There is no multi-authorization atomic helper; if you want to authorize multiple addresses in one transaction, call `setAuthorization` for each via a multicall wrapper on your side.

**See also:** [`isAuthorized`](#isauthorized).

***

## isAuthorized

```solidity
mapping(address => mapping(address => bool)) public isAuthorized;
```

Solidity auto-generates a getter:

```solidity
function isAuthorized(address owner, address operator) external view returns (bool);
```

**Who calls:** anyone — UIs, indexers, other contracts that want to know whether they're currently permitted to call guarded functions on behalf of `owner`.

**What it returns:** `true` if `owner` has previously called `setAuthorization(operator, true)` and has not since revoked. **Returning `true` from this function is not the same as `_isSenderAuthorized` returning true** — the internal helper also accepts `owner == operator` as an implicit self-authorization, but the public mapping does not set that bit. A contract checking the mapping directly should reproduce the `operator == owner || isAuthorized[owner][operator]` logic if it wants to mirror SwapCore's rule.

**State:** `isAuthorized` is per-address, not per-market and not per-function. Once you authorize an operator, they can act for you on any market and any guarded function.

**See also:** [`setAuthorization`](#setauthorization).


# Views

Read-only paths into SwapCore, plus the one helper that exists only to refresh the oracle index for consumers who want a guaranteed-fresh read. Everything on this page is safe for off-chain consumers (frontends, indexers) to call via `staticCall`, with the one documented exception.

Source: `SwapCore.sol` (most entries) and `lib/Views.sol` (the `getCalculatedAvailableLiquidity` helper).

## Why two flavors of P\&L reads?

SwapCore stores a **cumulative rate index** per oracle (`rateIndex`) that is only written when a state-changing function runs — `buySwap`, `supplyCollateral`, `withdrawCollateral`, `makePayment`, `exitSwapEarly`, `liquidateSwap`, or the dedicated `updateMarketRateIndex`. Between those updates, a pure view read (`getSwapNetAmount`) projects a fresh index on the fly (via `getFreshIndex`) so the answer still reflects time elapsed since the last write. For consumers who want the **stored** index actually written to storage first, `getFreshSwapNetAmount` runs the update as a side effect, then returns the value — useful when a frontend wants to `staticCall` a mutable function and know the same number would be used by a subsequent `makePayment`.

See also [`updateMarketRateIndex`](#updatemarketrateindex) for the standalone index-refresh path.

***

## getSwapNetAmount

```solidity
function getSwapNetAmount(bytes32 swapId)
    public view returns (uint256 netAmount, uint8 netRecipient);
```

**Who calls:** anyone. Pure `view`.

**What it does:** computes the **current net payment obligation** of an open swap — how much one side would owe the other if it settled right now. Projects the reference rate oracle forward with `getFreshIndex` (no state mutation) to account for time elapsed since the last index write, then runs the same fixed/floating payment math that `makePayment` uses at actual settlement.

Steps:

1. Load the swap and market. Revert if the swap doesn't exist (`E500`) or the market is gone (`E300`).
2. Compute `timeElapsed`, capped at `swapTerm`. If no time has elapsed (`entryTimestamp == block.timestamp`), return `(0, 1)` — no obligation, buyer is the trivial recipient.
3. Resolve the effective oracle index:
   * **Active swap:** use `getFreshIndex(oracle)` — a projected-forward read.
   * **Expired swap:** use the snapshot written at `expiryTimestamp` via `rateIndex.getIndexAt`; if no snapshot exists yet, fall back to `getIndexAtVirtual` — which projects a fresh index at `block.timestamp` and interpolates it back to `expiryTimestamp` (matching what settlement would resolve, so a stale view can't mask a pending settlement).
4. Derive `floatingPaymentRate` from `(entryFloatingIndex, effectiveIndex, rateDuration)` using `SwapFormulas.deriveRateFromIndex`.
5. Compute `fixedPayment` and `floatingPayment` via `SwapFormulas.calculateSwapPayment`. For **BUY\_FLOATING**, `utilFee` and `riskPremium` are added to the floating leg; for **BUY\_FIXED**, fees are already baked into `swap.swapRate`.
6. Net them with `SwapFormulas.calculateNetObligation(fixedPayment, floatingPayment)`. Flip the `buyerOwes` bit if this is a BUY\_FLOATING market (the buyer's side of the swap is the floating leg, not the fixed one).
7. **Cap by collateral.** If the buyer is the one receiving, the net is capped at `swap.poolCollateralBacking`. If the buyer is the one paying, it's capped at `swap.collateralBalance`. This mirrors exactly what `makePayment` and `liquidateSwap` would do.

**Returns:**

| Field          | Meaning                                                          |
| -------------- | ---------------------------------------------------------------- |
| `netAmount`    | The net obligation in `swapToken` decimals. Always non-negative. |
| `netRecipient` | `0` = LP pool receives, `1` = buyer receives.                    |

**Reverts:**

| Code   | Reason                                                                                         |
| ------ | ---------------------------------------------------------------------------------------------- |
| `E500` | Swap doesn't exist.                                                                            |
| `E300` | Market doesn't exist (swap references a purged market — shouldn't happen in normal operation). |
| `E606` | Reference rate oracle returned invalid data during `getFreshIndex`.                            |

**Notes:**

* Returns `(0, 1)` for a swap where `block.timestamp == entryTimestamp` — useful sentinel for "just-opened".
* **Both return fields are only meaningful while the swap is open.** Once `swap.settled == true`, the net-computing branch is skipped, so this function returns the sentinel `(0, 1)` — `netAmount` defaults to `0` and `netRecipient` defaults to `1` ("buyer receives") regardless of who actually won. Do **not** read either field on a settled swap. To recover settlement data after the fact:
  * **Direction + net amount** (who received the net, and how much) → the `SwapClosed` event's `netRecipient` and `amountSettled` fields (emitted by every closure path; also returned synchronously as `SettlementResult` from `makePayment` / `exitSwapEarly`). There is no storage getter for `netRecipient` — it lives only in the event/return.
  * **The buyer's total payout** → `settlementPayouts[swapId]` (plus `escrowedCollateral[swapId]` if the buyer-side transfer failed). Note this is the buyer's *credited amount* — returned collateral plus any winnings, or `0` if the buyer lost everything or was liquidated — **not** the net obligation and **not** a direction flag. A losing buyer can still have `settlementPayouts > 0`, so you can't infer `netRecipient` from it.

**See also:** [`getFreshSwapNetAmount`](#getfreshswapnetamount), [`settlementPayouts`](#settlementpayouts), and the [`SwapClosed`](/protocol/events#swapclosed) event for post-settlement direction and net amount.

***

## getFreshSwapNetAmount

```solidity
function getFreshSwapNetAmount(bytes32 swapId)
    external returns (uint256 netAmount, uint8 netRecipient);
```

**Who calls:** frontends via `eth_call` / `staticCall`. **Not marked `view`** — this function mutates the global rate index.

**What it does:**

1. Reads `swap.marketId`.
2. Calls `rateIndex.update(market.referenceRateOracle)` — a real storage write under normal execution.
3. Delegates to `getSwapNetAmount(swapId)`.

The only reason this exists is so a frontend can `staticCall` a mutable function and receive the exact number `makePayment` would use in the next block, with zero divergence from the stored index. When called via `staticCall`, the index update is rolled back at the end of the simulation, so consumers get the fresh number without persisting the write.

**⚠️ Do NOT call this on-chain.** Every call performs a `rateIndex.update`, which is wasted gas if you're already about to call a state-changing function (that function will update the index itself). On-chain callers should use `getSwapNetAmount` directly.

**Returns:** same as `getSwapNetAmount`.

**Reverts:** same as `getSwapNetAmount`, plus any revert from `rateIndex.update` (which itself reverts with `E606` if the oracle is unhealthy).

**See also:** [`getSwapNetAmount`](#getswapnetamount), [`updateMarketRateIndex`](#updatemarketrateindex).

***

## getCalculatedAvailableLiquidity

```solidity
function getCalculatedAvailableLiquidity(bytes32 marketId)
    external view returns (uint256);
```

**Deployed on the `Views` helper contract, not SwapCore.** Call it against the `Views` address.

**Who calls:** anyone.

**What it does:** returns the **maximum notional** a new swap can use right now, given the pool's unlocked collateral and the current base rate. Computed as:

```
availableLiquidity = SwapFormulas.calculateAvailableLiquidity(
    totalLpAvailableCollateral(marketId),
    baseRate,
    swapTerm,
    leverageMultiplier
)
```

In effect: "how much notional can this pool safely back, given that each notional unit consumes `|baseRate| × swapTerm × leverageMultiplier` worth of collateral for worst-case LP exposure?"

Use this to size the `notionalAmount` you pass to `buySwap`. If `notionalAmount > getCalculatedAvailableLiquidity(marketId)` at tx execution time, `buySwap` reverts via `Utils.validateSwapParams`. Because base rates move, quote-time and execute-time values can differ slightly — if that matters, pair it with `buySwap`'s `rateBound` slippage guard.

**Reverts:** can revert with `E611` / oracle-related errors if the base rate oracle is unhealthy.

**See also:** [`totalLPAvailableCollateral`](/protocol/liquidity#totallpavailablecollateral), [`buySwap`](/protocol/swaps#buyswap).

***

## getLpVestState

```solidity
function getLpVestState(bytes32 marketId, address lpAddress)
    external view returns (uint256 entryPrice, uint64 vestEndTimestamp, bool capActive);
```

**Deployed on the `Views` helper contract, not SwapCore.** Call it against the `Views` address.

**Who calls:** anyone — primarily frontends that want to warn an LP about the profit-vest cap before they supply or withdraw.

**What it does:** reports the LP's current profit-vest state for a market. Reads the LP's position via `SwapCore.getLpPosition`, then returns the vest anchor, the window end, and whether the cap is currently binding:

* `entryPrice` — the profit-vest anchor (share price recorded at the deposit that began the current vest window), WAD.
* `vestEndTimestamp` — unix seconds; the cap applies while `block.timestamp < vestEndTimestamp`.
* `capActive` — `true` iff `block.timestamp < vestEndTimestamp && getPoolBurnPrice(marketId) > entryPrice`. When `true`, a `withdrawCollateral` right now would burn shares at `entryPrice` rather than the live (higher) price.

  The comparison is against [`getPoolBurnPrice`](/protocol/liquidity#getpoolburnprice) — the mark `withdrawCollateral` actually burns at. Mirror that price if you reimplement the check; the fair mark sits above it and would report the cap as binding across a band where it does not.

For a fully-exited position (`shares == 0`), returns `(0, 0, false)` — stale anchor storage is ignored. See [Liquidity provision → Profit-vest cap](/protocol/liquidity#how-the-pool-works) for the full mechanism.

**Reverts:** `E300` if the market doesn't exist (propagated from `SwapCore.getLpPosition`).

**See also:** [`getLpValue`](/protocol/liquidity#getlpvalue) (applies the cap to return the true redeemable amount), [`withdrawCollateral`](/protocol/liquidity#withdrawcollateral).

***

## updateMarketRateIndex

```solidity
function updateMarketRateIndex(bytes32 marketId) external;
```

**Who calls:** anyone — typically a keeper, but there's no gate.

**What it does:** first best-effort pokes any of the market's oracles (`referenceRateOracle`, `baseSwapRateOracle`, `riskPremiumOracle`) that advertised `IPokeable` at market creation — refreshing their internal state (e.g. TWAR buffers) so the index snapshot reflects the freshest values. Pokes are de-duplicated per oracle per block and are best-effort (they don't revert the call). It then calls `rateIndex.update(market.referenceRateOracle)`, writing a fresh cumulative index entry keyed by `block.timestamp`. Used to:

* Force an index snapshot at a specific timestamp (useful when a market has been quiet for a while and a keeper wants to refresh before heavy read activity).
* Pre-update the index immediately before batch settlement so `makePayment` doesn't re-read the oracle for every swap in the batch — though `makePayment` already does this per-call, so it's rarely necessary.

Under normal operation every state-changing function already updates the index, so you typically don't need to call `updateMarketRateIndex` directly.

**Reverts:**

| Code   | Reason                                          |
| ------ | ----------------------------------------------- |
| `E300` | Market doesn't exist.                           |
| `E606` | Oracle returned invalid data during the update. |

**See also:** [`getFreshSwapNetAmount`](#getfreshswapnetamount), [`getPoolSharePrice`](/protocol/liquidity#getpoolshareprice).

***

## State-variable getters

SwapCore exposes several public mappings and arrays. Solidity auto-generates a getter for each; these are the ones integrators most commonly read.

### markets

```solidity
mapping(bytes32 => Types.Market) public markets;
// getter: function markets(bytes32) external view returns (...)
```

Returns the full `Types.Market` struct. `Market` has no mappings or dynamic arrays, so the auto-getter returns every field, including the nested `Pool` sub-struct (`totalShares`, `totalCollateral`, `lockedCollateral`). Use this for market-config reads: oracles, pool (shares/collateral/locked), fee-curve parameters, `earlyExitAllowed`, `earlyExitFee`, `liquidationIncentive`, `terminated`, `marketOwner`, `lpWhitelistEnabled`, `minCollateral`, `bucketInterval`, `riskPremiumOracle`, `numBuckets`.

Non-existent markets return the zero-initialized struct with `exists = false`. Check `.exists` before treating the response as live data.

### swapPositions

```solidity
mapping(bytes32 => Types.SwapPosition) public swapPositions;
// getter: function swapPositions(bytes32) external view returns (...)
```

Returns the 14-field `Types.SwapPosition` tuple:

```
(marketId, userAddress, settled, isEarlyExit, collateralBalance,
 notionalAmount, baseRate, swapRate, entryTimestamp, entryFloatingIndex,
 poolCollateralBacking, utilFee, riskPremium, liquidationBounty)
```

A `swapId` that doesn't exist returns all zeros — check `entryTimestamp != 0` to confirm the swap is real.

### settlementPayouts

```solidity
mapping(bytes32 => uint256) public settlementPayouts;
```

After settlement, records the amount **actually transferred to the buyer** for `swapId` (`buyerCollateralReleased`). If the buyer-side transfer **fails** and the payout is escrowed instead, this slot is set to `0` and the amount is recorded in [`escrowedCollateral`](#escrowedcollateral) — the two are mutually exclusive, never double-counted. So `0` means one of: the swap isn't settled yet, the buyer was due nothing (lost the P\&L / was liquidated), **or** the payout failed to transfer and is now sitting in escrow.

Because of that last case, integrators should read post-settlement entitlement as **`settlementPayouts[swapId] + escrowedCollateral[swapId]`**, not `settlementPayouts` alone — exactly the sum `exitSwapEarly` uses for its slippage check, and the pattern `SwapPositionWrapper` follows.

### escrowedCollateral

```solidity
mapping(bytes32 => uint256) public escrowedCollateral;
```

Non-zero when a buyer-side settlement or liquidation transfer failed (usually because the recipient became unable to receive the token). The balance is held until the buyer (or an authorized delegate) calls [`claimEscrow`](/protocol/swaps#claimescrow). Funds are earmarked to the **original** `swap.userAddress` — no one can redirect them. If the recipient is still blocked, the `claimEscrow` transfer reverts and the escrow stays in place.

### isAuthorized

```solidity
mapping(address => mapping(address => bool)) public isAuthorized;
```

See [Authorization](/protocol/authorization#isauthorized). Returns `true` only for explicitly-set delegations; the implicit self-path (`msg.sender == onBehalfOf`) is **not** stored in this mapping, so code that mirrors SwapCore's auth rule should check `operator == owner || isAuthorized[owner][operator]`.

### marketLpWhitelist

```solidity
mapping(bytes32 => mapping(address => bool)) public marketLpWhitelist;
```

Returns whether an address is whitelisted to supply LP collateral in a specific market. Only meaningful when `markets[marketId].lpWhitelistEnabled == true`. The market creator is auto-whitelisted at `createMarket` time.

### pendingMarketOwner

```solidity
mapping(bytes32 => address) public pendingMarketOwner;
```

Address that has been designated as the new owner of `marketId` but has not yet accepted via [`acceptMarketOwnership`](/protocol/market-admin#acceptmarketownership). `address(0)` if no transfer is in flight.

### allMarketIds (array)

```solidity
bytes32[] public allMarketIds;
// getter: function allMarketIds(uint256 index) external view returns (bytes32)
```

Positional read by index. Use [`getAllMarketIds()`](/protocol/market-admin#getallmarketids) to fetch the whole array at once.

### buckets

```solidity
mapping(bytes32 => mapping(uint256 => Types.Bucket)) public buckets;
// getter: function buckets(bytes32 marketId, uint256 bucketId) external view returns (...)
```

Aggregated swap data for `(marketId, bucketId)`. `Bucket` has no mappings or dynamic arrays, so the auto-getter returns **all 28 fields** in declaration order:

```
( 1) lpNotional            uint256    (15) minLpRate          int128
( 2) weightedLpRate        int256     (16) maxLpRate          int128
( 3) weightedEntryTime     uint256    (17) minEntryIndex      uint112
( 4) weightedInverseIndex  uint256    (18) maxEntryIndex      uint112
( 5) weightedEntryIndex    uint256    (19) memberCount        uint32
( 6) weightedLnIndex       int256     (20) minUtilFee         uint128
( 7) weightedUtilFee       uint256    (21) maxUtilFee         uint128
( 8) weightedRiskPremium   uint256    (22) minRiskPremium     uint128
( 9) totalBuyerCollateral  uint256    (23) maxRiskPremium     uint128
(10) totalPoolBacking      uint256    (24) minEntryTime       uint40
(11) weightedFeeTime       uint256    (25) maxEntryTime       uint40
(12) weightedRateSq        uint256    (26) weightedLpRateSq   uint256
(13) weightedRateTime      int256     (27) weightedIndexSq    uint256
(14) weightedTimeSq        uint256    (28) minMemberNotional  uint256
```

Positional decoding against a wrong field count silently returns wrong values rather than reverting — **decode by name** against the live `Types.Bucket`.

`Types.Bucket` in `interfaces/Types.sol` is the source of truth. Treat the list above as a convenience snapshot and re-derive from the ABI if exactness matters.

Two notes for anyone rebuilding valuation from these aggregates:

* `weightedLnIndex` (6) is populated for **`SpotCompoundRate` markets only** and is zero on other conventions. It is signed — a zero is a legitimate value, not an absence — so key netting eligibility on `weightedInverseIndex` instead.
* Fields 15–25 are the **value boxes**: per-member parameter ranges that bracket every active member, consumed by `BucketBoundsLib` for conservative NAV bounds. They are not sums and must not be aggregated like the weighted moments.

This is mostly of interest to offline analysts rebuilding the share-price calculation. For ordinary consumers, [`getPoolBurnPrice`](/protocol/liquidity#getpoolburnprice) is the right read for redeemable value and [`getPoolSharePrice`](/protocol/liquidity#getpoolshareprice) for an informational mark — see [The three share prices](/protocol/liquidity#the-three-share-prices).


# Events

Reference for every integrator-facing event that SwapCore (or a library it delegatecalls into) emits. Use these to drive indexers, subgraphs, monitoring dashboards, and UI feeds.

Sources:

* `lib/SwapEvents.sol` — most event declarations (SwapCore inherits this).
* `lib/Utils.sol` — declares `BuyerTransferFailed` and `ExpiryIndexExtrapolated`, and emits them from the settlement / liquidation paths.
* `lib/PoolInternalsLib.sol` — declares and emits `EarlyExitValueVested` / `LiquidationValueVested`.
* `lib/RateIndexLib.sol` — declares and emits `IndexClamped`.

`Utils`, `PoolInternalsLib`, and `RateIndexLib` are Solidity **libraries that SwapCore delegatecalls**, so their events are emitted in SwapCore's execution context and logged under the SwapCore address. Subscribing to the SwapCore address alone captures every event on this page — you do not need to watch the library addresses, and watching them would yield nothing.

Event topic signatures below are the canonical `keccak256("EventName(type1,type2,...)")` — use them as the `topic[0]` filter in log queries.

***

## Lifecycle events

### MarketCreated

```solidity
event MarketCreated(
    bytes32 indexed marketId,
    address indexed creator,
    address         referenceRateOracle,
    address         baseSwapRateOracle,
    address indexed swapToken,
    uint64          leverageMultiplier,
    uint32          swapTerm,
    Types.RateType  rateType,
    bytes32         correspondingMarketId
);
```

Emitted once per side of the pair from `createMarket` → `_initializeMarket`. A single `createMarket` call therefore produces **two** `MarketCreated` events, one with `rateType = 0` (BUY\_FIXED) and one with `rateType = 1` (BUY\_FLOATING). Use `correspondingMarketId` to stitch them together into a pair.

Indexed topics: `marketId`, `creator`, `swapToken`.

### MarketConfigured

```solidity
event MarketConfigured(
    bytes32 indexed marketId,
    uint32  numBuckets,
    uint32  bucketInterval,
    uint256 utilFeeSlopeWad,
    uint256 kinkUtilization,
    uint256 maxKinkFeeWad,
    uint256 earlyExitFee,
    uint256 liquidationIncentive,
    address riskPremiumOracle,
    bool    lpWhitelistEnabled,
    address marketOwner,
    uint88  minCollateral
);
```

Emitted alongside `MarketCreated` from `_initializeMarket`, carrying the rest of the config that didn't fit in `MarketCreated`. Use this to snapshot the full market configuration at creation time. The economic parameters (fee curve, buckets, oracles, term, collateral) are immutable — nothing in SwapCore rewrites them — so one `MarketConfigured` is all an indexer needs for those. The one field here that can change later is `marketOwner`: track `MarketOwnerChanged` to keep it current.

### MarketCreationFeeCollected

```solidity
event MarketCreationFeeCollected(
    bytes32 indexed fixedMarketId,
    bytes32 indexed floatingMarketId,
    address indexed creator,
    address feeToken,
    uint256 feeAmount
);
```

Emitted once per `createMarket` call **if** a creation fee was configured in `Admin` at call time. `feeToken == address(0)` means the fee was paid in ETH; otherwise it's the ERC20 token address. Paired with `MarketCreated` events on the two market IDs.

### MarketCreationFeeForegone

```solidity
event MarketCreationFeeForegone(
    bytes32 indexed fixedMarketId,
    bytes32 indexed floatingMarketId,
    address indexed creator,
    address feeToken,
    uint256 feeAmount
);
```

Emitted by `createMarket` **in place of** `MarketCreationFeeCollected` when the creation fee was charged but could not be delivered to the multisig — a blacklisted or reverting recipient, a paused fee token, or a fee token whose `transfer` is expensive enough that the creator can starve it of gas under the 63/64 rule. The fee is refunded to the creator and market creation **succeeds**.

This fail-open is deliberate: creation must not be brickable by a fee token the multisig cannot receive. For accounting, treat this as "fee waived — creator made whole," not "fee collected." Mirrors `MarketCreationFeeCollected`'s scope exactly (both paired market IDs, same fee denomination), so a revenue indexer should watch both events on the same footing or it will over-count creation fees.

***

## Ownership & admin

### MarketOwnershipTransferStarted

```solidity
event MarketOwnershipTransferStarted(
    bytes32 indexed marketId,
    address indexed currentOwner,
    address indexed newOwner
);
```

Emitted by `transferMarketOwnership`. Marks the start of the 2-step handoff. `newOwner == address(0)` means an in-flight transfer was cancelled.

### MarketOwnerChanged

```solidity
event MarketOwnerChanged(bytes32 indexed marketId, address newOwner);
```

Emitted by `acceptMarketOwnership` when the pending owner accepts the role. `newOwner` is the new owner (`msg.sender` of `acceptMarketOwnership`).

### MarketLpWhitelistUpdated

```solidity
event MarketLpWhitelistUpdated(
    bytes32 indexed marketId,
    address indexed lp,
    bool    isWhitelisted
);
```

Emitted by `setMarketLpWhitelist`. Only meaningful on markets created with `lpWhitelistEnabled = true`.

### MarketAdminChange

```solidity
event MarketAdminChange(bytes32 indexed marketId, string controlType, bool isPaused);
```

Emitted by `terminateMarket` with `controlType = "terminateMarket"` and `isPaused = true`. The `string` and `bool` arguments exist for forward-compatibility with future admin levers, but today this event only ever fires for termination.

***

## Liquidity

### CollateralSupplied

```solidity
event CollateralSupplied(
    bytes32 indexed marketId,
    address indexed onBehalfOf,
    address caller,
    uint256 amount,
    uint256 sharesMinted,
    uint256 sharePrice,
    uint256 mintPrice,
    uint256 entryPrice,
    uint64  vestEndTimestamp
);
```

Emitted by `supplyCollateral`. `onBehalfOf` is the address that got the shares; `caller` is `msg.sender` (same unless a bundler is acting).

This event carries **nine** arguments — note `mintPrice` between `sharePrice` and `entryPrice`. Compute `topic[0]` from the declaration above; an argument list that omits `mintPrice` hashes to a different topic and matches nothing.

The two prices are distinct and both are needed:

* `sharePrice` — the fair (vesting-adjusted) mark.
* `mintPrice` — the raw price the shares were actually minted at (`amount / sharesMinted`). This is the one to use for reconciling a deposit against shares received.

They differ only while an early-exit vesting reserve is draining, during which `mintPrice >= sharePrice`. `entryPrice` and `vestEndTimestamp` are the LP's profit-vest anchor and window end set by this deposit; `entryPrice` is the **burn** mark, not `sharePrice` (see [Liquidity provision](/protocol/liquidity#the-three-share-prices)).

### CollateralTokenWithdrawn

```solidity
event CollateralTokenWithdrawn(
    bytes32 indexed marketId,
    address indexed onBehalfOf,
    address caller,
    address receiver,
    uint256 amount,
    uint256 sharesRedeemed,
    uint256 sharePrice,
    bool    capApplied
);
```

Emitted by `withdrawCollateral`. `amount` is in `swapToken` decimals and was delivered to `receiver` (which may differ from both `onBehalfOf` and `caller` when a bundler passes itself as `receiver` to forward the redeemed tokens elsewhere). `capApplied` is `true` when the profit-vest cap bound the burn (shares priced at `entryPrice` rather than the live `sharePrice`).

### EarlyExitValueVested

```solidity
event EarlyExitValueVested(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    uint256 amount,
    uint64  dripEnd
);
```

Emitted when an early exit diverts a NAV crystallization into the market's **vesting reserve**. `amount` is held **out of** `pool.totalCollateral` and streams back in linearly until `dripEnd` (the swap's original expiry), so the settlement jump can't be captured just-in-time by a deposit-then-withdraw sandwich.

Indexers that reconstruct pool NAV from `pool.totalCollateral` **must** account for this: between the event and `dripEnd` there is value owed to the pool that is not yet in `totalCollateral`. Ignoring it under-reports NAV immediately after an early exit and then shows a phantom drift upward as the reserve drains.

### LiquidationValueVested

```solidity
event LiquidationValueVested(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    uint256 amount,
    uint64  dripEnd
);
```

The liquidation-side twin of `EarlyExitValueVested`, and the same NAV caveat applies. A buyer-side liquidation force-settles the swap and realizes the **full** buyer collateral into the pool, beyond the swap's fair mark; `amount` is that **excess** only, held out of `pool.totalCollateral` and streamed back by `dripEnd`.

Both events fire from the same site in `PoolInternalsLib` and are mutually exclusive per closure — branch on which one you receive to distinguish an early exit from a liquidation as the cause of the reserve credit.

***

## Swap lifecycle

### SwapCreated

```solidity
event SwapCreated(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed onBehalfOf,
    address caller,
    uint256 notionalAmount,
    uint256 buyerCollateral,
    uint256 poolCollateralBacking,
    int256  swapRate,
    int256  baseRate,
    uint256 entryTimestamp,
    uint256 entryFloatingIndex,
    uint256 utilFee,
    uint256 riskPremium,
    uint32  swapTerm,
    uint8   rateType,
    uint256 protocolFee,
    uint256 liquidationBounty
);
```

Emitted by `buySwap`. The indexer reconstructs a position from this event alone — every field that `makePayment` will need later is included. `swapRate` is the all-in rate locked by the buyer; `baseRate`, `utilFee`, and `riskPremium` are broken out for analytics.

### SwapClosed

```solidity
event SwapClosed(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed onBehalfOf,
    address caller,
    uint8   closureType,            // 0 = expired, 1 = earlyExit, 2 = liquidated
    uint256 buyerCollateralReleased,
    uint256 lpCollateralReleased,
    uint256 amountSettled,
    uint256 totalPaymentsMade,
    uint8   netRecipient,           // 0 = LP receives, 1 = buyer receives
    uint256 floatingPayment,        // UNSIGNED accrued-leg magnitude; display-only
    uint256 fixedPayment,           // UNSIGNED accrued-leg magnitude; display-only
    uint256 earlyExitFee,           // fee actually retained; may be < configured (even 0) when payout is backing-capped; 0 for non-early-exit
    int256  floatingRate,           // annualized floating rate realized over the period (WAD; can be negative)
    int256  netSettlement           // signed realized net transfer, buyer perspective; = ±amountSettled per netRecipient
);
```

Emitted once per swap from every closure path: `makePayment`, `exitSwapEarly`, and `liquidateSwap`. Branch on `closureType` to distinguish.

For normal expiry and early exit, `floatingPayment` / `fixedPayment` carry the absolute leg amounts over the settled period and `floatingRate` is the realized annualized floating rate that drove them. For liquidations, `floatingPayment`, `fixedPayment`, and `earlyExitFee` are all `0` (not computed — liquidation uses an accrual model rather than full settlement math), but `floatingRate` **is** populated: it's the rate that drove the `Utils.calculateLiquidationObligation` decision, reused here rather than recomputed.

`netSettlement` is the signed realized net transfer from the **buyer's perspective** — positive when the buyer received, negative when the buyer paid, with magnitude equal to `amountSettled`. It encodes direction and magnitude in one field, so an indexer can use `netSettlement` alone instead of combining `amountSettled` with `netRecipient`.

### SwapLiquidated

```solidity
event SwapLiquidated(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed liquidatedParty,
    bool    buyerLiquidated,
    bool    poolLiquidated,
    uint256 collateralTransferred,
    address liquidator
);
```

Emitted by `liquidateSwap` **in addition to** `SwapClosed`. Use this when you need the liquidator identity or the boolean flags distinguishing a buyer-side vs. pool-side liquidation. `liquidatedParty` is the buyer's address (`swap.userAddress`) on a **buyer-side** liquidation, and `address(0)` on a **pool-side** liquidation — the liquidated party is the LP pool, which has no single address, so the field is left zero and you read `poolLiquidated == true` instead. Exactly one of `buyerLiquidated` / `poolLiquidated` is `true` in a real liquidation; both being `false` is a defensive path that shouldn't occur in production.

### SwapTransferred

```solidity
event SwapTransferred(
    bytes32 indexed swapId,
    address indexed previousOwner,
    address indexed newOwner,
    address caller
);
```

Emitted by `transferSwapPosition`. The `caller` field captures whether the transfer was done directly (`caller == previousOwner`) or via a delegate (`caller != previousOwner`, typically the wrapper / bundler).

### PoolCollateralZeroed

```solidity
event PoolCollateralZeroed(bytes32 indexed marketId);
```

Emitted when settlement **or a pool-side liquidation** drives the LP pool's collateral to zero (the pool lost its entire position). Rare — the last-LP guard and bucket accounting usually prevent this from happening except in extreme-loss scenarios. Monitor this event for risk dashboards; it's the canonical "pool wiped" signal.

***

## Settlement escrow

### BuyerTransferFailed

```solidity
event BuyerTransferFailed(
    bytes32 indexed swapId,
    address indexed recipient,
    uint256 amount
);
```

Declared and emitted inside `Utils._transferOrEscrow`. Because Utils is a Solidity library, the emit happens in SwapCore's execution context and indexers should subscribe to the SwapCore address.

Fires when SwapCore couldn't deliver `amount` of `swapToken` to `recipient` during `_settleSwaps` or `liquidateSwap` (typically a blacklisted recipient). The amount is then held in `escrowedCollateral[swapId]` for later retrieval via `claimEscrow`. Pair with `EscrowClaimed` for the full failed-then-recovered cycle.

### EscrowClaimed

```solidity
event EscrowClaimed(
    bytes32 indexed swapId,
    address indexed recipient,
    address caller,
    uint256 amount
);
```

Emitted by `claimEscrow`. `recipient` is always the original `swap.userAddress` regardless of who called `claimEscrow` — the `caller` field captures the delegate (or the owner themselves).

***

## Fees

### ProtocolFeeCollected

```solidity
event ProtocolFeeCollected(
    bytes32 indexed marketId,
    address indexed buyer,
    uint256 feeAmount
);
```

Emitted by `buySwap` when `protocolFee > 0` **and** the protocol's share was successfully delivered to the multisig.

`feeAmount` is the multisig's portion **net of any creator share already paid out** — not the gross fee charged to the buyer. `Utils.distributeProtocolFee` transfers the creator's cut first, subtracts it, and emits what remains. So:

```
gross fee charged to buyer = ProtocolFeeCollected.feeAmount
                           + CreatorFeeCollected.feeAmount   (same tx, if present)
```

**Add** the two events; do not subtract one from the other. Treating `ProtocolFeeCollected.feeAmount` as the gross and deducting the creator share double-counts the split and under-reports the multisig take on every market that has a creator fee configured. When no creator fee applies (or the transfer to the market owner failed), `CreatorFeeCollected` is absent and `feeAmount` happens to equal the gross — which is why this error can hide until the first creator-fee market goes live.

If the multisig can't receive the fee, SwapCore emits [`ProtocolFeeForegone`](#protocolfeeforegone) instead of `ProtocolFeeCollected` (even though `protocolFee > 0`) and refunds the payer, so don't treat the absence of `ProtocolFeeCollected` on a swap as "no fee was charged."

### ProtocolFeeForegone

```solidity
event ProtocolFeeForegone(
    bytes32 indexed marketId,
    address indexed payer,
    uint256 amount
);
```

Emitted when a protocol/creation fee that was charged could **not** be delivered to the multisig (e.g. the multisig is a contract that reverts on receive, or a blacklisted recipient), so the `amount` was refunded to `payer` (`msg.sender`) rather than collected. Fires from two paths: `buySwap` (in place of `ProtocolFeeCollected`, for the protocol fee) and `createMarket` (in place of `MarketCreationFeeCollected`, for the ETH creation fee). For accounting, treat a swap/market with `ProtocolFeeForegone` as "fee waived — payer made whole," not "fee collected."

### CreatorFeeCollected

```solidity
event CreatorFeeCollected(
    bytes32 indexed marketId,
    address indexed creator,
    uint256 feeAmount
);
```

Emitted by `buySwap` when a creator fee split is configured (`Admin.creatorFeeShare > 0`) and the graceful transfer to `market.marketOwner` succeeded. `feeAmount` here is the **creator's portion only**.

The multisig's portion is already reported directly by `ProtocolFeeCollected.feeAmount` in the same tx — it needs no adjustment, because the creator share was subtracted on-chain before that event was emitted. Sum the two to recover the gross fee the buyer paid.

If the transfer to the market owner failed (rare — happens when the owner is a contract that reverts on receive), this event is **not** emitted and the full protocol fee is sent to the multisig.

***

## Oracle & index

### ExpiryIndexExtrapolated

```solidity
event ExpiryIndexExtrapolated(
    bytes32 indexed swapId,
    address indexed oracle,
    uint256 expiryTime,
    uint256 lastUpdate,
    uint256 expiryIndex
);
```

The **dead-oracle settlement signal**, and the highest-severity event on this page for a risk dashboard.

Normal expiry resolves the floating index from a real snapshot at or after maturity. If none exists, settlement reverts `E450` — but only until `expiry + Admin.settlementGracePeriodSec()` (default 14d, bounds `[7d, 90d]`, timelocked). Past that, the failed index update is retried once with a guaranteed gas budget (`E451` if the caller can't fund it, so gas starvation can never *force* this path). Only if that retry also fails — a genuinely dead, invalid, or garbage oracle — does settlement proceed against an **extrapolation** of the frozen snapshot history, and this event fires.

`expiryIndex` was not observed. It is projected from the trailing slope behind `lastUpdate`, so the settlement price is a deterministic estimate, not market data. `expiryTime - lastUpdate` is the size of the gap being papered over — surface it, because it is the direct measure of how far the estimate reaches.

**This event is always accompanied by market termination.** The same code path sets `terminated = true` on the settling market **and on its paired twin** (the twin prices new swaps off the same scarred index history), emitting `MarketAdminChange(marketId, "terminateMarket", true)` for each. So a single `ExpiryIndexExtrapolated` implies up to two `MarketAdminChange` logs in the same tx, and the market pair is closed to new swaps from that point on. Alert on this event directly rather than inferring it from the terminations.

### IndexClamped

```solidity
event IndexClamped(
    address indexed oracle,
    uint256 reported,
    uint256 floor
);
```

Emitted from `RateIndexLib.update` when a `Cumulative`-convention oracle reports an index **below** its last recorded value. The `Cumulative` convention promises a monotonic non-decreasing index, so the reading is clamped up to `floor` and settlement proceeds on the clamped value — this prevents a phantom negative floating rate, but it **deliberately masks a genuine decrease**.

That masking is the reason to monitor it. A recurring `IndexClamped` on the same oracle means the feed is not behaving as a cumulative index and the market pair on it is accruing float against a frozen floor — the protocol keeps working while the underlying signal is wrong. `reported` vs. `floor` gives the size of the suppressed decrease.

Because indices are **global per oracle**, not per market, one clamp affects every market sharing that oracle. Key any alerting on the `oracle` address and fan out to the affected markets, not the reverse.

Note that the oracle contracts `IndexBaseRateV1` and `MorphoBaseRateV1` declare their own two-argument `IndexClamped(uint256,uint256)`. That is a **different event with a different signature**, emitted from the oracle's own address rather than SwapCore's — deliberately named to mirror this one so monitoring is symmetric across the stack. Don't mix the two in one filter.

***

## Authorization

### AuthorizationSet

```solidity
event AuthorizationSet(
    address indexed authorizer,
    address indexed authorized,
    bool    isAuthorized
);
```

Emitted by `setAuthorization`. `authorizer` is the account whose authorization list changed; `authorized` is the delegate; `isAuthorized` is the new state.


# Error Codes

Generated from `packages/hardhat/contracts/lib/Errors.sol` — that file is the source of truth. If a code here disagrees with the contract, the contract wins.

```solidity
/**
 * @title Errors
 * @notice Custom error definitions for SwapCore contract
 * @dev Using custom errors instead of require strings for gas efficiency
 * Error codes are organized by category for easier maintenance
 */
library Errors {
    // ========== CONFIGURATION ERRORS (100-199) ==========
    error E102(); // Invalid admin address
    error E103(); // Invalid address (cannot be zero)
    error E150(); // Insufficient ETH for market creation fee
    error E151(); // ETH transfer failed
    error E152(); // Non-standard fee token: amount received != amount requested
    error E153(); // Market creation deadline passed
    error E154(); // Live creation fee token differs from expectedFeeToken
    error E155(); // Live creation fee exceeds maxFeeAmount

    // ========== AUTHORIZATION ERRORS (200-299) ==========
    error E201(); // User not whitelisted
    error E202(); // Not market owner
    error E203(); // Not swap owner
    error E205(); // Market LP whitelist not enabled
    error E206(); // Not authorized to act on behalf

    // ========== MARKET ERRORS (300-399) ==========
    error E300(); // Market does not exist
    error E301(); // Market already exists
    error E304(); // Market terminated

    // ========== COLLATERAL ERRORS (400-499) ==========
    error E400(); // Invalid amount
    error E404(); // Insufficient pool collateral
    error E405(); // Insufficient available collateral
    error E407(); // No pool shares exist
    error E411(); // Supply slippage: shares minted below minSharesOut
    error E412(); // Withdraw slippage: collateral received below minCollateralOut
    error E413(); // Withdrawal blocked: LP shares still vesting (same block as deposit)
    error E414(); // Pool underwater with active swaps: deposits blocked
    error E415(); // Expired unsettled swaps exist: settle before depositing/withdrawing
    error E416(); // Last share locked: active swaps require at least 1 share outstanding
    error E417(); // Profit-vest cap is active on a full last-LP exit of an idle pool: wait for vest to expire or do a partial withdrawal that leaves shares outstanding
    error E418(); // Withdraw slippage: shares to redeem above maxSharesToRedeem
    error E419(); // Outstanding early-exit vesting on a full last-LP exit of an idle pool: wait for it to drip in, or do a partial withdrawal that leaves shares outstanding

    // ========== SETTLEMENT INDEX ERRORS (450-469) ==========
    error E450(); // No snapshot at/after expiry and the settlement grace period has not elapsed: call updateMarketRateIndex while the oracle is healthy, then retry. After expiry + Admin.settlementGracePeriodSec, settlement proceeds permissionlessly at the extrapolated last-known index (emits ExpiryIndexExtrapolated)
    error E451(); // Insufficient gas to adjudicate the dead-oracle settlement fallback: retry with more gas (prevents gas starvation from forcing extrapolated settlement)

    // ========== SWAP ERRORS (500-599) ==========
    error E500(); // Swap does not exist
    error E501(); // Swap already settled
    error E502(); // Swap is expired
    error E503(); // Buyer or LP collateral below minimum threshold
    error E506(); // Exit too early
    error E507(); // Swap not liquidatable
    error E508(); // Insufficient liquidity
    error E509(); // Zero notional amount
    error E510(); // Swap rate exceeds maxSwapRate (slippage protection)
    error E511(); // Markup exceeds maxMarkup (utilizationFee + riskPremium)
    error E512(); // Negative base rate not allowed on a Cumulative reference oracle (floating leg cannot go negative, fixed leg would be mispriced / one-sided). Enforced at entry (full-term quote) and on every remaining-tenor consumer: MtM/virtual share price and early-exit/NAV settlement
    error E513(); // Total tokens pulled exceeds maxTotalIn (slippage protection)

    // ========== EARLY EXIT ERRORS (550-559) ==========
    error E550(); // Early exit not allowed
    error E552(); // Early exit slippage: payout below minExitAmount

    // ========== ORACLE ERRORS (600-699) ==========
    error E600(); // baseSwapRateOracle must implement IBaseRateOracle
    error E602(); // riskPremiumOracle must implement IRateOracle
    error E603(); // baseSwapRateOracle rate invalid
    error E605(); // riskPremiumOracle rate invalid
    error E606(); // Oracle returned invalid or zero data
    error E607(); // Convention mismatch for already-initialized oracle
    error E608(); // Oracle not initialized (call initialize() first)
    error E609(); // Invalid oracle address (address zero)
    error E610(); // Stale or invalid oracle answer
    error E611(); // Invalid base rate oracle
    error E612(); // Risk premium cannot be negative
    error E613(); // Fixed-slot base oracle quote attestation invalid for the market's convention (Cumulative requires isCompoundedTenorQuote() == true; Spot conventions forbid it)
    error E614(); // Floating-slot base oracle must not attest a compounded-equivalent tenor quote (and must attest explicitly on Cumulative markets)
    error E615(); // Paired base-rate oracles must derive from one shared observation ring: if either advertises baseRateRing(), both must resolve to the same non-zero ring (H-02 independent-ring inversion guard)

    // ========== VALIDATION ERRORS (700-799) ==========
    error E702(); // Trade size exceeds available liquidity
    error E703(); // numBuckets must be between 1 and MAX_NUM_BUCKETS (181)
    error E704(); // bucketInterval must be between 1200 and swapTerm
    error E705(); // numBuckets * bucketInterval must be >= swapTerm
    error E706(); // totalLiquidity is zero
    error E707(); // availableLiquidity exceeds totalLiquidity
    error E710(); // Invalid reference rate oracle address
    error E711(); // Invalid base swap rate oracle address
    error E712(); // Invalid swap token address
    error E713(); // Invalid leverage multiplier
    error E714(); // Invalid liquidation incentive
    error E715(); // Invalid swap term
    error E716(); // Kink utilization must be < 100%
    error E717(); // Early exit fee must be <= 100%
    error E718(); // utilFeeSlopeWad must be <= 1000%
    error E719(); // maxKinkFeeWad must be <= 1000%
    error E720(); // Invalid rate type
    error E721(); // minCollateral must be > 0
    error E722(); // Bucket interval must be > 0
    error E723(); // Number of buckets must be > 0
    error E724(); // SafeCast: uint256 to int256 overflow
    error E725(); // Spot rate magnitude out of range: reading exceeds MAX_RATE_WAD before rate*time accumulation
    error E726(); // Compound exponent overflow: accumulated rate * time exceeds expWad limit
    error E728(); // Oracle index at floor: refuse swap entry to prevent corrupted entryFloatingIndex
    error E729(); // bucketInterval too fine (DoS): bucketInterval * MAX_ACTIVE_WINDOWS must be >= swapTerm
    error E730(); // Internal invariant: sub-bucket id absent from the occupancy set during virtual settlement
}
```


# Opportunity

Kairos LP Guide

### **Become a Day One Kairos Liquidity Provider (KLP) and help bootstrap the deepest, most capital-efficient interest rate swap markets in DeFi.**

Kairos is a permissionless onchain protocol that brings the massive $500T+ interest rate swap market onchain. Anyone can create fixed/floating rate swap markets, and **Kairos Liquidity Providers (KLPs)** act as the sophisticated counterparty — supplying collateral and earning yields from swap premiums, fees, and protocol incentives.

***

<h3 align="center">Interested?</h3>

<p align="center">Please take 2 minutes to fill out the Kairos Liquidity Provider (KLP) interest form</p>

<p align="center"><a href="https://forms.gle/j3M6gpTqzVjYwcyv8" class="button primary">KLP Interest Form ⇒</a></p>

***

#### **Why Provide Liquidity on Kairos?**

* **High capital efficiency** — LPs back specific swap directions with targeted collateral rather than 50/50 pools.
* **Attractive risk-adjusted returns** — Earn from swap flows, performance-based incentives, and potential governance rewards.
* **Day One advantage** — Early LPs get priority allocation, custom terms, and the opportunity to shape the initial market depth and incentive structure.
* **Composability** — Liquidity on Kairos powers a new wave of DeFi primitives built around interest rate exposure.
* **Backed by strong partners** — Supported by top-tier investors and a team with deep experience in crypto financial products.

We are currently onboarding serious LPs who want to deploy from launch. Whether you’re a fund, family office, quant shop, or high-conviction individual, we want to tailor terms that meet your hurdle rate and size.

**Ready to move fast?**

* Fill out the [**Day One LP Interest Form**](https://forms.gle/kK1LrqXQWbkzAkZr5) to share your preferred size, hurdle rate, desired incentives, and target markets. Qualified respondents receive personalized details and a direct 1:1 call within 48 hours.\
  [**Submit Interest Form →**](https://forms.gle/kK1LrqXQWbkzAkZr5)
* View the full [**Kairos Liquidity Deck**](https://docsend.com/view/8k5hmg7vg89ncbm8) for more details on the protocol and LP opportunity.\
  [**View KLP Deck →**](https://docsend.com/view/8k5hmg7vg89ncbm8)

Questions? Reach out directly on [X (@KairosSwap)](https://x.com/KairosSwap) or the [Telegram](https://t.me/+IM8fDT0MJBBlNzUx), our core team will get back to you quickly.

Let’s build the onchain interest rate layer together.

### Legal Disclaimers & Risk Disclosures

Read IMPORTANT [Legal Disclaimers & Risk Disclosures here](#legal-disclaimers-and-risk-disclosures)


# How it Works

### The Opportunity

DeFi has surpassed $40B in outstanding loans, nearly all at variable rates. The missing primitive is an interest rate swap market. In TradFi, swaps enable lenders to offer fixed rates by hedging their floating exposure, forming the backbone of fixed-income markets. Kairos is built to fill this gap onchain, enabling lenders to hedge fixed-rate loan exposure, builders to create fixed-income products with predictable cash flows, and traders to take leveraged directional positions on rate movements. Kairos LPs are the counterparty to all of this activity, earning a risk premium and utilization fee on every swap they back.

***

<h3 align="center">Interested?</h3>

<p align="center">Please take 2 minutes to fill out the Kairos Liquidity Provider (KLP) interest form</p>

<p align="center"><a href="https://forms.gle/j3M6gpTqzVjYwcyv8" class="button primary">KLP Interest Form ⇒</a></p>

***

### Kairos Protocol

Kairos is a non-custodial, permissionless interest rate swap protocol deployed on Ethereum. Buyers lock in a fixed or floating rate for a defined term; the LP pool takes the opposing side. Both parties post collateral when a swap is purchased as a guarantee of future payments. At settlement, each party's payment is calculated as notional × rate × term, and only the net difference between the two legs changes hands, keeping capital requirements efficient. Because swaps are fully synthetic, they enable leveraged rate exposure without requiring principal exposure to the underlying asset.

### How LPs Make Money

LPs earn two fee components on every swap: a risk premium, which compensates for uncertainty in the base rate (an oracle-provided projected TWAR during the swap tenor), and a utilization fee, which increases as pool utilization rises. Because swap pricing is oracle-derived rather than bonding-curve-based, LPs are not exposed to the arbitrage-driven adverse selection endemic to traditional AMMs. Margin requirements are fully deterministic, so the pool's maximum loss is capped per position.

### LP Risk Management

Each market is single-sided (sell fixed or floating), so LPs can supply liquidity based on risk tolerance and directional views. As each swap is independently collateralized, the pool's maximum loss on any individual position is capped at its posted collateral for that swap. Bilateral liquidation is permissionless and evaluated in real time, keeping the protocol solvent without requiring active management from LPs. Market parameters including oracles, leverage multiplier, and fee curves are immutable at creation, reducing governance risk post-deployment.

### Vault & Curator Layer

LPs can supply liquidity directly or through curated vaults. At launch, Morpho Vaults V2 are supported through a specialized adapter, so curators can allocate capital across markets and handle position management on behalf of depositors. Share pricing uses a bucketed mark-to-market system updated continuously as swaps open and settle. A share price floor and deposit block mechanism protect existing LPs from dilution when a pool is underwater.

### Permissionless Market Creation

Anyone can create a market with custom oracles, collateral token, swap term, and fee parameters. LPs and curators can compete on pricing quality, creating natural selection toward well-calibrated markets that attract more volume and fee revenue. Market creators can earn a share of protocol fees generated by swap volume in their markets. An optional LP whitelist supports permissioned markets for institutional liquidity providers with compliance requirements.

***

<h3 align="center">View the Deck</h3>

<p align="center"><a href="https://docsend.com/view/8k5hmg7vg89ncbm8" class="button primary">KLP Deck ⇒</a></p>

***

### Company

Kairos Labs, Inc. is backed by 6th Man Ventures, Lattice, Advancit Capital, and Alliance.

### Legal Disclaimers & Risk Disclosures

Read IMPORTANT [Legal Disclaimers & Risk Disclosures here](#legal-disclaimers-and-risk-disclosures)

<br>


# Legal Disclaimers & Risk Disclosures

Last updated: Jun 19, 2026

These Legal Disclaimers & Risk Disclosures are provided in addition to, and should be read together with, our [Terms of Service](https://kairosswap.com/terms) and [Privacy Policy](https://kairosswap.com/privacy). They supplement those documents and do not supersede them; in the event of any conflict, the Terms of Service govern. The Kairos Krystals rewards program is governed by its own separate terms (see Related Documents below).

#### 1. Informational Purposes Only

This website and all materials made available by Kairos Labs, Inc. ("Kairos Labs," "we," or "us"), including any descriptions of the Kairos Protocol, the Kairos Liquidity Provider ("KLP") program, and related products, are provided for informational purposes only. Nothing on this website constitutes an offer to sell, a solicitation of an offer to buy, or a recommendation of any security, commodity interest, swap, token, derivative, or other financial instrument, nor does it constitute investment, legal, tax, accounting, or regulatory advice.

Kairos Labs makes no representation or warranty, express or implied, regarding the accuracy, completeness, reliability, or timeliness of the information presented, and accepts no liability for any loss or damage arising from reliance on it. Information may be updated, revised, or withdrawn at any time without notice.

#### 2. No Fiduciary or Advisory Relationship

Kairos Labs is not acting as your investment adviser, broker-dealer, commodity trading advisor, commodity pool operator, futures commission merchant, swap dealer, fiduciary, or agent. No information provided should be construed as personalized advice. You are solely responsible for evaluating the merits and risks of any decision to interact with the Kairos Protocol, and you should consult your own legal, tax, financial, and regulatory advisors before doing so.

#### 3. Protocol Risk

The Kairos Protocol is experimental, non-custodial software. Participation — whether as a Kairos Liquidity Provider, swap buyer, vault depositor, market creator, or otherwise — involves substantial risk, including but not limited to:

* Smart contract risk, including undiscovered bugs, vulnerabilities, and exploits
* Oracle failure, manipulation, or stale data
* Liquidation risk and adverse movements in underlying rates
* Counterparty and protocol insolvency risk
* Risks associated with third-party infrastructure, including vault curators, adapters, and integrations such as Morpho Vaults V2
* Evolving and uncertain regulatory, tax, and legal treatment across jurisdictions
* Total loss of deposited capital

No level of return, protection of principal, or continued availability of the protocol is guaranteed. You should not participate with funds you cannot afford to lose in full.

#### 3A. Interface and Application Risk

The Kairos web application and related interfaces are a convenience layer for interacting with the Kairos Protocol; they are not the protocol itself. Use of the interface involves risks distinct from protocol risk, including but not limited to:

* Compromise of the website, frontend code, domain, or hosting infrastructure (including code injection, DNS hijacking, or supply-chain attacks) that could present fraudulent information or prompt you to sign harmful transactions
* Inaccurate, stale, incomplete, or delayed information — including market data, rates, balances, and projected figures — which is sourced from oracles, indexers, and other third-party infrastructure
* Interruption, downtime, modification, or discontinuation of the interface, and the curated, non-exhaustive selection of markets it displays
* Reliance on third-party infrastructure such as RPC providers, indexers, wallet software, and wallet-connection libraries

You are solely responsible for verifying any transaction before signing it. The Protocol's smart contracts are immutable and remain accessible on-chain independent of the interface; conversely, neither the availability nor the accuracy of the interface is guaranteed.

#### 4. Hypothetical Performance and Projected Returns

Any yield figures, APYs, projected returns, target returns, illustrative scenarios, backtests, or other performance estimates presented in connection with the Kairos Protocol or the KLP program are hypothetical and model-derived. They do not represent actual trading or actual returns earned by any person, are not a guarantee or prediction of future performance, and should not be relied upon as the basis for any decision to participate.

Hypothetical performance results have many inherent limitations. They are generally prepared with the benefit of hindsight, do not involve financial risk, and cannot completely account for the impact of financial risk in actual trading — including the ability to withstand losses or adhere to a particular strategy in the face of adverse market conditions. Projected returns depend on assumptions regarding, among other things, swap volume, pool utilization, realized versus projected base rates, rate volatility, collateralization levels, default and liquidation outcomes, gas and oracle costs, protocol and vault fees, and the continued operation of third-party infrastructure. Any of these assumptions may prove materially incorrect, and actual returns may differ materially from those projected, including the possibility of zero or negative returns and total loss of deposited capital.

Where backtests are presented, they are based on historical data from third-party sources and reflect the performance the model would have produced had it been applied to that data; they do not reflect actual trading and are subject to all the limitations described above.

Past performance, whether actual or simulated, is not indicative of future results. No representation is made that any participant will or is likely to achieve returns similar to those shown. Kairos Labs reserves the right to revise its methodology and assumptions at any time; the version in effect at the time a projection is published governs that projection.

#### 5. Forward-Looking Statements

Statements regarding the Kairos Protocol's design, features, roadmap, integrations, token mechanics (if any), governance, and economics are forward-looking and subject to change without notice. Forward-looking statements reflect current expectations and assumptions, are not guarantees of future outcomes, and involve known and unknown risks that may cause actual results to differ materially. Kairos Labs undertakes no obligation to update any forward-looking statement.

#### 6. KLP Interest Form — Non-Binding

Submission of the KLP Interest Form is a non-binding indication of interest only. It does not create any agreement, commitment, allocation, priority, or obligation on the part of Kairos Labs or the submitter, and no legal relationship will arise until definitive documentation (if any) is executed by the parties. Kairos Labs reserves the right to modify, suspend, or discontinue the KLP program at any time in its sole discretion.

#### 7. U.S. Persons Exclusion

The KLP program, the KLP Interest Form, and any related offering materials are not directed to, and are not available to, U.S. Persons.

For purposes of this disclaimer, "U.S. Person" has the meaning given in Rule 902(k) of Regulation S under the U.S. Securities Act of 1933, as amended, and also includes any "U.S. person" as defined in U.S. Commodity Futures Trading Commission Regulation 23.23(a)(23) and any "United States person" under Section 7701(a)(30) of the U.S. Internal Revenue Code of 1986, as amended.

By submitting the KLP Interest Form or otherwise engaging with the KLP program, you represent and warrant that:

(i) you are not a U.S. Person;

(ii) you are not located in, organized under the laws of, or a resident of the United States or any other jurisdiction in which such participation would be unlawful or would require registration, licensing, or other regulatory action by Kairos Labs;

(iii) you are not acting for the account or benefit of any U.S. Person or any person in a Prohibited Jurisdiction; and

(iv) you will not transfer, assign, or otherwise make available any interest obtained in connection with the KLP program to any U.S. Person or person in a Prohibited Jurisdiction.

Access from Prohibited Jurisdictions is void where prohibited. Kairos Labs reserves the right to block, reject, unwind, or refuse to recognize any participation that it determines, in its sole discretion, violates these restrictions, and to implement technical, contractual, or procedural measures to enforce this exclusion.

#### 7A. Sanctions and Prohibited Jurisdictions

Access to and participation in the Kairos Protocol and the KLP program is prohibited for any person or entity that is:

(i) located in, organized under the laws of, ordinarily resident in, or accessing the protocol or program from any jurisdiction subject to comprehensive sanctions administered by the U.S. Department of the Treasury's Office of Foreign Assets Control ("OFAC") or any other applicable governmental authority (each, a "Prohibited Jurisdiction");

(ii) identified on OFAC's Specially Designated Nationals and Blocked Persons List ("SDN List"), OFAC's Sectoral Sanctions Identifications List, OFAC's Foreign Sanctions Evaders List, or any other sanctions-related list maintained by OFAC, the U.S. Department of State, the U.S. Department of Commerce (including the Entity List and Denied Persons List), the United Nations Security Council, the European Union, any EU member state, the United Kingdom's HM Treasury, or any other applicable governmental authority (collectively, "Sanctions Lists");

(iii) owned or controlled, directly or indirectly, 50% or more in the aggregate by one or more persons described in clause (ii), or otherwise acting on behalf of any such person; or

(iv) otherwise the target of economic or trade sanctions administered or enforced by any applicable governmental authority (a "Sanctioned Person").

By accessing the Kairos Protocol or KLP program, submitting the KLP Interest Form, or otherwise engaging with Kairos Labs, you represent and warrant on a continuing basis that you are not a Sanctioned Person, are not located in or accessing from a Prohibited Jurisdiction, and are not acting, directly or indirectly, for the account or benefit of any Sanctioned Person or any person in a Prohibited Jurisdiction. You agree to notify Kairos Labs immediately if any of these representations ceases to be true.

Kairos Labs reserves the right to screen participants against applicable Sanctions Lists, to block, reject, freeze, unwind, or refuse to process any participation or transaction that it determines, in its sole discretion, may involve a Sanctioned Person or Prohibited Jurisdiction, and to cooperate with and disclose information to governmental authorities as required by applicable law. Kairos Labs may implement technical measures, including geographic blocking (geofencing), wallet screening, and other controls, to enforce these restrictions. Any personal data or wallet information processed in connection with such screening and enforcement is handled in accordance with our [Privacy Policy](https://kairosswap.com/privacy). Applicable sanctions regimes and Sanctions Lists are subject to change; you are responsible for informing yourself of the current status of applicable sanctions and for ensuring your continued compliance.

#### 8. No Reliance

You acknowledge that you have not relied on any statement, representation, or warranty made by Kairos Labs or any of its affiliates, officers, employees, advisors, or agents in connection with any decision to participate in the Kairos Protocol or KLP program, other than as expressly set forth in definitive written agreements executed by Kairos Labs.

#### 9. Jurisdictional Notice

The information on this website may not be appropriate or available for use in all jurisdictions. Access to the Kairos Protocol or KLP program may be restricted by local law, and it is your responsibility to inform yourself of, and comply with, any applicable legal or regulatory requirements in your jurisdiction. Kairos Labs makes no representation that the protocol or program is appropriate or available for use in any particular jurisdiction.

#### 10. Changes to This Disclaimer

Kairos Labs may update this disclaimer from time to time. The version in effect at the time of your access governs your use of the website and any related materials. Continued access following any update constitutes acceptance of the revised terms.

***

**Related documents**

* [Terms of Service](https://kairosswap.com/terms)
* [Privacy Policy](https://kairosswap.com/privacy)
* [Kairos Krystals Terms](https://docs.kairosswap.com/kairos-quests/token-terms)

Contact: <legal@kairosswap.com>

<br>


# Kairos Protocol Whitepaper

April 2026

<p align="center">Thomas Harrison thomas@kairosswap.com<br>Vince DePalma vince@kairosswap.com</p>

{% file src="/files/mw4W3FyZtqmELviAxC8o" %}

#### Abstract

Kairos is a non-custodial interest rate swap protocol implemented for the Ethereum Virtual Machine. Kairos introduces a new onchain primitive for hedging interest rate risk and taking directional positions on rate movements through permissionless, trustless swap markets. Anyone can create a market by specifying immutable parameters that define the underlying rate, swap term, collateral token, risk management rules, and oracles used for rate pricing. In each market, one party pays a fixed rate and the other pays a floating rate over a defined term; at settlement, only the net difference is paid from posted collateral. Because the swaps are fully synthetic, they enable leveraged exposure to rate movements without market reliance on principal exposure to the underlying asset or rate. Liquidity is supplied to markets either directly or through vaults managed by curators, who select which markets to support and set risk parameters for their depositors. The protocol is implemented as a singleton contract with minimal governance footprint.

### Introduction

#### Motivation

Decentralized finance (DeFi) crypto loan balances have surpassed $40B, exceeding centralized finance (CeFi) crypto loan balances, yet the infrastructure supporting this credit market remains incomplete. Nearly all DeFi lending operates on variable rates, which can exhibit high volatility depending on utilization, liquidity conditions, and broader market dynamics. Volatile rates are tolerable for short-term positions, but they are impractical for the use cases that drive the majority of borrowing demand in traditional finance: real estate investment, capital expenditure financing, and large structured purchases. Both retail and institutional borrowers cannot take on long-duration liabilities with unpredictable interest expense, and lenders cannot commit capital at extended durations without tools to hedge the associated rate risk.

The missing primitive is an interest rate swap market: the mechanism that allows lenders to offer fixed rates by hedging their own floating-rate exposure. In traditional finance, interest rate swaps serve exactly this function, forming the backbone of fixed-income hedging for corporations, banks, and asset managers. Their core function is simple: one party pays a fixed rate, another pays a floating rate, and the difference is settled periodically. This mechanism is what makes long-duration fixed-rate lending possible. Often, lenders will offer borrowers a fixed rate while using a swap to hedge their own exposure to rate movements, making it economically viable to commit capital at duration. Without this hedging layer, neither side can confidently enter long-duration credit arrangements, which is a core reason why long-duration, fixed-rate loans are largely absent from DeFi today.

Previous attempts at fixed-rate lending and interest rate swap protocols have largely been unsuccessful, in part due to timing and a fundamental chicken-and-egg problem. The primary use cases for DeFi borrowing have historically been short-term: yield farming, leveraged longs, and arbitrage trading, which don’t require fixed rates. But the deeper issue is that fixed-rate lending and interest rate swaps are codependent: lenders won't offer fixed-rate loans at duration without a way to hedge their rate exposure, and swap markets won't develop without underlying lending demand to drive it. Prior protocols attempted to solve one side of this equation in isolation, which is why neither gained traction. This dynamic is beginning to shift as DeFi borrowing matures and new entrants, including institutions and retail users (many interacting with DeFi through centralized exchange front-ends), create sustained demand for longer-duration, fixed-rate credit that didn't exist in DeFi's earlier years. As dominant lending protocols work to meet this demand by supporting fixed-rate loans (e.g. Morpho V2), Kairos operates as a complementary primitive that enables participants of those protocols to hedge the rate risk of their loan positions.

Beyond hedging, onchain interest rate swaps unlock a broader opportunity: the development of fixed-income products in DeFi such as those tied to staking and lending. Fixed-income instruments such as bonds, structured credit, and yield-bearing instruments with predictable cash flows are the largest asset class in traditional finance, yet they are limited in DeFi today. Their existence depends on predictable interest rates, which in turn depend on the ability to swap between fixed and floating exposures. By enabling deep, liquid interest rate swap markets onchain, Kairos provides the foundational infrastructure for a new class of fixed-income products.

#### Kairos Protocol

Kairos enables permissionless creation of interest rate swap markets in which parties can exchange fixed and floating-rate payments based on an underlying rate. To purchase a swap, a buyer supplies margin and locks in a swap rate for a fixed duration. A liquidity pool takes the opposing side of each swap, posting its own collateral, and is compensated through fees added to the buyer’s rate. When the swap expires, the buyer's rate is netted against the pool's rate and the difference is paid from the losing party's collateral. Collateral requirements are deterministic, enabling buyers to calculate their maximum profit or loss ahead of a trade. Equally important, each swap is independently collateralized so the pool's maximum loss on any individual position is limited to its posted collateral for that swap.

A buyer who purchases a fixed-rate swap is effectively betting that the floating rate will rise above the fixed rate they locked in; a buyer who purchases a floating-rate swap is betting the opposite. This allows participants to hedge rate exposure or take leveraged directional views on where rates are headed.

Because swaps are settled on margin rather than delivered against the underlying asset, markets are not constrained by the supply or liquidity of the asset whose rate they reference. This enables derivatives markets with the potential to significantly exceed those of the underlying markets. Additionally, a single collateral token can back swaps across many different rates, enabling participants to express rate views without taking on multi-token price risk.

The following sections describe the protocol architecture in detail, covering market creation, swap pricing, core protocol functions, and the liquidity provision and vault layer.

### Protocol Architecture

#### Market Creation

Kairos markets are created in pairs via a public contract call. Each market is isolated in the pair: one market sells fixed-rate swaps and its paired market sells floating-rate swaps, with the pool taking the opposing exposure in each case. This separation allows liquidity providers to allocate capital based on their risk tolerance and directional view on rates. When creating a market, the creator specifies immutable parameters that define how the market functions:

* **Collateral token**: the asset used for all margin and liquidity deposits as well as payments in the market.
* **Swap term**: the fixed tenor of every swap purchased in the market.
* **Rate convention:** the interest rate type which determines how the rate index is tracked for the market's reference rate oracle
* **Oracles**: a reference rate oracle that tracks the underlying floating rate, a base swap rate oracle used to price new swaps, and an optional risk premium oracle that adds a spread to the buyer's rate.
* **Risk parameters**: the leverage multiplier, which governs margin requirements and maximum leverage, minimum collateral, which sets a floor value, and the liquidation incentive paid to third parties who liquidate undercollateralized positions.
* **Utilization fee parameters**: a kinked fee model that increases the cost of purchasing swaps as pool utilization rises, protecting LPs from concentration risk.
* **LP whitelist**: this enables permissioned markets where only approved LPs may provide liquidity, a feature designed to support institutional liquidity providers with compliance requirements. Market creators can update this list at any time.

Since these parameters are immutable, market participants can rely on consistent behavior without exposure to governance changes or risk parameter modification after the fact.

**Inherent Incentive for Model Improvement**

Market creation is permissionless which allows LPs and curators to compete on offering the most accurate and attractive pricing. Markets that better reflect true risk-to-reward will attract more liquidity, enabling greater swap volume and fee revenue for LPs. This creates a natural selection dynamic: markets with well-calibrated base rate oracles, appropriately priced risk premiums, and well-structured utilization fees will outcompete those with poor pricing models, as liquidity aggregates in best-calibrated markets, which in turn creates more attractive pricing through lower utilization fees. Over time, this incentivizes continuous improvement in pricing accuracy as curators refine their models to capture greater swap volume and buyers gravitate toward markets where they believe they are getting the fairest price.

Market creators are further incentivized through a market creator fee. If enabled by the protocol admin, a share of the protocol fee collected on each swap is paid to the creator of the market in which the swap was purchased.

#### Swap Pricing

Buyers pay the swap rate on each swap they purchase, although the components differ based on whether they are purchasing a fixed-rate or floating-rate swap. The swap rate is composed of three components: the base rate (for fixed-rate markets) or the realized floating rate (for floating-rate markets), plus a risk premium and utilization fee. Each component addresses a distinct concern and their sum produces the final rate the buyer pays at settlement. The protocol supports negative rates throughout: base rates, floating rates, and swap rates are all signed values, enabling markets referencing rates that may go negative.

The pool takes the opposing side of each swap and pays a corresponding rate, but without the risk premium or utilization fee. The pool pays only the base rate for floating-rate swaps and the realized floating rate for fixed-rate swaps. This asymmetry is intentional: the risk premium and utilization fee exist to compensate LPs for the risks and opportunity costs of providing liquidity. The net difference between the buyer's rate and the pool's rate determines the settlement payment between the two parties.

Because swap pricing is derived from oracle-provided rates rather than a bonding curve, the protocol mitigates adverse selection risk for LPs. In traditional AMMs, liquidity providers are exposed to informed traders who arbitrage stale prices. In Kairos, the base rate, risk premium, and utilization fee all reflect current market conditions at the time of each swap purchase, ensuring LPs are not systematically disadvantaged by lagging price discovery. LPs further control their exposure by selecting which markets and oracles to supply liquidity to, or by creating markets with pricing models/oracles they believe best reflect risk.

**Base Rate**

The base rate is an oracle-provided value representing the expected time-weighted average floating rate (TWAR) over the duration of the swap. It serves as the foundation of the fixed swap rate, anchoring pricing to the underlying market rate the swap is referencing. Because fixed-rate buyers are locking in a rate at purchase, the base rate must reflect the best estimate of where the floating rate will average over the swap's tenor.

**Realized Floating Rate**

The realized floating rate is derived from a continuously updated onchain rate index that tracks the cumulative floating rate over time. It determines the buyer's rate obligation in floating-rate markets and the pool's rate obligation in fixed-rate markets. Each rate index is tied to a reference rate oracle and is updated during core protocol interactions. Because multiple markets can reference the same underlying rate oracle, a single rate index can serve many markets simultaneously, updating whenever any of those markets are interacted with.

The rate index updates based on its specific rate convention, set at initialization. The protocol supports three conventions: cumulative index oracles that provide the index directly, spot rate oracles accumulated via simple interest, and spot rate oracles accumulated via continuous compounding. This flexibility enables support of a variety of interest rates, across chains as well as off-chain. Additional rate types or specifications can be accommodated through adapters.

All three conventions ultimately produce an index value, and the formula operates on those indices uniformly. The realized rate for a swap is derived from two index snapshots; one taken at swap entry and one at closing. The annualized rate is calculated as

`(closingIndex / entryIndex - 1) × (seconds per year / time elapsed)`

producing a measure of the floating rate actually realized over the swap's duration.

**Risk Premium**

The risk premium compensates LPs for the uncertainty inherent in the base rate: the possibility that the actual time-weighted average rate diverges meaningfully from what the oracle projected at the time of purchase. The risk premium can be set independently for fixed and floating markets, reflecting the fact that the two sides carry different risk profiles. A higher base rate uncertainty warrants a higher risk premium, allowing LPs to price risk more precisely and earn greater compensation for taking on more uncertain exposures.

**Utilization Fee**

The utilization fee incentivizes liquidity provision and compensates LPs as the pool becomes more utilized. It is defined by a marginal fee curve that is a function of global pool utilization:<br>

`u = lockedLiquidity / totalLiquidity`<br>

where lockedLiquidity is the pool's collateral currently backing open swaps and totalLiquidity is the pool's total collateral. The marginal fee follows a piecewise model, growing linearly below a configurable kink point and quadratically above it:

<pre><code>if u ≤ kink:
<strong>    f(u) = u × slope
</strong>if u > kink:
    f(u) = kink × slope + ((u − kink) / (1 − kink))² × maxKinkFee
</code></pre>

Three immutable parameters, set at market creation, define the curve:

| Parameter  | Description                                                                    |
| ---------- | ------------------------------------------------------------------------------ |
| slope      | Linear fee coefficient applied below the kink                                  |
| kink       | Utilization threshold (e.g. 0.9 = 90%) where the quadratic region begins       |
| maxKinkFee | Maximum additional fee from the quadratic portion, reached at 100% utilization |

The fee charged on a given swap is not a point evaluation of this curve but rather its integral over the utilization range the trade consumes. When a swap is purchased, the protocol computes pre-trade utilization (uPre) and post-trade utilization (uPost), then calculates the average fee rate as:

`fee = (F(uPost) − F(uPre)) / (uPost − uPre)`

where F(u) is the antiderivative of f(u):

```
if u ≤ kink:
    F(u) = slope × u² / 2
if u > kink:
    F(u) = F(kink) + slope × kink × (u − kink) + maxKinkFee × (u − kink)³ / (3 × (1 − kink)²)
```

F is continuous and differentiable at the kink by construction. This integral-based approach ensures that the fee is path-independent: splitting a trade into multiple smaller trades produces the same total fee as a single trade of the same size, eliminating any incentive to game trade sizing.

The quadratic region produces a steep, convex fee curve beyond the kink that serves two purposes: it ensures markets retain sufficient available liquidity for LPs to withdraw, and it compensates LPs when there is structural imbalance in demand between fixed and floating swaps. Swaps that would exceed available liquidity are rejected.

The total maximum utilization fee at 100% utilization is: `kink × slope + maxKinkFee`.

#### Core Protocol Functions

**Purchasing a Swap**

By purchasing a swap, the buyer is agreeing to pay the pool a certain rate (i.e. swap rate) over the course of the term in exchange for a different rate paid by the pool. To ensure the buyer will make good on their payment, they must supply margin to purchase a swap, which is calculated based on the expected payment the buyer may owe over the duration of the swap. A leverage multiplier, set immutably in the market parameters, determines the amount of margin required and can enable lower or higher leverage swaps. The pool posts a corresponding amount of collateral to cover its expected payments to the buyer. This bilateral collateralization ensures both parties have capital pledged for the duration of the swap.

Buyer margin is calculated as:

`buyerMargin = notional × (|baseRate| + utilizationFee + riskPremium) × term / (SECONDS_PER_YEAR × leverageMultiplier)`

The numerator represents the buyer's expected rate exposure over the swap's duration based on current oracle rates, scaled to the swap's term as a fraction of a year. The leverage multiplier acts as a divisor: a higher ratio reduces the required margin relative to notional, enabling higher leverage. A leverageMultiplier of 2 means the buyer must post margin covering half the expected rate-time exposure; a ratio of 1 requires full coverage. Since the leverage multiplier is specific to each market, buyers and LPs can choose to participate in lower or higher margin markets.

Pool collateral is sized using only the base rate:

`poolCollateral = notional × |baseRate| × term / (SECONDS_PER_YEAR × leverageMultiplier)`

The pool's collateral excludes the utilization fee and risk premium since only the buyers pay these components as part of their overall rate to compensate LPs for providing liquidity. This asymmetry means the buyer always posts more collateral than the pool for the same swap, reflecting the additional fee burden the buyer carries. The base rate is floored at a protocol-defined minimum (0.01%) to prevent degenerate margin calculations when rates approach zero.

For fixed-rate swaps, the swap rate is locked at purchase and comprises the base rate, risk premium, and utilization fee. For floating-rate swaps, the risk premium and utilization fee are fixed at purchase while the time-weighted average floating rate is determined at settlement. This structure gives fixed-rate buyers certainty on their cost while preserving the floating-rate buyer's exposure to actual rate movements over the swap period.

Protocol Fee

If enabled by the Kairos protocol admin, a protocol fee will be charged on each swap purchase, calculated as a percentage of notional proportional to the swap's tenor (e.g. a 0.2% fee on a 6-month swap charges 0.1% of notional). The fee is paid by the buyer at purchase and transferred directly to the protocol admin multisig, separate from the buyer's margin. The fee rate is set by the protocol admin and capped at 3%.

Market Creator Fee

If enabled by the Kairos protocol admin, a portion of the protocol fee is paid to market creators on each swap purchased in their created markets. This incentivizes new market creation and rewards markets where there is significant volume. The fee rate is set by the protocol admin and capped at 25% of the protocol fee.

**Swap Settlement**

When a swap expires, anyone can call the contract to settle. Settlement computes two payment legs (one for the buyer and one for the pool), nets them, and transfers the difference from the losing party's collateral to the winning party.

Payment legs are calculated as:

`payment = notional × rate × timeElapsed / SECONDS_PER_YEAR`

For fixed-rate swaps (BUY\_FIXED), the two legs are:

`buyerPayment = notional × swapRate × timeElapsed / SECONDS_PER_YEAR`

`poolPayment = notional × realizedFloatingRate × timeElapsed / SECONDS_PER_YEAR`

where `swapRate = baseRate + utilizationFee + riskPremium` (all locked at purchase), and realizedFloatingRate is derived from the rate index:

`realizedFloatingRate = (closingIndex / entryIndex − 1) × SECONDS_PER_YEAR / timeElapsed`

For floating-rate swaps (BUY\_FLOATING), the structure is inverted:

`buyerPayment = notional × realizedFloatingRate × timeElapsed / SECONDS_PER_YEAR + notional × (utilizationFee + riskPremium) × timeElapsed / SECONDS_PER_YEAR`

`poolPayment = notional × baseRate × timeElapsed / SECONDS_PER_YEAR`

The floating-rate buyer pays the realized floating rate plus the utilization fee and risk premium locked at purchase. The pool pays only the base rate.

Netting determines the settlement direction and amount:

`netObligation = |buyerPayment − poolPayment|`

For fixed-rate swaps, the buyer owes if the realized floating rate comes in below the swap rate as their fixed payment exceeds what the pool owes. For floating-rate swaps, the buyer owes if the realized floating rate plus fees exceeds the base rate as their payment exceeds the pool's base rate obligation.

The settlement amount is capped at the losing party's posted collateral:

`settlementAmount = min(netObligation, losingPartyCollateral)`

If the buyer owes, the settlement amount is deducted from the buyer's margin and added to the pool. If the pool owes, the amount is deducted from the pool's collateral backing and returned to the buyer along with their full margin. Because settlement only pulls from posted collateral, the maximum profit and loss for each party is capped at the counterparty's posted margin, and buyers know their maximum potential profit and loss before purchasing a swap.

**Early Exit**

Market creators can allow early swap settlement in markets. When a market is created with this setting, buyers may exit a swap before expiry, subject to an early exit fee, as a percentage of net settlement obligation. Early exits settle accrued payments using the realized floating rate and project remaining payments using the current base rate. The projection is asymmetric: if the remaining term favors the buyer, those projected gains are forfeited; if it favors the pool, the buyer must pay the projected losses. This asymmetry is a deliberate design choice: protecting the pool from selective early exits on profitable positions allows LPs to offer tighter pricing on all swaps, since they are not bearing the cost of one-sided optionality. Buyers retain optionality to seek other bids through minting an NFT as described below.

**NFT Minting**

Buyers may optionally mint an ERC721 token representing their swap position through a separate wrapper contract. This enables positions to be transferred or sold on secondary markets, while keeping the NFT layer decoupled from the core contract. The core contract exposes a view function that returns the current settlement value of any open swap, giving holders and prospective buyers real-time price transparency.

**Liquidation**

The buyer prefunds a liquidation bounty at swap purchase, stored separately from their margin. This separation ensures that the pool's mark-to-market valuation accurately reflects the collateral the pool can claim, since the bounty is paid to the liquidator rather than the pool. The bounty is returned to the buyer on normal settlement or early exit.<br>

A swap can be liquidated by anyone before expiry if the net accrued obligation exceeds the at-risk party's available collateral. For the buyer, this threshold is their posted margin; for the pool, it is the pool's collateral backing minus the liquidation incentive. Liquidation is bilateral: if the buyer is liquidated, the pool receives the buyer's full margin and the liquidator receives the prefunded bounty. If the pool side is liquidated, the buyer receives the pool's collateral backing minus the liquidator's reward, along with their own margin and prefunded bounty returned in full. Because liquidation is permissionless and evaluated in real time, the protocol remains solvent without requiring active management from either counterparty. Collateral is denominated in the same asset as swap payments, eliminating price risk for liquidators and simplifying liquidation. The liquidation incentive is set as an immutable market parameter, allowing for greater incentives in markets with more volatile rates.

#### Liquidity Provision and Vault Layer

**Liquidity Provision**

Liquidity is supplied to markets directly by LPs or through vaults managed by curators. Supplying liquidity to a market allocates shares to the depositor, which represent proportional ownership of the pool and track each LP's claim on the pool's collateral as swaps are opened, settled, and as other LPs enter and exit. When withdrawing, LPs redeem shares at the current share price based on available unlocked collateral.

**Curators and Vaults**

Curators manage vaults that allocate depositor funds across Kairos markets, selecting which markets to support and setting risk parameters for their depositors. This model separates the role of liquidity provision from risk management as depositors benefit from a curator's market expertise without having to actively manage their own allocations. Two vault types are supported: LP vaults that supply liquidity to market pools, and buyer vaults that open and manage swap positions on behalf of depositors. Each vault connects to the protocol through a dedicated adapter contract that translates between the vault's interface and the protocol's core functions, tracks positions across multiple markets, and reports mark-to-market value so the vault can accurately price its shares at all times.

At launch, the initial adapters integrate with Morpho Vault V2, which implements the ERC4626 tokenized vault standard. Because the adapter layer is modular, additional adapters can be built to support other vault architectures or custom integrations without changes to the core protocol.

**LP Share Price**

LP share price equals the pool's total collateral plus unrealized P\&L across all open swaps, divided by total shares outstanding. It is calculated through bucketing swaps and determining the Mark-to-Market (MtM) value of each bucket.

**Bucket System**

To ensure gas-efficient computations, Kairos uses a bucket aggregation system that reduces the computation from `O(n)` in swaps to `O(k)` in buckets. Each market is configured with a fixed number of buckets and a bucket interval. Swaps are deterministically assigned to a bucket based on entry timestamp. Rather than storing individual swap details for MtM purposes, each bucket maintains notional-weighted aggregates: fixed rate, entry time, floating rate index, fee components, and collateral totals for both parties. Opening or settling a swap updates only its bucket's aggregates, a constant-cost operation regardless of how many swaps the bucket contains.

**Mark-to-Market Calculation**

To compute total unrealized P\&L, the protocol iterates over all buckets, deriving each bucket's weighted-average remaining tenor and querying the base rate oracle for a tenor-appropriate rate. This reflects the yield curve's term structure as buckets approaching expiry are valued with short-dated rates while recently opened buckets use longer-dated rates. The accrued floating leg is derived from the cumulative index between entry and now, and the fixed-versus-floating difference scaled by notional gives each bucket's unrealized P\&L. Each bucket's unrealized P\&L is capped by the collateral posted by both parties, ensuring the pool cannot gain more than the total buyer collateral in a bucket or lose more than its own posted backing. A minimum share price floor prevents shares from reaching zero when a pool is deeply underwater; deposits are blocked while the pool is at this floor with active swaps to prevent dilution of existing LPs. This design ensures accurate, up-to-date share pricing as LPs enter and exit markets, while keeping gas costs fixed regardless of the number of open positions.

### Notable Smart Contract Patterns

#### Singleton Contract

The protocol is implemented as a singleton contract that holds all markets. This design reduces deployment costs, simplifies multi-market interactions, and provides a unified settlement layer across the entire protocol.

#### Account Delegation

All core functions accept an onBehalfOf parameter, allowing users to authorize third parties to act on their positions. This enables composability with smart contract vaults, relayers, and bundlers without requiring users to transfer custody of their collateral.

#### Adapter Pattern for External Vaults

Dedicated adapter contracts serve as the integration boundary between external vault protocols and the Kairos core contract. Each adapter is an independent contract that holds positions on behalf of its vault, isolating vault-specific logic from settlement mechanics. New vault integrations require only deploying a new adapter; no changes to the core contract are needed.

#### Governance

The protocol is designed to minimize governance surface area. All market-level parameters (e.g. oracle addresses, swap term, leverage multiplier, utilization fee curve, liquidation incentive, and early exit settings) are immutable once a market is created. The only mutable state is protocol-level fee configuration, managed by the admin, and market-owner controls over the LP whitelist and market termination.

**Protocol Admin**

A dedicated admin contract manages protocol-wide settings through a multisig with timelocked changes. The protocol fee rate is capped at 3% and requires a 3-day timelock to increase; disabling fees takes effect immediately since it always benefits users. The market creator fee share, capped at 25% of the protocol fee, follows the same 3-day timelock when enabling. Ownership transfers require a 7-day timelock with two-step acceptance, and renouncing ownership is permanently disabled. The admin may also set an optional market creation fee, payable in ETH or a designated ERC20 token.

**Market Owner**

Each market has an owner, initially its creator, who may transfer ownership. The market owner can manage the LP whitelist (if enabled at creation), and permanently terminate the market, which blocks new swaps while allowing existing swaps to settle normally.

### Acknowledgments

This whitepaper draws on the contributions of many individuals and teams. The authors are grateful for the support from the Kairos Labs investors, advisors, and friends.

### Disclaimer

This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal, or tax advice or investment recommendations. The opinions reflected herein are subject to change without being updated.


# Bug Bounty

<h2 align="center">Kairos x Guardian: Defender Bug Bounty Contest</h2>

<h3 align="center">💵 <strong>$100,000 Bounty</strong> 🐞</h3>

**A new** $100,000 **Guardian Defender contest is&#x20;*****COMING SOON!***

Hunt for bugs in **Kairos**’s onchain interest rate swap (IRS) protocol. ***Critical*** severities will be eligible for payouts.

Full details and how to participate below 👇

**Contest Details**

* **Bounty Pool**: **$100,000** (with a focus on ***critical*** findings)
* **Duration**: 30 days
* **Start Date**: TBD
* **End Date**: Start +30 days

The Kairos x Guardian Defender Contest will run for 30 days. Hunt for ***Critical*** severity vulnerabilities for a chance at the $100,000 Bounty Pool.

Contest details, payout information, and full terms of use will be available on the official Defender platform.

**Official Contest Page**

**Guardian Defender Dashboard**: <https://defender.guardianaudits.com/dashboard>

(The direct Kairos contest link will be posted here and [shared on X](https://x.com/kairosswap) as soon as the contest goes live.)

**How to Participate**

1. **Register on Defender** Visit the [Defender dashboard](https://defender.guardianaudits.com/dashboard) and log in / create an account. KYC may be required for reward eligibility.
2. **Access the Contest** Navigate to the Kairos contest page (link shared at launch).
3. **Review Scope & Rules** Read the in-scope contracts, out-of-scope items, reward tiers, and submission guidelines.
4. **Submit Your Findings** Provide clear, high-quality reports with reproduction steps and impact assessment directly on the platform.

Valid reports will be triaged by Guardian. Top discoveries will be rewarded based on severity and report quality.

**Why Join?**

This contest builds on our [Guardian audits](https://guardianaudits.com/) to further secure the protocol powering permissionless fixed-rate swaps, hedging, and yield strategies onchain. Help strengthen KairosSwap while competing for substantial rewards from the $100,000 pool.

**Ready to hunt?** Bookmark the Defender dashboard and [follow us on X](https://x.com/kairosswap) for the official contest launch announcement.


# Audit Reports

Protocol Security Audits

KairosSwap has been independently audited by two leading smart contract security firms. Both audits cover the core protocol contracts, including swap mechanics, the rate index library, swap formulas and other contracts critical to the protocol.

* [**Octane** — Read the report](/dev-docs/audit-reports/octane)
* [**Guardian** — Coming Soon](/dev-docs/audit-reports/guardian)

All findings have been addressed or formally acknowledged. We strongly encourage integrators to review both reports before building on top of the protocol.


# Octane

## Octane Security Adversarial Engagement

**Auditor:** Octane Security\
**Engagement type:** Adversarial Research Engagement (proof-of-concept driven)\
**Audit:** March 2026\
**Remediation verified:** May 22, 2026\
**Status:** All findings resolved

[Read the full report (PDF)](https://kairosswap.com/audit/kairosswap_octane_audit_report_fixlog.pdf)

***

### Summary

Octane Security ran a deep adversarial engagement against the KairosSwap core protocol: the full swap lifecycle from creation through settlement and liquidation, LP pool accounting and share pricing, oracle integration, the Morpho Vault V2 adapter layer, and the permissionless settlement and liquidation mechanics.

The engagement surfaced 20 findings. **Every one was reproduced with a runnable proof-of-concept, and every one has been fixed and re-verified against the latest codebase.** Nothing was accepted on description alone, and nothing was left open.

| Severity      | Count  | Resolved |
| ------------- | ------ | -------- |
| Critical      | 1      | ✅        |
| High          | 3      | ✅        |
| Medium        | 7      | ✅        |
| Low           | 8      | ✅        |
| Informational | 1      | ✅        |
| **Total**     | **20** | **20**   |

The audit was conducted at commit `1c7f9f29`. Because the contracts were substantially refactored during remediation, each finding was re-located by its underlying code semantics and confirmed resolved against the latest commit `b0e76c0d`.

***

### For degens

Octane tried to break KairosSwap, with strong incentive to find anything that could lose user funds. They probed every place where money moves: how positions are opened and closed, how liquidity providers are paid, how the protocol reads interest rates, and how it talks to integrations like Morpho vaults.

They found issues, which is the point of an audit. What matters is the outcome: the most serious issue (a single Critical that could have frozen vault funds under a specific settlement ordering) and all 19 others were fixed, and Octane re-checked the fixes against the live code rather than taking our word for it.

A few of the fixes intentionally keep certain behaviors. For example, LP deposits and withdrawals now refuse to execute if the protocol can't read a valid interest rate, rather than guessing at a price. That's by design: it protects LPs from entering or exiting at the wrong value. We've called those out explicitly below so nothing is hidden behind a "fixed" label.

***

### For developers

#### What was in scope

`SwapCore`, `Utils`, `SwapFormulas`, `RateIndexLib`, `Admin`, the `KairosBuyerAdapter` / `KairosMarketAdapter` layer and their factories, the oracle factory and `SimpleOracle`, and the read-only `Views` contract.

#### Methodology

Three phases: an analytical foundation (architecture mapping, threat modeling, fund-flow analysis, invariant identification, historical exploit research), the Octane platform's directed analysis across protocol-specific attack surfaces, and an exploitation phase where every candidate finding was confirmed or discarded by a dedicated PoC. In total, 20 PoC files with 155 passing test functions.

#### Finding classes and what changed

The 20 findings collapsed into 9 structural classes. Fixes were applied at the class level where possible, not just patched per-instance.

**Adapter and external balance accounting** — Added a sweep action and an escrow-claim path to `KairosBuyerAdapter`; `deallocate` now forwards recorded proceeds on an already-settled swap instead of reverting. The out-of-band stranding vector (the Critical) is closed. `realAssets()` still counts genuine out-of-band settlement proceeds by design, and donated tokens are now recoverable by sweep rather than permanently stuck.

**Transaction-ordering share price manipulation** — Bucket mark-to-market is now capped to actually posted collateral, removing the uncapped-vs-capped gap the deposit-settle-withdraw extraction relied on. `supplyCollateral` / `withdrawCollateral` gained `minSharesOut` / `minCollateralOut` guards, and a one-block LP vest blocks atomic deposit-then-exit cycles.

**Accounting path inconsistency** — The buyer-liquidation branch now zeroes `swap.collateralBalance` before crediting the pool, matching the other two settlement paths. Token Balance Solvency invariant holds.

**Non-binding preview / missing slippage** — `previewEarlyExitSettlement` was removed; `exitSwapEarly` now enforces a per-swap `minExitAmount` against the realized payout. `buySwap` gained a core-level `rateBound` and `maxMarkup`, so slippage protection no longer depends on the adapter.

**Algebraic time-window mismatch** — Both bucket MtM and normal settlement now resolve the floating rate from the index at expiry over a fixed term, so settlement timing no longer leaks post-expiry rate drift into payouts.

**Oracle integration gaps** — Decimal normalization applied consistently via the batched tenor-rate interface; the base oracle is read only when needed; `RateIndexLib.update()` now fails closed on invalid oracles rather than returning a stale index; cumulative oracles always re-sample the live index; and the view-based NAV path projects a fresh index instead of reading the stored one.

**LP dilution via share price floor** — `supplyCollateral` reverts at `MIN_SHARE_PRICE` when the pool has active swaps, blocking opportunistic floor deposits while keeping genuine rescue deposits possible when no active swaps exist.

**Blacklistable token interaction** — Settlement and liquidation payouts route through `_transferOrEscrow`, which escrows for later claim if the transfer reverts. A blacklisted recipient can no longer brick settlement or lock pool collateral.

**Inconsistent rate source** — Early-exit settlement and MtM now both read the tenor-aware oracle for the same market, eliminating the spot-vs-tenor divergence on non-flat curves.

#### By-design residuals (called out for transparency)

* **LP ops fail closed on an invalid oracle when the pool has active swaps.** Pricing shares against open MtM exposure without a valid rate would let LPs enter or exit at the wrong NAV, so this is intended, not a residual DoS.
* **`realAssets()` still includes raw `balanceOf`.** It must count genuine settlement proceeds. A donation can only transiently pressure a curator-configured finite cap until swept, which is recoverable self-funded griefing rather than a permanent condition.
* **The global raw-balance collateral cap was removed entirely**, eliminating the donation-DoS surface rather than patching it.

***

### Bottom line

The protocol went through an adversarial, proof-driven review, came out with a full set of reproduced findings, and every finding was remediated and independently re-verified against the live code. The full report, including each PoC and the per-finding remediation detail, is linked above.


# Guardian

Coming soon


# Contract Addresses

## Base (beta)

<table><thead><tr><th width="173.3515625">Contract</th><th>Address</th></tr></thead><tbody><tr><td>Swap Core</td><td>0x652C91cd2356feE8857Dc9FD69Cb036B53741725</td></tr><tr><td>SwapFormulas</td><td>0x434ACe9279c41611566D521f8cAC86C70F0FC66f</td></tr><tr><td>Kairos Krystals</td><td>0xdC583a3834347ff61A0B347d7Af8D6542e01F03C</td></tr></tbody></table>


# Liquidation Bot

How to build a liquidation bot

## Building a Liquidation Bot on Kairos

Kairos liquidations are unusually simple to keep. There is no flash loan, no collateral auction, no debt to assume, and no token you are forced to hold. A liquidator is a pure **keeper**: it spots a position whose margin has run out, calls one function, and walks away with a bounty that was escrowed up front. This guide explains the mechanism, then walks through building a bot end to end.

{% hint style="info" %}
**TLDR** — Call `liquidateSwap(swapId)` on an undercollateralized swap. You pay gas; the protocol pays you a prefunded bounty in the market's settlement token (e.g. USDC). You never take on the position, inventory, or price risk.
{% endhint %}

### 1. How liquidation works

#### What a swap looks like

Each Kairos market is an onchain interest rate swap. A **buyer** posts collateral to take a fixed-vs-floating position for a fixed term; the **LP pool** backs the other side. As the reference rate moves, one side accrues an obligation to the other. That obligation is settled from collateral both sides posted at entry.

A position can close three ways:

| Closure         | How                                                           | `closureType` |
| --------------- | ------------------------------------------------------------- | ------------- |
| **Expiry**      | Anyone calls `makePayment` after the term ends                | `0`           |
| **Early exit**  | Buyer exits before expiry (if the market allows it)           | `1`           |
| **Liquidation** | Anyone calls `liquidateSwap` while the position is underwater | `2`           |

Liquidation exists for the case where the rate moves so far against one side, *before* expiry, that its posted collateral can no longer cover what it owes. Rather than let the position drift further underwater, anyone may force-settle it early.

#### When is a swap liquidatable?

A position is liquidatable when **all** of these hold:

1. **It is not settled.** Closed positions are skipped.
2. **It has not expired.** If `block.timestamp >= entryTimestamp + swapTerm`, it is *not* liquidatable — expired positions are settled normally via `makePayment`, not liquidated.
3. **Accrued obligation has consumed the owing side's collateral.**

The third condition uses **accrued payments only** — the mark-to-market obligation up to *now*, derived from the move in the reference-rate index since entry. Projected future payments are not counted.

The threshold differs by which side owes:

```solidity
// Buyer is the owing side:
isBuyerLiquidatable = netObligation >= swap.collateralBalance;

// Pool is the owing side (triggers slightly early so the bounty is always covered):
liquidatorReward   = (poolCollateralBacking * market.liquidationIncentive) / WAD;
isPoolLiquidatable = netObligation >= (poolCollateralBacking - liquidatorReward);
```

In plain terms: the buyer becomes liquidatable once its accrued obligation reaches the collateral it posted. The pool side becomes liquidatable just *before* its backing is fully consumed, so there is always enough left to pay the liquidator.

#### Where the bounty comes from

The liquidator's reward is always a `liquidationIncentive` (a WAD percentage set per market) slice of the **owing side's** collateral — but the *source* depends on which side is liquidated.

**Buyer-side liquidation — paid from the buyer's prefunded bounty.** When the swap is created, the buyer prefunds a bounty inside `buySwap`:

```solidity
// Calculated at entry and held by the contract, separate from collateralBalance
uint256 liquidationBounty = (requiredBuyerCollateral * market.liquidationIncentive) / WAD;
uint256 totalBuyerRequired = requiredBuyerCollateral + protocolFee + liquidationBounty;
```

It is stored on the position as `swap.liquidationBounty` and kept out of the bucket accounting so it cannot be confused with margin. If the buyer is liquidated, this prefunded amount is paid to whoever called `liquidateSwap`.

**Pool-side liquidation — paid from the pool's collateral.** When the *pool* is the owing side, the buyer's prefunded bounty is **not** used — it is returned to the buyer in full. Instead the reward is computed fresh from the pool's backing at liquidation time and carved out of it:

```solidity
liquidatorReward = (poolCollateralBacking * market.liquidationIncentive) / WAD;
```

In both cases the reward is already sitting in the contract before your bot shows up — either as the buyer's prefunded bounty or as part of the pool's backing. You are never fronting it.

{% hint style="info" %}
**Why it matters for a bot:** your expected payout is `liquidationIncentive × (collateral of whichever side is underwater)`. For a buyer-side liquidation that is the buyer's prefunded `liquidationBounty`; for a pool-side liquidation it is a slice of `poolCollateralBacking`. Size your gas-vs-reward check against the correct side.
{% endhint %}

#### What happens on liquidation

`liquidateSwap` updates the oracle index, re-checks eligibility, force-settles the position, and pays out — strictly checks-effects-interactions, with external transfers last.

**If the buyer is liquidated:**

* Liquidator receives the prefunded `swap.liquidationBounty`.
* The buyer's entire `collateralBalance` is credited to the pool.
* The buyer receives nothing back.

**If the pool is liquidated:**

* Liquidator receives `poolCollateralBacking * liquidationIncentive / WAD`.
* The buyer receives the remaining pool backing, **plus** their own original collateral, **plus** their own prefunded bounty back.

Either way the position is marked `settled`, removed from its bucket, and the bounty is sent to `msg.sender` with a direct `safeTransfer` in the market's `swapToken`:

```solidity
if (liquidatorTransfer > 0) {
    IERC20(swapToken).safeTransfer(liquidator, liquidatorTransfer);
}
```

#### Events to index

Two events fire on every liquidation:

```solidity
event SwapLiquidated(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed liquidatedParty, // buyer address if buyer-side, else address(0)
    bool buyerLiquidated,
    bool poolLiquidated,
    uint256 collateralTransferred,
    address liquidator               // = msg.sender
);

event SwapClosed(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed onBehalfOf,
    address caller,
    uint8 closureType,               // 2 = liquidated
    /* ...settlement amounts... */
    int256 floatingRate
);
```

A bot can confirm its own fills by watching `SwapLiquidated` for its address, or filter `SwapClosed` on `closureType == 2`.

### 2. How to build a liquidation bot

A liquidation bot is a loop with four stages: **discover** open positions, **filter** to likely candidates, **confirm** onchain, and **submit**.

```
┌───────────┐   ┌──────────┐   ┌───────────────┐   ┌────────┐
│ Discover  │──▶│  Filter  │──▶│  Confirm via  │──▶│ Submit │
│ open swaps│   │ off-chain│   │ eth_call sim  │   │  tx    │
└───────────┘   └──────────┘   └───────────────┘   └────────┘
```

{% hint style="warning" %}
**There is no public onchain `isLiquidatable` view.** `Utils.isSwapLiquidatable` is a library function over storage references — it is not externally callable, and SwapCore does not re-expose it. The robust way to confirm eligibility is to **simulate** `liquidateSwap` with `eth_call`: if the position is not liquidatable, it reverts with `E507`. Use off-chain math only to *cheaply pre-filter* candidates before simulating.
{% endhint %}

#### Step 1 — Discover open positions

There is no onchain array of all swap IDs. Three discovery sources, easiest first:

**A. The indexer (recommended).** Kairos runs a Ponder indexer exposing a GraphQL API (default dev endpoint `https://idxdev.kairosswap.com/graphql`). The `swaps` table carries a `status` field (`"open" | "expired" | "earlyExit" | "liquidated"`) and every entry field you need for the math:

```graphql
query OpenSwaps {
  swaps(where: { status: "open" }, limit: 1000) {
    items {
      swapId
      marketId
      rateType
      entryTimestamp
      swapTerm
      buyerCollateral
      poolCollateralBacking
      liquidationBounty
      baseRate
      entryFloatingIndex
    }
  }
}
```

**B. The onchain expiry queue.** Each market keeps an append-only queue of swap IDs with a pointer to the earliest unsettled one:

```
getExpiryQueueState(marketId) -> (length, pointer)
getExpiryQueueEntry(marketId, index) -> swapId
```

Walk `[pointer, length)` to enumerate live swaps without the indexer. (The queue is ordered by entry for expiry settlement, not pruned of unexpired-but-liquidatable entries, so you still filter yourself.)

**C. `SwapCreated` event logs.** Backfill historical swaps directly from chain if you do not want to depend on the indexer.

#### Step 2 — Pre-filter off-chain

For each open swap, drop anything that cannot be liquidated, cheaply:

* **Expired?** If `now >= entryTimestamp + swapTerm`, skip — that is a `makePayment`, not a liquidation.
* **Estimate the obligation.** Read the live reference index (`getFreshIndex(marketId)`), derive the floating rate from `entryFloatingIndex` versus the live index over the elapsed time, and compare accrued fixed vs floating legs against the owing side's collateral. Treat this only as a ranking heuristic — the contract's math is authoritative.

A lightweight proxy for "how close to the edge" is `getSwapNetAmount(swapId)`, which returns the current net settlement amount and direction.

#### Step 3 — Confirm with a simulation

For each surviving candidate, simulate the real call. If it would succeed, it is liquidatable right now.

```ts
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const swapCore = { address: SWAPCORE_ADDRESS, abi: SWAPCORE_ABI } as const;

async function isLiquidatableNow(swapId: `0x${string}`): Promise<boolean> {
  try {
    await publicClient.simulateContract({
      ...swapCore,
      functionName: "liquidateSwap",
      args: [swapId],
      account, // the bot's address
    });
    return true; // simulation succeeded -> liquidatable
  } catch (err) {
    // Reverts with E507 when not liquidatable; treat any revert as "skip"
    return false;
  }
}
```

#### Step 4 — Submit

When the simulation passes, send the transaction. The call takes only the `swapId` — no amount, no token, no approval.

```ts
async function liquidate(swapId: `0x${string}`) {
  const { request } = await publicClient.simulateContract({
    ...swapCore,
    functionName: "liquidateSwap",
    args: [swapId],
    account,
  });
  const hash = await walletClient.writeContract(request);
  await publicClient.waitForTransactionReceipt({ hash });
  return hash;
}
```

#### Putting it together

```ts
async function tick() {
  const open = await fetchOpenSwaps();          // Step 1 (indexer GraphQL)
  const now = Math.floor(Date.now() / 1000);

  const candidates = open
    .filter(s => now < s.entryTimestamp + s.swapTerm) // Step 2: drop expired
    .filter(estimateUnderwater);                       // Step 2: cheap ranking

  for (const s of candidates) {
    if (await isLiquidatableNow(s.swapId)) {           // Step 3: confirm
      try {
        const hash = await liquidate(s.swapId);        // Step 4: submit
        console.log(`liquidated ${s.swapId} -> ${hash}`);
      } catch {
        // Lost the race (someone else liquidated, or rate moved back). Move on.
      }
    }
  }
}

setInterval(tick, 12_000); // roughly one block on most L2s; tune to your chain
```

#### Operational notes

{% hint style="info" %}
**Liquidation is permissionless and competitive.** `liquidateSwap` has no whitelist — anyone can call it. Expect other keepers. Win on latency (poll frequently, simulate fast) and gas strategy, not on privileged access.
{% endhint %}

* **Idempotency / races.** Once a swap is liquidated it is `settled`; a second call reverts. Always rely on the pre-send `simulateContract` so you do not burn gas on a position someone else just took.
* **Keep the index fresh.** `liquidateSwap` updates the oracle index itself before checking, so you do not have to. But your *off-chain* estimate uses the last snapshot — a position can cross the threshold the instant a new index lands. Re-simulate close to send time.
* **Watch both sides.** A market has a buyer side and a pool side, and either can be the one that gets liquidated. Your math must handle `rateType` (`BUY_FIXED` = buyer pays fixed; `BUY_FLOATING` = buyer receives fixed), which flips who owes.
* **Gas vs bounty.** The bounty is `liquidationIncentive` (a WAD percentage set per market) of the owing side's collateral. Before sending, confirm the bounty exceeds your gas cost — small positions on expensive chains may not be worth it.

### 3. The benefits — and the absence of collateral price risk

Kairos's liquidation model is deliberately a **trigger**, not a takeover. That removes the risks that make liquidation bots elsewhere capital-intensive and dangerous.

#### You provide gas, nothing else

There is no `transferFrom` anywhere in the liquidation path. You do not deposit collateral, you do not need token approvals, and you do not need a flash loan to fund a repayment. The bounty is already escrowed inside the contract — you call one function and receive it. The entire capital requirement is the gas for the transaction.

#### You never hold the position or any inventory

In an AMM or lending-market liquidation you typically *buy* the collateral, assume the debt, or take the seized asset onto your balance sheet — and then you have to sell it, exposing you to slippage and price moves in the window before you do. On Kairos you become a counterparty to **nothing**. `liquidateSwap` force-settles the position in place: collateral is routed between the existing buyer and pool, the position is marked settled, and you receive a fixed bounty. You hold no swap, no leg, no seized collateral.

#### No collateral price risk

Because you never acquire an asset, there is no asset to mark against the market. Your payout is the prefunded bounty, denominated in the market's `swapToken` — typically a stablecoin like USDC. The only "price exposure" you ever have is whatever you choose by holding that token afterward. There is:

* **No volatile collateral to offload** before the price moves against you.
* **No debt to assume** and refinance.
* **No flash-loan leg** that can fail and strand you.
* **No inventory** carried between blocks.

{% hint style="success" %}
**Net effect:** the worst case for a Kairos liquidator is a reverted transaction (you lose only gas, and the pre-send simulation prevents most of those). This makes the role well suited to a lightweight keeper that optimizes purely for latency and gas, without requiring a treasury, hedging, or inventory management.
{% endhint %}

#### Why the protocol wants you there

Liquidations keep the protocol solvent: they close out underwater positions before the obligation exceeds posted collateral, protecting the counterparty (buyer or LP pool) from being left with an unbacked claim. The prefunded bounty exists precisely to make sure that work is always profitable to perform and always available to anyone — no permissions, no capital, no price risk.

***

#### Quick reference

|                            |                                                                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Entry point**            | `liquidateSwap(bytes32 swapId)` — permissionless, gas only                                               |
| **Eligibility**            | not settled, not expired, accrued obligation ≥ owing side's collateral                                   |
| **Reward**                 | prefunded `liquidationBounty` (buyer-side) or `poolBacking × incentive` (pool-side), paid in `swapToken` |
| **Confirm before sending** | `eth_call` simulate `liquidateSwap`; reverts `E507` if not liquidatable                                  |
| **Discover positions**     | indexer GraphQL `swaps(status:"open")`, or onchain expiry queue, or `SwapCreated` logs                   |
| **Events**                 | `SwapLiquidated`, `SwapClosed (closureType=2)`                                                           |
| **Risk**                   | gas on a reverted tx only — no collateral, inventory, or price risk                                      |


# Trade on Interest Rates

Learn how and why to trade Interest Rate Swaps

## How to Trade Interest Rate Swaps: A Step-by-Step Guide

### Understanding the Basics

Before you start trading, understand what you're doing:

* **Pay Fixed, Receive Variable**: You think rates will go UP
* **Pay Variable, Receive Fixed**: You think rates will go DOWN
* **Notional Amount**: The size of your position (you don't need to deposit the full amount)
* **Collateral**: What you actually deposit to back your position

### Step 1: Choose Your Market

Interest rate swap markets are created with specific parameters:

* **Reference Rate**: Which rate you're betting on (Aave USDC, Morpho ETH, Compound DAI, etc.)
* **Term Length**: How long the swap lasts (1 month, 3 months, 1 year, etc.)
* **Collateral Token**: What you deposit to secure your position

**Example Markets:**

* 3-month Aave USDC rates (collateral: USDC)
* 6-month Morpho WETH rates (collateral: WETH)
* 1-year Compound DAI rates (collateral: DAI)

### Step 2: Determine Your Position Size

**Key Concepts:**

* **Notional Amount**: The swap size (e.g., $100,000)
* **Collateral Required**: Typically 10-20% of notional (e.g., $10,000-$20,000)
* **Leverage**: Notional ÷ Collateral = Your leverage multiplier

**Example:**

* You want $50,000 notional exposure to Aave USDC rates
* Market requires 15% collateral = $7,500 USDC deposit
* Your leverage = 3.33x

### Step 3: Choose Your Direction

#### Bullish on Rates (Expect rates to RISE)

**Position**: Pay Fixed, Receive Variable

* You pay a fixed rate (e.g., 5%)
* You receive whatever the variable rate becomes
* **Profit if**: Variable rate goes above your fixed rate
* **Loss if**: Variable rate stays below your fixed rate

**Example**:

* Current Aave rate: 4%
* You pay fixed 5%, receive variable
* If Aave hits 8%, you earn 3% on your notional
* On $50k notional with $7.5k collateral = $1,500 profit (20% return)

#### Bearish on Rates (Expect rates to FALL)

**Position**: Pay Variable, Receive Fixed

* You pay whatever the variable rate is
* You receive a fixed rate (e.g., 4%)
* **Profit if**: Variable rate falls below your fixed rate
* **Loss if**: Variable rate rises above your fixed rate

**Example**:

* Current Aave rate: 6%
* You pay variable, receive fixed 4%
* If Aave drops to 2%, you earn 2% on your notional
* On $50k notional with $7.5k collateral = $1,000 profit (13% return)

### Step 4: Execute Your Trade

#### Through the Protocol Interface:

1. **Connect Wallet**: Use MetaMask, WalletConnect, etc.
2. **Select Market**: Choose your reference rate and term
3. **Enter Position Details**:
   * Notional amount
   * Direction (pay fixed/receive variable or vice versa)
   * Review the fixed rate being offered
4. **Approve Collateral**: Allow the protocol to use your tokens
5. **Submit Transaction**: Confirm the swap creation

#### Key Elements:

* **Reference Rate**: Current variable rate for a given market
* **Current Fixed Rate**: Fixed rate for a given market at a point in time
* **Collateral Requirement**: How much you need to deposit
* **Liquidation Risk**: When your position might get closed

### Step 5: Monitor Your Position

#### What to Watch:

* **Current Variable Rate**: How the reference rate is moving
* **Unrealized P\&L**: Your current profit/loss
* **Collateral Ratio**: Make sure you don't get liquidated
* **Time Decay**: How much time is left on your swap

#### Managing Risk:

* **Close Early**: Exit before expiration if desired. Please be aware there may be fees for closing a swap early.
* **Roll Position**: Close current swap and open a new one.

### Step 6: Settlement

#### At Expiration:

* **Calculate Average Rate**: The protocol determines the average variable rate and fixed rate over the term.
* **Net Settlement**: You pay or receive the difference.
* **Collateral Return**: Get back your remaining collateral after settlement plus any profits if the net settlement difference was in your favor.

#### Example Settlement:

* You paid fixed 5%, received variable on $50k notional for 3 months
* Average variable rate was 7%
* You receive: (7% - 5%) × $50,000 × (3/12) = $250
* Plus your original collateral

### Common Strategies

#### Hedging:

* **Lender Protection**: You're lending on Aave, worried about rate drops → Pay variable, receive fixed
* **Borrower Protection**: You're borrowing variable rates, worried about spikes → Pay fixed, receive variable

#### Speculation:

* **Bull Market Play**: Expect DeFi rates to spike → Pay fixed, receive variable across multiple markets
* **Bear Market Play**: Expect rates to crash → Pay variable, receive fixed
* **Volatility Play**: Trade around major events (Fed meetings, protocol updates, etc.)

#### Arbitrage:

* **Cross-Protocol**: If Aave and Compound rates are misaligned
* **Term Structure**: If short-term and long-term rates seem mispriced
* **Basis Trading**: Combine with underlying lending positions

### Risk Management Tips

1. **Start Small**: Use lower leverage until you understand the mechanics
2. **Diversify**: Don't put all capital in one rate/term
3. **Watch Liquidations**: Keep collateral ratios healthy
4. **Understand Correlation**: DeFi rates often move together
5. **Monitor Gas**: Factor in transaction costs for smaller positions
6. **Time Decay**: Longer terms give more time to be right, but tie up capital longer

### Advanced Features

* **Partial Closing**: Exit part of your position early
* **Rolling**: Close and reopen in new terms
* **Portfolio View**: Manage multiple swaps across different markets

*DISCLAIMER: Interest rate swaps are leveraged instruments. You can lose all of you initial collateral if positions move significantly against you. Always understand the risks before trading.*


# Read Kairos Charts

Learn how to read the Kairos Pro Mode charts

First you'll need to toggle on Pro Mode, then click into a Market

<figure><img src="/files/v3U2bIS6R1bgcUbjeT4m" alt=""><figcaption></figcaption></figure>

Then zoom into [the chart](https://kairosswap.com/market/0xf0acc2cd18052975cc6b469e6f6a34aedcde63da995086855e9a1fdd257de351) for your preferred timeline or swap duration, choose a swap direction:

* LONG: Pays the fixed rate, recieves a variable rate
* SHORT: Pays the variable rate, recieves the fixed rate

The cash is in the crossover. When the trend spikes above or below your swap's Fixed Rate, you're in the money:

<figure><img src="/files/BrR6JZ2VO8jAHUY0f3s3" alt=""><figcaption></figcaption></figure>


# Buy a Rate Swap

Make your first swap purchase

So you're ready to buy your first swap? Here is an easy, practical walkthrough:

### Degen Mode

**Step 1: Choose a Direction**

Will interest rates go up or down?

<figure><img src="/files/lcsCNxr1341CDd5FFpY8" alt="" width="375"><figcaption></figcaption></figure>

**Step 2: How much to wager?**

Slide the widget up to the amount you want to bet on rate movement

<figure><img src="/files/DtrJqtr9K0scP55gRK5q" alt="" width="375"><figcaption></figcaption></figure>

**Step 3: Finalize your Prediction**

Lastly, you'll need to click `[Approve]` to enable trading, `[Post Collateral]` to fund your Prediction, and `[Place Prediction]` to finalize your bet

**Step 4: Profit**

If rates move in your favor, collect that sweet sweet yield

<figure><img src="/files/SG7gSA87MbYsJbn19AJf" alt="" width="375"><figcaption></figcaption></figure>

### Pro Mode

**Toggle on Pro Mode:**

<figure><img src="/files/WYE19WXTalfkmAbPDKPe" alt="" width="375"><figcaption></figcaption></figure>

**Step 1: Select a Market**

<figure><img src="/files/aJ53ZgIIS77O9WzWkQFm" alt=""><figcaption></figcaption></figure>

**Step 2: Choose a Notional Value**

This is the total amount that will accrue interest (gain or loss), during the duration of your swap. For example $5,000,000 USDC

<figure><img src="/files/Kv2Zy8akWa2PxX6zYMq9" alt="" width="155"><figcaption></figcaption></figure>

**Step 3: Connect your Wallet**

Using Coinbase Wallet, Metamask, Wallet Connect, or your favorite DeFi wallet. Connect to the swap market.

**Step 4: Approve Collateral**

You should only need to do this once, to approve your collateral for the swap market. Cick "Add Collalteral," on the order widget:

<figure><img src="/files/paoCQ9mUgPMawCXbLBHu" alt="" width="156"><figcaption></figcaption></figure>

Then, click "Approve" and sign the approval transaction:

<figure><img src="/files/McfttHCNagWeEUB6vBlk" alt=""><figcaption></figcaption></figure>

Wait for this transaction to complete.

**Step 5: Add Your Collateral**

Add the prefilled amount of collateral to execute your swap, by click "Add Collateral":

<figure><img src="/files/ybUPO38aF7pCsWBoUDDF" alt=""><figcaption></figcaption></figure>

Wait for this transaction to complete

**Step 6: Execute your Swap Trade**

Finalize your Swap trade by clicking "Buy Swap" on the order widget:

<figure><img src="/files/oWLuLem4ALhXAYSmE506" alt=""><figcaption></figcaption></figure>

**Profit (with a little luck!)**

You'll now see your open swap position on the market page below the order widget:

<figure><img src="/files/X2o3qi9iYzLctMqOd7ey" alt=""><figcaption></figcaption></figure>

Next up, learn how to Manage Collateral and view your Portfolio

*DISCLAIMER: Swaps are leveraged instruments. You can lose all of your collateral if positions move significantly against you. Always understand the risks before trading.*


# Close a Rate Swap

Once your swap term has expired, you will see an option to "Claim Payments". Execute this transaction to settle your swap, unlock collateral, and settle any payments or obligations due:

<figure><img src="/files/SE9JDrnbwjMPgNxHCpBP" alt=""><figcaption></figcaption></figure>


# Portfolio Management

Portfolio overview and Managing your collateral

View your Portfolio page to see positions across all markets and Manage Collateral:

<figure><img src="/files/z6Fm5qvvUhMqlNczjybQ" alt=""><figcaption></figcaption></figure>

**Add or Remove Collateral**

In the Collateral by Market table, adjust your collateral in any given market by clicking "Adjust Collateral"

<figure><img src="/files/uVq602cvUnMAR1R8xPj5" alt="" width="188"><figcaption></figcaption></figure>


# Liquidation

How Liquidation Works

In a swap, both parties post collateral to guarantee they can pay the obligations from their side of the trade. The buyer's collateral is sized to cover the maximum payment they could owe over the term, calculated from the swap rate and the length of the swap — not from the notional amount.

On top of that collateral, the buyer prefunds a small **liquidation bounty** at the moment they open the swap. The bounty is a percentage of their required collateral, set per market by the `liquidationIncentive` parameter and capped at **5%**. So a buyer whose maximum payment obligation is $5,000 posts $5,000 in collateral plus a bounty of up to $250, for a total of up to $5,250 pulled from their wallet at entry (alongside the protocol fee).

**The bounty is refundable.** It's held separately from the collateral that backs your payments, and if your swap is never liquidated it comes back to you in full when the swap settles at expiry. You only forfeit it if you're actually liquidated.

### When does a position become liquidatable?

A position becomes liquidatable when one side's collateral can no longer cover the payment it **already owes** on accrued interest up to right now.

This is worth being precise about: liquidation is evaluated on **accrued** payments, not projected ones. Where the rate might go between now and expiry is irrelevant to whether you can be liquidated today. A swap is only liquidatable when one side's collateral is mathematically insufficient at this moment.

### Who can liquidate?

**Anyone.** Liquidation is permissionless — there is no whitelist and no privileged liquidator role. Whoever calls `liquidateSwap` on an underwater position becomes the liquidator and collects the reward.

Where that reward comes from depends on which side went underwater:

* **Buyer-side liquidation** — the liquidator receives the buyer's prefunded liquidation bounty.
* **Pool-side liquidation** — the liquidator receives `liquidationIncentive × poolCollateralBacking`, drawn from the LP collateral reserved for that swap.

At most one side is liquidated per call. The other side receives its normal settlement.

### What happens when you're liquidated

The swap closes immediately. Your remaining collateral, after the liquidator's reward is paid out, transfers to the counterparty. In the example above, being liquidated means losing the $250 bounty on top of settling what you owed.

Importantly, this works **symmetrically**. The LP pool faces liquidation on exactly the same terms as the buyer — if the pool's backing for a swap can't cover what the pool owes, the pool gets liquidated and the buyer is made whole. Neither side gets preferential treatment.

Your maximum loss on a swap is what you posted. Because each swap is independently collateralized and settlement caps each side's payment at its own collateral, you can never end up owing more than you put in.


# Risks

Risks of using the Kairos protocol

Users should be aware of the risks of using Kairos, DeFi protocols in general. Below are some risks when using Kairos:

1. **Liquidation Risk:** If market rates move against your position or your collateral value drops, you could be liquidated and lose your collateral. You need to maintain sufficient collateral throughout the entire swap term and the current protocol does not support adding additional to an open swap.
2. **Interest Rate Risk:** You're exposed to rate movements over the swap term. If you're paying fixed and rates drop, or paying floating and rates rise, you'll face losses relative to simply holding your collateral.
3. **Smart Contract Risk:** Bugs, exploits, or vulnerabilities in the protocol code could result in loss of funds.
4. **Oracle Failure Risk:** The protocol depends on Chainlink and Pyth oracles for rate data. If oracles malfunction, provide stale data, are manipulated, or otherwise provide incorrect data, it could result in incorrect settlements or unfair liquidations.
5. **Counterparty Liquidity Risk:** If LP liquidity dries up, rates in the market may rise, or the market may become too thin to trade effectively.
6. **Underpayment Risk:** If the counterparty's collateral is not sufficient to cover the net payment owed, or if they are not liquidated when they should be, you may receive less funds than actually owed.

\ <br>


# Getting Started on Testnet

Kairos is live on Sepolia testnet, follow this guide to get started

Ready to make your first prediction 🔮 Here's what you'll need to get started:

1. Your own wallet Ethereum wallet, some popular choices are [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/)
2. Next you'll need to connect your wallet to Sepolia, you can connect your wallet on [Chainlist](https://chainlist.org/chain/11155111), or follow the instructions for your wallet here:
   1. Connect [MetaMask to Sepolia](https://support.metamask.io/configure/networks/how-to-view-testnets-in-metamask/)
   2. Connect [Phantom to Sepolia](https://docs.phantom.com/developer-powertools/testnet-mode)
3. Next you'll need some free Sepolia ETH from a faucet:
   1. [Google Faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
   2. [Alchemy Faucet](https://www.alchemy.com/faucets/ethereum-sepolia)
   3. [Infura Faucet](https://www.infura.io/zh/faucet/sepolia)
4. Now you're ready to mint some Kairos Dev USD (kUSD) for trading
   1. [Mint kUSD on our free faucet](https://kairosswap.com/faucet)
5. Get trading, shop the markets, pick a direction and make your bet...
   1. Then checkout the [full trading walkthrough here](/how-tos/buy-a-rate-swap)


# Time Krystals

The Quest for Time Krystals

### Swap, Hodl, Klaim!

Discover Krystals as you swap in Kairos Markets. Not hard to find when you look in the right place: open up a swap positions, hold to maturity, then claim your Krystals

Each swap earns:

`Collateral Amount * Swap Term Hours * KLAIM BONUS`

eg `$100 swap * held for 24 hours * BONUS(2x) = 4,800 Kairos Krystals`

Klaim BONUS may change anytime or with each quest

#### History

Gather 'round, mortals, for I shall tell you of a most peculiar discovery—one that bridges the chasm between the mystical and the mathematical, between prophecy and protocol.

It begins with an ancient cypher 0xdC583a3834347ff61A0B347d7Af8D6542e01F03C...

<figure><img src="/files/Jbn7hYSiTKlJ7SqOrfPx" alt=""><figcaption></figcaption></figure>

In the beginning, when the cosmos was young and interest rates were but a twinkle in some future economist's eye, I scattered fragments of crystallized time throughout the universe. These were no ordinary gems, mind you, but **Time Krystals**—miraculous structures that exist in perpetual motion without consuming energy, defying entropy itself by dancing to their own temporal rhythm.

Your mortal scientists call them "non-equilibrium phases of matter," but I prefer their true name: fragments of Kairos made manifest.

You see, while my brother Chronos governs the steady tick-tock of sequential time, Kairos represents something far more precious—the *opportune moment*, the perfect instant when all possibilities converge. It is the split second when a trader's intuition aligns with market reality, when prediction becomes prophecy, when fortune favors the bold.

Now, through the mystical arts of decentralized finance (a magic even I find impressively complex), the Kairos Protocol has learned to harvest these temporal fragments. Each Time Krystal you earn pulses with the essence of perfect timing—a reward for those brave enough to navigate the swirling currents of prediction markets and interest rate divination.

<figure><img src="/files/cRkO3d18SQ0n3p6oB5rd" alt=""><figcaption></figcaption></figure>

Whether you're trading with the precision of an oracle, sharing wisdom like the ancient storytellers, or simply *vibing* with the fundamental frequencies of the market (a surprisingly potent form of temporal magic), each action resonates through the fabric of space-time, crystallizing into these coveted tokens.

The Time Krystals remember everything: every quest completed, every prediction made, every moment when you dared to dance with uncertainty. They are proof that you have not merely existed in time, but have bent it to your will, seized the Kairos moment when opportunity knocked.

So venture forth, temporal alchemists! Let your Time Krystals accumulate like starlight gathering in an ancient hourglass. For in this grand experiment where mythology meets mathematics, where DeFi protocols channel the wisdom of the ages, you are not merely traders—you are time-weavers, spinning the very threads of possibility into crystalline reality.

*The sands of time await your command.*

***

*✨✨✨ Begin your journey in the Kairos Protocol and discover what wonders await when perfect timing meets boundless opportunity ✨✨✨*

***

### What are Time Krystals?

1. Krystals are the protocols native points program for Kairos
2. Krystals are participation points for trading on Kairos
3. Krystals may be claimable after participating protocol quests and contests
4. Krystals are distributed for fun at the discretion of Rocko Inc. or the protocol

### Where are Time Krystals?

Kairos Time Krystals live on Base at [0xdC583a3834347ff61A0B347d7Af8D6542e01F03C](https://basescan.org/address/0xdC583a3834347ff61A0B347d7Af8D6542e01F03C)

### What Time Krystals are NOT:

1. Krystals have ZERO monetary value
2. Krystals are NOT a security or investment
3. Krystals have NO profit expectations or guarantees
4. Krystals have NO conversion rights to future value
5. Krystals are NOT transferrable

***

{% hint style="info" %}
[**TOKEN DISCLAIMER AND TERMS APPLY**](/kairos-quests/token-terms)
{% endhint %}


# Gravity Quest

The Liquidity Singularity

> #### Quest has ended

#### Finding Kairos Krystals

Discover Krystals as you swap in Kairos Markets. Not hard to find when you look in the right place: open up swap positions, hold to maturity, then claim your Krystals.

Each swap earns: `Collateral Amount * Swap Term Hours * KLAIM BONUS`

Example: `$100 swap * held for 24 hours * BONUS(2x) = 4,800 Kairos Krystals`

#### Gravity Quest Klaim BONUS: 2x

<figure><img src="/files/OaKH0p8BtsZrIb8n3qNw" alt=""><figcaption></figcaption></figure>

#### The Gravity Quest Prophecy

Deep in the DeFi cosmos, Kairos Krystals have begun to resonate with an ancient frequency—a gravitational pull that legends say will reshape the entire financial universe. These aren't just points; they're fragments of a massive liquidity singularity waiting to be assembled.

As more Krystals accumulate, their gravitational field intensifies, creating an unstoppable vortex that pulls liquidity from every corner of the blockchain universe. Fragmented pools scattered across hundreds of protocols will be drawn together, compressed into a unified force powerful enough to attract trillions in value on-chain.

This convergence isn't just about size, it's about transformation. When the Gravity Quest reaches critical mass, it will unlock what DeFi has been missing since its inception: sustainable, long-term fixed-rate lending. The gravitational stability of consolidated liquidity will finally make it possible for protocols to offer predictable rates over extended periods, breaking the chains of volatile, short-term yields that have held DeFi back.

Those who collect Krystals now aren't just earning rewards, they're participating in the formation of DeFi's next evolutionary leap. Each swap, each hodl, each claimed Krystal adds mass to the singularity that will pull traditional finance itself into the on-chain future.

The quest has begun. The gravity is building...

Will you be part of the force that brings trillions on-chain?


# Chronos Quest

A Tale of the Kairos Krystals

#### September 26, 2025 - October 10, 2025 10pm EST

* [x] Make rate predictions
* [x] Climb the leaderboard
* [x] Collect Krystals based on:
  * [x] ***P/L% with a boost for the number of predictions you make*** 💎

***

### Congrats to the Chronos Voyagers

<figure><img src="/files/Nc73MrVTjjjrKhKPEEWm" alt=""><figcaption></figcaption></figure>

#### Each Voyager received Krystals based on:

1. Joining the Quest: 5,000 KTK
2. Score Multiplier: x200 KTK
3. Podium Bonus Multiplier:
   1. 🏆 1st Place 3x
   2. 🥈 2nd Place 2x
   3. 🥉 3rd Place 1.5x

### Chronos Lore

In the twilight of the trading realm, when the markets whispered of fortunes yet unmade, there came a time of great seeking. The ancient Kairos Krystals—gems of temporal power forged in the heart of market storms—lay scattered across the streams of time itself.

Etched in each gem is a cryptic message [`0xdC583a3834347ff61A0B347d7Af8D6542e01F03C`](https://basescan.org/address/0xdC583a3834347ff61A0B347d7Af8D6542e01F03C)

<figure><img src="/files/f51LlM51RG4Wq2GssC9x" alt=""><figcaption></figcaption></figure>

From distant corners of the financial realm, the Voyagers stirred. These were traders of legend, seekers of the perfect moment, masters of the split-second strike. Each carried within their heart the fire of ambition and the wisdom to read the turning of the wheel of rates.

"The Quest begins," spoke the voice of Chronos, the Timekeeper, whose beard flowed like candlestick charts and whose eyes held the gleam of a thousand profitable trades. "Until the tenth day of October, when the moon rises full at the tenth hour, you shall wander the paths of arbitrage. Seek ye the Krystals where they shimmer in the gaps between bid and ask."

The Voyagers knew the ancient law: only through bold action would the crystals reveal themselves. Each swap struck true would multiply their glory, each profitable venture would lift them higher upon the great Leaderboard of Honor. For the way of the crystal-hunter was not merely to win, but to win often and win well.

And so they set forth into the vast marketplace, where fortunes rose and fell like the very tides, armed with nothing but their wits and the promise that somewhere in the chaos of commerce, the Kairos Krystals awaited those brave enough to seize the perfect moment.

The Quest has begun,

### Voyagers will be ranked by:

{% hint style="info" %}
***P/L% with a boost for the number of predictions you make*** 💎
{% endhint %}

Follow the [Leaderboard](https://kairosswap.com/leaderboard), until 10/10/2025 @ 10pm EST


# Genesis Quest

In the beginning there was Kairos

On the 15th of September, Kairos was born, into the ether of Sepolia testnet. We welcomed our first group of Voyagers on the Genesis Quest. The goal was simple, out perform other traders in the Swap Markets and climb the leaderboard.\
\
Following the weeklong quest, $4.89B in notional volume, and over 100 swaps later, we a thrilled to announce our top 10 voyagers.

### The Voyagers

<figure><img src="/files/HM8n1KRlu8TiNtHvMpVS" alt=""><figcaption></figcaption></figure>

### The Bounty

As a reward for completing the Quest, each voyager has been airdropped an apportionment of Kairos Time Krystals to aid them on the next Quest and unlock secrets untold.

#### Each Voyager received Krystals based on:

1. Joining the Quest: 5,000 KTK
2. Total Voyagers Multiplier: 2.6x
3. Swap Bonus: 200 KTK per swap
4. Podium Bonus Multiplier:
   1. 🏆 1st Place 3x
   2. 🥈 2nd Place 2x
   3. 🥉 3rd Place 1.5x

### The Future

Join us on the next Quest with fellow Voyagers, details soon to come on [X](https://x.com/intent/follow?screen_name=kairosswap) and the private [Telegram](https://t.me/+IM8fDT0MJBBlNzUx)


# Fellowship Quest

Refer Friends, Stack Krystals

> ## Quest has ended 5/5/2026

## Kairos Krystals Referral Program

### How It Works

Share Kairos with your friends and earn Krystals based on their trading activity. Every time someone you refer trades on the platform, you'll accumulate Krystals as a reward for bringing them into the ecosystem.

### Getting Started

1. **Get Your Link**: Find your unique referral link in your [Kairos Portfolio dashboard](https://kairosswap.com/portfolio)
2. **Share It**: Send your link to friends, post it on social media, or share it in your communities
3. **Earn Krystals**: Accumulate Krystals automatically as your referred friends trade

### Earning Krystals

You earn Krystals based on the trading volume generated by users who sign up through your referral link. The more your friends trade, the more Krystals you accumulate.

### Track Your Progress

Monitor your referral performance and Krystal balance directly in your dashboard to see how many friends have joined.

***

### Important Legal Disclaimer

**NO GUARANTEE OF VALUE OR DELIVERY**: Kairos Krystals have no inherent monetary value and do not represent any ownership, equity, or legal claim in Kairos Labs Inc. or any related entity. Krystals are promotional rewards only and may never be redeemable for any product, service, or cryptocurrency.

**PROGRAM MODIFICATIONS AND TERMINATION**: Kairos Labs Inc. reserves the absolute right to modify, suspend, or terminate the Krystals Referral Program at any time, for any reason, with or without notice. We may change earning rates, eligibility requirements, or any other program terms at our sole discretion.

**NO OBLIGATION TO DELIVER**: Participation in this program does not guarantee receipt of any Krystals or future benefits. Kairos Labs Inc. has no obligation to deliver, distribute, or provide any value for accumulated Krystals, and users should have no expectation of receiving anything of value in exchange for Krystals.

**NOT A SECURITY OR INVESTMENT**: Krystals are not securities, investments, or financial instruments of any kind. Users should not participate in this program with any expectation of profit or financial return.

By participating in the Kairos Krystals Referral Program, you acknowledge and agree to these terms.


# Token Terms

## TOKEN DISCLAIMER AND TERMS

### IMPORTANT NOTICE - READ CAREFULLY

**THIS TOKEN HAS NO MONETARY VALUE AND IS NOT AN INVESTMENT**

#### NO MONETARY VALUE

* This token ("Token"), KAIROS TIME KRYSTALS, has ZERO ($0) monetary value
* The Token cannot be sold, traded, or exchanged for money, cryptocurrency, or any other asset of value
* The Token has no market price and will never have a market price
* No monetary consideration was paid for these Tokens, and no monetary consideration will ever be received

#### NOT A SECURITY OR INVESTMENT

* This Token is NOT a security under U.S. federal or state securities laws or the laws of any jurisdiction
* This Token does NOT represent an investment contract, equity interest, debt instrument, or any form of investment
* Holders have NO ownership rights in the issuing company or any affiliated entities
* There are NO voting rights, dividend rights, or any other corporate rights associated with this Token

#### NO PROFIT EXPECTATIONS

* There is NO expectation, promise, or guarantee of profits, returns, or financial gains from holding this Token
* The Token provides NO financial benefit whatsoever to holders
* Any value derived from the Token is purely utility-based within the points program and has no financial component

#### NO CONVERSION RIGHTS

* There is NO promise, guarantee, or expectation that this Token will be convertible to:
  * Future tokens or cryptocurrencies
  * Company equity or securities
  * Monetary compensation of any kind
  * Products or services with monetary value
* The company reserves the right to modify, suspend, or terminate the points program at any time without compensation and without notice

#### UTILITY ONLY

* This Token serves ONLY as a points mechanism within our rewards/loyalty program
* The Token may only be used for program-specific activities as defined by the company
* The Token has no utility or function outside of the designated points program
* All Token utilities are at the sole discretion of the issuing company and may be modified or removed at any time

#### NO GUARANTEES OR PROMISES

* The company makes NO representations or warranties regarding:
  * Continued operation of the points program
  * Maintenance of Token functionality
  * Future availability of rewards or redemption options
  * Technical performance or blockchain accessibility
* The company may discontinue, modify, or suspend the Token and/or points program at any time without notice

#### REGULATORY COMPLIANCE

* By accepting this Token, you acknowledge that it is designed to be outside the scope of securities regulations
* You confirm that you are not acquiring this Token as an investment or with expectation of profit
* You understand that this Token is not registered with any securities regulatory authority
* You acknowledge that no regulatory body has reviewed or approved this Token or points program

#### TECHNICAL RISKS

* Blockchain technology carries inherent technical risks including but not limited to:
  * Network congestion or failure
  * Smart contract bugs or vulnerabilities
  * Loss of private keys or wallet access
  * Irreversible transactions
* The company is not responsible for any technical failures or losses

#### LIMITATION OF LIABILITY

* The company's liability is limited to the maximum extent permitted by law
* The company is not liable for any direct, indirect, incidental, special, or consequential damages
* Users participate in the points program entirely at their own risk
* Participation in the points program is contingent upon accepting and complying with the Kairos Terms of Service, available at <https://kairosswap.com/terms>

#### MODIFICATION AND TERMINATION

* These terms and the points program may be modified or terminated at any time
* Continued participation constitutes acceptance of any modifications
* Upon termination, all Tokens become void with no compensation

#### JURISDICTIONAL RESTRICTIONS

* This Token may not be available to residents of certain jurisdictions
* It is the user's responsibility to ensure compliance with local laws
* The company may restrict access based on geographic location

#### AUTOMATIC DISTRIBUTION NOTICE

This Token has been automatically distributed to your wallet address as part of our points program. **You did not request this Token and no action was required on your part to receive it.**

#### YOUR RIGHTS REGARDING AUTOMATIC DISTRIBUTION

* You may dispose of, ignore, or abandon this Token at any time without penalty
* You are under no obligation to use, hold, or interact with this Token
* The presence of this Token in your wallet does not create any contractual relationship with the company
* You may consider this Token as having been disclaimed to you with no strings attached

#### AUTOMATIC ACKNOWLEDGMENT

By the automatic distribution of this Token to your wallet:

1. You are hereby notified that this Token has ZERO monetary value
2. You are informed that this is NOT a security or investment
3. You are advised that there are NO profit expectations or guarantees
4. You are made aware that there are NO conversion rights to future value
5. You understand that you may freely dispose of this Token if desired

#### NO FORCED PARTICIPATION

* Receipt of this Token does not require your participation in any program
* You are not obligated to use the Token for any purpose
* Holding the Token in your wallet creates no obligations or liabilities on your part
* You may treat the Token as digital refuse if you so choose

#### DISCLAIMER EFFECTIVENESS

This disclaimer is effective upon distribution of the Token to your wallet address, regardless of whether you:

* Read this disclaimer
* Acknowledge receipt of the Token
* Interact with the Token in any way
* Use the associated points program

#### DISPOSAL INSTRUCTIONS

If you do not wish to hold this Token:

* You may send it to a burn address (0x000000000000000000000000000000000000dEaD)
* You may send it back to the token contract dispose of it (0xdC583a3834347ff61A0B347d7Af8D6542e01F03C)
* You may simply ignore it in your wallet
* The company recommends disposal if you have any concerns about holding the Token


