時continuous-time
the only difference between an hourly funding rate and a per-block funding rate is sampling frequency. continuous-time finance is what happens when the sampling cost approaches zero.
tick vs settlement
These two terms get conflated. They are not the same thing.
- tick— a unit of time during which the protocol's state can change. On solana that is 1 slot ≈ 400ms. The smallest dt the chain can attest to without leaving solana.
- settlement — the moment a value transfer becomes a fact on the ledger. Discrete-time defi settles on a cron (1h, 1d). Continuous-time defi settles every tick.
Hourly funding settlement on a chain that ticks every 400ms is throwing away 9000 ticks of resolution per cycle. That lost resolution shows up as predictable spike behavior at every cron boundary, which is what sophisticated traders eat.
anatomy of a discrete spike
Consider a perp dex with hourly funding. Funding accrues mathematically at the target rate r, but the chain does not move usdc until t = 1h. Then in one block, the entire hour's worth of usdc transfers from longs to shorts. That single block is the spike.
1balance ▲2 │3 +N ┤ ╭───── jump at t=1h4 │ │5 +0 ┤────────────────────────────────────────────────┤6 │7 └────────────────────────────────────────────────┴─→ t (sec)8 0 3600The continuous-time version of the same yield is a smooth ramp:
1balance ▲2 │3 +N ┤ ╭────╮4 │ ╭────╯5 +N/2 ┤ ╭────╯6 │ ╭─────╯7 +0 ┤──────────────────╮────╮────╯8 │9 └────────────────────────────────────────────────┴─→ t (sec)10 0 3600Same total accrual at t = 3600. Different shape. The continuous ramp has no spike to front-run.
how kagura attests a tick
kagura-core stores the last unix_timestamp_ms per registered protocol. On record_tick it computes now - last_tick_ms, updates the stored timestamp, and returns the delta to the caller.
1pub fn handler(ctx: Context<RecordTick>) -> Result<TickResult> {2 let p = &mut ctx.accounts.protocol;3 require!(!p.paused, KaguraError::ProtocolPaused);4 5 let now = now_unix_ms()?;6 require!(now >= p.last_tick_unix_ms, KaguraError::ClockRegression);7 8 let elapsed = now.saturating_sub(p.last_tick_unix_ms);9 let elapsed_ms = if elapsed > u32::MAX as i64 { u32::MAX } else { elapsed as u32 };10 11 p.last_tick_unix_ms = now;12 p.total_ticks = p.total_ticks.checked_add(1).ok_or(...)?;13 14 emit!(TickEmitted { protocol: p.key(), tick_number: p.total_ticks, now_unix_ms: now, elapsed_ms });15 Ok(TickResult { now_unix_ms: now, elapsed_ms, tick_number: p.total_ticks })16}permissionless ticking
The current default is that the registered protocol's authority callsrecord_tick. That means the operator runs a ticker bot. We document that pattern in operations/ticker-bot.
A future version of kagura-core will drop the has_one on the authority for record_tick, making the call permissionless: anyone with sol can advance the protocol's clock. This is safe by design because the per-tick accrual is a function of wall-clock dt, so total accrual over real time is invariant under tick frequency. See threat model for the formal argument.
why not just rely on solana's clock
Solana exposes Clock::get()?.unix_timestamp. Why introduce a registry?
- shared state. Many protocols want the same tick-history view. kagura-core is the canonical place for a tick number and last-tick timestamp.
- composable events.
TickEmittedis a single event signature any indexer or off-chain service can subscribe to. - permissionless ticking (v2). Today the registered protocol's authority signs each tick. v2 drops that constraint so anyone with sol can advance the protocol's clock — operator-resilient by default.