From 3486a48c02f1c8bfa58bfe6f60cc241b3e3dbe28 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 6 Nov 2025 18:20:52 +0000 Subject: [PATCH 1/2] fix: ctoken address Merkle tree check with cpi context --- .../instructions/mint_action/cpi_context.rs | 23 ++++- programs/compressed-token/anchor/src/lib.rs | 6 ++ programs/compressed-token/program/Cargo.toml | 1 + .../src/mint_action/actions/create_mint.rs | 41 +++++++-- .../program/src/mint_action/processor.rs | 1 + .../program/src/mint_action/queue_indices.rs | 5 ++ .../program/tests/mint_action.rs | 2 + .../program/tests/queue_indices.rs | 85 ++++++++++++++++++- .../instructions/mint_action/instruction.rs | 3 +- 9 files changed, 151 insertions(+), 16 deletions(-) diff --git a/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs index d87c094f9f..b48b1006ee 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs @@ -1,12 +1,10 @@ use light_compressed_account::instruction_data::zero_copy_set::CompressedCpiContextTrait; use light_zero_copy::{ZeroCopy, ZeroCopyMut}; -use crate::{AnchorDeserialize, AnchorSerialize}; +use crate::{AnchorDeserialize, AnchorSerialize, CMINT_ADDRESS_TREE}; #[repr(C)] -#[derive( - Debug, Clone, AnchorSerialize, Default, AnchorDeserialize, ZeroCopy, ZeroCopyMut, PartialEq, -)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut, PartialEq)] pub struct CpiContext { pub set_context: bool, pub first_set_context: bool, @@ -20,6 +18,23 @@ pub struct CpiContext { /// Placeholder to enable cmints in multiple address trees. /// Currently set to 0. pub read_only_address_trees: [u8; 4], + pub address_tree_pubkey: [u8; 32], +} + +impl Default for CpiContext { + fn default() -> Self { + Self { + set_context: false, + first_set_context: false, + in_tree_index: 0, + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: CMINT_ADDRESS_TREE, + } + } } impl CompressedCpiContextTrait for ZCpiContext<'_> { diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index f069863506..ea3970ac2c 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -416,6 +416,12 @@ pub enum ErrorCode { OneEpochPrefundingNotAllowed, #[msg("Duplicate mint index detected in inputs, outputs, or compressions")] DuplicateMint, + #[msg("Invalid compressed mint address derivation")] + MintActionInvalidCompressedMintAddress, + #[msg("Invalid CPI context for create mint operation")] + MintActionInvalidCpiContextForCreateMint, + #[msg("Invalid address tree pubkey in CPI context")] + MintActionInvalidCpiContextAddressTreePubkey, } impl From for ProgramError { diff --git a/programs/compressed-token/program/Cargo.toml b/programs/compressed-token/program/Cargo.toml index 1713e51ff8..b0ab0753ea 100644 --- a/programs/compressed-token/program/Cargo.toml +++ b/programs/compressed-token/program/Cargo.toml @@ -77,6 +77,7 @@ light-account-checks = { workspace = true, features = [ "test-only", ] } lazy_static = { workspace = true } +light-compressed-account = { workspace = true, features = ["keccak", "sha256"] } [lints.rust.unexpected_cfgs] level = "allow" diff --git a/programs/compressed-token/program/src/mint_action/actions/create_mint.rs b/programs/compressed-token/program/src/mint_action/actions/create_mint.rs index 942041ee94..b1ff382946 100644 --- a/programs/compressed-token/program/src/mint_action/actions/create_mint.rs +++ b/programs/compressed-token/program/src/mint_action/actions/create_mint.rs @@ -1,12 +1,12 @@ use anchor_compressed_token::ErrorCode; use anchor_lang::prelude::ProgramError; -use light_compressed_account::{ - instruction_data::with_readonly::ZInstructionDataInvokeCpiWithReadOnlyMut, Pubkey, -}; +use light_compressed_account::instruction_data::with_readonly::ZInstructionDataInvokeCpiWithReadOnlyMut; use light_ctoken_types::{ - instructions::mint_action::ZMintActionCompressedInstructionData, COMPRESSED_MINT_SEED, + instructions::mint_action::ZMintActionCompressedInstructionData, CMINT_ADDRESS_TREE, + COMPRESSED_MINT_SEED, }; use light_program_profiler::profile; +use pinocchio::pubkey::pubkey_eq; use spl_pod::solana_msg::msg; /// Processes the create mint action by validating parameters and setting up the new address. @@ -23,7 +23,7 @@ pub fn process_create_mint_action( // - The spl mint pda is used as mint in compressed token accounts. // Note: we cant use pinocchio_pubkey::derive_address because don't use the mint_pda in this ix. // The pda would be unvalidated and an invalid bump could be used. - let spl_mint_pda: Pubkey = solana_pubkey::Pubkey::create_program_address( + let spl_mint_pda = solana_pubkey::Pubkey::create_program_address( &[ COMPRESSED_MINT_SEED, mint_signer.as_slice(), @@ -35,15 +35,38 @@ pub fn process_create_mint_action( ], &crate::ID, )? - .into(); + .to_bytes(); - if spl_mint_pda.to_bytes() != parsed_instruction_data.mint.metadata.mint.to_bytes() { + if spl_mint_pda != parsed_instruction_data.mint.metadata.mint.to_bytes() { msg!("Invalid mint PDA derivation"); return Err(ErrorCode::MintActionInvalidMintPda.into()); } + // With cpi context this program does not have access + // to the address Merkle tree account that is used in the cpi to the light system program. + // This breaks the implicit check of new_address_params_assigned. + // -> Therefore we manually verify the compressed address derivation here. + // + // else is not required since for new_address_params_assigned + // the light system program checks correct address derivation and we check the + if let Some(cpi_context) = &parsed_instruction_data.cpi_context { + if !pubkey_eq(&cpi_context.address_tree_pubkey, &CMINT_ADDRESS_TREE) { + msg!("Invalid address tree pubkey in cpi context"); + return Err(ErrorCode::MintActionInvalidCpiContextAddressTreePubkey.into()); + } + let address = light_compressed_account::address::derive_address( + &spl_mint_pda, + &cpi_context.address_tree_pubkey, + &crate::LIGHT_CPI_SIGNER.program_id, + ); + if address != parsed_instruction_data.compressed_address { + msg!("Invalid compressed mint address derivation"); + return Err(ErrorCode::MintActionInvalidCompressedMintAddress.into()); + } + } + // 2. Create NewAddressParams cpi_instruction_struct.new_address_params[0].set( - spl_mint_pda.to_bytes(), + spl_mint_pda, parsed_instruction_data.root_index, Some( parsed_instruction_data @@ -80,7 +103,7 @@ pub fn process_create_mint_action( // Validate extensions - only TokenMetadata is supported and at most one extension allowed if let Some(extensions) = &parsed_instruction_data.mint.extensions { - if extensions.len() > 1 { + if extensions.len() != 1 { msg!( "Only one extension allowed for compressed mints, found {}", extensions.len() diff --git a/programs/compressed-token/program/src/mint_action/processor.rs b/programs/compressed-token/program/src/mint_action/processor.rs index 4651641563..e04c2c2a6e 100644 --- a/programs/compressed-token/program/src/mint_action/processor.rs +++ b/programs/compressed-token/program/src/mint_action/processor.rs @@ -68,6 +68,7 @@ pub fn process_mint_action( parsed_instruction_data.create_mint.is_some(), tokens_out_queue_exists, queue_keys_match, + accounts_config.write_to_cpi_context, )?; // If create mint diff --git a/programs/compressed-token/program/src/mint_action/queue_indices.rs b/programs/compressed-token/program/src/mint_action/queue_indices.rs index fbb08232b9..6d7359c1dd 100644 --- a/programs/compressed-token/program/src/mint_action/queue_indices.rs +++ b/programs/compressed-token/program/src/mint_action/queue_indices.rs @@ -19,10 +19,15 @@ impl QueueIndices { create_mint: bool, tokens_out_queue_exists: bool, queue_keys_match: bool, + write_to_cpi_context: bool, ) -> Result { if let Some(ctx) = cpi_context { // Path when cpi_context is provided let (in_tree_index, address_merkle_tree_index) = if create_mint { + // if executing with cpi context address tree index must be 1. + if !write_to_cpi_context && ctx.in_tree_index != 1 { + return Err(ErrorCode::MintActionInvalidCpiContextForCreateMint); + } (0, ctx.in_tree_index) // in_tree_index is 0, address_merkle_tree_index from context } else { (ctx.in_tree_index, 0) // in_tree_index from context, address_merkle_tree_index is 0 diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index 5acd46bde6..df95b2e3f8 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -16,6 +16,7 @@ use light_ctoken_types::{ }, }, state::CompressedMintMetadata, + CMINT_ADDRESS_TREE, }; use light_zero_copy::traits::ZeroCopyAt; use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng}; @@ -138,6 +139,7 @@ fn random_cpi_context(rng: &mut StdRng) -> CpiContext { token_out_queue_index: rng.gen::(), assigned_account_index: rng.gen::(), read_only_address_trees: [0u8; 4], + address_tree_pubkey: CMINT_ADDRESS_TREE, } } diff --git a/programs/compressed-token/program/tests/queue_indices.rs b/programs/compressed-token/program/tests/queue_indices.rs index b7616dd198..fbb4ee90df 100644 --- a/programs/compressed-token/program/tests/queue_indices.rs +++ b/programs/compressed-token/program/tests/queue_indices.rs @@ -1,3 +1,4 @@ +use anchor_compressed_token::ErrorCode; use anchor_lang::AnchorSerialize; use light_compressed_token::mint_action::queue_indices::QueueIndices; use light_ctoken_types::instructions::mint_action::CpiContext; @@ -33,7 +34,7 @@ fn test_queue_indices_comprehensive() { cpi_context: Some(CpiContext { set_context: false, first_set_context: false, - in_tree_index: 5, + in_tree_index: 1, // Must be 1 for execute mode with create_mint (address tree at index 1 in tree_pubkeys) in_queue_index: 6, out_queue_index: 7, token_out_queue_index: 8, @@ -46,7 +47,7 @@ fn test_queue_indices_comprehensive() { }, expected: QueueIndices { in_tree_index: 0, // 0 when create_mint=true - address_merkle_tree_index: 5, // cpi.in_tree_index when create_mint=true + address_merkle_tree_index: 1, // cpi.in_tree_index when create_mint=true (must be 1 in execute mode) in_queue_index: 6, // cpi.in_queue_index out_token_queue_index: 8, // cpi.token_out_queue_index output_queue_index: 7, // cpi.out_queue_index @@ -252,6 +253,7 @@ fn test_queue_indices_comprehensive() { test_case.input.create_mint, test_case.input.tokens_out_queue_exists, test_case.input.queue_keys_match, + cpi_context.first_set_context || cpi_context.set_context, ) } else { QueueIndices::new( @@ -259,6 +261,7 @@ fn test_queue_indices_comprehensive() { test_case.input.create_mint, test_case.input.tokens_out_queue_exists, test_case.input.queue_keys_match, + false, ) }; @@ -273,3 +276,81 @@ fn test_queue_indices_comprehensive() { } } } + +#[test] +fn test_queue_indices_invalid_address_tree_index() { + println!("\n=== Testing Invalid Address Tree Index in Execute Mode ==="); + + // Test case: Execute mode (not write_to_cpi_context) with create_mint=true + // and in_tree_index != 1 should fail with MintActionInvalidCpiContextForCreateMint + let cpi_context = CpiContext { + set_context: false, + first_set_context: false, // Execute mode + in_tree_index: 5, // Invalid! Must be 1 in execute mode with create_mint + in_queue_index: 6, + out_queue_index: 7, + token_out_queue_index: 8, + assigned_account_index: 0, + ..Default::default() + }; + + let serialized = create_zero_copy_cpi_context(&cpi_context); + let (zero_copy_context, _) = CpiContext::zero_copy_at(&serialized).unwrap(); + + let result = QueueIndices::new( + Some(&zero_copy_context), + true, // create_mint=true + false, // tokens_out_queue_exists + false, // queue_keys_match + false, // write_to_cpi_context (execute mode) + ); + + match result { + Ok(_) => { + panic!("Expected MintActionInvalidCpiContextForCreateMint error, but got Ok"); + } + Err(e) => { + // Compare error codes by their discriminant values + assert!( + matches!(e, ErrorCode::MintActionInvalidCpiContextForCreateMint), + "Expected MintActionInvalidCpiContextForCreateMint, got {:?}", + e + ); + println!( + "✅ Correctly rejected invalid in_tree_index={} in execute mode", + cpi_context.in_tree_index + ); + println!(" Error: {:?}", e); + } + } + + // Test that in_tree_index=1 works correctly (positive case) + let valid_cpi_context = CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, // Valid! + in_queue_index: 6, + out_queue_index: 7, + token_out_queue_index: 8, + assigned_account_index: 0, + ..Default::default() + }; + + let serialized = create_zero_copy_cpi_context(&valid_cpi_context); + let (zero_copy_context, _) = CpiContext::zero_copy_at(&serialized).unwrap(); + + let result = QueueIndices::new( + Some(&zero_copy_context), + true, // create_mint=true + false, // tokens_out_queue_exists + false, // queue_keys_match + false, // write_to_cpi_context (execute mode) + ); + + assert!( + result.is_ok(), + "Expected Ok with in_tree_index=1, got error: {:?}", + result.err() + ); + println!("✅ Correctly accepted valid in_tree_index=1 in execute mode"); +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs index 7aef50702b..e27aedfebf 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs @@ -508,7 +508,8 @@ impl MintActionInputsCpiWrite { out_queue_index: inputs.output_queue_index, token_out_queue_index: 0, // Set when adding MintTo action assigned_account_index: inputs.assigned_account_index, - ..Default::default() + read_only_address_trees: [0; 4], + address_tree_pubkey: light_ctoken_types::CMINT_ADDRESS_TREE, }; Self { From 1aaae1e63971b7997a6cfaafdd20da3823b01513 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 6 Nov 2025 20:03:44 +0000 Subject: [PATCH 2/2] test: failing write to cpi context --- .../system/src/cpi_context_account.rs | 38 +- .../compressed-token-test/Cargo.toml | 6 +- .../compressed-token-test/src/lib.rs | 71 +++- .../compressed-token-test/tests/mint.rs | 3 + .../tests/mint/cpi_context.rs | 387 ++++++++++++++++++ .../program/tests/queue_indices.rs | 5 - 6 files changed, 464 insertions(+), 46 deletions(-) create mode 100644 program-tests/compressed-token-test/tests/mint/cpi_context.rs diff --git a/anchor-programs/system/src/cpi_context_account.rs b/anchor-programs/system/src/cpi_context_account.rs index 737d90065c..bb770eb48c 100644 --- a/anchor-programs/system/src/cpi_context_account.rs +++ b/anchor-programs/system/src/cpi_context_account.rs @@ -1,12 +1,6 @@ -use std::slice; - use aligned_sized::aligned_sized; use anchor_lang::prelude::*; -use light_compressed_account::instruction_data::{ - invoke_cpi::InstructionDataInvokeCpi, zero_copy::ZInstructionDataInvokeCpi, -}; -use light_zero_copy::{errors::ZeroCopyError, traits::ZeroCopyAt}; -use zerocopy::{little_endian::U32, Ref}; +use light_compressed_account::instruction_data::invoke_cpi::InstructionDataInvokeCpi; /// Collects instruction data without executing a compressed transaction. /// Signer checks are performed on instruction data. @@ -40,33 +34,3 @@ impl CpiContextAccount { self.context = Vec::new(); } } - -pub struct ZCpiContextAccount2<'a> { - pub fee_payer: Ref<&'a mut [u8], light_compressed_account::pubkey::Pubkey>, - pub associated_merkle_tree: Ref<&'a mut [u8], light_compressed_account::pubkey::Pubkey>, - pub context: Vec>, -} - -pub fn deserialize_cpi_context_account<'info, 'a>( - account_info: &AccountInfo<'info>, -) -> std::result::Result, ZeroCopyError> { - let mut account_data = account_info.try_borrow_mut_data().unwrap(); - let data = unsafe { slice::from_raw_parts_mut(account_data.as_mut_ptr(), account_data.len()) }; - let (fee_payer, data) = - Ref::<&'a mut [u8], light_compressed_account::pubkey::Pubkey>::from_prefix(&mut data[8..])?; - let (associated_merkle_tree, data) = - Ref::<&'a mut [u8], light_compressed_account::pubkey::Pubkey>::from_prefix(data)?; - let (len, data) = Ref::<&'a mut [u8], U32>::from_prefix(data)?; - let mut data = &*data; - let mut context = Vec::new(); - for _ in 0..(u64::from(*len)) as usize { - let (context_item, new_data) = ZInstructionDataInvokeCpi::zero_copy_at(data)?; - context.push(context_item); - data = new_data; - } - Ok(ZCpiContextAccount2 { - fee_payer, - associated_merkle_tree, - context, - }) -} diff --git a/program-tests/compressed-token-test/Cargo.toml b/program-tests/compressed-token-test/Cargo.toml index 4ef9ec9580..6be4045545 100644 --- a/program-tests/compressed-token-test/Cargo.toml +++ b/program-tests/compressed-token-test/Cargo.toml @@ -15,13 +15,13 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] test-sbf = [] -custom-heap = [] -default = ["custom-heap"] +default = [] [dependencies] +anchor-lang = { workspace = true } +light-sdk = { workspace = true, features = ["anchor"] } [dev-dependencies] -anchor-lang = { workspace = true } light-compressed-token = { workspace = true } light-system-program-anchor = { workspace = true } account-compression = { workspace = true } diff --git a/program-tests/compressed-token-test/src/lib.rs b/program-tests/compressed-token-test/src/lib.rs index ff7bd09c0c..e2e0ef87bc 100644 --- a/program-tests/compressed-token-test/src/lib.rs +++ b/program-tests/compressed-token-test/src/lib.rs @@ -1 +1,70 @@ -// placeholder +#![allow(clippy::too_many_arguments)] +#![allow(unexpected_cfgs)] +#![allow(deprecated)] + +use anchor_lang::{prelude::*, solana_program::instruction::Instruction}; + +declare_id!("CompressedTokenTestProgram11111111111111111"); + +#[program] +pub mod compressed_token_test { + use super::*; + + /// Wrapper for write_to_cpi_context mode mint_action CPI + /// All accounts are in remaining_accounts (unchecked) + pub fn write_to_cpi_context_mint_action<'info>( + ctx: Context<'_, '_, '_, 'info, MintActionCpiWrapper<'info>>, + inputs: Vec, + ) -> Result<()> { + execute_mint_action_cpi(ctx, inputs) + } + + /// Wrapper for execute_cpi_context mode mint_action CPI + /// All accounts are in remaining_accounts (unchecked) + pub fn execute_cpi_context_mint_action<'info>( + ctx: Context<'_, '_, '_, 'info, MintActionCpiWrapper<'info>>, + inputs: Vec, + ) -> Result<()> { + execute_mint_action_cpi(ctx, inputs) + } +} + +/// Minimal account structure - only compressed token program ID +/// Everything else goes in remaining_accounts with no validation +#[derive(Accounts)] +pub struct MintActionCpiWrapper<'info> { + /// CHECK: Compressed token program - no validation + pub compressed_token_program: AccountInfo<'info>, +} + +/// Shared implementation for both wrapper instructions +/// Passes through raw instruction bytes and accounts without any validation +fn execute_mint_action_cpi<'info>( + ctx: Context<'_, '_, '_, 'info, MintActionCpiWrapper<'info>>, + inputs: Vec, +) -> Result<()> { + // Build account_metas from remaining_accounts - pass through as-is + let account_metas: Vec = ctx + .remaining_accounts + .iter() + .map(|acc| { + if acc.is_writable { + AccountMeta::new(*acc.key, acc.is_signer) + } else { + AccountMeta::new_readonly(*acc.key, acc.is_signer) + } + }) + .collect(); + + // Build instruction with raw bytes (no validation) + let instruction = Instruction { + program_id: *ctx.accounts.compressed_token_program.key, + accounts: account_metas, + data: inputs, // Pass through raw instruction bytes + }; + + // Simple invoke without any signer seeds + anchor_lang::solana_program::program::invoke(&instruction, ctx.remaining_accounts)?; + + Ok(()) +} diff --git a/program-tests/compressed-token-test/tests/mint.rs b/program-tests/compressed-token-test/tests/mint.rs index 0e65b62441..4cffcafeeb 100644 --- a/program-tests/compressed-token-test/tests/mint.rs +++ b/program-tests/compressed-token-test/tests/mint.rs @@ -2,6 +2,9 @@ // This file serves as the entry point for the mint test module // Declare submodules from the mint/ directory +#[path = "mint/cpi_context.rs"] +mod cpi_context; + #[path = "mint/edge_cases.rs"] mod edge_cases; diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs new file mode 100644 index 0000000000..7b40a10339 --- /dev/null +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -0,0 +1,387 @@ +use anchor_lang::InstructionData; +use compressed_token_test::ID as WRAPPER_PROGRAM_ID; +use light_client::indexer::Indexer; +use light_compressed_token_sdk::instructions::{ + derive_compressed_mint_address, find_spl_mint_address, + mint_action::instruction::{ + create_mint_action_cpi, CreateMintCpiWriteInputs, MintActionInputs, + MintActionInputsCpiWrite, + }, +}; +use light_ctoken_types::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMintMetadata, + COMPRESSED_TOKEN_PROGRAM_ID, +}; +use light_program_test::{utils::assert::assert_rpc_error, LightProgramTest, ProgramTestConfig}; +use light_test_utils::Rpc; +use light_verifier::CompressedProof; +use serial_test::serial; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +struct TestSetup { + rpc: LightProgramTest, + mint_action_inputs: MintActionInputsCpiWrite, + payer: Keypair, + mint_seed: Keypair, + mint_authority: Keypair, + compressed_mint_address: [u8; 32], + cpi_context_pubkey: Pubkey, +} + +async fn test_setup() -> TestSetup { + // 1. Setup test environment with wrapper program + let rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + true, + Some(vec![("compressed_token_test", WRAPPER_PROGRAM_ID)]), + )) + .await + .expect("Failed to setup test programs"); + + let payer = rpc.get_payer().insecure_clone(); + let address_tree_info = rpc.get_address_tree_v2(); + let address_tree = address_tree_info.tree; + + // Get CPI context from test accounts + let tree_info = rpc.test_accounts.v2_state_trees[0]; + let cpi_context_pubkey = tree_info.cpi_context; + + // 2. Create mint parameters + let mint_seed = Keypair::new(); + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + let decimals = 9u8; + + // Derive addresses + let compressed_mint_address = + derive_compressed_mint_address(&mint_seed.pubkey(), &address_tree); + let (spl_mint_pda, mint_bump) = find_spl_mint_address(&mint_seed.pubkey()); + + // 3. Build mint action instruction using SDK + let compressed_mint_inputs = CompressedMintWithContext { + leaf_index: 0, + prove_by_index: false, + root_index: 0, + address: compressed_mint_address, + mint: CompressedMintInstructionData { + supply: 0, + decimals, + metadata: CompressedMintMetadata { + version: 3, + spl_mint_initialized: false, + mint: spl_mint_pda.into(), + }, + mint_authority: Some(mint_authority.pubkey().into()), + freeze_authority: Some(freeze_authority.into()), + extensions: None, + }, + }; + + let create_mint_inputs = CreateMintCpiWriteInputs { + compressed_mint_inputs, + mint_seed: mint_seed.pubkey(), + mint_bump, + authority: mint_authority.pubkey(), + payer: payer.pubkey(), + cpi_context_pubkey, + first_set_context: true, + address_tree_index: 1, + output_queue_index: 0, + assigned_account_index: 0, + }; + + let mint_action_inputs = MintActionInputsCpiWrite::new_create_mint(create_mint_inputs); + + TestSetup { + rpc, + mint_action_inputs, + payer, + mint_seed, + mint_authority, + compressed_mint_address, + cpi_context_pubkey, + } +} + +#[tokio::test] +#[serial] +async fn test_write_to_cpi_context_create_mint() { + let TestSetup { + mut rpc, + mint_action_inputs, + payer, + mint_seed, + mint_authority, + compressed_mint_address, + cpi_context_pubkey, + } = test_setup().await; + + // Get the compressed token program instruction + let ctoken_instruction = + light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( + mint_action_inputs, + ) + .expect("Failed to build mint action instruction"); + + // Build wrapper program instruction + // The wrapper just passes through all accounts and instruction data + + // Build the wrapper instruction using Anchor's InstructionData + let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { + inputs: ctoken_instruction.data.clone(), + }; + + let wrapper_instruction = Instruction { + program_id: WRAPPER_PROGRAM_ID, + accounts: vec![AccountMeta::new_readonly( + Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + false, + )] + .into_iter() + .chain(ctoken_instruction.accounts.clone()) + .collect(), + data: wrapper_ix_data.data(), + }; + + // Execute wrapper instruction + rpc.create_and_send_transaction( + &[wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await + .expect("Failed to execute wrapper instruction"); + + // Verify CPI context account has data written + let cpi_context_account_data = rpc + .get_account(cpi_context_pubkey) + .await + .expect("Failed to get CPI context account") + .expect("CPI context account should exist"); + + // Verify the account has data (not empty) + assert!( + !cpi_context_account_data.data.is_empty(), + "CPI context account should have data" + ); + + // Verify the account is owned by light system program + assert_eq!( + cpi_context_account_data.owner, + light_system_program::ID, + "CPI context account should be owned by light system program" + ); + + // Verify no on-chain compressed mint was created (write mode doesn't execute) + let indexer_result = rpc + .indexer() + .unwrap() + .get_compressed_account(compressed_mint_address, None) + .await + .unwrap() + .value; + + assert!( + indexer_result.is_none(), + "Compressed mint should NOT exist (write mode doesn't execute)" + ); +} + +#[tokio::test] +#[serial] +async fn test_write_to_cpi_context_invalid_address_tree() { + let TestSetup { + mut rpc, + mut mint_action_inputs, + payer, + mint_seed, + mint_authority, + compressed_mint_address: _, + cpi_context_pubkey: _, + } = test_setup().await; + + // Swap the address tree pubkey to a random one (this should fail validation) + let invalid_address_tree = Pubkey::new_unique(); + mint_action_inputs.cpi_context.address_tree_pubkey = invalid_address_tree.to_bytes(); + + // Get the compressed token program instruction + let ctoken_instruction = + light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( + mint_action_inputs, + ) + .expect("Failed to build mint action instruction"); + + // Build wrapper program instruction + let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { + inputs: ctoken_instruction.data.clone(), + }; + + let wrapper_instruction = Instruction { + program_id: WRAPPER_PROGRAM_ID, + accounts: vec![AccountMeta::new_readonly( + Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + false, + )] + .into_iter() + .chain(ctoken_instruction.accounts.clone()) + .collect(), + data: wrapper_ix_data.data(), + }; + + // Execute wrapper instruction - should fail + let result = rpc + .create_and_send_transaction( + &[wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await; + + // Assert that the transaction failed with MintActionInvalidCpiContextAddressTreePubkey error + // Error code 105 = MintActionInvalidCpiContextAddressTreePubkey + assert_rpc_error(result, 0, 105).unwrap(); +} + +#[tokio::test] +#[serial] +async fn test_write_to_cpi_context_invalid_compressed_address() { + let TestSetup { + mut rpc, + mut mint_action_inputs, + payer, + mint_seed, + mint_authority, + compressed_mint_address: _, + cpi_context_pubkey: _, + } = test_setup().await; + + // Swap the compressed address to a random one (this should fail validation) + // Keep the correct address_tree_pubkey (CMINT_ADDRESS_TREE) but provide wrong address + let invalid_compressed_address = [42u8; 32]; + mint_action_inputs.compressed_mint_inputs.address = invalid_compressed_address; + + // Get the compressed token program instruction + let ctoken_instruction = + light_compressed_token_sdk::instructions::mint_action::instruction::mint_action_cpi_write( + mint_action_inputs, + ) + .expect("Failed to build mint action instruction"); + + // Build wrapper program instruction + let wrapper_ix_data = compressed_token_test::instruction::WriteToCpiContextMintAction { + inputs: ctoken_instruction.data.clone(), + }; + + let wrapper_instruction = Instruction { + program_id: WRAPPER_PROGRAM_ID, + accounts: vec![AccountMeta::new_readonly( + Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + false, + )] + .into_iter() + .chain(ctoken_instruction.accounts.clone()) + .collect(), + data: wrapper_ix_data.data(), + }; + + // Execute wrapper instruction - should fail + let result = rpc + .create_and_send_transaction( + &[wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await; + + // Assert that the transaction failed with MintActionInvalidCompressedMintAddress error + // Error code 103 = MintActionInvalidCompressedMintAddress + assert_rpc_error(result, 0, 103).unwrap(); +} + +#[tokio::test] +#[serial] +async fn test_execute_cpi_context_invalid_tree_index() { + let TestSetup { + mut rpc, + mint_action_inputs, + payer, + mint_seed, + mint_authority, + compressed_mint_address: _, + cpi_context_pubkey, + } = test_setup().await; + + // Now try to execute with invalid in_tree_index (should fail) + // Build execute mode CPI context with invalid tree index + let execute_cpi_context = light_ctoken_types::instructions::mint_action::CpiContext { + set_context: false, + first_set_context: false, // Execute mode + in_tree_index: 5, // Invalid! Should be 1 + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: light_ctoken_types::CMINT_ADDRESS_TREE, + }; + + // Get tree info for execute mode + let tree_info = rpc.test_accounts.v2_state_trees[0]; + + // Build MintActionInputs for execute mode + let execute_inputs = MintActionInputs { + compressed_mint_inputs: mint_action_inputs.compressed_mint_inputs.clone(), + mint_seed: mint_seed.pubkey(), + mint_bump: mint_action_inputs.mint_bump, + create_mint: true, + authority: mint_action_inputs.authority, + payer: mint_action_inputs.payer, + proof: Some(CompressedProof::default()), + actions: vec![], + address_tree_pubkey: Pubkey::new_from_array(light_ctoken_types::CMINT_ADDRESS_TREE), + input_queue: None, + output_queue: tree_info.output_queue, + tokens_out_queue: None, + token_pool: None, + }; + + let execute_instruction = create_mint_action_cpi( + execute_inputs, + Some(execute_cpi_context), + Some(cpi_context_pubkey), + ) + .expect("Failed to build execute instruction"); + + let execute_wrapper_ix_data = compressed_token_test::instruction::ExecuteCpiContextMintAction { + inputs: execute_instruction.data.clone(), + }; + + let execute_wrapper_instruction = Instruction { + program_id: WRAPPER_PROGRAM_ID, + accounts: vec![AccountMeta::new_readonly( + Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID), + false, + )] + .into_iter() + .chain(execute_instruction.accounts.clone()) + .collect(), + data: execute_wrapper_ix_data.data(), + }; + + // Execute wrapper instruction - should fail + let result = rpc + .create_and_send_transaction( + &[execute_wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await; + + // Assert that the transaction failed with MintActionInvalidCpiContextForCreateMint error + // Error code 104 = MintActionInvalidCpiContextForCreateMint + assert_rpc_error(result, 0, 104).unwrap(); +} diff --git a/programs/compressed-token/program/tests/queue_indices.rs b/programs/compressed-token/program/tests/queue_indices.rs index fbb4ee90df..cfa11339e8 100644 --- a/programs/compressed-token/program/tests/queue_indices.rs +++ b/programs/compressed-token/program/tests/queue_indices.rs @@ -316,10 +316,6 @@ fn test_queue_indices_invalid_address_tree_index() { "Expected MintActionInvalidCpiContextForCreateMint, got {:?}", e ); - println!( - "✅ Correctly rejected invalid in_tree_index={} in execute mode", - cpi_context.in_tree_index - ); println!(" Error: {:?}", e); } } @@ -352,5 +348,4 @@ fn test_queue_indices_invalid_address_tree_index() { "Expected Ok with in_tree_index=1, got error: {:?}", result.err() ); - println!("✅ Correctly accepted valid in_tree_index=1 in execute mode"); }