Skip to content
Merged
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
24 changes: 12 additions & 12 deletions artifacts/stablecoin-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,28 +327,28 @@
],
"types": [
{
"name": "ObservationEntry",
"kind": "struct",
"fields": [
"name": "MetadataStandard",
"kind": "enum",
"variants": [
{
"name": "timestamp",
"type": "u64"
"name": "Simple"
},
{
"name": "tick_cumulative",
"type": "i64"
"name": "Expanded"
}
]
},
{
"name": "MetadataStandard",
"kind": "enum",
"variants": [
"name": "ObservationEntry",
"kind": "struct",
"fields": [
{
"name": "Simple"
"name": "timestamp",
"type": "u64"
},
{
"name": "Expanded"
"name": "tick_cumulative",
"type": "i64"
}
]
}
Expand Down
51 changes: 48 additions & 3 deletions programs/twap_oracle/src/create_price_observations.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clock_core::ClockAccountData;
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{AccountPostState, Claim, ProgramId},
Expand All @@ -14,12 +14,17 @@ use twap_oracle_core::{
/// from `price_source.account_id` and `window_duration`, so whoever controls the price source
/// controls the observations account.
///
/// The initial observation timestamp is read from `clock`, which must be the canonical 1-block
/// LEZ system clock ([`CLOCK_01_PROGRAM_ACCOUNT_ID`]). Enforcing this prevents a caller from
/// supplying an account they control to seed the TWAP with a forged base timestamp.
///
/// # Panics
/// Panics if:
/// - `price_observations.account_id` does not match
/// `compute_price_observations_pda(oracle_program_id, price_source.account_id, window_duration)`.
/// - `price_observations.account` is not the default (already initialised).
/// - `price_source.is_authorized` is false (caller does not control the price source account).
/// - `clock.account_id` is not [`CLOCK_01_PROGRAM_ACCOUNT_ID`].
pub fn create_price_observations(
price_observations: AccountWithMetadata,
price_source: AccountWithMetadata,
Expand All @@ -43,6 +48,10 @@ pub fn create_price_observations(
price_source.is_authorized,
"CreatePriceObservations: price source account must be authorized (caller must control it via a PDA)"
);
assert_eq!(
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
"CreatePriceObservations: clock account must be the canonical 1-block LEZ clock account"
);

let clock_data = ClockAccountData::from_bytes(clock.account.data.as_ref());

Expand Down Expand Up @@ -95,7 +104,7 @@ mod tests {
AccountId::new([1u8; 32])
}

fn clock_account_with_timestamp(timestamp: u64) -> AccountWithMetadata {
fn clock_account_with_id(timestamp: u64, account_id: AccountId) -> AccountWithMetadata {
let data = ClockAccountData {
block_id: 0,
timestamp,
Expand All @@ -109,10 +118,14 @@ mod tests {
nonce: Nonce(0),
},
is_authorized: false,
account_id: AccountId::new([99u8; 32]),
account_id,
}
}

fn clock_account_with_timestamp(timestamp: u64) -> AccountWithMetadata {
clock_account_with_id(timestamp, CLOCK_01_PROGRAM_ACCOUNT_ID)
}

fn price_source_authorized() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
Expand Down Expand Up @@ -385,4 +398,36 @@ mod tests {
ORACLE_PROGRAM_ID,
);
}

/// The coarser-cadence clock accounts (10-block, 50-block) are still rejected: the oracle
/// must read the most fine-grained 1-block clock.
#[test]
#[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")]
fn non_canonical_clock_account_id_panics() {
use clock_core::CLOCK_10_PROGRAM_ACCOUNT_ID;
create_price_observations(
price_observations_uninit(),
price_source_authorized(),
clock_account_with_id(0, CLOCK_10_PROGRAM_ACCOUNT_ID),
0,
WINDOW_24H,
ORACLE_PROGRAM_ID,
);
}

/// An attacker cannot supply an account they control — even one whose data deserializes as a
/// valid [`ClockAccountData`] with a forged timestamp — in place of the system clock.
#[test]
#[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")]
fn forged_clock_account_panics() {
let forged_clock = clock_account_with_id(9_999_999_999, AccountId::new([7u8; 32]));
create_price_observations(
price_observations_uninit(),
price_source_authorized(),
forged_clock,
0,
WINDOW_24H,
ORACLE_PROGRAM_ID,
);
}
}
Loading