diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index 8246e0e8..2d34f648 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -24,6 +24,7 @@ use { }, }; +mod add_mapping; mod add_price; mod add_product; mod add_publisher; @@ -45,6 +46,7 @@ pub use add_publisher::{ ENABLE_ACCUMULATOR_V2, }; pub use { + add_mapping::add_mapping, add_price::add_price, add_product::add_product, add_publisher::add_publisher, @@ -84,7 +86,7 @@ pub fn process_instruction( match load_command_header_checked(instruction_data)? { InitMapping => init_mapping(program_id, accounts, instruction_data), - AddMapping => Err(OracleError::UnrecognizedInstruction.into()), + 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), diff --git a/program/rust/src/processor/add_mapping.rs b/program/rust/src/processor/add_mapping.rs new file mode 100644 index 00000000..bcaa8eea --- /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/tests/mod.rs b/program/rust/src/tests/mod.rs index 33178ccd..0554bdbf 100644 --- a/program/rust/src/tests/mod.rs +++ b/program/rust/src/tests/mod.rs @@ -1,4 +1,5 @@ mod pyth_simulator; +mod test_add_mapping; mod test_add_price; mod test_add_product; mod test_add_publisher; diff --git a/program/rust/src/tests/test_add_mapping.rs b/program/rust/src/tests/test_add_mapping.rs new file mode 100644 index 00000000..2719a61e --- /dev/null +++ b/program/rust/src/tests/test_add_mapping.rs @@ -0,0 +1,128 @@ +use { + crate::{ + accounts::{ + clear_account, + MappingAccount, + PythAccount, + }, + c_oracle_header::{ + PC_MAGIC, + PC_MAP_TABLE_SIZE, + PC_VERSION, + }, + deserialize::{ + load_account_as_mut, + load_checked, + }, + error::OracleError, + instruction::{ + CommandHeader, + OracleCommand, + }, + processor::process_instruction, + tests::test_utils::AccountSetup, + }, + bytemuck::bytes_of, + solana_program::{ + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +#[test] +fn test_add_mapping() { + let hdr: CommandHeader = OracleCommand::AddMapping.into(); + let instruction_data = bytes_of::(&hdr); + + let program_id = Pubkey::new_unique(); + + let mut funding_setup = AccountSetup::new_funding(); + let funding_account = funding_setup.as_account_info(); + + let mut curr_mapping_setup = AccountSetup::new::(&program_id); + let cur_mapping = curr_mapping_setup.as_account_info(); + 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(); + + { + let mut cur_mapping_data = + load_checked::(&cur_mapping, PC_VERSION).unwrap(); + cur_mapping_data.number_of_products = PC_MAP_TABLE_SIZE; + } + + assert!(process_instruction( + &program_id, + &[ + funding_account.clone(), + cur_mapping.clone(), + next_mapping.clone() + ], + instruction_data + ) + .is_ok()); + + { + let next_mapping_data = load_checked::(&next_mapping, PC_VERSION).unwrap(); + let mut cur_mapping_data = + load_checked::(&cur_mapping, PC_VERSION).unwrap(); + + assert!(cur_mapping_data.next_mapping_account == *next_mapping.key); + assert!(next_mapping_data.next_mapping_account == Pubkey::default()); + cur_mapping_data.next_mapping_account = Pubkey::default(); + cur_mapping_data.number_of_products = 0; + } + + clear_account(&next_mapping).unwrap(); + + assert_eq!( + process_instruction( + &program_id, + &[ + funding_account.clone(), + cur_mapping.clone(), + next_mapping.clone() + ], + instruction_data + ), + Err(ProgramError::InvalidArgument) + ); + + { + let mut cur_mapping_data = + load_checked::(&cur_mapping, PC_VERSION).unwrap(); + assert!(cur_mapping_data.next_mapping_account == Pubkey::default()); + cur_mapping_data.number_of_products = PC_MAP_TABLE_SIZE; + cur_mapping_data.header.magic_number = 0; + } + + assert_eq!( + process_instruction( + &program_id, + &[ + funding_account.clone(), + cur_mapping.clone(), + next_mapping.clone() + ], + instruction_data + ), + Err(OracleError::InvalidAccountHeader.into()) + ); + + { + let mut cur_mapping_data = load_account_as_mut::(&cur_mapping).unwrap(); + cur_mapping_data.header.magic_number = PC_MAGIC; + } + + assert!(process_instruction( + &program_id, + &[ + funding_account.clone(), + cur_mapping.clone(), + next_mapping.clone() + ], + instruction_data + ) + .is_ok()); +} diff --git a/program/rust/src/tests/test_permission_migration.rs b/program/rust/src/tests/test_permission_migration.rs index f324c1d4..419106db 100644 --- a/program/rust/src/tests/test_permission_migration.rs +++ b/program/rust/src/tests/test_permission_migration.rs @@ -16,6 +16,7 @@ use { DelPublisherArgs, InitPriceArgs, OracleCommand::{ + AddMapping, AddPrice, AddProduct, AddPublisher, @@ -116,6 +117,20 @@ fn test_permission_migration() { ) .unwrap(); + assert_eq!( + process_instruction( + &program_id, + &[ + attacker_account.clone(), + mapping_account.clone(), + next_mapping_account.clone(), + permissions_account.clone() + ], + bytes_of::(&AddMapping.into()) + ), + Err(OracleError::PermissionViolation.into()) + ); + assert_eq!( process_instruction( &program_id, diff --git a/program/rust/src/utils.rs b/program/rust/src/utils.rs index 97ef9b53..0d8623ec 100644 --- a/program/rust/src/utils.rs +++ b/program/rust/src/utils.rs @@ -57,6 +57,26 @@ pub fn check_valid_funding_account(account: &AccountInfo) -> Result<(), ProgramE ) } +pub fn valid_signable_account( + program_id: &Pubkey, + account: &AccountInfo, +) -> Result { + Ok(account.is_signer + && account.is_writable + && account.owner == program_id + && get_rent()?.is_exempt(account.lamports(), account.data_len())) +} + +pub fn check_valid_signable_account( + program_id: &Pubkey, + account: &AccountInfo, +) -> Result<(), ProgramError> { + pyth_assert( + valid_signable_account(program_id, account)?, + OracleError::InvalidSignableAccount.into(), + ) +} + /// Check that `account` is a valid signable pyth account or /// that `funding_account` is a signer and is permissioned by the `permission_account` pub fn check_permissioned_funding_account( @@ -80,6 +100,34 @@ pub fn check_permissioned_funding_account( check_valid_writable_account(program_id, account) } +/// Check that `account` is a valid signable pyth account or +/// that `funding_account` is a signer and is permissioned by the `permission_account` +pub fn check_valid_signable_account_or_permissioned_funding_account( + program_id: &Pubkey, + account: &AccountInfo, + funding_account: &AccountInfo, + permissions_account_option: Option<&AccountInfo>, + cmd_hdr: &CommandHeader, +) -> Result<(), ProgramError> { + if let Some(permissions_account) = permissions_account_option { + check_valid_permissions_account(program_id, permissions_account)?; + let permissions_account_data = + load_checked::(permissions_account, cmd_hdr.version)?; + check_valid_funding_account(funding_account)?; + pyth_assert( + permissions_account_data.is_authorized( + funding_account.key, + OracleCommand::from_i32(cmd_hdr.command) + .ok_or(OracleError::UnrecognizedInstruction)?, + ), + OracleError::PermissionViolation.into(), + )?; + check_valid_writable_account(program_id, account) + } else { + check_valid_signable_account(program_id, account) + } +} + /// Returns `true` if the `account` is fresh, i.e., its data can be overwritten. /// Use this check to prevent accidentally overwriting accounts whose data is already populated. pub fn valid_fresh_account(account: &AccountInfo) -> bool {