← Back to Blog

How Kairos Secures Its Oracles

Vince DePalma
Vince DePalma𝕏in@
August 4, 2026
SecurityOraclesProtocol Design

Every derivative is a bet on a number. In a rate swap, that number is not a price you can look up on an exchange — it is an interest rate, which means it has to be measured, modeled, and delivered on-chain before anything can settle. That makes the oracle layer the most security-sensitive part of the protocol. A lending market that misreads a price gets a bad liquidation. A swap protocol that misreads a rate mispriced every position it wrote that day.

Kairos uses three distinct oracle types, and they do genuinely different jobs:

  • Reference rate — the floating leg. The index the swap settles against.
  • Base rate — the fixed leg. The rate a buyer locks in for a given term.
  • Risk premium — the spread added on top, compensating liquidity providers for warehousing the other side.

They have different trust models, different failure modes, and different defenses:

Reference rateBase rateRisk premium
PricesThe floating legThe fixed legThe spread over either leg
Source of truthThird-party protocol stateDerived from that same stateKairos pricing model
Trust modelTrust the source protocolTrustless — anyone can verify or refresh itTrust N-of-M signers, bounded on-chain
Update pathPermissionless index readPermissionless poke()Signed post, every 15 min
Worst realistic failureSource is exploited or diesKeeper stops pokingSigner key compromised
What that failure doesMarket winds down, LPs exitQuotes go invalid; open swaps settle normallySpread moves within immutable bounds, slowly

What they share is a design principle we apply without exception: an oracle that does not know the answer must say so, and the protocol must decline to trade rather than trade on a guess. Every read in the system returns a value and a validity flag, and an invalid read blocks new swaps rather than substituting a stale or fabricated number.

Reference Rate: measuring what actually happened

The reference rate is the floating leg of the swap. If you buy the fixed side of a 90-day USDC market on Aave, the reference rate oracle is what determines the variable payment you receive back. It is the ground truth of the contract, so its single most important property is that it is not our number. It is read from a third-party protocol we do not control, through an adapter thin enough to audit in one sitting.

The primary implementations are cumulative index adapters. Aave V3's is about forty lines: it calls getReserveNormalizedVariableDebt, which returns the protocol's variable borrow index projected to the current block in RAY (27 decimals), and hands it back. There are equivalents for Compound and for Morpho Blue, on both the borrow and supply sides. For rates that have no native on-chain accumulator — SOFR, for instance — we use spot-rate adapters over Chainlink and Pyth feeds instead.

The reason cumulative indices are the preferred shape is that they make manipulation structurally uninteresting. Settlement does not read a rate; it reads the ratio of the index at entry to the index at expiry:

floatingRate = f(indexAtExpiry / indexAtEntry)

That ratio is path-independent. An attacker who spikes utilization on the source protocol for a single block moves the instantaneous rate but barely moves the accumulated index, and moving it at one endpoint requires holding the position for economically meaningful time rather than one flash-loaned transaction. For markets that must use a spot feed, the protocol integrates rate over time into a synthetic index itself, in either simple or continuously-compounded form — the compounded convention is deliberately update-frequency-independent, so nobody can manufacture accrual by polling the oracle faster.

The defensive work sits in the seams:

Adapters never revert; they return (0, false). A protocol upgrade that breaks a view call surfaces as an invalid read, not an opaque bubbled revert that could brick unrelated code paths.

Indices are monotonically clamped, with a high-water mark. Cumulative indices should never go down. Some of them technically can, by sub-wei rounding amounts — Morpho's share-ratio math is not strictly monotone. Tiny decreases are clamped up and logged; a material decrease is a different animal entirely (bad debt socialization, for example) and is never masked.

Morpho adapters fail closed below exposure floors. A Morpho market's share ratio is only a trustworthy index while real exposure backs it. Fully repay a market and the ratio snaps back to its virtual-offset baseline — a reading that, spliced into recorded history, would look like an enormous index move. Below configured floors on assets and shares, the adapter reports invalid, making a full unwind a hard epoch boundary instead of a phantom rate.

