diff --git a/.gitignore b/.gitignore index 908c59c66..1897cb585 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ cmake-build-* # IntelliJ / CLion configuration .idea -*.iml \ No newline at end of file +*.iml + +# CMake files +features.h \ No newline at end of file diff --git a/program/rust/src/instruction.rs b/program/rust/src/instruction.rs index aeedde4e7..c3d8a3def 100644 --- a/program/rust/src/instruction.rs +++ b/program/rust/src/instruction.rs @@ -99,6 +99,10 @@ pub enum OracleCommand { // key[2] permissions account [writable] // key[3] system program [] UpdPermissions = 17, + /// Set max latency + // account[0] funding account [signer writable] + // account[1] price account [signer writable] + SetMaxLatency = 18, } #[repr(C)] @@ -162,3 +166,11 @@ pub struct UpdPermissionsArgs { pub data_curation_authority: Pubkey, pub security_authority: Pubkey, } + +#[repr(C)] +#[derive(Zeroable, Clone, Copy, Pod)] +pub struct SetMaxLatencyArgs { + pub header: CommandHeader, + pub max_latency: u8, + pub unused_: [u8; 3], +} diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index ec2a5b59e..b8902165f 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -21,6 +21,7 @@ mod del_product; mod del_publisher; mod init_mapping; mod init_price; +mod set_max_latency; mod set_min_pub; mod upd_permissions; mod upd_price; @@ -35,6 +36,7 @@ pub use { del_publisher::del_publisher, init_mapping::init_mapping, init_price::init_price, + set_max_latency::set_max_latency, set_min_pub::set_min_pub, upd_permissions::upd_permissions, upd_price::{ @@ -76,5 +78,6 @@ pub fn process_instruction( DelPrice => del_price(program_id, accounts, instruction_data), DelProduct => del_product(program_id, accounts, instruction_data), UpdPermissions => upd_permissions(program_id, accounts, instruction_data), + SetMaxLatency => set_max_latency(program_id, accounts, instruction_data), } } diff --git a/program/rust/src/processor/set_max_latency.rs b/program/rust/src/processor/set_max_latency.rs new file mode 100644 index 000000000..046f10bce --- /dev/null +++ b/program/rust/src/processor/set_max_latency.rs @@ -0,0 +1,59 @@ +use { + crate::{ + accounts::PriceAccount, + deserialize::{ + load, + load_checked, + }, + instruction::SetMaxLatencyArgs, + 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 max latency +// account[0] funding account [signer writable] +// account[1] price account [signer writable] +pub fn set_max_latency( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let cmd = load::(instruction_data)?; // Loading SetMaxLatencyArgs + + pyth_assert( + instruction_data.len() == size_of::(), // Checking size of SetMaxLatencyArgs + 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.max_latency_ = cmd.max_latency; + + Ok(()) +} diff --git a/program/rust/src/tests/mod.rs b/program/rust/src/tests/mod.rs index 1e5bfeb4f..1263ae604 100644 --- a/program/rust/src/tests/mod.rs +++ b/program/rust/src/tests/mod.rs @@ -16,6 +16,7 @@ mod test_message; mod test_permission_migration; mod test_publish; mod test_publish_batch; +mod test_set_max_latency; mod test_set_min_pub; mod test_sizes; mod test_upd_aggregate; @@ -25,7 +26,6 @@ mod test_upd_price_no_fail_on_error; mod test_upd_product; mod test_utils; - #[cfg(feature = "pythnet")] mod test_twap; #[cfg(feature = "pythnet")] diff --git a/program/rust/src/tests/test_permission_migration.rs b/program/rust/src/tests/test_permission_migration.rs index ff47aeef8..abffcbe66 100644 --- a/program/rust/src/tests/test_permission_migration.rs +++ b/program/rust/src/tests/test_permission_migration.rs @@ -24,9 +24,11 @@ use { DelPublisher, InitMapping, InitPrice, + SetMaxLatency, SetMinPub, UpdProduct, }, + SetMaxLatencyArgs, SetMinPubArgs, }, processor::process_instruction, @@ -65,13 +67,11 @@ fn test_permission_migration() { let mut price_account = price_setup.as_account_info(); PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); - product_account.is_signer = false; mapping_account.is_signer = false; price_account.is_signer = false; next_mapping_account.is_signer = false; - { let mut permissions_account_data = PermissionAccount::initialize(&permissions_account, PC_VERSION).unwrap(); @@ -92,7 +92,6 @@ fn test_permission_migration() { Err(OracleError::PermissionViolation.into()) ); - assert_eq!( process_instruction( &program_id, @@ -211,6 +210,23 @@ fn test_permission_migration() { Err(OracleError::PermissionViolation.into()) ); + assert_eq!( + process_instruction( + &program_id, + &[ + attacker_account.clone(), + price_account.clone(), + permissions_account.clone() + ], + bytes_of::(&SetMaxLatencyArgs { + header: SetMaxLatency.into(), + max_latency: 5, + unused_: [0; 3], + }) + ), + Err(OracleError::PermissionViolation.into()) + ); + assert_eq!( process_instruction( &program_id, @@ -256,7 +272,6 @@ fn test_permission_migration() { Err(OracleError::PermissionViolation.into()) ); - // Security authority can't change minimum number of publishers assert_eq!( process_instruction( @@ -275,6 +290,24 @@ fn test_permission_migration() { Err(OracleError::PermissionViolation.into()) ); + // Security authority can't change maximum latency + assert_eq!( + process_instruction( + &program_id, + &[ + security_auth_account.clone(), + price_account.clone(), + permissions_account.clone(), + ], + bytes_of::(&SetMaxLatencyArgs { + header: SetMaxLatency.into(), + max_latency: 5, + unused_: [0; 3], + }), + ), + Err(OracleError::PermissionViolation.into()) + ); + // Security authority can't add publishers assert_eq!( process_instruction( diff --git a/program/rust/src/tests/test_set_max_latency.rs b/program/rust/src/tests/test_set_max_latency.rs new file mode 100644 index 000000000..3b719275b --- /dev/null +++ b/program/rust/src/tests/test_set_max_latency.rs @@ -0,0 +1,91 @@ +use { + crate::{ + accounts::{ + PermissionAccount, + PriceAccount, + PythAccount, + }, + c_oracle_header::PC_VERSION, + deserialize::{ + load_checked, + load_mut, + }, + instruction::{ + OracleCommand, + SetMaxLatencyArgs, + }, + processor::set_max_latency, + tests::test_utils::AccountSetup, + }, + solana_program::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::Pubkey, + }, + std::mem::size_of, +}; + +#[test] +fn test_set_max_latency() { + let mut instruction_data = [0u8; size_of::()]; + + let program_id = Pubkey::new_unique(); + + let mut funding_setup = AccountSetup::new_funding(); + let funding_account = funding_setup.as_account_info(); + + let mut price_setup = AccountSetup::new::(&program_id); + let price_account = price_setup.as_account_info(); + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); + + let mut permissions_setup = AccountSetup::new_permission(&program_id); + let permissions_account = permissions_setup.as_account_info(); + + { + let mut permissions_account_data = + PermissionAccount::initialize(&permissions_account, PC_VERSION).unwrap(); + permissions_account_data.master_authority = *funding_account.key; + permissions_account_data.data_curation_authority = *funding_account.key; + permissions_account_data.security_authority = *funding_account.key; + } + + assert_eq!(get_max_latency(&price_account), Ok(0)); + + populate_instruction(&mut instruction_data, 10); + assert!(set_max_latency( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + permissions_account.clone() + ], + &instruction_data + ) + .is_ok()); + assert_eq!(get_max_latency(&price_account), Ok(10)); + + populate_instruction(&mut instruction_data, 5); + assert!(set_max_latency( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + permissions_account.clone() + ], + &instruction_data + ) + .is_ok()); + assert_eq!(get_max_latency(&price_account), Ok(5)); +} + +// Populate the instruction data with SetMaxLatencyArgs +fn populate_instruction(instruction_data: &mut [u8], max_latency: u8) { + let mut hdr = load_mut::(instruction_data).unwrap(); + hdr.header = OracleCommand::SetMaxLatency.into(); + hdr.max_latency = max_latency; +} + +// Helper function to get the max latency from a PriceAccount +fn get_max_latency(account: &AccountInfo) -> Result { + Ok(load_checked::(account, PC_VERSION)?.max_latency_) +} diff --git a/program/rust/src/tests/test_sizes.rs b/program/rust/src/tests/test_sizes.rs index 28e2ce93c..153e82356 100644 --- a/program/rust/src/tests/test_sizes.rs +++ b/program/rust/src/tests/test_sizes.rs @@ -27,6 +27,7 @@ use { CommandHeader, DelPublisherArgs, InitPriceArgs, + SetMaxLatencyArgs, SetMinPubArgs, UpdPriceArgs, }, @@ -98,6 +99,7 @@ fn test_sizes() { assert_eq!(size_of::(), 16); assert_eq!(size_of::(), 16); assert_eq!(size_of::(), 12); + assert_eq!(size_of::(), 12); assert_eq!(size_of::(), 40); assert_eq!(size_of::(), 40); assert_eq!(size_of::(), 40);