Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions artifacts/stablecoin-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@
},
{
"name": "source_id",
"type": "string"
"type": "account_id"
},
{
"name": "confidence_interval",
Expand All @@ -326,18 +326,6 @@
}
],
"types": [
{
"name": "MetadataStandard",
"kind": "enum",
"variants": [
{
"name": "Simple"
},
{
"name": "Expanded"
}
]
},
{
"name": "ObservationEntry",
"kind": "struct",
Expand All @@ -351,6 +339,18 @@
"type": "i64"
}
]
},
{
"name": "MetadataStandard",
"kind": "enum",
"variants": [
{
"name": "Simple"
},
{
"name": "Expanded"
}
]
}
],
"instruction_type": "stablecoin_core::Instruction"
Expand Down
43 changes: 42 additions & 1 deletion artifacts/twap_oracle-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,47 @@
"type": "u64"
}
]
},
{
"name": "create_oracle_price_account",
"accounts": [
{
"name": "oracle_price_account",
"writable": false,
"signer": false,
"init": false
},
{
"name": "price_source",
"writable": false,
"signer": false,
"init": false
},
{
"name": "clock",
"writable": false,
"signer": false,
"init": false
}
],
"args": [
{
"name": "base_asset",
"type": "account_id"
},
{
"name": "quote_asset",
"type": "account_id"
},
{
"name": "initial_price",
"type": "u128"
},
{
"name": "window_duration",
"type": "u64"
}
]
}
],
"accounts": [
Expand Down Expand Up @@ -92,7 +133,7 @@
},
{
"name": "source_id",
"type": "string"
"type": "account_id"
},
{
"name": "confidence_interval",
Expand Down
82 changes: 78 additions & 4 deletions programs/twap_oracle/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use spel_framework_macros::account_type;
/// TWAP Oracle Program Instruction.
#[derive(Debug, Serialize, Deserialize)]
pub enum Instruction {
/// Creates and initialises a price feed account for a price source and time window.
/// Creates and initialises a price observations account for a price source and time window.
///
/// Required accounts (in order):
/// 1. Price feed account — uninitialized PDA derived from
/// 1. Price observations account — uninitialized PDA derived from
/// `compute_price_observations_pda(self_program_id, price_source.account_id,
/// window_duration)`.
/// 2. Price source account — the account whose ID acts as the feed identifier (e.g. an AMM
Expand All @@ -29,6 +29,37 @@ pub enum Instruction {
/// It is also part of the PDA seed, so each window gets a distinct account.
window_duration: u64,
},
/// Creates and initialises a canonical [`OraclePriceAccount`] for a price source and time
/// window.
///
/// The account is initialised with the non-zero `initial_price` and the timestamp read from
/// the canonical 1-block clock. A zero price or zero timestamp is the "no valid price"
/// sentinel consumers reject, so an account is never created in that state.
///
/// Required accounts (in order):
/// 1. Oracle price account — uninitialized PDA derived from
/// `compute_oracle_price_account_pda(self_program_id, price_source.account_id,
/// window_duration)`.
/// 2. Price source account — must be passed with `is_authorized = true` to prove the caller
/// controls it. Its ID ties this price account to the same source as the corresponding
/// [`PriceObservations`] account for the same window.
/// 3. Clock account — the canonical 1-block LEZ clock; supplies the initial timestamp.
CreateOraclePriceAccount {
/// Canonical identifier of the base asset being priced.
base_asset: AccountId,
/// Canonical identifier of the quote asset that denominates `price`.
quote_asset: AccountId,
/// Initial price as a `Q64.64` fixed-point value (real price = `initial_price / 2^64`).
///
/// Must be non-zero; the caller is responsible for supplying a correctly-scaled
/// fixed-point value rather than a plain integer.
initial_price: u128,
/// Duration of the TWAP window this price account serves, in milliseconds.
///
/// Part of the PDA seed, so each `(price_source, window)` pair maps to a distinct
/// oracle price account.
window_duration: u64,
},
}

// ──────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -156,6 +187,48 @@ pub fn compute_price_observations_pda_seed(
)
}