Pyth reads pass a confidence gate. Every Pyth print carries a give-or-take band, and we reject prints whose band is too wide relative to the rate. That ratio breaks down near 0%, so the denominator is floored at a configured level — otherwise a sustained low-rate regime would reject every print and stall the market. Negative and zero rates are explicitly valid throughout the protocol; deep negatives are saturated at a floor rather than rejected, because rejecting them would block index updates for as long as the regime lasted.

Finally, a reference oracle cannot be swapped out behind a market's back. Index history is keyed by oracle address and records absolute levels, so repointing a market at a different accumulator would splice two unrelated histories together — an upward jump manufactures accrual for every open swap. Our deployment tooling refuses that topology outright. The tradeoff is that a genuinely dead reference oracle cannot be replaced, so we handle that case directly: after a grace period, settlement retries the oracle once with a guaranteed gas budget (so nobody can starve it into the fallback), and only if that fails does it settle on deterministic extrapolation of frozen history and terminate the market to new swaps. A dead oracle winds a market down; it never traps LP capital.

Base Rate: pricing what happens next

The base rate is the fixed leg — the rate a buyer locks in. Unlike the reference rate, it is inherently forward-looking, and there is no third-party contract to read it off. It has to be derived. That makes it the place where model risk lives, and the design goal is to make the model boring, bounded, and reproducible.

Every base rate oracle in the launch set is fully on-chain and permissionless — no signer, no off-chain pipeline, nothing to compromise. The rate is derived from the same public cumulative indices the reference oracles read, using a trailing time-weighted average rate, or TWAR, optionally floored by a source-protocol minimum such as Aave's zero-utilization intercept.

The mechanic is a ring buffer of eight (timestamp, index) observations, refreshed by an open poke() that anyone can call. A read needs to know where the index stood one window ago, and resolves that by bracketing the boundary now − window: it takes the youngest stored observation at or before that moment, the oldest at or after it, and interpolates linearly between the two. Bracketing rather than grabbing the single nearest observation is what keeps movement that happened after the boundary from smearing backward into the historical estimate. The trailing rate is then the continuously-compounded growth from that historical index to the live one:

r = ln(liveIndex / historicalIndex) · YEAR / window

Read plainly: what borrowers actually paid over the window, annualized. Averaging over a window rather than sampling an instant is what makes the number expensive to push. An instantaneous borrow rate is a function of utilization, and utilization moves for one block with borrowed capital; a window-averaged rate only moves if you hold utilization off its natural level for a real fraction of the window, paying interest the whole time. Window length is the dial — shorter tracks the market more closely and costs less to distort, longer does the reverse — and it is fixed at deployment, per market, so governance cannot retune it under an open position.

Each tenor then gets a compounding transform:

baseRate(T) = (exp(r · T / YEAR) − 1) · YEAR / T

That transform is not decoration. SwapCore charges the fixed leg linearly, as notional · rate · T / YEAR. Quoting the raw continuously-compounded rate would leave the fixed leg sitting below the compound growth the floating leg actually realizes at sub-year tenors — a structural, permanent leak from the LP pool to fixed buyers. The bump makes the linear charge reproduce the compound basis exactly.

The Morpho variant goes further, because Morpho's adaptive IRM publishes something Aave's does not: a rate-at-target the curve mean-reverts toward. Rather than extrapolating the trailing window flat, it blends the realized TWAR with that target using a closed-form mean-reversion weight, w(T) = (1 − exp(−λT)) / (λT), clamped into the band the IRM itself enforces. Short tenors lean on recent realized behavior, long tenors on the target. Both parameters — adjustment speed and curve steepness — are taken verbatim from Morpho's own constants. Nothing is fitted, so there is nothing to overfit.

