diff --git a/program/rust/src/accounts.rs b/program/rust/src/accounts.rs new file mode 100644 index 000000000..b1f000bd6 --- /dev/null +++ b/program/rust/src/accounts.rs @@ -0,0 +1,180 @@ +//! Account types and utilities for working with Pyth accounts. + +use { + crate::{ + c_oracle_header::PC_MAGIC, + deserialize::load_account_as_mut, + error::OracleError, + utils::{ + check_valid_fresh_account, + get_rent, + pyth_assert, + send_lamports, + try_convert, + }, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::{ + account_info::AccountInfo, + program::invoke_signed, + program_error::ProgramError, + program_memory::sol_memset, + pubkey::Pubkey, + system_instruction::{ + allocate, + assign, + }, + }, + std::{ + borrow::BorrowMut, + cell::RefMut, + mem::size_of, + }, +}; + +mod mapping; +mod permission; +mod price; +mod product; + +pub use { + mapping::MappingAccount, + permission::{ + PermissionAccount, + PERMISSIONS_SEED, + }, + price::{ + PriceAccount, + PriceComponent, + PriceEma, + PriceInfo, + }, + product::ProductAccount, +}; + +#[repr(C)] +#[derive(Copy, Clone, Zeroable, Pod)] +pub struct AccountHeader { + pub magic_number: u32, + pub version: u32, + pub account_type: u32, + pub size: u32, +} + +/// The PythAccount trait's purpose is to attach constants to the 3 types of accounts that Pyth has +/// (mapping, price, product). This allows less duplicated code, because now we can create generic +/// functions to perform common checks on the accounts and to load and initialize the accounts. +pub trait PythAccount: Pod { + /// `ACCOUNT_TYPE` is just the account discriminator, it is different for mapping, product and + /// price + const ACCOUNT_TYPE: u32; + + /// `INITIAL_SIZE` is the value that the field `size_` will take when the account is first + /// initialized this one is slightly tricky because for mapping (resp. price) `size_` won't + /// include the unpopulated entries of `prod_` (resp. `comp_`). At the beginning there are 0 + /// products (resp. 0 components) therefore `INITIAL_SIZE` will be equal to the offset of + /// `prod_` (resp. `comp_`) Similarly the product account `INITIAL_SIZE` won't include any + /// key values. + const INITIAL_SIZE: u32; + + /// `minimum_size()` is the minimum size that the solana account holding the struct needs to + /// have. `INITIAL_SIZE` <= `minimum_size()` + const MINIMUM_SIZE: usize = size_of::(); + + /// Given an `AccountInfo`, verify it is sufficiently large and has the correct discriminator. + fn initialize<'a>( + account: &'a AccountInfo, + version: u32, + ) -> Result, ProgramError> { + pyth_assert( + account.data_len() >= Self::MINIMUM_SIZE, + OracleError::AccountTooSmall.into(), + )?; + + check_valid_fresh_account(account)?; + clear_account(account)?; + + { + let mut account_header = load_account_as_mut::(account)?; + account_header.magic_number = PC_MAGIC; + account_header.version = version; + account_header.account_type = Self::ACCOUNT_TYPE; + account_header.size = Self::INITIAL_SIZE; + } + load_account_as_mut::(account) + } + + // Creates PDA accounts only when needed, and initializes it as one of the Pyth accounts + fn initialize_pda<'a>( + account: &AccountInfo<'a>, + funding_account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + program_id: &Pubkey, + seeds: &[&[u8]], + version: u32, + ) -> Result<(), ProgramError> { + let target_rent = get_rent()?.minimum_balance(Self::MINIMUM_SIZE); + + if account.lamports() < target_rent { + send_lamports( + funding_account, + account, + system_program, + target_rent - account.lamports(), + )?; + } + + if account.data_len() == 0 { + allocate_data(account, system_program, Self::MINIMUM_SIZE, seeds)?; + assign_owner(account, program_id, system_program, seeds)?; + Self::initialize(account, version)?; + } + Ok(()) + } +} + +/// Given an already empty `AccountInfo`, allocate the data field to the given size. This make no +/// assumptions about owner. +fn allocate_data<'a>( + account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + space: usize, + seeds: &[&[u8]], +) -> Result<(), ProgramError> { + let allocate_instruction = allocate(account.key, try_convert(space)?); + invoke_signed( + &allocate_instruction, + &[account.clone(), system_program.clone()], + &[seeds], + )?; + Ok(()) +} + +/// Given a newly created `AccountInfo`, assign the owner to the given program id. +fn assign_owner<'a>( + account: &AccountInfo<'a>, + owner: &Pubkey, + system_program: &AccountInfo<'a>, + seeds: &[&[u8]], +) -> Result<(), ProgramError> { + let assign_instruction = assign(account.key, owner); + invoke_signed( + &assign_instruction, + &[account.clone(), system_program.clone()], + &[seeds], + )?; + Ok(()) +} + +/// Sets the data of account to all-zero +pub fn clear_account(account: &AccountInfo) -> Result<(), ProgramError> { + let mut data = account + .try_borrow_mut_data() + .map_err(|_| ProgramError::InvalidArgument)?; + let length = data.len(); + sol_memset(data.borrow_mut(), 0, length); + Ok(()) +} diff --git a/program/rust/src/accounts/mapping.rs b/program/rust/src/accounts/mapping.rs new file mode 100644 index 000000000..a149390ff --- /dev/null +++ b/program/rust/src/accounts/mapping.rs @@ -0,0 +1,39 @@ +use { + super::{ + AccountHeader, + PythAccount, + }, + crate::c_oracle_header::{ + PC_ACCTYPE_MAPPING, + PC_MAP_TABLE_SIZE, + PC_MAP_TABLE_T_PROD_OFFSET, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::pubkey::Pubkey, +}; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct MappingAccount { + pub header: AccountHeader, + pub number_of_products: u32, + pub unused_: u32, + pub next_mapping_account: Pubkey, + pub products_list: [Pubkey; PC_MAP_TABLE_SIZE as usize], +} + +impl PythAccount for MappingAccount { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_MAPPING; + /// Equal to the offset of `prod_` in `MappingAccount`, see the trait comment for more detail + const INITIAL_SIZE: u32 = PC_MAP_TABLE_T_PROD_OFFSET as u32; +} + +// Unsafe impl because product_list is of size 640 and there's no derived trait for this size +unsafe impl Pod for MappingAccount { +} + +unsafe impl Zeroable for MappingAccount { +} diff --git a/program/rust/src/accounts/permission.rs b/program/rust/src/accounts/permission.rs new file mode 100644 index 000000000..22145bfdc --- /dev/null +++ b/program/rust/src/accounts/permission.rs @@ -0,0 +1,57 @@ +use { + super::{ + AccountHeader, + PythAccount, + }, + crate::{ + c_oracle_header::PC_ACCTYPE_PERMISSIONS, + instruction::OracleCommand, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::pubkey::Pubkey, + std::mem::size_of, +}; + +pub const PERMISSIONS_SEED: &str = "permissions"; + +/// This account stores the pubkeys that can execute administrative instructions in the Pyth +/// program. Only the upgrade authority of the program can update these permissions. +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct PermissionAccount { + /// pyth account header + pub header: AccountHeader, + /// An authority that can do any administrative task + pub master_authority: Pubkey, + /// An authority that can : + /// - Add mapping accounts + /// - Add price accounts + /// - Add product accounts + /// - Delete price accounts + /// - Delete product accounts + /// - Update product accounts + pub data_curation_authority: Pubkey, + /// An authority that can : + /// - Add publishers + /// - Delete publishers + /// - Set minimum number of publishers + pub security_authority: Pubkey, +} + +impl PermissionAccount { + pub fn is_authorized(&self, key: &Pubkey, command: OracleCommand) -> bool { + #[allow(clippy::match_like_matches_macro)] + match (*key, command) { + (pubkey, OracleCommand::InitMapping) if pubkey == self.master_authority => true, + _ => false, + } + } +} + +impl PythAccount for PermissionAccount { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PERMISSIONS; + const INITIAL_SIZE: u32 = size_of::() as u32; +} diff --git a/program/rust/src/accounts/price.rs b/program/rust/src/accounts/price.rs new file mode 100644 index 000000000..49f7548b4 --- /dev/null +++ b/program/rust/src/accounts/price.rs @@ -0,0 +1,93 @@ +use { + super::{ + AccountHeader, + PythAccount, + }, + crate::c_oracle_header::{ + PC_ACCTYPE_PRICE, + PC_COMP_SIZE, + PC_PRICE_T_COMP_OFFSET, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::pubkey::Pubkey, +}; + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct PriceAccount { + pub header: AccountHeader, + /// Type of the price account + pub price_type: u32, + /// Exponent for the published prices + pub exponent: i32, + /// Current number of authorized publishers + pub num_: u32, + /// Number of valid quotes for the last aggregation + pub num_qt_: u32, + /// Last slot with a succesful aggregation (status : TRADING) + pub last_slot_: u64, + /// Second to last slot where aggregation was attempted + pub valid_slot_: u64, + /// Ema for price + pub twap_: PriceEma, + /// Ema for confidence + pub twac_: PriceEma, + /// Last time aggregation was attempted + pub timestamp_: i64, + /// Minimum valid publisher quotes for a succesful aggregation + pub min_pub_: u8, + pub unused_1_: i8, + pub unused_2_: i16, + pub unused_3_: i32, + /// Corresponding product account + pub product_account: Pubkey, + /// Next price account in the list + pub next_price_account: Pubkey, + /// Second to last slot where aggregation was succesful (i.e. status : TRADING) + pub prev_slot_: u64, + /// Aggregate price at prev_slot_ + pub prev_price_: i64, + /// Confidence interval at prev_slot_ + pub prev_conf_: u64, + /// Timestamp of prev_slot_ + pub prev_timestamp_: i64, + /// Last attempted aggregate results + pub agg_: PriceInfo, + /// Publishers' price components + pub comp_: [PriceComponent; PC_COMP_SIZE as usize], +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct PriceComponent { + pub pub_: Pubkey, + pub agg_: PriceInfo, + pub latest_: PriceInfo, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +pub struct PriceInfo { + pub price_: i64, + pub conf_: u64, + pub status_: u32, + pub corp_act_status_: u32, + pub pub_slot_: u64, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Pod, Zeroable)] +pub struct PriceEma { + pub val_: i64, + pub numer_: i64, + pub denom_: i64, +} + +impl PythAccount for PriceAccount { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRICE; + /// Equal to the offset of `comp_` in `PriceAccount`, see the trait comment for more detail + const INITIAL_SIZE: u32 = PC_PRICE_T_COMP_OFFSET as u32; +} diff --git a/program/rust/src/accounts/product.rs b/program/rust/src/accounts/product.rs new file mode 100644 index 000000000..7acfb5650 --- /dev/null +++ b/program/rust/src/accounts/product.rs @@ -0,0 +1,29 @@ +use { + super::{ + AccountHeader, + PythAccount, + }, + crate::c_oracle_header::{ + PC_ACCTYPE_PRODUCT, + PC_PROD_ACC_SIZE, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::pubkey::Pubkey, + std::mem::size_of, +}; + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct ProductAccount { + pub header: AccountHeader, + pub first_price_account: Pubkey, +} + +impl PythAccount for ProductAccount { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRODUCT; + const INITIAL_SIZE: u32 = size_of::() as u32; + const MINIMUM_SIZE: usize = PC_PROD_ACC_SIZE as usize; +} diff --git a/program/rust/src/c_oracle_header.rs b/program/rust/src/c_oracle_header.rs index e45f04a84..d1578bff8 100644 --- a/program/rust/src/c_oracle_header.rs +++ b/program/rust/src/c_oracle_header.rs @@ -2,23 +2,10 @@ #![allow(non_snake_case)] // We do not use all the variables in oracle.h, so this helps with the warnings #![allow(dead_code)] -// All the custom trait imports should go here -use { - crate::instruction::OracleCommand, - bytemuck::{ - Pod, - Zeroable, - }, - solana_program::pubkey::Pubkey, - std::mem::size_of, -}; // Bindings.rs is generated by build.rs to include things defined in bindings.h include!("../bindings.rs"); -pub const PERMISSIONS_SEED: &str = "permissions"; - - /// If ci > price / PC_MAX_CI_DIVISOR, set publisher status to unknown. /// (e.g., 20 means ci must be < 5% of price) pub const MAX_CI_DIVISOR: i64 = 20; @@ -26,182 +13,3 @@ pub const MAX_CI_DIVISOR: i64 = 20; /// Bound on the range of the exponent in price accounts. This number is set such that the /// PD-based EMA computation does not lose too much precision. pub const MAX_NUM_DECIMALS: i32 = 8; - -/// The PythAccount trait's purpose is to attach constants to the 3 types of accounts that Pyth has -/// (mapping, price, product). This allows less duplicated code, because now we can create generic -/// functions to perform common checks on the accounts and to load and initialize the accounts. -pub trait PythAccount: Pod { - /// `ACCOUNT_TYPE` is just the account discriminator, it is different for mapping, product and - /// price - const ACCOUNT_TYPE: u32; - /// `INITIAL_SIZE` is the value that the field `size_` will take when the account is first - /// initialized this one is slightly tricky because for mapping (resp. price) `size_` won't - /// include the unpopulated entries of `prod_` (resp. `comp_`). At the beginning there are 0 - /// products (resp. 0 components) therefore `INITIAL_SIZE` will be equal to the offset of - /// `prod_` (resp. `comp_`) Similarly the product account `INITIAL_SIZE` won't include any - /// key values. - const INITIAL_SIZE: u32; - /// `minimum_size()` is the minimum size that the solana account holding the struct needs to - /// have. `INITIAL_SIZE` <= `minimum_size()` - const MINIMUM_SIZE: usize = size_of::(); -} - -impl PythAccount for MappingAccount { - const ACCOUNT_TYPE: u32 = PC_ACCTYPE_MAPPING; - /// Equal to the offset of `prod_` in `MappingAccount`, see the trait comment for more detail - const INITIAL_SIZE: u32 = PC_MAP_TABLE_T_PROD_OFFSET as u32; -} - -impl PythAccount for ProductAccount { - const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRODUCT; - const INITIAL_SIZE: u32 = size_of::() as u32; - const MINIMUM_SIZE: usize = PC_PROD_ACC_SIZE as usize; -} - -impl PythAccount for PriceAccount { - const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRICE; - /// Equal to the offset of `comp_` in `PriceAccount`, see the trait comment for more detail - const INITIAL_SIZE: u32 = PC_PRICE_T_COMP_OFFSET as u32; -} - -impl PythAccount for PermissionAccount { - const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PERMISSIONS; - const INITIAL_SIZE: u32 = size_of::() as u32; -} - -/// This account stores the pubkeys that can execute administrative instructions in the Pyth -/// program. Only the upgrade authority of the program can update these permissions. -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -pub struct PermissionAccount { - /// pyth account header - pub header: AccountHeader, - /// An authority that can do any administrative task - pub master_authority: Pubkey, - /// An authority that can : - /// - Add mapping accounts - /// - Add price accounts - /// - Add product accounts - /// - Delete price accounts - /// - Delete product accounts - /// - Update product accounts - pub data_curation_authority: Pubkey, - /// An authority that can : - /// - Add publishers - /// - Delete publishers - /// - Set minimum number of publishers - pub security_authority: Pubkey, -} - -impl PermissionAccount { - pub fn is_authorized(&self, key: &Pubkey, command: OracleCommand) -> bool { - #[allow(clippy::match_like_matches_macro)] - match (*key, command) { - (pubkey, OracleCommand::InitMapping) if pubkey == self.master_authority => true, - _ => false, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -pub struct PriceAccount { - pub header: AccountHeader, - /// Type of the price account - pub price_type: u32, - /// Exponent for the published prices - pub exponent: i32, - /// Current number of authorized publishers - pub num_: u32, - /// Number of valid quotes for the last aggregation - pub num_qt_: u32, - /// Last slot with a succesful aggregation (status : TRADING) - pub last_slot_: u64, - /// Second to last slot where aggregation was attempted - pub valid_slot_: u64, - /// Ema for price - pub twap_: PriceEma, - /// Ema for confidence - pub twac_: PriceEma, - /// Last time aggregation was attempted - pub timestamp_: i64, - /// Minimum valid publisher quotes for a succesful aggregation - pub min_pub_: u8, - pub unused_1_: i8, - pub unused_2_: i16, - pub unused_3_: i32, - /// Corresponding product account - pub product_account: Pubkey, - /// Next price account in the list - pub next_price_account: Pubkey, - /// Second to last slot where aggregation was succesful (i.e. status : TRADING) - pub prev_slot_: u64, - /// Aggregate price at prev_slot_ - pub prev_price_: i64, - /// Confidence interval at prev_slot_ - pub prev_conf_: u64, - /// Timestamp of prev_slot_ - pub prev_timestamp_: i64, - /// Last attempted aggregate results - pub agg_: PriceInfo, - /// Publishers' price components - pub comp_: [PriceComponent; PC_COMP_SIZE as usize], -} - -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -pub struct PriceComponent { - pub pub_: Pubkey, - pub agg_: PriceInfo, - pub latest_: PriceInfo, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, Pod, Zeroable)] -pub struct PriceInfo { - pub price_: i64, - pub conf_: u64, - pub status_: u32, - pub corp_act_status_: u32, - pub pub_slot_: u64, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone, Pod, Zeroable)] -pub struct PriceEma { - pub val_: i64, - pub numer_: i64, - pub denom_: i64, -} - -#[repr(C)] -#[derive(Copy, Clone, Zeroable, Pod)] -pub struct AccountHeader { - pub magic_number: u32, - pub version: u32, - pub account_type: u32, - pub size: u32, -} - -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -pub struct ProductAccount { - pub header: AccountHeader, - pub first_price_account: Pubkey, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct MappingAccount { - pub header: AccountHeader, - pub number_of_products: u32, - pub unused_: u32, - pub next_mapping_account: Pubkey, - pub products_list: [Pubkey; PC_MAP_TABLE_SIZE as usize], -} - -// Unsafe impl because product_list is of size 640 and there's no derived trait for this size -unsafe impl Pod for MappingAccount { -} -unsafe impl Zeroable for MappingAccount { -} diff --git a/program/rust/src/deserialize.rs b/program/rust/src/deserialize.rs index 1ede25495..9583dae67 100644 --- a/program/rust/src/deserialize.rs +++ b/program/rust/src/deserialize.rs @@ -1,20 +1,12 @@ use { crate::{ - c_oracle_header::{ + accounts::{ AccountHeader, PythAccount, - PC_MAGIC, }, + c_oracle_header::PC_MAGIC, error::OracleError, - utils::{ - allocate_data, - assign_owner, - check_valid_fresh_account, - clear_account, - get_rent, - pyth_assert, - send_lamports, - }, + utils::pyth_assert, }, bytemuck::{ try_from_bytes, @@ -24,7 +16,6 @@ use { solana_program::{ account_info::AccountInfo, program_error::ProgramError, - pubkey::Pubkey, }, std::{ cell::{ @@ -101,51 +92,3 @@ pub fn load_checked<'a, T: PythAccount>( load_account_as_mut::(account) } - -pub fn initialize_pyth_account_checked<'a, T: PythAccount>( - account: &'a AccountInfo, - version: u32, -) -> Result, ProgramError> { - pyth_assert( - account.data_len() >= T::MINIMUM_SIZE, - OracleError::AccountTooSmall.into(), - )?; - check_valid_fresh_account(account)?; - clear_account(account)?; - - { - let mut account_header = load_account_as_mut::(account)?; - account_header.magic_number = PC_MAGIC; - account_header.version = version; - account_header.account_type = T::ACCOUNT_TYPE; - account_header.size = T::INITIAL_SIZE; - } - - load_account_as_mut::(account) -} - -// Creates pda if needed and initializes it as one of the Pyth accounts -pub fn create_pda_if_needed<'a, T: PythAccount>( - account: &AccountInfo<'a>, - funding_account: &AccountInfo<'a>, - system_program: &AccountInfo<'a>, - program_id: &Pubkey, - seeds: &[&[u8]], - version: u32, -) -> Result<(), ProgramError> { - let target_rent = get_rent()?.minimum_balance(T::MINIMUM_SIZE); - if account.lamports() < target_rent { - send_lamports( - funding_account, - account, - system_program, - target_rent - account.lamports(), - )?; - } - if account.data_len() == 0 { - allocate_data(account, system_program, T::MINIMUM_SIZE, seeds)?; - assign_owner(account, program_id, system_program, seeds)?; - initialize_pyth_account_checked::(account, version)?; - } - Ok(()) -} diff --git a/program/rust/src/instruction.rs b/program/rust/src/instruction.rs index d97d7d769..a0799e382 100644 --- a/program/rust/src/instruction.rs +++ b/program/rust/src/instruction.rs @@ -18,82 +18,82 @@ use { /// WARNING : NEW COMMANDS SHOULD BE ADDED AT THE END OF THE LIST #[repr(i32)] -#[derive(FromPrimitive, ToPrimitive)] +#[derive(PartialEq, Eq, FromPrimitive, ToPrimitive)] pub enum OracleCommand { - // initialize first mapping list account + /// Initialize first mapping list account // account[0] funding account [signer writable] // account[1] mapping account [signer writable] InitMapping = 0, - // initialize and add new mapping account + /// Initialize and add new mapping account // account[0] funding account [signer writable] // account[1] tail mapping account [signer writable] // account[2] new mapping account [signer writable] AddMapping = 1, - // initialize and add new product reference data account + /// Initialize and add new product reference data account // account[0] funding account [signer writable] // account[1] mapping account [signer writable] // account[2] new product account [signer writable] AddProduct = 2, - // update product account + /// Update product account // account[0] funding account [signer writable] // account[1] product account [signer writable] UpdProduct = 3, - // add new price account to a product account + /// Add new price account to a product account // account[0] funding account [signer writable] // account[1] product account [signer writable] // account[2] new price account [signer writable] AddPrice = 4, - // add publisher to symbol account + /// Add publisher to symbol account // account[0] funding account [signer writable] // account[1] price account [signer writable] AddPublisher = 5, - // delete publisher from symbol account + /// Delete publisher from symbol account // account[0] funding account [signer writable] // account[1] price account [signer writable] DelPublisher = 6, - // publish component price + /// Publish component price // account[0] funding account [signer writable] // account[1] price account [writable] // account[2] sysvar_clock account [] UpdPrice = 7, - // compute aggregate price + /// Compute aggregate price // account[0] funding account [signer writable] // account[1] price account [writable] // account[2] sysvar_clock account [] AggPrice = 8, - // (re)initialize price account + /// (Re)initialize price account // account[0] funding account [signer writable] // account[1] new price account [signer writable] InitPrice = 9, - // deprecated + /// deprecated InitTest = 10, - // deprecated + /// deprecated UpdTest = 11, - // set min publishers + /// Set min publishers // account[0] funding account [signer writable] // account[1] price account [signer writable] SetMinPub = 12, - // publish component price, never returning an error even if the update failed + /// Publish component price, never returning an error even if the update failed // account[0] funding account [signer writable] // account[1] price account [writable] // account[2] sysvar_clock account [] UpdPriceNoFailOnError = 13, - // resizes a price account so that it fits the Time Machine + /// Resizes a price account so that it fits the Time Machine // account[0] funding account [signer writable] // account[1] price account [signer writable] // account[2] system program [] ResizePriceAccount = 14, - // deletes a price account + /// Deletes a price account // account[0] funding account [signer writable] // account[1] product account [signer writable] // account[2] price account [signer writable] DelPrice = 15, - // deletes a product account + /// Deletes a product account // key[0] funding account [signer writable] // key[1] mapping account [signer writable] // key[2] product account [signer writable] DelProduct = 16, - // Update authorities + /// Update authorities // key[0] upgrade authority [signer writable] // key[1] program account [] // key[2] programdata account [] diff --git a/program/rust/src/lib.rs b/program/rust/src/lib.rs index 3ded58934..186ceeeda 100644 --- a/program/rust/src/lib.rs +++ b/program/rust/src/lib.rs @@ -2,12 +2,12 @@ // Allow non upper case globals from C #![allow(non_upper_case_globals)] +mod accounts; mod c_oracle_header; mod deserialize; mod error; mod instruction; mod processor; -mod rust_oracle; mod time_machine_types; mod utils; diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index 8aefe4623..1483d1cad 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -5,23 +5,6 @@ use { load_command_header_checked, OracleCommand, }, - rust_oracle::{ - add_mapping, - add_price, - add_product, - add_publisher, - del_price, - del_product, - del_publisher, - init_mapping, - init_price, - resize_price_account, - set_min_pub, - upd_permissions, - upd_price, - upd_price_no_fail_on_error, - upd_product, - }, }, solana_program::{ entrypoint::ProgramResult, @@ -30,34 +13,68 @@ use { }, }; +mod add_mapping; +mod add_price; +mod add_product; +mod add_publisher; +mod del_price; +mod del_product; +mod del_publisher; +mod init_mapping; +mod init_price; +mod resize_price_account; +mod set_min_pub; +mod upd_permissions; +mod upd_price; +mod upd_product; + +pub use { + add_mapping::add_mapping, + add_price::add_price, + add_product::add_product, + add_publisher::add_publisher, + del_price::del_price, + del_product::del_product, + del_publisher::del_publisher, + init_mapping::init_mapping, + init_price::init_price, + resize_price_account::resize_price_account, + set_min_pub::set_min_pub, + upd_permissions::upd_permissions, + upd_price::{ + c_upd_aggregate, + upd_price, + upd_price_no_fail_on_error, + }, + upd_product::upd_product, +}; + /// Dispatch to the right instruction in the oracle. pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { + use OracleCommand::*; + match load_command_header_checked(instruction_data)? { - OracleCommand::InitMapping => init_mapping(program_id, accounts, instruction_data), - OracleCommand::AddMapping => add_mapping(program_id, accounts, instruction_data), - OracleCommand::AddProduct => add_product(program_id, accounts, instruction_data), - OracleCommand::UpdProduct => upd_product(program_id, accounts, instruction_data), - OracleCommand::AddPrice => add_price(program_id, accounts, instruction_data), - OracleCommand::AddPublisher => add_publisher(program_id, accounts, instruction_data), - OracleCommand::DelPublisher => del_publisher(program_id, accounts, instruction_data), - OracleCommand::UpdPrice => upd_price(program_id, accounts, instruction_data), - OracleCommand::AggPrice => upd_price(program_id, accounts, instruction_data), - OracleCommand::InitPrice => init_price(program_id, accounts, instruction_data), - OracleCommand::InitTest => Err(OracleError::UnrecognizedInstruction.into()), - OracleCommand::UpdTest => Err(OracleError::UnrecognizedInstruction.into()), - OracleCommand::SetMinPub => set_min_pub(program_id, accounts, instruction_data), - OracleCommand::UpdPriceNoFailOnError => { - upd_price_no_fail_on_error(program_id, accounts, instruction_data) - } - OracleCommand::ResizePriceAccount => { - resize_price_account(program_id, accounts, instruction_data) - } - OracleCommand::DelPrice => del_price(program_id, accounts, instruction_data), - OracleCommand::DelProduct => del_product(program_id, accounts, instruction_data), - OracleCommand::UpdPermissions => upd_permissions(program_id, accounts, instruction_data), + InitMapping => init_mapping(program_id, accounts, instruction_data), + AddMapping => add_mapping(program_id, accounts, instruction_data), + AddProduct => add_product(program_id, accounts, instruction_data), + UpdProduct => upd_product(program_id, accounts, instruction_data), + AddPrice => add_price(program_id, accounts, instruction_data), + AddPublisher => add_publisher(program_id, accounts, instruction_data), + DelPublisher => del_publisher(program_id, accounts, instruction_data), + UpdPrice => upd_price(program_id, accounts, instruction_data), + AggPrice => upd_price(program_id, accounts, instruction_data), + InitPrice => init_price(program_id, accounts, instruction_data), + InitTest => Err(OracleError::UnrecognizedInstruction.into()), + UpdTest => Err(OracleError::UnrecognizedInstruction.into()), + SetMinPub => set_min_pub(program_id, accounts, instruction_data), + UpdPriceNoFailOnError => upd_price_no_fail_on_error(program_id, accounts, instruction_data), + ResizePriceAccount => resize_price_account(program_id, accounts, instruction_data), + DelPrice => del_price(program_id, accounts, instruction_data), + DelProduct => del_product(program_id, accounts, instruction_data), + UpdPermissions => upd_permissions(program_id, accounts, instruction_data), } } diff --git a/program/rust/src/processor/add_mapping.rs b/program/rust/src/processor/add_mapping.rs new file mode 100644 index 000000000..bcaa8eea5 --- /dev/null +++ b/program/rust/src/processor/add_mapping.rs @@ -0,0 +1,72 @@ +use { + crate::{ + accounts::{ + MappingAccount, + PythAccount, + }, + c_oracle_header::PC_MAP_TABLE_SIZE, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +/// Initialize and add new mapping account +// account[0] funding account [signer writable] +// account[1] tail mapping account [signer writable] +// account[2] new mapping account [signer writable] +pub fn add_mapping( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, cur_mapping, next_mapping, permissions_account_option) = match accounts { + [x, y, z] => Ok((x, y, z, None)), + [x, y, z, p] => Ok((x, y, z, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + cur_mapping, + funding_account, + permissions_account_option, + hdr, + )?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + next_mapping, + funding_account, + permissions_account_option, + hdr, + )?; + + let mut cur_mapping = load_checked::(cur_mapping, hdr.version)?; + pyth_assert( + cur_mapping.number_of_products == PC_MAP_TABLE_SIZE + && cur_mapping.next_mapping_account == Pubkey::default(), + ProgramError::InvalidArgument, + )?; + + MappingAccount::initialize(next_mapping, hdr.version)?; + cur_mapping.next_mapping_account = *next_mapping.key; + + Ok(()) +} diff --git a/program/rust/src/processor/add_price.rs b/program/rust/src/processor/add_price.rs new file mode 100644 index 000000000..ae07164b6 --- /dev/null +++ b/program/rust/src/processor/add_price.rs @@ -0,0 +1,82 @@ +use { + crate::{ + accounts::{ + PriceAccount, + ProductAccount, + PythAccount, + }, + c_oracle_header::PC_PTYPE_UNKNOWN, + deserialize::{ + load, + load_checked, + }, + instruction::AddPriceArgs, + utils::{ + check_exponent_range, + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +/// Add new price account to a product account +// account[0] funding account [signer writable] +// account[1] product account [signer writable] +// account[2] new price account [signer writable] +pub fn add_price( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd_args = load::(instruction_data)?; + + check_exponent_range(cmd_args.exponent)?; + pyth_assert( + cmd_args.price_type != PC_PTYPE_UNKNOWN, + ProgramError::InvalidArgument, + )?; + + + let (funding_account, product_account, price_account, permissions_account_option) = + match accounts { + [x, y, z] => Ok((x, y, z, None)), + [x, y, z, p] => Ok((x, y, z, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + product_account, + funding_account, + permissions_account_option, + &cmd_args.header, + )?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + &cmd_args.header, + )?; + + let mut product_data = + load_checked::(product_account, cmd_args.header.version)?; + + let mut price_data = PriceAccount::initialize(price_account, cmd_args.header.version)?; + price_data.exponent = cmd_args.exponent; + price_data.price_type = cmd_args.price_type; + price_data.product_account = *product_account.key; + price_data.next_price_account = product_data.first_price_account; + product_data.first_price_account = *price_account.key; + + Ok(()) +} diff --git a/program/rust/src/processor/add_product.rs b/program/rust/src/processor/add_product.rs new file mode 100644 index 000000000..ce8c42107 --- /dev/null +++ b/program/rust/src/processor/add_product.rs @@ -0,0 +1,87 @@ +use { + crate::{ + accounts::{ + MappingAccount, + ProductAccount, + PythAccount, + }, + c_oracle_header::PC_MAP_TABLE_SIZE, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + try_convert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, + std::mem::{ + size_of, + size_of_val, + }, +}; + +/// Initialize and add new product reference data account +// account[0] funding account [signer writable] +// account[1] mapping account [signer writable] +// account[2] new product account [signer writable] +pub fn add_product( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, tail_mapping_account, new_product_account, permissions_account_option) = + match accounts { + [x, y, z] => Ok((x, y, z, None)), + [x, y, z, p] => Ok((x, y, z, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + tail_mapping_account, + funding_account, + permissions_account_option, + hdr, + )?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + new_product_account, + funding_account, + permissions_account_option, + hdr, + )?; + + + let mut mapping_data = load_checked::(tail_mapping_account, hdr.version)?; + // The mapping account must have free space to add the product account + pyth_assert( + mapping_data.number_of_products < PC_MAP_TABLE_SIZE, + ProgramError::InvalidArgument, + )?; + + ProductAccount::initialize(new_product_account, hdr.version)?; + + let current_index: usize = try_convert(mapping_data.number_of_products)?; + mapping_data.products_list[current_index] = *new_product_account.key; + mapping_data.number_of_products += 1; + mapping_data.header.size = try_convert::<_, u32>( + size_of::() - size_of_val(&mapping_data.products_list), + )? + mapping_data.number_of_products + * try_convert::<_, u32>(size_of::())?; + + Ok(()) +} diff --git a/program/rust/src/processor/add_publisher.rs b/program/rust/src/processor/add_publisher.rs new file mode 100644 index 000000000..100858be3 --- /dev/null +++ b/program/rust/src/processor/add_publisher.rs @@ -0,0 +1,91 @@ +use { + crate::{ + accounts::{ + PriceAccount, + PriceComponent, + }, + c_oracle_header::PC_COMP_SIZE, + deserialize::{ + load, + load_checked, + }, + instruction::AddPublisherArgs, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + try_convert, + }, + OracleError, + }, + bytemuck::bytes_of_mut, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + program_memory::sol_memset, + pubkey::Pubkey, + }, + std::mem::{ + size_of, + size_of_val, + }, +}; + +/// Add publisher to symbol account +// account[0] funding account [signer writable] +// account[1] price account [signer writable] +pub fn add_publisher( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd_args = load::(instruction_data)?; + + pyth_assert( + instruction_data.len() == size_of::() + && cmd_args.publisher != Pubkey::default(), + ProgramError::InvalidArgument, + )?; + + let (funding_account, price_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + &cmd_args.header, + )?; + + + let mut price_data = load_checked::(price_account, cmd_args.header.version)?; + + if price_data.num_ >= PC_COMP_SIZE { + return Err(ProgramError::InvalidArgument); + } + + for i in 0..(try_convert::(price_data.num_)?) { + if cmd_args.publisher == price_data.comp_[i].pub_ { + return Err(ProgramError::InvalidArgument); + } + } + + let current_index: usize = try_convert(price_data.num_)?; + sol_memset( + bytes_of_mut(&mut price_data.comp_[current_index]), + 0, + size_of::(), + ); + price_data.comp_[current_index].pub_ = cmd_args.publisher; + price_data.num_ += 1; + price_data.header.size = + try_convert::<_, u32>(size_of::() - size_of_val(&price_data.comp_))? + + price_data.num_ * try_convert::<_, u32>(size_of::())?; + Ok(()) +} diff --git a/program/rust/src/processor/del_price.rs b/program/rust/src/processor/del_price.rs new file mode 100644 index 000000000..e4fad0a05 --- /dev/null +++ b/program/rust/src/processor/del_price.rs @@ -0,0 +1,89 @@ +use { + crate::{ + accounts::{ + PriceAccount, + ProductAccount, + }, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +/// Delete a price account. This function will remove the link between the price account and its +/// corresponding product account, then transfer any SOL in the price account to the funding +/// account. This function can only delete the first price account in the linked list of +// account[0] funding account [signer writable] +// account[1] product account [signer writable] +// account[2] price account [signer writable] +/// Warning: This function is dangerous and will break any programs that depend on the deleted +/// price account! +pub fn del_price( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, product_account, price_account, permissions_account_option) = + match accounts { + [w, x, y] => Ok((w, x, y, None)), + [w, x, y, p] => Ok((w, x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let cmd_args = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + product_account, + funding_account, + permissions_account_option, + cmd_args, + )?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + cmd_args, + )?; + + { + let mut product_data = load_checked::(product_account, cmd_args.version)?; + let price_data = load_checked::(price_account, cmd_args.version)?; + pyth_assert( + product_data.first_price_account == *price_account.key, + ProgramError::InvalidArgument, + )?; + + pyth_assert( + price_data.product_account == *product_account.key, + ProgramError::InvalidArgument, + )?; + + product_data.first_price_account = price_data.next_price_account; + } + + // Zero out the balance of the price account to delete it. + // Note that you can't use the system program's transfer instruction to do this operation, as + // that instruction fails if the source account has any data. + let lamports = price_account.lamports(); + **price_account.lamports.borrow_mut() = 0; + **funding_account.lamports.borrow_mut() += lamports; + + Ok(()) +} diff --git a/program/rust/src/processor/del_product.rs b/program/rust/src/processor/del_product.rs new file mode 100644 index 000000000..35ee056d4 --- /dev/null +++ b/program/rust/src/processor/del_product.rs @@ -0,0 +1,120 @@ +use { + crate::{ + accounts::{ + MappingAccount, + ProductAccount, + }, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + try_convert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, + std::mem::{ + size_of, + size_of_val, + }, +}; + +/// Delete a product account and remove it from the product list of its associated mapping account. +/// The deleted product account must not have any price accounts. +/// +/// This function allows you to delete products from non-tail mapping accounts. This ability is a +/// little weird, as it allows you to construct a list of multiple mapping accounts where non-tail +/// accounts have empty space. This is fine however; users should simply add new products to the +/// first available spot. +// key[0] funding account [signer writable] +// key[1] mapping account [signer writable] +// key[2] product account [signer writable] +pub fn del_product( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, mapping_account, product_account, permissions_account_option) = + match accounts { + [w, x, y] => Ok((w, x, y, None)), + [w, x, y, p] => Ok((w, x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let cmd_args = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + mapping_account, + funding_account, + permissions_account_option, + cmd_args, + )?; + + check_valid_signable_account_or_permissioned_funding_account( + program_id, + product_account, + funding_account, + permissions_account_option, + cmd_args, + )?; + + + { + let mut mapping_data = load_checked::(mapping_account, cmd_args.version)?; + let product_data = load_checked::(product_account, cmd_args.version)?; + + // This assertion is just to make the subtractions below simpler + pyth_assert( + mapping_data.number_of_products >= 1, + ProgramError::InvalidArgument, + )?; + pyth_assert( + product_data.first_price_account == Pubkey::default(), + ProgramError::InvalidArgument, + )?; + + let product_key = product_account.key; + let product_index = mapping_data + .products_list + .iter() + .position(|x| *x == *product_key) + .ok_or(ProgramError::InvalidArgument)?; + + let num_after_removal: usize = try_convert( + mapping_data + .number_of_products + .checked_sub(1) + .ok_or(ProgramError::InvalidArgument)?, + )?; + + let last_key_bytes = mapping_data.products_list[num_after_removal]; + mapping_data.products_list[product_index] = last_key_bytes; + mapping_data.products_list[num_after_removal] = Pubkey::default(); + mapping_data.number_of_products = try_convert::<_, u32>(num_after_removal)?; + mapping_data.header.size = try_convert::<_, u32>( + size_of::() - size_of_val(&mapping_data.products_list), + )? + mapping_data.number_of_products + * try_convert::<_, u32>(size_of::())?; + } + + // Zero out the balance of the price account to delete it. + // Note that you can't use the system program's transfer instruction to do this operation, as + // that instruction fails if the source account has any data. + let lamports = product_account.lamports(); + **product_account.lamports.borrow_mut() = 0; + **funding_account.lamports.borrow_mut() += lamports; + + Ok(()) +} diff --git a/program/rust/src/processor/del_publisher.rs b/program/rust/src/processor/del_publisher.rs new file mode 100644 index 000000000..ad7e239f0 --- /dev/null +++ b/program/rust/src/processor/del_publisher.rs @@ -0,0 +1,86 @@ +use { + crate::{ + accounts::{ + PriceAccount, + PriceComponent, + }, + deserialize::{ + load, + load_checked, + }, + instruction::DelPublisherArgs, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + try_convert, + }, + OracleError, + }, + bytemuck::bytes_of_mut, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + program_memory::sol_memset, + pubkey::Pubkey, + }, + std::mem::{ + size_of, + size_of_val, + }, +}; + +/// Delete publisher from symbol account +// account[0] funding account [signer writable] +// account[1] price account [signer writable] +pub fn del_publisher( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd_args = load::(instruction_data)?; + + pyth_assert( + instruction_data.len() == size_of::() + && cmd_args.publisher != Pubkey::default(), + ProgramError::InvalidArgument, + )?; + + let (funding_account, price_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + &cmd_args.header, + )?; + + let mut price_data = load_checked::(price_account, cmd_args.header.version)?; + + for i in 0..(try_convert::(price_data.num_)?) { + if cmd_args.publisher == price_data.comp_[i].pub_ { + for j in i + 1..(try_convert::(price_data.num_)?) { + price_data.comp_[j - 1] = price_data.comp_[j]; + } + price_data.num_ -= 1; + let current_index: usize = try_convert(price_data.num_)?; + sol_memset( + bytes_of_mut(&mut price_data.comp_[current_index]), + 0, + size_of::(), + ); + price_data.header.size = + try_convert::<_, u32>(size_of::() - size_of_val(&price_data.comp_))? + + price_data.num_ * try_convert::<_, u32>(size_of::())?; + return Ok(()); + } + } + Err(ProgramError::InvalidArgument) +} diff --git a/program/rust/src/processor/init_mapping.rs b/program/rust/src/processor/init_mapping.rs new file mode 100644 index 000000000..0f24a489a --- /dev/null +++ b/program/rust/src/processor/init_mapping.rs @@ -0,0 +1,51 @@ +use { + crate::{ + accounts::{ + MappingAccount, + PythAccount, + }, + deserialize::load, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + pubkey::Pubkey, + }, +}; + +/// Initialize first mapping list account +// account[0] funding account [signer writable] +// account[1] mapping account [signer writable] +pub fn init_mapping( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, fresh_mapping_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + fresh_mapping_account, + funding_account, + permissions_account_option, + hdr, + )?; + + // Initialize by setting to zero again (just in case) and populating the account header + MappingAccount::initialize(fresh_mapping_account, hdr.version)?; + + Ok(()) +} diff --git a/program/rust/src/processor/init_price.rs b/program/rust/src/processor/init_price.rs new file mode 100644 index 000000000..b260ff51a --- /dev/null +++ b/program/rust/src/processor/init_price.rs @@ -0,0 +1,104 @@ +use { + crate::{ + accounts::{ + PriceAccount, + PriceEma, + PriceInfo, + }, + deserialize::{ + load, + load_checked, + }, + instruction::InitPriceArgs, + utils::{ + check_exponent_range, + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + }, + OracleError, + }, + bytemuck::bytes_of_mut, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + program_memory::sol_memset, + pubkey::Pubkey, + }, + std::mem::size_of, +}; + +/// (Re)initialize price account +// account[0] funding account [signer writable] +// account[1] new price account [signer writable] +pub fn init_price( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd_args = load::(instruction_data)?; + + check_exponent_range(cmd_args.exponent)?; + + let (funding_account, price_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + &cmd_args.header, + )?; + + + let mut price_data = load_checked::(price_account, cmd_args.header.version)?; + pyth_assert( + price_data.price_type == cmd_args.price_type, + ProgramError::InvalidArgument, + )?; + + price_data.exponent = cmd_args.exponent; + + price_data.last_slot_ = 0; + price_data.valid_slot_ = 0; + price_data.agg_.pub_slot_ = 0; + price_data.prev_slot_ = 0; + price_data.prev_price_ = 0; + price_data.prev_conf_ = 0; + price_data.prev_timestamp_ = 0; + sol_memset( + bytes_of_mut(&mut price_data.twap_), + 0, + size_of::(), + ); + sol_memset( + bytes_of_mut(&mut price_data.twac_), + 0, + size_of::(), + ); + sol_memset( + bytes_of_mut(&mut price_data.agg_), + 0, + size_of::(), + ); + for i in 0..price_data.comp_.len() { + sol_memset( + bytes_of_mut(&mut price_data.comp_[i].agg_), + 0, + size_of::(), + ); + sol_memset( + bytes_of_mut(&mut price_data.comp_[i].latest_), + 0, + size_of::(), + ); + } + + Ok(()) +} diff --git a/program/rust/src/processor/resize_price_account.rs b/program/rust/src/processor/resize_price_account.rs new file mode 100644 index 000000000..5f968ac7a --- /dev/null +++ b/program/rust/src/processor/resize_price_account.rs @@ -0,0 +1,111 @@ +use { + crate::{ + accounts::{ + PriceAccount, + PythAccount, + }, + c_oracle_header::PC_VERSION, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + time_machine_types::PriceAccountWrapper, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + get_rent, + pyth_assert, + send_lamports, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_program::check_id, + }, + std::mem::size_of, +}; + +/// Resizes a price account so that it fits the Time Machine +// account[0] funding account [signer writable] +// account[1] price account [signer writable] +// account[2] system program [] +pub fn resize_price_account( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, price_account, system_program, permissions_account_option) = + match accounts { + [x, y, z] => Ok((x, y, z, None)), + [x, y, z, p] => Ok((x, y, z, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + hdr, + )?; + pyth_assert( + check_id(system_program.key), + OracleError::InvalidSystemAccount.into(), + )?; + // Check that it is a valid initialized price account + { + load_checked::(price_account, PC_VERSION)?; + } + let account_len = price_account.try_data_len()?; + match account_len { + PriceAccount::MINIMUM_SIZE => { + // Ensure account is still rent exempt after resizing + let rent: Rent = get_rent()?; + let lamports_needed: u64 = rent + .minimum_balance(size_of::()) + .saturating_sub(price_account.lamports()); + if lamports_needed > 0 { + send_lamports( + funding_account, + price_account, + system_program, + lamports_needed, + )?; + } + // We do not need to zero allocate because we won't access the data in the same + // instruction + price_account.realloc(size_of::(), false)?; + + // Check that everything is ok + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + hdr, + )?; + + #[cfg(test)] + // Sma feature disabled except in tests + { + let mut price_account = + load_checked::(price_account, PC_VERSION)?; + // Initialize Time Machine + price_account.initialize_time_machine()?; + } + + Ok(()) + } + PriceAccountWrapper::MINIMUM_SIZE => Ok(()), + _ => Err(ProgramError::InvalidArgument), + } +} diff --git a/program/rust/src/processor/set_min_pub.rs b/program/rust/src/processor/set_min_pub.rs new file mode 100644 index 000000000..ca57dfad2 --- /dev/null +++ b/program/rust/src/processor/set_min_pub.rs @@ -0,0 +1,60 @@ +use { + crate::{ + accounts::PriceAccount, + deserialize::{ + load, + load_checked, + }, + instruction::SetMinPubArgs, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + }, + std::mem::size_of, +}; + +/// Set min publishers +// account[0] funding account [signer writable] +// account[1] price account [signer writable] +pub fn set_min_pub( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd = load::(instruction_data)?; + + pyth_assert( + instruction_data.len() == size_of::(), + ProgramError::InvalidArgument, + )?; + + let (funding_account, price_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + price_account, + funding_account, + permissions_account_option, + &cmd.header, + )?; + + + let mut price_account_data = load_checked::(price_account, cmd.header.version)?; + price_account_data.min_pub_ = cmd.minimum_publishers; + + Ok(()) +} diff --git a/program/rust/src/processor/upd_permissions.rs b/program/rust/src/processor/upd_permissions.rs new file mode 100644 index 000000000..9fc28e869 --- /dev/null +++ b/program/rust/src/processor/upd_permissions.rs @@ -0,0 +1,91 @@ +use { + crate::{ + accounts::{ + PermissionAccount, + PythAccount, + PERMISSIONS_SEED, + }, + deserialize::{ + load, + load_checked, + }, + instruction::UpdPermissionsArgs, + utils::{ + check_is_upgrade_authority_for_program, + check_valid_funding_account, + check_valid_writable_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + pubkey::Pubkey, + system_program::check_id, + }, +}; + +/// Updates permissions for the pyth oracle program +/// This function can create and update the permissions accounts, which stores +/// several public keys that can execute administrative instructions in the pyth program +// key[0] upgrade authority [signer writable] +// key[1] program account [] +// key[2] programdata account [] +// key[3] permissions account [writable] +// key[4] system program [] +pub fn upd_permissions( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let [funding_account, program_account, programdata_account, permissions_account, system_program] = + match accounts { + [v, w, x, y, z] => Ok([v, w, x, y, z]), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let cmd_args = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_is_upgrade_authority_for_program( + funding_account, + program_account, + programdata_account, + program_id, + )?; + + let (permission_pda_address, bump_seed) = + Pubkey::find_program_address(&[PERMISSIONS_SEED.as_bytes()], program_id); + pyth_assert( + permission_pda_address == *permissions_account.key, + OracleError::InvalidPda.into(), + )?; + + pyth_assert( + check_id(system_program.key), + OracleError::InvalidSystemAccount.into(), + )?; + + + // Create PermissionAccount if it doesn't exist + PermissionAccount::initialize_pda( + permissions_account, + funding_account, + system_program, + program_id, + &[PERMISSIONS_SEED.as_bytes(), &[bump_seed]], + cmd_args.header.version, + )?; + + check_valid_writable_account(program_id, permissions_account)?; + + let mut permissions_account_data = + load_checked::(permissions_account, cmd_args.header.version)?; + permissions_account_data.master_authority = cmd_args.master_authority; + + permissions_account_data.data_curation_authority = cmd_args.data_curation_authority; + permissions_account_data.security_authority = cmd_args.security_authority; + + Ok(()) +} diff --git a/program/rust/src/processor/upd_price.rs b/program/rust/src/processor/upd_price.rs new file mode 100644 index 000000000..2d8cb0578 --- /dev/null +++ b/program/rust/src/processor/upd_price.rs @@ -0,0 +1,169 @@ +use { + crate::{ + accounts::{ + PriceAccount, + PriceInfo, + }, + c_oracle_header::{ + MAX_CI_DIVISOR, + PC_STATUS_UNKNOWN, + }, + deserialize::{ + load, + load_checked, + }, + instruction::UpdPriceArgs, + utils::{ + check_valid_funding_account, + check_valid_writable_account, + is_component_update, + pyth_assert, + try_convert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + clock::Clock, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + sysvar::Sysvar, + }, +}; + +#[cfg(target_arch = "bpf")] +#[link(name = "cpyth-bpf")] +extern "C" { + pub fn c_upd_aggregate(_input: *mut u8, clock_slot: u64, clock_timestamp: i64) -> bool; +} + +#[cfg(not(target_arch = "bpf"))] +#[link(name = "cpyth-native")] +extern "C" { + pub fn c_upd_aggregate(_input: *mut u8, clock_slot: u64, clock_timestamp: i64) -> bool; +} + +/// Publish component price, never returning an error even if the update failed +// account[0] funding account [signer writable] +// account[1] price account [writable] +// account[2] sysvar_clock account [] +pub fn upd_price_no_fail_on_error( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + match upd_price(program_id, accounts, instruction_data) { + Err(_) => Ok(()), + Ok(value) => Ok(value), + } +} + +/// Publish component price +// account[0] funding account [signer writable] +// account[1] price account [writable] +// account[2] sysvar_clock account [] +pub fn upd_price( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd_args = load::(instruction_data)?; + + let [funding_account, price_account, clock_account] = match accounts { + [x, y, z] => Ok([x, y, z]), + [x, y, _, z] => Ok([x, y, z]), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + check_valid_funding_account(funding_account)?; + check_valid_writable_account(program_id, price_account)?; + // Check clock + let clock = Clock::from_account_info(clock_account)?; + + let mut publisher_index: usize = 0; + let latest_aggregate_price: PriceInfo; + { + // Verify that symbol account is initialized + let price_data = load_checked::(price_account, cmd_args.header.version)?; + + // Verify that publisher is authorized + while publisher_index < try_convert::(price_data.num_)? { + if price_data.comp_[publisher_index].pub_ == *funding_account.key { + break; + } + publisher_index += 1; + } + pyth_assert( + publisher_index < try_convert::(price_data.num_)?, + ProgramError::InvalidArgument, + )?; + + + latest_aggregate_price = price_data.agg_; + let latest_publisher_price = price_data.comp_[publisher_index].latest_; + + // Check that publisher is publishing a more recent price + pyth_assert( + !is_component_update(cmd_args)? + || cmd_args.publishing_slot > latest_publisher_price.pub_slot_, + ProgramError::InvalidArgument, + )?; + } + + // Try to update the aggregate + #[allow(unused_variables)] + let mut aggregate_updated = false; + if clock.slot > latest_aggregate_price.pub_slot_ { + #[allow(unused_assignments)] + unsafe { + aggregate_updated = c_upd_aggregate( + price_account.try_borrow_mut_data()?.as_mut_ptr(), + clock.slot, + clock.unix_timestamp, + ); + } + } + + #[cfg(test)] + // Sma feature disabled in production for now + { + use crate::accounts::PythAccount; + let account_len = price_account.try_data_len()?; + if aggregate_updated + && account_len == crate::time_machine_types::PriceAccountWrapper::MINIMUM_SIZE + { + let mut price_account = load_checked::( + price_account, + cmd_args.header.version, + )?; + price_account.add_price_to_time_machine()?; + } + } + + // Try to update the publisher's price + if is_component_update(cmd_args)? { + let mut status: u32 = cmd_args.status; + let mut threshold_conf = cmd_args.price / MAX_CI_DIVISOR; + + if threshold_conf < 0 { + threshold_conf = -threshold_conf; + } + + if cmd_args.confidence > try_convert::<_, u64>(threshold_conf)? { + status = PC_STATUS_UNKNOWN + } + + { + let mut price_data = + load_checked::(price_account, cmd_args.header.version)?; + let publisher_price = &mut price_data.comp_[publisher_index].latest_; + publisher_price.price_ = cmd_args.price; + publisher_price.conf_ = cmd_args.confidence; + publisher_price.status_ = status; + publisher_price.pub_slot_ = cmd_args.publishing_slot; + } + } + + Ok(()) +} diff --git a/program/rust/src/processor/upd_product.rs b/program/rust/src/processor/upd_product.rs new file mode 100644 index 000000000..9f6f8633a --- /dev/null +++ b/program/rust/src/processor/upd_product.rs @@ -0,0 +1,98 @@ +use { + crate::{ + accounts::ProductAccount, + c_oracle_header::PC_PROD_ACC_SIZE, + deserialize::{ + load, + load_checked, + }, + instruction::CommandHeader, + utils::{ + check_valid_funding_account, + check_valid_signable_account_or_permissioned_funding_account, + pyth_assert, + read_pc_str_t, + try_convert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + program_memory::sol_memcpy, + pubkey::Pubkey, + }, + std::mem::size_of, +}; + +/// Update the metadata associated with a product, overwriting any existing metadata. +/// The metadata is provided as a list of key-value pairs at the end of the `instruction_data`. +// account[0] funding account [signer writable] +// account[1] product account [signer writable] +pub fn upd_product( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (funding_account, product_account, permissions_account_option) = match accounts { + [x, y] => Ok((x, y, None)), + [x, y, p] => Ok((x, y, Some(p))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_funding_account(funding_account)?; + check_valid_signable_account_or_permissioned_funding_account( + program_id, + product_account, + funding_account, + permissions_account_option, + hdr, + )?; + + + { + // Validate that product_account contains the appropriate account header + let mut _product_data = load_checked::(product_account, hdr.version)?; + } + + pyth_assert( + instruction_data.len() >= size_of::(), + ProgramError::InvalidInstructionData, + )?; + let new_data_len = instruction_data.len() - size_of::(); + let max_data_len = try_convert::<_, usize>(PC_PROD_ACC_SIZE)? - size_of::(); + pyth_assert(new_data_len <= max_data_len, ProgramError::InvalidArgument)?; + + let new_data = &instruction_data[size_of::()..instruction_data.len()]; + let mut idx = 0; + // new_data must be a list of key-value pairs, both of which are instances of pc_str_t. + // Try reading the key-value pairs to validate that new_data is properly formatted. + while idx < new_data.len() { + let key = read_pc_str_t(&new_data[idx..])?; + idx += key.len(); + let value = read_pc_str_t(&new_data[idx..])?; + idx += value.len(); + } + + // This assertion shouldn't ever fail, but be defensive. + pyth_assert(idx == new_data.len(), ProgramError::InvalidArgument)?; + + { + let mut data = product_account.try_borrow_mut_data()?; + // Note that this memcpy doesn't necessarily overwrite all existing data in the account. + // This case is handled by updating the .size_ field below. + sol_memcpy( + &mut data[size_of::()..], + new_data, + new_data.len(), + ); + } + + let mut product_data = load_checked::(product_account, hdr.version)?; + product_data.header.size = try_convert(size_of::() + new_data.len())?; + + Ok(()) +} diff --git a/program/rust/src/rust_oracle.rs b/program/rust/src/rust_oracle.rs deleted file mode 100644 index b6e0148f8..000000000 --- a/program/rust/src/rust_oracle.rs +++ /dev/null @@ -1,956 +0,0 @@ -use { - crate::{ - c_oracle_header::{ - MappingAccount, - PermissionAccount, - PriceAccount, - PriceComponent, - PriceEma, - PriceInfo, - ProductAccount, - PythAccount, - MAX_CI_DIVISOR, - PC_COMP_SIZE, - PC_MAP_TABLE_SIZE, - PC_PROD_ACC_SIZE, - PC_PTYPE_UNKNOWN, - PC_STATUS_UNKNOWN, - PC_VERSION, - PERMISSIONS_SEED, - }, - deserialize::{ - create_pda_if_needed, - initialize_pyth_account_checked, - load, - load_checked, - }, - instruction::{ - AddPriceArgs, - AddPublisherArgs, - CommandHeader, - DelPublisherArgs, - InitPriceArgs, - SetMinPubArgs, - UpdPermissionsArgs, - UpdPriceArgs, - }, - time_machine_types::PriceAccountWrapper, - utils::{ - check_exponent_range, - check_is_upgrade_authority_for_program, - check_valid_funding_account, - check_valid_signable_account_or_permissioned_funding_account, - check_valid_writable_account, - get_rent, - is_component_update, - pyth_assert, - read_pc_str_t, - send_lamports, - try_convert, - }, - OracleError, - }, - bytemuck::bytes_of_mut, - solana_program::{ - account_info::AccountInfo, - clock::Clock, - entrypoint::ProgramResult, - program_error::ProgramError, - program_memory::{ - sol_memcpy, - sol_memset, - }, - pubkey::Pubkey, - rent::Rent, - system_program::check_id, - sysvar::Sysvar, - }, - std::mem::{ - size_of, - size_of_val, - }, -}; - - -#[cfg(target_arch = "bpf")] -#[link(name = "cpyth-bpf")] -extern "C" { - pub fn c_upd_aggregate(_input: *mut u8, clock_slot: u64, clock_timestamp: i64) -> bool; -} - -#[cfg(not(target_arch = "bpf"))] -#[link(name = "cpyth-native")] -extern "C" { - pub fn c_upd_aggregate(_input: *mut u8, clock_slot: u64, clock_timestamp: i64) -> bool; -} - -/// resizes a price account so that it fits the Time Machine -/// key[0] funding account [signer writable] -/// key[1] price account [Signer writable] -/// key[2] system program [readable] -pub fn resize_price_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, price_account, system_program, permissions_account_option) = - match accounts { - [x, y, z] => Ok((x, y, z, None)), - [x, y, z, p] => Ok((x, y, z, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let hdr = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - hdr, - )?; - pyth_assert( - check_id(system_program.key), - OracleError::InvalidSystemAccount.into(), - )?; - // Check that it is a valid initialized price account - { - load_checked::(price_account, PC_VERSION)?; - } - let account_len = price_account.try_data_len()?; - match account_len { - PriceAccount::MINIMUM_SIZE => { - // Ensure account is still rent exempt after resizing - let rent: Rent = get_rent()?; - let lamports_needed: u64 = rent - .minimum_balance(size_of::()) - .saturating_sub(price_account.lamports()); - if lamports_needed > 0 { - send_lamports( - funding_account, - price_account, - system_program, - lamports_needed, - )?; - } - // We do not need to zero allocate because we won't access the data in the same - // instruction - price_account.realloc(size_of::(), false)?; - - // Check that everything is ok - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - hdr, - )?; - - #[cfg(test)] - // Sma feature disabled except in tests - { - let mut price_account = - load_checked::(price_account, PC_VERSION)?; - // Initialize Time Machine - price_account.initialize_time_machine()?; - } - - Ok(()) - } - PriceAccountWrapper::MINIMUM_SIZE => Ok(()), - _ => Err(ProgramError::InvalidArgument), - } -} - - -/// initialize the first mapping account in a new linked-list of mapping accounts -/// accounts[0] funding account [signer writable] -/// accounts[1] new mapping account [signer writable] -pub fn init_mapping( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, fresh_mapping_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let hdr = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - fresh_mapping_account, - funding_account, - permissions_account_option, - hdr, - )?; - - // Initialize by setting to zero again (just in case) and populating the account header - initialize_pyth_account_checked::(fresh_mapping_account, hdr.version)?; - - Ok(()) -} - -pub fn add_mapping( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, cur_mapping, next_mapping, permissions_account_option) = match accounts { - [x, y, z] => Ok((x, y, z, None)), - [x, y, z, p] => Ok((x, y, z, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let hdr = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - cur_mapping, - funding_account, - permissions_account_option, - hdr, - )?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - next_mapping, - funding_account, - permissions_account_option, - hdr, - )?; - - let mut cur_mapping = load_checked::(cur_mapping, hdr.version)?; - pyth_assert( - cur_mapping.number_of_products == PC_MAP_TABLE_SIZE - && cur_mapping.next_mapping_account == Pubkey::default(), - ProgramError::InvalidArgument, - )?; - - initialize_pyth_account_checked::(next_mapping, hdr.version)?; - cur_mapping.next_mapping_account = *next_mapping.key; - - Ok(()) -} - -/// a publisher updates a price -/// accounts[0] publisher account [signer writable] -/// accounts[1] price account to update [writable] -/// accounts[2] sysvar clock [] -pub fn upd_price( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd_args = load::(instruction_data)?; - - let [funding_account, price_account, clock_account] = match accounts { - [x, y, z] => Ok([x, y, z]), - [x, y, _, z] => Ok([x, y, z]), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_writable_account(program_id, price_account)?; - // Check clock - let clock = Clock::from_account_info(clock_account)?; - - let mut publisher_index: usize = 0; - let latest_aggregate_price: PriceInfo; - { - // Verify that symbol account is initialized - let price_data = load_checked::(price_account, cmd_args.header.version)?; - - // Verify that publisher is authorized - while publisher_index < try_convert::(price_data.num_)? { - if price_data.comp_[publisher_index].pub_ == *funding_account.key { - break; - } - publisher_index += 1; - } - pyth_assert( - publisher_index < try_convert::(price_data.num_)?, - ProgramError::InvalidArgument, - )?; - - - latest_aggregate_price = price_data.agg_; - let latest_publisher_price = price_data.comp_[publisher_index].latest_; - - // Check that publisher is publishing a more recent price - pyth_assert( - !is_component_update(cmd_args)? - || cmd_args.publishing_slot > latest_publisher_price.pub_slot_, - ProgramError::InvalidArgument, - )?; - } - - // Try to update the aggregate - #[allow(unused_variables)] - let mut aggregate_updated = false; - if clock.slot > latest_aggregate_price.pub_slot_ { - #[allow(unused_assignments)] - unsafe { - aggregate_updated = c_upd_aggregate( - price_account.try_borrow_mut_data()?.as_mut_ptr(), - clock.slot, - clock.unix_timestamp, - ); - } - } - - #[cfg(test)] - // Sma feature disabled in production for now - { - let account_len = price_account.try_data_len()?; - if aggregate_updated && account_len == PriceAccountWrapper::MINIMUM_SIZE { - let mut price_account = - load_checked::(price_account, cmd_args.header.version)?; - price_account.add_price_to_time_machine()?; - } - } - - // Try to update the publisher's price - if is_component_update(cmd_args)? { - let mut status: u32 = cmd_args.status; - let mut threshold_conf = cmd_args.price / MAX_CI_DIVISOR; - - if threshold_conf < 0 { - threshold_conf = -threshold_conf; - } - - if cmd_args.confidence > try_convert::<_, u64>(threshold_conf)? { - status = PC_STATUS_UNKNOWN - } - - { - let mut price_data = - load_checked::(price_account, cmd_args.header.version)?; - let publisher_price = &mut price_data.comp_[publisher_index].latest_; - publisher_price.price_ = cmd_args.price; - publisher_price.conf_ = cmd_args.confidence; - publisher_price.status_ = status; - publisher_price.pub_slot_ = cmd_args.publishing_slot; - } - } - - Ok(()) -} - -pub fn upd_price_no_fail_on_error( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - match upd_price(program_id, accounts, instruction_data) { - Err(_) => Ok(()), - Ok(value) => Ok(value), - } -} - - -/// add a price account to a product account -/// accounts[0] funding account [signer writable] -/// accounts[1] product account to add the price account to [signer writable] -/// accounts[2] newly created price account [signer writable] -pub fn add_price( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd_args = load::(instruction_data)?; - - check_exponent_range(cmd_args.exponent)?; - pyth_assert( - cmd_args.price_type != PC_PTYPE_UNKNOWN, - ProgramError::InvalidArgument, - )?; - - - let (funding_account, product_account, price_account, permissions_account_option) = - match accounts { - [x, y, z] => Ok((x, y, z, None)), - [x, y, z, p] => Ok((x, y, z, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - product_account, - funding_account, - permissions_account_option, - &cmd_args.header, - )?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - &cmd_args.header, - )?; - - let mut product_data = - load_checked::(product_account, cmd_args.header.version)?; - - let mut price_data = - initialize_pyth_account_checked::(price_account, cmd_args.header.version)?; - price_data.exponent = cmd_args.exponent; - price_data.price_type = cmd_args.price_type; - price_data.product_account = *product_account.key; - price_data.next_price_account = product_data.first_price_account; - product_data.first_price_account = *price_account.key; - - Ok(()) -} - -/// Delete a price account. This function will remove the link between the price account and its -/// corresponding product account, then transfer any SOL in the price account to the funding -/// account. This function can only delete the first price account in the linked list of -/// price accounts for the given product. -/// -/// Warning: This function is dangerous and will break any programs that depend on the deleted -/// price account! -pub fn del_price( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, product_account, price_account, permissions_account_option) = - match accounts { - [w, x, y] => Ok((w, x, y, None)), - [w, x, y, p] => Ok((w, x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let cmd_args = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - product_account, - funding_account, - permissions_account_option, - cmd_args, - )?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - cmd_args, - )?; - - { - let mut product_data = load_checked::(product_account, cmd_args.version)?; - let price_data = load_checked::(price_account, cmd_args.version)?; - pyth_assert( - product_data.first_price_account == *price_account.key, - ProgramError::InvalidArgument, - )?; - - pyth_assert( - price_data.product_account == *product_account.key, - ProgramError::InvalidArgument, - )?; - - product_data.first_price_account = price_data.next_price_account; - } - - // Zero out the balance of the price account to delete it. - // Note that you can't use the system program's transfer instruction to do this operation, as - // that instruction fails if the source account has any data. - let lamports = price_account.lamports(); - **price_account.lamports.borrow_mut() = 0; - **funding_account.lamports.borrow_mut() += lamports; - - Ok(()) -} - -pub fn init_price( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd_args = load::(instruction_data)?; - - check_exponent_range(cmd_args.exponent)?; - - let (funding_account, price_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - &cmd_args.header, - )?; - - - let mut price_data = load_checked::(price_account, cmd_args.header.version)?; - pyth_assert( - price_data.price_type == cmd_args.price_type, - ProgramError::InvalidArgument, - )?; - - price_data.exponent = cmd_args.exponent; - - price_data.last_slot_ = 0; - price_data.valid_slot_ = 0; - price_data.agg_.pub_slot_ = 0; - price_data.prev_slot_ = 0; - price_data.prev_price_ = 0; - price_data.prev_conf_ = 0; - price_data.prev_timestamp_ = 0; - sol_memset( - bytes_of_mut(&mut price_data.twap_), - 0, - size_of::(), - ); - sol_memset( - bytes_of_mut(&mut price_data.twac_), - 0, - size_of::(), - ); - sol_memset( - bytes_of_mut(&mut price_data.agg_), - 0, - size_of::(), - ); - for i in 0..price_data.comp_.len() { - sol_memset( - bytes_of_mut(&mut price_data.comp_[i].agg_), - 0, - size_of::(), - ); - sol_memset( - bytes_of_mut(&mut price_data.comp_[i].latest_), - 0, - size_of::(), - ); - } - - Ok(()) -} - -/// add a publisher to a price account -/// accounts[0] funding account [signer writable] -/// accounts[1] price account to add the publisher to [signer writable] -pub fn add_publisher( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd_args = load::(instruction_data)?; - - pyth_assert( - instruction_data.len() == size_of::() - && cmd_args.publisher != Pubkey::default(), - ProgramError::InvalidArgument, - )?; - - let (funding_account, price_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - &cmd_args.header, - )?; - - - let mut price_data = load_checked::(price_account, cmd_args.header.version)?; - - if price_data.num_ >= PC_COMP_SIZE { - return Err(ProgramError::InvalidArgument); - } - - for i in 0..(try_convert::(price_data.num_)?) { - if cmd_args.publisher == price_data.comp_[i].pub_ { - return Err(ProgramError::InvalidArgument); - } - } - - let current_index: usize = try_convert(price_data.num_)?; - sol_memset( - bytes_of_mut(&mut price_data.comp_[current_index]), - 0, - size_of::(), - ); - price_data.comp_[current_index].pub_ = cmd_args.publisher; - price_data.num_ += 1; - price_data.header.size = - try_convert::<_, u32>(size_of::() - size_of_val(&price_data.comp_))? - + price_data.num_ * try_convert::<_, u32>(size_of::())?; - Ok(()) -} - -/// add a publisher to a price account -/// accounts[0] funding account [signer writable] -/// accounts[1] price account to delete the publisher from [signer writable] -pub fn del_publisher( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd_args = load::(instruction_data)?; - - pyth_assert( - instruction_data.len() == size_of::() - && cmd_args.publisher != Pubkey::default(), - ProgramError::InvalidArgument, - )?; - - let (funding_account, price_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - &cmd_args.header, - )?; - - let mut price_data = load_checked::(price_account, cmd_args.header.version)?; - - for i in 0..(try_convert::(price_data.num_)?) { - if cmd_args.publisher == price_data.comp_[i].pub_ { - for j in i + 1..(try_convert::(price_data.num_)?) { - price_data.comp_[j - 1] = price_data.comp_[j]; - } - price_data.num_ -= 1; - let current_index: usize = try_convert(price_data.num_)?; - sol_memset( - bytes_of_mut(&mut price_data.comp_[current_index]), - 0, - size_of::(), - ); - price_data.header.size = - try_convert::<_, u32>(size_of::() - size_of_val(&price_data.comp_))? - + price_data.num_ * try_convert::<_, u32>(size_of::())?; - return Ok(()); - } - } - Err(ProgramError::InvalidArgument) -} - -pub fn add_product( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, tail_mapping_account, new_product_account, permissions_account_option) = - match accounts { - [x, y, z] => Ok((x, y, z, None)), - [x, y, z, p] => Ok((x, y, z, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let hdr = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - tail_mapping_account, - funding_account, - permissions_account_option, - hdr, - )?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - new_product_account, - funding_account, - permissions_account_option, - hdr, - )?; - - - let mut mapping_data = load_checked::(tail_mapping_account, hdr.version)?; - // The mapping account must have free space to add the product account - pyth_assert( - mapping_data.number_of_products < PC_MAP_TABLE_SIZE, - ProgramError::InvalidArgument, - )?; - - initialize_pyth_account_checked::(new_product_account, hdr.version)?; - - let current_index: usize = try_convert(mapping_data.number_of_products)?; - mapping_data.products_list[current_index] = *new_product_account.key; - mapping_data.number_of_products += 1; - mapping_data.header.size = try_convert::<_, u32>( - size_of::() - size_of_val(&mapping_data.products_list), - )? + mapping_data.number_of_products - * try_convert::<_, u32>(size_of::())?; - - Ok(()) -} - -/// Update the metadata associated with a product, overwriting any existing metadata. -/// The metadata is provided as a list of key-value pairs at the end of the `instruction_data`. -pub fn upd_product( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, product_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let hdr = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - product_account, - funding_account, - permissions_account_option, - hdr, - )?; - - - { - // Validate that product_account contains the appropriate account header - let mut _product_data = load_checked::(product_account, hdr.version)?; - } - - pyth_assert( - instruction_data.len() >= size_of::(), - ProgramError::InvalidInstructionData, - )?; - let new_data_len = instruction_data.len() - size_of::(); - let max_data_len = try_convert::<_, usize>(PC_PROD_ACC_SIZE)? - size_of::(); - pyth_assert(new_data_len <= max_data_len, ProgramError::InvalidArgument)?; - - let new_data = &instruction_data[size_of::()..instruction_data.len()]; - let mut idx = 0; - // new_data must be a list of key-value pairs, both of which are instances of pc_str_t. - // Try reading the key-value pairs to validate that new_data is properly formatted. - while idx < new_data.len() { - let key = read_pc_str_t(&new_data[idx..])?; - idx += key.len(); - let value = read_pc_str_t(&new_data[idx..])?; - idx += value.len(); - } - - // This assertion shouldn't ever fail, but be defensive. - pyth_assert(idx == new_data.len(), ProgramError::InvalidArgument)?; - - { - let mut data = product_account.try_borrow_mut_data()?; - // Note that this memcpy doesn't necessarily overwrite all existing data in the account. - // This case is handled by updating the .size_ field below. - sol_memcpy( - &mut data[size_of::()..], - new_data, - new_data.len(), - ); - } - - let mut product_data = load_checked::(product_account, hdr.version)?; - product_data.header.size = try_convert(size_of::() + new_data.len())?; - - Ok(()) -} - -pub fn set_min_pub( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd = load::(instruction_data)?; - - pyth_assert( - instruction_data.len() == size_of::(), - ProgramError::InvalidArgument, - )?; - - let (funding_account, price_account, permissions_account_option) = match accounts { - [x, y] => Ok((x, y, None)), - [x, y, p] => Ok((x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account_option, - &cmd.header, - )?; - - - let mut price_account_data = load_checked::(price_account, cmd.header.version)?; - price_account_data.min_pub_ = cmd.minimum_publishers; - - Ok(()) -} - -/// Delete a product account and remove it from the product list of its associated mapping account. -/// The deleted product account must not have any price accounts. -/// -/// This function allows you to delete products from non-tail mapping accounts. This ability is a -/// little weird, as it allows you to construct a list of multiple mapping accounts where non-tail -/// accounts have empty space. This is fine however; users should simply add new products to the -/// first available spot. -pub fn del_product( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (funding_account, mapping_account, product_account, permissions_account_option) = - match accounts { - [w, x, y] => Ok((w, x, y, None)), - [w, x, y, p] => Ok((w, x, y, Some(p))), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let cmd_args = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_valid_signable_account_or_permissioned_funding_account( - program_id, - mapping_account, - funding_account, - permissions_account_option, - cmd_args, - )?; - - check_valid_signable_account_or_permissioned_funding_account( - program_id, - product_account, - funding_account, - permissions_account_option, - cmd_args, - )?; - - - { - let mut mapping_data = load_checked::(mapping_account, cmd_args.version)?; - let product_data = load_checked::(product_account, cmd_args.version)?; - - // This assertion is just to make the subtractions below simpler - pyth_assert( - mapping_data.number_of_products >= 1, - ProgramError::InvalidArgument, - )?; - pyth_assert( - product_data.first_price_account == Pubkey::default(), - ProgramError::InvalidArgument, - )?; - - let product_key = product_account.key; - let product_index = mapping_data - .products_list - .iter() - .position(|x| *x == *product_key) - .ok_or(ProgramError::InvalidArgument)?; - - let num_after_removal: usize = try_convert( - mapping_data - .number_of_products - .checked_sub(1) - .ok_or(ProgramError::InvalidArgument)?, - )?; - - let last_key_bytes = mapping_data.products_list[num_after_removal]; - mapping_data.products_list[product_index] = last_key_bytes; - mapping_data.products_list[num_after_removal] = Pubkey::default(); - mapping_data.number_of_products = try_convert::<_, u32>(num_after_removal)?; - mapping_data.header.size = try_convert::<_, u32>( - size_of::() - size_of_val(&mapping_data.products_list), - )? + mapping_data.number_of_products - * try_convert::<_, u32>(size_of::())?; - } - - // Zero out the balance of the price account to delete it. - // Note that you can't use the system program's transfer instruction to do this operation, as - // that instruction fails if the source account has any data. - let lamports = product_account.lamports(); - **product_account.lamports.borrow_mut() = 0; - **funding_account.lamports.borrow_mut() += lamports; - - Ok(()) -} - - -/// Updates permissions for the pyth oracle program -/// This function can create and update the permissions accounts, which stores -/// several public keys that can execute administrative instructions in the pyth program -pub fn upd_permissions( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let [funding_account, program_account, programdata_account, permissions_account, system_program] = - match accounts { - [v, w, x, y, z] => Ok([v, w, x, y, z]), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - let cmd_args = load::(instruction_data)?; - - check_valid_funding_account(funding_account)?; - check_is_upgrade_authority_for_program( - funding_account, - program_account, - programdata_account, - program_id, - )?; - - let (permission_pda_address, bump_seed) = - Pubkey::find_program_address(&[PERMISSIONS_SEED.as_bytes()], program_id); - pyth_assert( - permission_pda_address == *permissions_account.key, - OracleError::InvalidPda.into(), - )?; - - pyth_assert( - check_id(system_program.key), - OracleError::InvalidSystemAccount.into(), - )?; - - - // Create permissions account if it doesn't exist - create_pda_if_needed::( - permissions_account, - funding_account, - system_program, - program_id, - &[PERMISSIONS_SEED.as_bytes(), &[bump_seed]], - cmd_args.header.version, - )?; - - check_valid_writable_account(program_id, permissions_account)?; - - let mut permissions_account_data = - load_checked::(permissions_account, cmd_args.header.version)?; - permissions_account_data.master_authority = cmd_args.master_authority; - - permissions_account_data.data_curation_authority = cmd_args.data_curation_authority; - permissions_account_data.security_authority = cmd_args.security_authority; - - Ok(()) -} diff --git a/program/rust/src/tests/pyth_simulator.rs b/program/rust/src/tests/pyth_simulator.rs index b9314ed22..c06771684 100644 --- a/program/rust/src/tests/pyth_simulator.rs +++ b/program/rust/src/tests/pyth_simulator.rs @@ -1,11 +1,13 @@ use { crate::{ - c_oracle_header::{ + accounts::{ MappingAccount, PriceAccount, + PERMISSIONS_SEED, + }, + c_oracle_header::{ PC_PROD_ACC_SIZE, PC_PTYPE_PRICE, - PERMISSIONS_SEED, }, deserialize::load, instruction::{ diff --git a/program/rust/src/tests/test_add_mapping.rs b/program/rust/src/tests/test_add_mapping.rs index 087ce94a1..2719a61eb 100644 --- a/program/rust/src/tests/test_add_mapping.rs +++ b/program/rust/src/tests/test_add_mapping.rs @@ -1,13 +1,16 @@ use { crate::{ - c_oracle_header::{ + accounts::{ + clear_account, MappingAccount, + PythAccount, + }, + c_oracle_header::{ PC_MAGIC, PC_MAP_TABLE_SIZE, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_account_as_mut, load_checked, }, @@ -18,7 +21,6 @@ use { }, processor::process_instruction, tests::test_utils::AccountSetup, - utils::clear_account, }, bytemuck::bytes_of, solana_program::{ @@ -39,7 +41,7 @@ fn test_add_mapping() { let mut curr_mapping_setup = AccountSetup::new::(&program_id); let cur_mapping = curr_mapping_setup.as_account_info(); - initialize_pyth_account_checked::(&cur_mapping, PC_VERSION).unwrap(); + MappingAccount::initialize(&cur_mapping, PC_VERSION).unwrap(); let mut next_mapping_setup = AccountSetup::new::(&program_id); let next_mapping = next_mapping_setup.as_account_info(); diff --git a/program/rust/src/tests/test_add_price.rs b/program/rust/src/tests/test_add_price.rs index 62b617e39..f5e7bbbc9 100644 --- a/program/rust/src/tests/test_add_price.rs +++ b/program/rust/src/tests/test_add_price.rs @@ -1,15 +1,14 @@ use { crate::{ - c_oracle_header::{ + accounts::{ + clear_account, MappingAccount, PriceAccount, ProductAccount, - PC_VERSION, - }, - deserialize::{ - initialize_pyth_account_checked, - load_checked, + PythAccount, }, + c_oracle_header::PC_VERSION, + deserialize::load_checked, error::OracleError, instruction::{ AddPriceArgs, @@ -18,7 +17,6 @@ use { }, processor::process_instruction, tests::test_utils::AccountSetup, - utils::clear_account, }, bytemuck::bytes_of, solana_program::{ @@ -46,7 +44,7 @@ fn test_add_price() { let mut mapping_setup = AccountSetup::new::(&program_id); let mapping_account = mapping_setup.as_account_info(); - initialize_pyth_account_checked::(&mapping_account, PC_VERSION).unwrap(); + MappingAccount::initialize(&mapping_account, PC_VERSION).unwrap(); let mut product_setup = AccountSetup::new::(&program_id); let product_account = product_setup.as_account_info(); diff --git a/program/rust/src/tests/test_add_product.rs b/program/rust/src/tests/test_add_product.rs index 4d9ec2316..4ec62d201 100644 --- a/program/rust/src/tests/test_add_product.rs +++ b/program/rust/src/tests/test_add_product.rs @@ -1,9 +1,12 @@ use { crate::{ - c_oracle_header::{ + accounts::{ + clear_account, MappingAccount, ProductAccount, PythAccount, + }, + c_oracle_header::{ PC_ACCTYPE_PRODUCT, PC_MAGIC, PC_MAP_TABLE_SIZE, @@ -11,7 +14,6 @@ use { PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_account_as, load_checked, }, @@ -22,7 +24,6 @@ use { }, processor::process_instruction, tests::test_utils::AccountSetup, - utils::clear_account, }, bytemuck::bytes_of, solana_program::{ @@ -48,7 +49,7 @@ fn test_add_product() { let mut mapping_setup = AccountSetup::new::(&program_id); let mapping_account = mapping_setup.as_account_info(); - initialize_pyth_account_checked::(&mapping_account, PC_VERSION).unwrap(); + MappingAccount::initialize(&mapping_account, PC_VERSION).unwrap(); let mut product_setup = AccountSetup::new::(&program_id); let product_account = product_setup.as_account_info(); @@ -132,7 +133,7 @@ fn test_add_product() { // test fill up of mapping table clear_account(&mapping_account).unwrap(); - initialize_pyth_account_checked::(&mapping_account, PC_VERSION).unwrap(); + MappingAccount::initialize(&mapping_account, PC_VERSION).unwrap(); for i in 0..PC_MAP_TABLE_SIZE { clear_account(&product_account).unwrap(); diff --git a/program/rust/src/tests/test_add_publisher.rs b/program/rust/src/tests/test_add_publisher.rs index 1a233a1a0..d803694cc 100644 --- a/program/rust/src/tests/test_add_publisher.rs +++ b/program/rust/src/tests/test_add_publisher.rs @@ -1,23 +1,22 @@ use { crate::{ - c_oracle_header::{ + accounts::{ + clear_account, PriceAccount, PriceComponent, PythAccount, + }, + c_oracle_header::{ PC_COMP_SIZE, PC_VERSION, }, - deserialize::{ - initialize_pyth_account_checked, - load_checked, - }, + deserialize::load_checked, instruction::{ AddPublisherArgs, OracleCommand, }, processor::process_instruction, tests::test_utils::AccountSetup, - utils::clear_account, OracleError, }, bytemuck::bytes_of, @@ -45,7 +44,7 @@ fn test_add_publisher() { let mut price_setup = AccountSetup::new::(&program_id); let price_account = price_setup.as_account_info(); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); **price_account.try_borrow_mut_lamports().unwrap() = 100; @@ -104,7 +103,7 @@ fn test_add_publisher() { Err(OracleError::InvalidAccountHeader.into()) ); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); // Fill up price node for i in 0..PC_COMP_SIZE { diff --git a/program/rust/src/tests/test_del_price.rs b/program/rust/src/tests/test_del_price.rs index cfa0d222b..27c3f7fe7 100644 --- a/program/rust/src/tests/test_del_price.rs +++ b/program/rust/src/tests/test_del_price.rs @@ -1,6 +1,6 @@ use { crate::{ - c_oracle_header::ProductAccount, + accounts::ProductAccount, tests::pyth_simulator::PythSimulator, }, solana_program::pubkey::Pubkey, diff --git a/program/rust/src/tests/test_del_product.rs b/program/rust/src/tests/test_del_product.rs index b3d9c1500..02e315ef8 100644 --- a/program/rust/src/tests/test_del_product.rs +++ b/program/rust/src/tests/test_del_product.rs @@ -1,6 +1,6 @@ use { crate::{ - c_oracle_header::MappingAccount, + accounts::MappingAccount, tests::pyth_simulator::PythSimulator, }, solana_program::pubkey::Pubkey, diff --git a/program/rust/src/tests/test_del_publisher.rs b/program/rust/src/tests/test_del_publisher.rs index ce828634e..9b83bc309 100644 --- a/program/rust/src/tests/test_del_publisher.rs +++ b/program/rust/src/tests/test_del_publisher.rs @@ -1,15 +1,16 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, PriceComponent, PriceInfo, PythAccount, + }, + c_oracle_header::{ PC_STATUS_TRADING, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -56,7 +57,7 @@ fn test_del_publisher() { let mut price_setup = AccountSetup::new::(&program_id); let price_account = price_setup.as_account_info(); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); { let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); price_data.num_ = 1; diff --git a/program/rust/src/tests/test_init_mapping.rs b/program/rust/src/tests/test_init_mapping.rs index 4bcc19ee2..95c182b93 100644 --- a/program/rust/src/tests/test_init_mapping.rs +++ b/program/rust/src/tests/test_init_mapping.rs @@ -1,16 +1,17 @@ use { crate::{ - c_oracle_header::{ + accounts::{ + clear_account, MappingAccount, PermissionAccount, + PythAccount, + }, + c_oracle_header::{ PC_ACCTYPE_MAPPING, PC_MAGIC, PC_VERSION, }, - deserialize::{ - initialize_pyth_account_checked, - load_account_as, - }, + deserialize::load_account_as, error::OracleError, instruction::{ CommandHeader, @@ -18,7 +19,6 @@ use { }, processor::process_instruction, tests::test_utils::AccountSetup, - utils::clear_account, }, bytemuck::bytes_of, solana_program::pubkey::Pubkey, @@ -180,8 +180,7 @@ fn test_init_mapping() { { let mut permissions_account_data = - initialize_pyth_account_checked::(&permissions_account, PC_VERSION) - .unwrap(); + PermissionAccount::initialize(&permissions_account, PC_VERSION).unwrap(); permissions_account_data.master_authority = *funding_account.key; } @@ -205,11 +204,7 @@ fn test_init_mapping() { { let mut impersonating_permission_account_data = - initialize_pyth_account_checked::( - &impersonating_permission_account, - PC_VERSION, - ) - .unwrap(); + PermissionAccount::initialize(&impersonating_permission_account, PC_VERSION).unwrap(); impersonating_permission_account_data.master_authority = *attacker_account.key; } diff --git a/program/rust/src/tests/test_init_price.rs b/program/rust/src/tests/test_init_price.rs index 5211704b4..a3f60da38 100644 --- a/program/rust/src/tests/test_init_price.rs +++ b/program/rust/src/tests/test_init_price.rs @@ -1,14 +1,14 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, + PythAccount, + }, + c_oracle_header::{ MAX_NUM_DECIMALS, PC_VERSION, }, - deserialize::{ - initialize_pyth_account_checked, - load_checked, - }, + deserialize::load_checked, instruction::{ InitPriceArgs, OracleCommand, @@ -58,7 +58,7 @@ fn test_init_price() { Err(OracleError::InvalidAccountHeader.into()) ); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); { let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); price_data.price_type = ptype; diff --git a/program/rust/src/tests/test_permission_migration.rs b/program/rust/src/tests/test_permission_migration.rs index 480fcdf32..e9808e30c 100644 --- a/program/rust/src/tests/test_permission_migration.rs +++ b/program/rust/src/tests/test_permission_migration.rs @@ -1,13 +1,13 @@ use { crate::{ - c_oracle_header::{ + accounts::{ MappingAccount, PermissionAccount, PriceAccount, ProductAccount, - PC_VERSION, + PythAccount, }, - deserialize::initialize_pyth_account_checked, + c_oracle_header::PC_VERSION, error::OracleError, instruction::{ AddPriceArgs, @@ -72,8 +72,7 @@ fn test_permission_migration() { { let mut permissions_account_data = - initialize_pyth_account_checked::(&permissions_account, PC_VERSION) - .unwrap(); + PermissionAccount::initialize(&permissions_account, PC_VERSION).unwrap(); permissions_account_data.master_authority = *funding_account.key; } diff --git a/program/rust/src/tests/test_set_min_pub.rs b/program/rust/src/tests/test_set_min_pub.rs index d1a0c6d08..031271c97 100644 --- a/program/rust/src/tests/test_set_min_pub.rs +++ b/program/rust/src/tests/test_set_min_pub.rs @@ -1,11 +1,11 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, - PC_VERSION, + PythAccount, }, + c_oracle_header::PC_VERSION, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -35,7 +35,7 @@ fn test_set_min_pub() { let mut price_setup = AccountSetup::new::(&program_id); let price_account = price_setup.as_account_info(); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); assert_eq!(get_min_pub(&price_account), Ok(0)); diff --git a/program/rust/src/tests/test_sizes.rs b/program/rust/src/tests/test_sizes.rs index 62c33f6ed..60f195f55 100644 --- a/program/rust/src/tests/test_sizes.rs +++ b/program/rust/src/tests/test_sizes.rs @@ -1,6 +1,6 @@ use { crate::{ - c_oracle_header::{ + accounts::{ AccountHeader, MappingAccount, PermissionAccount, @@ -10,13 +10,14 @@ use { PriceInfo, ProductAccount, PythAccount, + }, + c_oracle_header::{ PC_COMP_SIZE, PC_MAP_TABLE_SIZE, PC_VERSION, PRICE_ACCOUNT_SIZE, }, deserialize::{ - initialize_pyth_account_checked, load, load_checked, }, @@ -87,7 +88,7 @@ fn test_offsets() { let mut price_setup = AccountSetup::new::(&program_id); let price_account = price_setup.as_account_info(); - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); assert_eq!( @@ -98,7 +99,7 @@ fn test_offsets() { let mut mapping_setup = AccountSetup::new::(&program_id); let mapping_account = mapping_setup.as_account_info(); - initialize_pyth_account_checked::(&mapping_account, PC_VERSION).unwrap(); + MappingAccount::initialize(&mapping_account, PC_VERSION).unwrap(); let mapping_data = load_checked::(&mapping_account, PC_VERSION).unwrap(); assert_eq!( diff --git a/program/rust/src/tests/test_sma_epoch_transition.rs b/program/rust/src/tests/test_sma_epoch_transition.rs index 111bf7ae8..848d280f1 100644 --- a/program/rust/src/tests/test_sma_epoch_transition.rs +++ b/program/rust/src/tests/test_sma_epoch_transition.rs @@ -1,12 +1,12 @@ use { crate::{ + accounts::PythAccount, c_oracle_header::{ PC_MAX_SEND_LATENCY, PC_STATUS_TRADING, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -43,7 +43,7 @@ fn test_sma_epoch_transition() { let mut price_setup = AccountSetup::new::(&program_id); let mut price_account = price_setup.as_account_info(); price_account.is_signer = false; - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccountWrapper::initialize(&price_account, PC_VERSION).unwrap(); { diff --git a/program/rust/src/tests/test_upd_aggregate.rs b/program/rust/src/tests/test_upd_aggregate.rs index c792ce1a3..c4a8f4977 100644 --- a/program/rust/src/tests/test_upd_aggregate.rs +++ b/program/rust/src/tests/test_upd_aggregate.rs @@ -1,14 +1,16 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, PriceInfo, + PythAccount, + }, + c_oracle_header::{ PC_STATUS_TRADING, PC_STATUS_UNKNOWN, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -16,7 +18,7 @@ use { OracleCommand, UpdPriceArgs, }, - rust_oracle::c_upd_aggregate, + processor::c_upd_aggregate, tests::test_utils::AccountSetup, }, solana_program::pubkey::Pubkey, @@ -65,7 +67,7 @@ fn test_upd_aggregate() { let mut price_setup = AccountSetup::new::(&program_id); let mut price_account = price_setup.as_account_info(); price_account.is_signer = false; - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); // single publisher { diff --git a/program/rust/src/tests/test_upd_permissions.rs b/program/rust/src/tests/test_upd_permissions.rs index 16e6ae64a..bd6c7c4df 100644 --- a/program/rust/src/tests/test_upd_permissions.rs +++ b/program/rust/src/tests/test_upd_permissions.rs @@ -1,6 +1,6 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PermissionAccount, PythAccount, }, diff --git a/program/rust/src/tests/test_upd_price.rs b/program/rust/src/tests/test_upd_price.rs index 59c69a4b5..b497a2660 100644 --- a/program/rust/src/tests/test_upd_price.rs +++ b/program/rust/src/tests/test_upd_price.rs @@ -1,13 +1,15 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, + PythAccount, + }, + c_oracle_header::{ PC_STATUS_TRADING, PC_STATUS_UNKNOWN, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -41,7 +43,7 @@ fn test_upd_price() { let mut price_setup = AccountSetup::new::(&program_id); let mut price_account = price_setup.as_account_info(); price_account.is_signer = false; - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); { let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); diff --git a/program/rust/src/tests/test_upd_price_no_fail_on_error.rs b/program/rust/src/tests/test_upd_price_no_fail_on_error.rs index 27e730e0f..fe4259bf4 100644 --- a/program/rust/src/tests/test_upd_price_no_fail_on_error.rs +++ b/program/rust/src/tests/test_upd_price_no_fail_on_error.rs @@ -1,13 +1,15 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, + PythAccount, + }, + c_oracle_header::{ PC_STATUS_TRADING, PC_STATUS_UNKNOWN, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -40,7 +42,7 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { let mut price_setup = AccountSetup::new::(&program_id); let mut price_account = price_setup.as_account_info(); price_account.is_signer = false; - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); let mut clock_setup = AccountSetup::new_clock(); let mut clock_account = clock_setup.as_account_info(); diff --git a/program/rust/src/tests/test_upd_product.rs b/program/rust/src/tests/test_upd_product.rs index 595172e79..8a2c7f200 100644 --- a/program/rust/src/tests/test_upd_product.rs +++ b/program/rust/src/tests/test_upd_product.rs @@ -1,13 +1,14 @@ use { crate::{ - c_oracle_header::{ + accounts::{ ProductAccount, PythAccount, + }, + c_oracle_header::{ PC_PROD_ACC_SIZE, PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -42,7 +43,7 @@ fn test_upd_product() { let mut product_setup = AccountSetup::new::(&program_id); let product_account = product_setup.as_account_info(); - initialize_pyth_account_checked::(&product_account, PC_VERSION).unwrap(); + ProductAccount::initialize(&product_account, PC_VERSION).unwrap(); let kvs = ["foo", "barz"]; let size = populate_instruction(&mut instruction_data, &kvs); diff --git a/program/rust/src/tests/test_upd_sma.rs b/program/rust/src/tests/test_upd_sma.rs index 1d757d819..f69a1e50c 100644 --- a/program/rust/src/tests/test_upd_sma.rs +++ b/program/rust/src/tests/test_upd_sma.rs @@ -1,3 +1,4 @@ +use crate::accounts::PythAccount; // use crate::processor::process_instruction; use { crate::{ @@ -8,7 +9,6 @@ use { PC_VERSION, }, deserialize::{ - initialize_pyth_account_checked, load_checked, load_mut, }, @@ -47,7 +47,7 @@ fn test_upd_sma() { let mut price_setup = AccountSetup::new::(&program_id); let mut price_account = price_setup.as_account_info(); price_account.is_signer = false; - initialize_pyth_account_checked::(&price_account, PC_VERSION).unwrap(); + PriceAccountWrapper::initialize(&price_account, PC_VERSION).unwrap(); { diff --git a/program/rust/src/tests/test_utils.rs b/program/rust/src/tests/test_utils.rs index 53a5d4890..6e97bc69d 100644 --- a/program/rust/src/tests/test_utils.rs +++ b/program/rust/src/tests/test_utils.rs @@ -1,11 +1,11 @@ use { crate::{ - c_oracle_header::{ + accounts::{ PermissionAccount, PythAccount, - PC_VERSION, PERMISSIONS_SEED, }, + c_oracle_header::PC_VERSION, error::OracleError, instruction::{ CommandHeader, diff --git a/program/rust/src/time_machine_types.rs b/program/rust/src/time_machine_types.rs index 2fbec88c5..767cf27ab 100644 --- a/program/rust/src/time_machine_types.rs +++ b/program/rust/src/time_machine_types.rs @@ -2,9 +2,11 @@ #![allow(dead_code)] use { crate::{ - c_oracle_header::{ + accounts::{ PriceAccount, PythAccount, + }, + c_oracle_header::{ EXTRA_PUBLISHER_SPACE, PC_ACCTYPE_PRICE, PC_MAX_SEND_LATENCY, diff --git a/program/rust/src/utils.rs b/program/rust/src/utils.rs index e9f066f83..7ac099570 100644 --- a/program/rust/src/utils.rs +++ b/program/rust/src/utils.rs @@ -1,11 +1,11 @@ use { crate::{ - c_oracle_header::{ + accounts::{ AccountHeader, PermissionAccount, - MAX_NUM_DECIMALS, PERMISSIONS_SEED, }, + c_oracle_header::MAX_NUM_DECIMALS, deserialize::{ load_account_as, load_checked, @@ -21,21 +21,12 @@ use { solana_program::{ account_info::AccountInfo, bpf_loader_upgradeable::UpgradeableLoaderState, - program::{ - invoke, - invoke_signed, - }, + program::invoke, program_error::ProgramError, - program_memory::sol_memset, pubkey::Pubkey, - system_instruction::{ - allocate, - assign, - transfer, - }, + system_instruction::transfer, sysvar::rent::Rent, }, - std::borrow::BorrowMut, }; @@ -47,16 +38,6 @@ pub fn pyth_assert(condition: bool, error_code: ProgramError) -> Result<(), Prog } } -/// Sets the data of account to all-zero -pub fn clear_account(account: &AccountInfo) -> Result<(), ProgramError> { - let mut data = account - .try_borrow_mut_data() - .map_err(|_| ProgramError::InvalidArgument)?; - let length = data.len(); - sol_memset(data.borrow_mut(), 0, length); - Ok(()) -} - pub fn valid_funding_account(account: &AccountInfo) -> bool { account.is_signer && account.is_writable } @@ -295,36 +276,6 @@ pub fn send_lamports<'a>( Ok(()) } -pub fn allocate_data<'a>( - account: &AccountInfo<'a>, - system_program: &AccountInfo<'a>, - space: usize, - seeds: &[&[u8]], -) -> Result<(), ProgramError> { - let allocate_instruction = allocate(account.key, try_convert(space)?); - invoke_signed( - &allocate_instruction, - &[account.clone(), system_program.clone()], - &[seeds], - )?; - Ok(()) -} - -pub fn assign_owner<'a>( - account: &AccountInfo<'a>, - owner: &Pubkey, - system_program: &AccountInfo<'a>, - seeds: &[&[u8]], -) -> Result<(), ProgramError> { - let assign_instruction = assign(account.key, owner); - invoke_signed( - &assign_instruction, - &[account.clone(), system_program.clone()], - &[seeds], - )?; - Ok(()) -} - #[cfg(not(test))] pub fn get_rent() -> Result { use solana_program::sysvar::Sysvar;