diff --git a/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs b/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs index 99be5f3aa7..e3ef7721ab 100644 --- a/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs @@ -988,8 +988,8 @@ async fn test_account_index_out_of_bounds() { ) .await; - // Should fail with NotEnoughAccountKeys - assert_rpc_error(result, 0, 20014).unwrap(); + // Should fail with TooManyCompressionTransfers (account index 99 >= 40) + assert_rpc_error(result, 0, 95).unwrap(); } /// Test 16: Authority index out of bounds diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index 77630ca7f3..c845890ca9 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -40,6 +40,7 @@ pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); pub const MAX_ACCOUNTS: usize = 30; +pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40; // Custom ctoken instructions start at 100 to skip spl-token program instrutions. // When adding new instructions check anchor discriminators for collisions! diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs index 375d05150a..4e8db7fe62 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs @@ -21,7 +21,8 @@ pub fn process_mint_to_ctoken_action( validated_accounts: &MintActionAccounts, packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, mint: Pubkey, -) -> Result, ProgramError> { + transfer_amount: &mut u64, +) -> Result<(), ProgramError> { check_authority( compressed_mint.base.mint_authority, validated_accounts.authority.key(), @@ -55,7 +56,7 @@ pub fn process_mint_to_ctoken_action( packed_accounts, ); - compress_or_decompress_ctokens(inputs) + compress_or_decompress_ctokens(inputs, transfer_amount) } #[profile] diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs index 3eb6bd7012..38829ccebe 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs @@ -28,11 +28,9 @@ use crate::{ convert_program_error, transfer_lamports::{multi_transfer_lamports, Transfer}, }, + MAX_PACKED_ACCOUNTS, }; -/// Maximum number of packed accounts allowed in a single instruction -const MAX_PACKED_ACCOUNTS: usize = 40; - #[allow(clippy::too_many_arguments)] #[profile] pub fn process_actions<'a>( @@ -94,29 +92,23 @@ pub fn process_actions<'a>( // compressed_mint.metadata.spl_mint_initialized = true; } ZAction::MintToCToken(mint_to_ctoken_action) => { - let transfer_amount = process_mint_to_ctoken_action( + let account_index = mint_to_ctoken_action.account_index as usize; + if account_index >= MAX_PACKED_ACCOUNTS { + msg!( + "Account index {} out of bounds, max {} allowed", + account_index, + MAX_PACKED_ACCOUNTS + ); + return Err(ErrorCode::TooManyCompressionTransfers.into()); + } + process_mint_to_ctoken_action( mint_to_ctoken_action, compressed_mint, validated_accounts, packed_accounts, parsed_instruction_data.mint.metadata.mint, + &mut transfer_map[account_index], )?; - - // Accumulate transfer amount if present (deduplication happens here) - if let Some(amount) = transfer_amount { - let account_index = mint_to_ctoken_action.account_index; - if account_index as usize >= MAX_PACKED_ACCOUNTS { - msg!( - "Too many compression transfers: {}, max {} allowed", - account_index, - MAX_PACKED_ACCOUNTS - ); - return Err(ErrorCode::TooManyCompressionTransfers.into()); - } - transfer_map[account_index as usize] = transfer_map[account_index as usize] - .checked_add(amount) - .ok_or(ProgramError::ArithmeticOverflow)?; - } } ZAction::UpdateMetadataField(update_metadata_action) => { process_update_metadata_field_action( diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index 6ecafe4a13..4fdcc69140 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -18,7 +18,7 @@ use crate::{ transfer2::accounts::Transfer2Accounts, }; -/// Process compress and close operation for a ctoken account +/// Process compress and close operation for a ctoken account. #[profile] pub fn process_compress_and_close( authority: Option<&AccountInfo>, @@ -27,7 +27,7 @@ pub fn process_compress_and_close( token_account_info: &AccountInfo, ctoken: &mut ZCompressedTokenMut, packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, -) -> Result, ProgramError> { +) -> Result<(), ProgramError> { let authority = authority.ok_or(ErrorCode::CompressAndCloseAuthorityMissing)?; check_signer(authority).map_err(|e| { anchor_lang::solana_program::msg!("Authority signer check failed: {:?}", e); @@ -64,7 +64,7 @@ pub fn process_compress_and_close( } *ctoken.amount = 0.into(); - Ok(None) + Ok(()) } /// Validate compressed token account for compress and close operation diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs index 05271961cc..ddeabda896 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs @@ -16,11 +16,12 @@ use spl_pod::solana_msg::msg; use super::{compress_and_close::process_compress_and_close, inputs::CTokenCompressionInputs}; use crate::shared::owner_validation::check_ctoken_owner; -/// Perform compression/decompression on a ctoken account +/// Perform compression/decompression on a ctoken account. #[profile] pub fn compress_or_decompress_ctokens( inputs: CTokenCompressionInputs, -) -> Result, ProgramError> { + transfer_amount: &mut u64, +) -> Result<(), ProgramError> { let CTokenCompressionInputs { authority, compress_and_close_inputs, @@ -78,6 +79,7 @@ pub fn compress_or_decompress_ctokens( ctoken.extensions.as_deref(), token_account_info, &mut current_slot, + transfer_amount, ) } ZCompressionMode::Decompress => { @@ -92,6 +94,7 @@ pub fn compress_or_decompress_ctokens( ctoken.extensions.as_deref(), token_account_info, &mut current_slot, + transfer_amount, ) } ZCompressionMode::CompressAndClose => process_compress_and_close( @@ -110,7 +113,12 @@ fn process_compressible_extension( extensions: Option<&[ZExtensionStructMut]>, token_account_info: &AccountInfo, current_slot: &mut u64, -) -> Result, ProgramError> { + transfer_amount: &mut u64, +) -> Result<(), ProgramError> { + if *transfer_amount != 0 { + return Ok(()); + } + if let Some(extensions) = extensions { for extension in extensions.iter() { if let ZExtensionStructMut::Compressible(compressible_extension) = extension { @@ -119,7 +127,7 @@ fn process_compressible_extension( .map_err(|_| CTokenError::SysvarAccessError)? .slot; } - let transfer_amount = compressible_extension + *transfer_amount = compressible_extension .calculate_top_up_lamports( token_account_info.data_len() as u64, *current_slot, @@ -128,9 +136,9 @@ fn process_compressible_extension( ) .map_err(|_| CTokenError::InvalidAccountData)?; - return Ok(Some(transfer_amount)); + return Ok(()); } } } - Ok(None) + Ok(()) } diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs index 26825fb47c..17445ac851 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs @@ -15,14 +15,15 @@ pub use compress_and_close::close_for_compress_and_close; pub use compress_or_decompress_ctokens::compress_or_decompress_ctokens; pub use inputs::{CTokenCompressionInputs, CompressAndCloseInputs}; -/// Process compression/decompression for ctoken accounts +/// Process compression/decompression for ctoken accounts. #[profile] pub(super) fn process_ctoken_compressions( inputs: &ZCompressedTokenInstructionDataTransfer2, compression: &ZCompression, token_account_info: &AccountInfo, packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, -) -> Result, anchor_lang::prelude::ProgramError> { + transfer_amount: &mut u64, +) -> Result<(), anchor_lang::prelude::ProgramError> { // Validate compression fields for the given mode validate_compression_mode_fields(compression)?; @@ -34,8 +35,5 @@ pub(super) fn process_ctoken_compressions( packed_accounts, )?; - let transfer_amount = compress_or_decompress_ctokens(compression_inputs)?; - - // Return account index and amount if there's a transfer needed - Ok(transfer_amount.map(|amount| (compression.source_or_recipient, amount))) + compress_or_decompress_ctokens(compression_inputs, transfer_amount) } diff --git a/programs/compressed-token/program/src/transfer2/compression/mod.rs b/programs/compressed-token/program/src/transfer2/compression/mod.rs index 3f7e5d77c5..a79734e64c 100644 --- a/programs/compressed-token/program/src/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/transfer2/compression/mod.rs @@ -15,7 +15,7 @@ use crate::{ convert_program_error, transfer_lamports::{multi_transfer_lamports, Transfer}, }, - LIGHT_CPI_SIGNER, + LIGHT_CPI_SIGNER, MAX_PACKED_ACCOUNTS, }; pub mod ctoken; @@ -38,21 +38,31 @@ pub fn process_token_compression( cpi_authority: &AccountInfo, ) -> Result<(), ProgramError> { if let Some(compressions) = inputs.compressions.as_ref() { - // Array to accumulate transfer amounts by account index (max 40 packed accounts) - let mut transfer_map = [0u64; 40]; + let mut transfer_map = [0u64; MAX_PACKED_ACCOUNTS]; for compression in compressions { + let account_index = compression.source_or_recipient as usize; + if account_index >= MAX_PACKED_ACCOUNTS { + msg!( + "Account index {} out of bounds, max {} allowed", + account_index, + MAX_PACKED_ACCOUNTS + ); + return Err(ErrorCode::TooManyCompressionTransfers.into()); + } + let source_or_recipient = packed_accounts.get_u8( compression.source_or_recipient, "compression source or recipient", )?; - let transfer = match source_or_recipient.owner() { + match source_or_recipient.owner() { ID => ctoken::process_ctoken_compressions( inputs, compression, source_or_recipient, packed_accounts, + &mut transfer_map[account_index], )?, SPL_TOKEN_ID => { spl::process_spl_compressions( @@ -62,8 +72,6 @@ pub fn process_token_compression( packed_accounts, cpi_authority, )?; - // SPL token compressions don't require lamport transfers for compressible extension´ - None } SPL_TOKEN_2022_ID => { spl::process_spl_compressions( @@ -73,8 +81,6 @@ pub fn process_token_compression( packed_accounts, cpi_authority, )?; - // SPL token compressions don't require lamport transfers for compressible extension´ - None } _ => { msg!( @@ -88,24 +94,9 @@ pub fn process_token_compression( return Err(ProgramError::InvalidInstructionData); } }; - - // Accumulate transfer amount if present - if let Some((account_index, amount)) = transfer { - if account_index >= 40 { - msg!( - "Too many compression transfers: {}, max 40 allowed", - account_index - ); - return Err(ErrorCode::TooManyCompressionTransfers.into()); - } - transfer_map[account_index as usize] = transfer_map[account_index as usize] - .checked_add(amount) - .ok_or(ProgramError::ArithmeticOverflow)?; - } } - // Build rent_return_transfers & top up array from accumulated amounts - let transfers: ArrayVec = transfer_map + let transfers: ArrayVec = transfer_map .iter() .enumerate() .filter_map(|(index, &amount)| { @@ -121,7 +112,7 @@ pub fn process_token_compression( amount, }) }) - .collect::, ProgramError>>()?; + .collect::, ProgramError>>()?; if !transfers.is_empty() { multi_transfer_lamports(fee_payer, &transfers).map_err(convert_program_error)?