The buffer carries its own invariants. A poke() either advances into the next slot — permitted only once a minimum spacing has elapsed — or refreshes the head in place within the same block; anything else reverts. The schedule is gated on the last advance, not on the head observation's timestamp, so a keeper spamming pokes cannot compress the buffer's span and evict the history a read depends on. Deployment enforces that the seven historical slots still span the full window even under minimum-spacing pokes, and the oracle ships with a seeded historical observation so it quotes validly from block zero rather than sitting dead while the buffer fills.

Several further guards are worth calling out because they encode attacks we specifically wanted closed:

Freshness is measured against the left anchor, not the newest observation. After a long keeper outage, a single poke() lays a fresh observation at the current block — but it cannot move the stale historical anchor. So one poke cannot revive a stale quote; recovery requires enough real observations to bracket the boundary tightly again. The tolerance is admin-tunable but hard-capped at deployment, so governance can tighten it as an emergency lever and can never disable it.

A material source rollback pauses quoting. The oracle keeps a running high-water of the source index. Sub-wei rounding drift gets clamped away, but a live index falling meaningfully below the high-water is a real economic event — bad debt, most likely — and precisely the case where a clamped historical index would quote a fixed leg the floating side cannot cover. Reads go invalid until the source recovers.

The fixed and floating base oracles of a market pair must share one observation ring. This came out of an audit. Because the un-bumped floating quote and the bumped fixed quote are two transforms of the same snapshot, compounded ≥ raw holds at every block by convexity. Two independent rings could invert that for a moment, and an equal-notional position on both sides of the pair would have drained both LP pools. Market creation now proves the shared ring at deploy time.

Risk Premium: pricing the LP's exposure

The reference rate says what happened; the base rate says what we expect. The risk premium says what the liquidity provider should be paid for being wrong. It is the compensation for warehousing the other side of a swap, and it is applied on top of whichever leg the LP is exposed to.

It is genuinely two-sided. For each market and tenor the service computes an upper (offer-side) and lower (bid-side) spread, and the contract composes:

fixed_offer = baseRate + upPremium
fixed_bid   = baseRate − loPremium

Note what is not in that posting: the base rate itself. It is derived on-chain at interaction time. The premium service never posts a level — only spreads — which structurally limits what a compromised signer could do to the price.

The service and its inputs are strictly public on-chain state read over standard JSON-RPCs. No off-chain APIs, no subgraphs, no database sits in the pricing path. Each network has a quorum of RPC candidates, including an archive-capable nodes. The historical samples the model needs are what require archive access; read volume is small and fixed per market per tick.

The model itself: it derives a small set of market-stress signals from the source pool's public state, smooths them to suppress transient noise, and normalizes them into a coordinate that is invariant to governance changes in the pool's rate-curve configuration. The core is an offline-calibrated lookup table indexed by market regime and tenor. Each cell encodes conservative tail statistics of historical forward rate movements, plus an add-on pricing the risk of a mid-swap governance change to the source protocol. At runtime the service looks up and interpolates that table, scales the result by the pool's live rate-curve sensitivities so governance changes reprice immediately, applies conservative structural priors and a minimum floor, and converts to APY.

Two properties of that design do most of the security work. First, calibration and quoting are strictly separated. The table is produced by a separate offline job from historical on-chain data and shipped as a versioned, content-hashed JSON artifact. The runtime path is a table lookup, an interpolation, and some scaling — deterministic, lightweight, no ML inference, no dependency on any model service. The same inputs always produce the same quotes, and every posted quote is traceable to the exact calibration artifact that produced it. Second, the tenor structure is conservative by construction. Tenors of 1, 30, 60, and 90 days are calibrated directly from history; 180 and 365 days are prior-priced off the 90-day anchor and are never quoted tighter than it. We do not pretend to have a year of forward distribution we did not measure.