const ORACLE_PRICE_ACCOUNT_PDA_SEED: [u8; 32] = [3; 32];

/// Derives the [`AccountId`] for a price source's [`OraclePriceAccount`] PDA.
///
/// The `window_duration` is included in the seed so that each `(price_source, window)` pair
/// maps to a distinct account, mirroring the [`PriceObservations`] PDA derivation.
#[must_use]
pub fn compute_oracle_price_account_pda(
oracle_program_id: ProgramId,
price_source_id: AccountId,
window_duration: u64,
) -> AccountId {
AccountId::for_public_pda(
&oracle_program_id,
&compute_oracle_price_account_pda_seed(price_source_id, window_duration),
)
}

/// Derives the [`PdaSeed`] for a price source's [`OraclePriceAccount`].
///
/// Hash input: `price_source_id (32 bytes) || window_duration_le (8 bytes) ||
/// ORACLE_PRICE_ACCOUNT_PDA_SEED (32 bytes)`.
#[must_use]
pub fn compute_oracle_price_account_pda_seed(
price_source_id: AccountId,
window_duration: u64,
) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256};
Comment thread
0x-r4bbit marked this conversation as resolved.

let mut bytes = [0u8; 72];
bytes[..32].copy_from_slice(&price_source_id.to_bytes());
bytes[32..40].copy_from_slice(&window_duration.to_le_bytes());
bytes[40..72].copy_from_slice(&ORACLE_PRICE_ACCOUNT_PDA_SEED);

PdaSeed::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}

/// Canonical oracle price account consumed by LEZ programs.
///
/// Oracle producers own how this account is written; consumers only read and
Expand All @@ -175,8 +248,9 @@ pub struct OraclePriceAccount {
/// Price observation timestamp. Consumers choose the time unit by matching this with
/// `max_age`.
pub timestamp: u64,
/// Identifier of the source that populated this account, such as a TWAP or external adaptor.
pub source_id: String,
/// Identifier of the source account that populated this account, such as a TWAP program or
/// external adaptor.
pub source_id: AccountId,
/// Source-provided confidence interval, or zero when the source does not provide one.
pub confidence_interval: u128,
}
Expand Down
40 changes: 39 additions & 1 deletion programs/twap_oracle/methods/guest/src/bin/twap_oracle.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![cfg_attr(not(test), no_main)]

use nssa_core::account::AccountWithMetadata;
use nssa_core::account::{AccountId, AccountWithMetadata};
use spel_framework::context::ProgramContext;
use spel_framework::prelude::*;

Expand Down Expand Up @@ -39,4 +39,42 @@ mod twap_oracle {
);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}

/// Creates and initialises a canonical oracle price account for a price source and time
/// window.
///
/// Expected accounts:
/// 1. `oracle_price_account` — uninitialized PDA owned by this oracle program.
/// 2. `price_source` — account the caller controls (proven via `is_authorized = true`);
/// its ID ties this price account to the same source as the corresponding
/// `PriceObservations` account for the same window.
/// 3. `clock` — canonical 1-block LEZ clock account; supplies the initial timestamp.
#[expect(
clippy::too_many_arguments,
reason = "instruction interface requires explicit price account, source, and clock accounts alongside the asset pair, initial price, and window"
)]
#[instruction]
pub fn create_oracle_price_account(
ctx: ProgramContext,
oracle_price_account: AccountWithMetadata,
price_source: AccountWithMetadata,
clock: AccountWithMetadata,
base_asset: AccountId,
quote_asset: AccountId,
initial_price: u128,
window_duration: u64,
) -> SpelResult {
let post_states =
twap_oracle_program::create_oracle_price_account::create_oracle_price_account(
oracle_price_account,
price_source,
clock,
base_asset,
quote_asset,
initial_price,
window_duration,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
}
}
Loading
Loading