feat(twap-oracle): implement CreateCurrentTickAccount and UpdateCurrentTick

Add CurrentTickAccount — an oracle-owned PDA (one per price source) that holds
the latest raw tick written by the price source and a timestamp. The price source
calls UpdateCurrentTick after each price-changing operation; anyone can then call
RecordTick (upcoming) to advance the PriceObservations accumulator without
requiring the price source to be present. PDA is derived from price_source_id
only (no window) since a single current tick serves all time windows.

Add price_to_tick(price: u128) -> i32 to twap_oracle_core: isqrt(price << 128)
-> sqrtPriceX96 -> get_tick_at_sqrt_ratio. The sqrtPriceX96 is clamped to
>= MIN_SQRT_RATIO so a zero/dust price maps to MIN_TICK rather than erroring.

Add a pure-integer integer_sqrt(U256) (bit-by-bit, no floating point): ruint's
root is gated behind its std feature and seeds with f64, neither available in
the guest. Uses wrapping_shr for the digit loop (checked_shr rejects the
intended lossy shifts).

Pull in uniswap_v3_math (for get_tick_at_sqrt_ratio) and alloy-primitives
(U256), with ruint pinned to =1.17.0 — 1.18 raised its MSRV to rustc 1.90,
above the risc0 guest toolchain's 1.88.
This commit is contained in:
r4bbit
2026-06-16 16:21:51 +02:00
parent b0ac30039b
commit 0ee58b8355
11 changed files with 3292 additions and 156 deletions
File diff suppressed because it is too large Load Diff
@@ -77,4 +77,57 @@ mod twap_oracle {
);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}
/// Creates and initialises a current tick account for a price source.
///
/// Expected accounts:
/// 1. `current_tick_account` — uninitialized PDA owned by this oracle program.
/// 2. `price_source` — account the caller controls (proven via `is_authorized = true`).
/// 3. `clock` — read-only LEZ clock account.
///
/// `initial_price` is a `Q64.64` spot price; the oracle converts it to a tick.
#[instruction]
pub fn create_current_tick_account(
ctx: ProgramContext,
current_tick_account: AccountWithMetadata,
price_source: AccountWithMetadata,
clock: AccountWithMetadata,
initial_price: u128,
) -> SpelResult {
let post_states =
twap_oracle_program::create_current_tick_account::create_current_tick_account(
current_tick_account,
price_source,
clock,
initial_price,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}
/// Updates the tick stored in an existing current tick account.
///
/// Expected accounts:
/// 1. `current_tick_account` — initialized PDA owned by this oracle program.
/// 2. `price_source` — account the caller controls (proven via `is_authorized = true`).
/// 3. `clock` — read-only LEZ clock account.
///
/// `price` is a `Q64.64` spot price; the oracle converts it to a tick.
#[instruction]
pub fn update_current_tick(
ctx: ProgramContext,
current_tick_account: AccountWithMetadata,
price_source: AccountWithMetadata,
clock: AccountWithMetadata,
price: u128,
) -> SpelResult {
let post_states = twap_oracle_program::update_current_tick::update_current_tick(
current_tick_account,
price_source,
clock,
price,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}
}