Posting is deliberately reluctant. Each tick fetches live state, computes quotes, runs anomaly checks, then reads the currently stored on-chain premiums and submits a transaction only if a quote has drifted materially since the last post or a heartbeat interval has elapsed. Most ticks read, decide, and exit without transacting. Before submitting, the service pre-clamps its post to the on-chain oracle's own guard rails — minimum update interval, absolute bounds, per-post change cap — so it never fires a transaction the contract would reject. Writes are fail-closed behind an explicit enable flag that defaults to dry-run, and signing is done by an HSM-backed key service for both the payload the contract verifies and the transaction itself. The private key never leaves the HSM. The contract supports N-of-M signers; we currently operate with one, and the path to more is a governance action, not a redeploy.

On the contract side, both sides of every tenor are posted together under one signature and one nonce, which is what keeps the bid and offer quantiles coherent — you cannot land a favorable upper premium without its matching lower. Premiums are enforced non-negative at the source rather than only at the consumer, because the consumer reverts on a negative value and a single bad post would otherwise block every buy on that market until the next one landed.

The shared spine

The risk premium oracle is the one priced input that arrives by signature rather than by on-chain derivation, and it inherits a hardened base contract built for exactly that job. It is worth describing what that contract enforces, because it is where most of the adversarial thinking ended up — and because any future signed oracle inherits the same spine.

Posts are EIP-712 typed data with a global replay nonce, an expiry deadline, and signatures that must be unique and sorted by recovered address. Every payload carries a signer-attested observedAt — the time the data was actually observed — and the contract range-checks it against the current block: not in the future, not already older than the staleness limit. This matters more than it sounds. All the freshness and circuit-breaker logic is gated on observation time, not relay time. A relayer who sits on a payload cannot let the clock run until the per-post change cap relaxes and then land a large move as if it were fresh, and a near-stale payload landing at the cadence boundary cannot reset the cooldown and lock out a fresher corrective post while its own data expires.

Layered on that: immutable absolute rate bounds fixed at deployment, with governance-adjustable bounds that can only be tightened inside them; a maximum change per post; a minimum interval between posts; and a maximum age after which reads simply go invalid. Every one of those parameters — plus signer set, signature threshold, and tenor geometry — moves only through a three-day timelock, queued publicly before it can activate. Ownership renouncement is disabled outright, because renouncing while paused would strand every read as invalid with nobody able to unpause; retiring the emergency controls is itself a timelocked operation.

Reads fail closed on every axis. Paused, stale, no data posted, a stored value outside the currently active bounds, or a tenor beyond the longest posted bucket — all return invalid rather than a plausible-looking number. Tenor interpolation is done on unsigned magnitudes with validated bucket geometry, specifically so that no arithmetic path can turn bounded interpolation into unbounded extrapolation and quote a rate outside the envelope while still reporting itself valid. That one was an audit finding, and the fix was to make the property structural rather than checked.

Base-rate and premium slots additionally sit behind thin upgradable wrappers, giving three escalating levers: a three-day timelocked repoint to a replacement oracle, an instant emergency cutoff that forces every read invalid, and a timelocked renouncement that permanently freezes the wrapper. Candidate downstreams are checked for interface support, decimal consistency, and a live healthy read at both queue time and activation time — so an oracle that breaks during the timelock window cannot slip through.

The through-line

None of this makes the numbers right. Models are wrong sometimes; sources get exploited; keepers go down. What this architecture buys is that being wrong is bounded, visible, and reversible: bounded by immutable envelopes and per-post caps, visible because every parameter change is queued days in advance and every clamp emits an event, and reversible because governance can repoint or cut off any priced input without touching a single open position's settlement path.

The one thing we will not do is guess. If the data is not there, Kairos does not quote.

If you want to read the contracts, they are in the protocol repo under contracts/oracles. If you find something we missed, we would rather hear it from you than from an attacker — and we pay for it. Details are on our bug bounty program.

Kairos

Permissionless Interest Rate Swap Markets

© 2026 Kairos Labs, Inc. All rights reserved.