From 6451e6d10768efa04645e5fae44802f3f3094afb Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 21 Feb 2026 22:43:54 +0000 Subject: [PATCH 1/5] chore: add mint creation fee Entire-Checkpoint: 3b001a578b94 --- .../tests/mint/cpi_context.rs | 4 +- .../tests/mint/failing.rs | 69 +++++++++++++++++++ .../legacy/instructions/mint_action.rs | 21 +++--- program-tests/utils/src/lib.rs | 1 + program-tests/utils/src/mint_assert.rs | 15 ++++ .../compressed_token/mint_action/accounts.rs | 20 +++++- .../compressed_token/mint_action/processor.rs | 17 ++++- programs/compressed-token/program/src/lib.rs | 1 + .../v2/create_compressed_mint/instruction.rs | 3 + .../v2/mint_action/account_metas.rs | 7 ++ .../v2/mint_action/cpi_accounts.rs | 18 +++++ .../tests/mint_action_cpi_accounts_tests.rs | 12 +++- .../sdk-token-test/src/pda_ctoken/mint.rs | 2 + .../sdk-token-test/src/pda_ctoken/mod.rs | 3 + sdk-tests/sdk-token-test/tests/pda_ctoken.rs | 2 + .../sdk-token-test/tests/test_4_transfer2.rs | 2 + .../tests/test_compress_full_and_close.rs | 1 + 17 files changed, 184 insertions(+), 14 deletions(-) diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs index c22c2f4ad8..85b51e8ff0 100644 --- a/program-tests/compressed-token-test/tests/mint/cpi_context.rs +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -448,13 +448,15 @@ async fn test_execute_cpi_context_invalid_tree_index() { .with_cpi_context(execute_cpi_context); // Build account metas using regular MintActionMetaConfig for execute mode + let rent_sponsor = rpc.test_accounts.funding_pool_config.rent_sponsor_pda; let mut config = MintActionMetaConfig::new_create_mint( payer.pubkey(), mint_authority.pubkey(), mint_seed.pubkey(), Pubkey::new_from_array(MINT_ADDRESS_TREE), output_queue, - ); + ) + .with_rent_sponsor(rent_sponsor); // Set CPI context for execute mode config.cpi_context = Some(cpi_context_pubkey); diff --git a/program-tests/compressed-token-test/tests/mint/failing.rs b/program-tests/compressed-token-test/tests/mint/failing.rs index 0a44925acd..a05851feb4 100644 --- a/program-tests/compressed-token-test/tests/mint/failing.rs +++ b/program-tests/compressed-token-test/tests/mint/failing.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::borsh::BorshDeserialize; use light_client::indexer::Indexer; +use light_compressed_token::MINT_CREATION_FEE; use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ derive_mint_compressed_address, find_mint_address, }; @@ -12,6 +13,7 @@ use light_test_utils::{ legacy::instructions::mint_action::{MintActionType, MintToRecipient}, }, assert_mint_action::assert_mint_action, + assert_mint_creation_fee, mint_assert::assert_compressed_mint_account, Rpc, }; @@ -1121,3 +1123,70 @@ async fn test_compress_and_close_mint_must_be_only_action() { ) .unwrap(); } + +/// Tests that the mint creation fee is charged from fee_payer to rent_sponsor. +/// Also tests that the fee is charged even without any actions (compressed-only mint). +#[tokio::test] +#[serial] +async fn test_mint_creation_fee_charged() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)) + .await + .unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let rent_sponsor = rpc.test_accounts.funding_pool_config.rent_sponsor_pda; + let mint_seed = Keypair::new(); + let mint_authority = Keypair::new(); + + // Capture balances before + let rent_sponsor_before = rpc + .get_account(rent_sponsor) + .await + .unwrap() + .unwrap() + .lamports; + let fee_payer_before = rpc + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // Create compressed mint (no actions) + create_mint( + &mut rpc, + &mint_seed, + 6, // decimals + &mint_authority, + None, + None, + &payer, + ) + .await + .unwrap(); + + // Capture balances after + let rent_sponsor_after = rpc + .get_account(rent_sponsor) + .await + .unwrap() + .unwrap() + .lamports; + let fee_payer_after = rpc + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + + // Assert fee was credited to rent_sponsor + assert_mint_creation_fee(rent_sponsor_before, rent_sponsor_after); + + // Assert fee was debited from fee_payer (use <= because tx base fees are also deducted) + assert!( + fee_payer_after <= fee_payer_before - MINT_CREATION_FEE, + "Fee payer should have paid at least {} lamports mint creation fee (before={}, after={})", + MINT_CREATION_FEE, + fee_payer_before, + fee_payer_after, + ); +} diff --git a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs index 5ae25f6984..b871247eed 100644 --- a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs +++ b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs @@ -354,10 +354,10 @@ pub async fn create_mint_action_instruction( // Add CMint account handling based on operation type: // 1. DecompressMint/CompressAndCloseMint: needs config, cmint, and rent_sponsor - // 2. Already decompressed (cmint_decompressed): only needs cmint account + // 2. Plain create_mint (no decompress/compress-and-close): needs only rent_sponsor for fee + // 3. Already decompressed (cmint_decompressed): only needs cmint account let (mint_pda, _) = find_mint_address(¶ms.mint_seed); - if has_decompress_mint || has_compress_and_close_mint { - // Get config and rent_sponsor from v1 config PDA + if is_creating_mint || has_decompress_mint || has_compress_and_close_mint { let config_address = CompressibleConfig::light_token_v1_config_pda(); let compressible_config: CompressibleConfig = rpc .get_anchor_account(&config_address) @@ -368,11 +368,16 @@ pub async fn create_mint_action_instruction( config_address )) })?; - config = config.with_compressible_mint( - mint_pda, - config_address, - compressible_config.rent_sponsor, - ); + if has_decompress_mint || has_compress_and_close_mint { + config = config.with_compressible_mint( + mint_pda, + config_address, + compressible_config.rent_sponsor, + ); + } else { + // Plain create_mint: only rent_sponsor needed, no config or cmint account + config = config.with_rent_sponsor(compressible_config.rent_sponsor); + } } else if cmint_decompressed { // For operations on already-decompressed mints, only need the cmint account config = config.with_mint(mint_pda); diff --git a/program-tests/utils/src/lib.rs b/program-tests/utils/src/lib.rs index 9af83e2d03..a7fa07c4f8 100644 --- a/program-tests/utils/src/lib.rs +++ b/program-tests/utils/src/lib.rs @@ -45,6 +45,7 @@ pub mod e2e_test_env; pub mod legacy_cpi_context_account; pub mod mint_2022; pub mod mint_assert; +pub use mint_assert::assert_mint_creation_fee; pub mod mock_batched_forester; pub mod pack; pub mod registered_program_accounts_v1; diff --git a/program-tests/utils/src/mint_assert.rs b/program-tests/utils/src/mint_assert.rs index 115ff2141c..d8e9fab434 100644 --- a/program-tests/utils/src/mint_assert.rs +++ b/program-tests/utils/src/mint_assert.rs @@ -92,3 +92,18 @@ pub fn assert_compressed_mint_account( expected_compressed_mint } + +/// Assert that the mint creation fee (50,000 lamports) was charged. +/// Compares rent_sponsor balance before and after mint creation. +#[track_caller] +pub fn assert_mint_creation_fee( + rent_sponsor_lamports_before: u64, + rent_sponsor_lamports_after: u64, +) { + assert_eq!( + rent_sponsor_lamports_after, + rent_sponsor_lamports_before + light_compressed_token::MINT_CREATION_FEE, + "Rent sponsor should receive {} lamports mint creation fee", + light_compressed_token::MINT_CREATION_FEE, + ); +} diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index 4df2429007..c9fa2473dd 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -110,9 +110,8 @@ impl<'info> MintActionAccounts<'info> { // CMint account required if already decompressed OR being decompressed/closed let cmint = iter.next_option_mut("cmint", config.needs_cmint_account())?; - // Parse rent_sponsor when creating or closing CMint - let rent_sponsor = - iter.next_option_mut("rent_sponsor", config.needs_compressible_accounts())?; + // Parse rent_sponsor when creating mint (fee recipient) or when creating/closing CMint + let rent_sponsor = iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor())?; let system = LightSystemAccounts::validate_and_parse( &mut iter, @@ -377,6 +376,13 @@ impl AccountsConfig { self.has_decompress_mint_action || self.has_compress_and_close_cmint_action } + /// Returns true if rent_sponsor account is needed. + /// Required when creating a new mint (mint creation fee recipient) or when compressible accounts are needed. + #[inline(always)] + pub fn needs_rent_sponsor(&self) -> bool { + self.create_mint || self.needs_compressible_accounts() + } + /// Returns true if CMint account is needed in the transaction. /// Required when: already decompressed, decompressing, or compressing and closing CMint. #[inline(always)] @@ -452,6 +458,14 @@ impl AccountsConfig { let cmint_decompressed = parsed_instruction_data.mint.is_none(); if write_to_cpi_context { + // Cannot create a compressed mint when writing to CPI context. + // Mint creation charges the creation fee and requires the rent_sponsor account, + // which is not available in the CPI context write path. + if parsed_instruction_data.create_mint.is_some() { + msg!("Compressed mint creation not allowed when writing to cpi context"); + return Err(ErrorCode::CpiContextSetNotUsable.into()); + } + // Must not have any MintToCToken actions let has_mint_to_ctoken_actions = parsed_instruction_data .actions diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index a7d0031c32..326dc17f1f 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -20,7 +20,8 @@ use crate::{ queue_indices::QueueIndices, zero_copy_config::get_zero_copy_configs, }, - shared::cpi::execute_cpi_invoke, + shared::{convert_program_error, cpi::execute_cpi_invoke, transfer_lamports_via_cpi}, + MINT_CREATION_FEE, }; pub fn process_mint_action( @@ -43,6 +44,20 @@ pub fn process_mint_action( let validated_accounts = MintActionAccounts::validate_and_parse(accounts, &accounts_config, cmint_pubkey.as_ref())?; + // Charge mint creation fee. create_mint is rejected in CPI context write path + // (validated in AccountsConfig::new), so executing and rent_sponsor are always present here. + if accounts_config.create_mint { + let executing = validated_accounts + .executing + .as_ref() + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + let rent_sponsor = executing + .rent_sponsor + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor) + .map_err(convert_program_error)?; + } + // Get mint data based on source: // 1. Creating new mint: mint data required in instruction // 2. Existing compressed mint: mint data in instruction (cmint_decompressed = false) diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index d88773e4dc..ec62bf0610 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -36,6 +36,7 @@ pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); pub const MAX_ACCOUNTS: usize = 30; +pub const MINT_CREATION_FEE: u64 = 50_000; pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40; /// Maximum number of compression operations per instruction. /// Used for compression_to_input lookup array sizing. diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs index 5e8e254928..466250fcfb 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs @@ -36,6 +36,8 @@ pub struct CreateMintInputs { pub output_queue: Pubkey, pub extensions: Option>, pub version: u8, + /// Rent sponsor PDA that receives the mint creation fee. + pub rent_sponsor: Pubkey, } /// Creates a compressed mint instruction (wrapper around mint_action) @@ -85,6 +87,7 @@ pub fn create_compressed_mint_cpi( input.address_tree_pubkey, input.output_queue, ) + .with_rent_sponsor(input.rent_sponsor) }; let account_metas = meta_config.to_account_metas(); diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs index e04b9eda11..c6d082fd82 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -124,6 +124,13 @@ impl MintActionMetaConfig { self } + /// Set only the rent_sponsor account (without compressible_config or mint/cmint). + /// Required for create_mint operations to receive the mint creation fee. + pub fn with_rent_sponsor(mut self, rent_sponsor: Pubkey) -> Self { + self.rent_sponsor = Some(rent_sponsor); + self + } + /// Configure compressible Mint with config and rent sponsor. /// Mint is always compressible - this sets all required accounts. pub fn with_compressible_mint( diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs index 051f727c39..850657e23a 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs @@ -45,6 +45,9 @@ pub struct MintActionCpiAccounts<'a, A: AccountInfoTrait + Clone> { pub mint_signer: Option<&'a A>, pub authority: &'a A, + /// Rent sponsor PDA โ€” required when creating a new compressed mint (receives the creation fee). + pub rent_sponsor: Option<&'a A>, + pub fee_payer: &'a A, pub compressed_token_cpi_authority: &'a A, pub registered_program_pda: &'a A, @@ -86,6 +89,8 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { return Err(AccountError::InvalidSigner.into()); } + let rent_sponsor = iter.next_option_mut("rent_sponsor", config.create_mint)?; + let fee_payer = iter.next_account("fee_payer")?; if !fee_payer.is_signer() || !fee_payer.is_writable() { msg!("Fee payer must be a signer and mutable"); @@ -153,6 +158,7 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { light_system_program, mint_signer, authority, + rent_sponsor, fee_payer, compressed_token_cpi_authority, registered_program_pda, @@ -204,6 +210,10 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { accounts.push(self.authority.clone()); + if let Some(sponsor) = self.rent_sponsor { + accounts.push(sponsor.clone()); + } + accounts.extend_from_slice( &[ self.fee_payer.clone(), @@ -261,6 +271,14 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { is_signer: true, }); + if let Some(sponsor) = self.rent_sponsor { + metas.push(AccountMeta { + pubkey: sponsor.key().into(), + is_writable: true, + is_signer: false, + }); + } + metas.push(AccountMeta { pubkey: self.fee_payer.key().into(), is_writable: true, diff --git a/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs b/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs index 48665a7269..42ce47cbb1 100644 --- a/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs +++ b/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs @@ -6,7 +6,7 @@ use light_compressed_token_sdk::compressed_token::mint_action::{ }; use light_sdk_types::{ ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, LIGHT_SYSTEM_PROGRAM_ID, - REGISTERED_PROGRAM_PDA, + LIGHT_TOKEN_RENT_SPONSOR, REGISTERED_PROGRAM_PDA, }; use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; use light_token_types::CPI_AUTHORITY_PDA; @@ -286,6 +286,15 @@ fn test_successful_create_mint() { create_test_account(mint_signer, [0u8; 32], true, false, false, vec![]), // Authority create_test_account(pubkey_unique(), [0u8; 32], true, false, false, vec![]), + // Rent sponsor (required for create_mint, known PDA) + create_test_account( + LIGHT_TOKEN_RENT_SPONSOR, + [0u8; 32], + false, + true, + false, + vec![], + ), // Fee payer create_test_account(pubkey_unique(), [0u8; 32], true, true, false, vec![]), // Core system accounts @@ -342,6 +351,7 @@ fn test_successful_create_mint() { let parsed = result.unwrap(); assert!(parsed.mint_signer.is_some()); + assert!(parsed.rent_sponsor.is_some()); // Required for create_mint assert!(parsed.in_output_queue.is_none()); // Not needed for create_mint } diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 0d803802b2..9bc7da619e 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -55,6 +55,7 @@ pub fn process_mint_action<'a, 'info>( light_system_program: cpi_accounts.system_program().unwrap(), mint_signer: Some(ctx.accounts.mint_seed.as_ref()), authority: ctx.accounts.mint_authority.as_ref(), + rent_sponsor: Some(ctx.accounts.rent_sponsor.as_ref()), fee_payer: ctx.accounts.payer.as_ref(), compressed_token_cpi_authority: ctx.accounts.light_token_cpi_authority.as_ref(), registered_program_pda: cpi_accounts.registered_program_pda().unwrap(), @@ -79,6 +80,7 @@ pub fn process_mint_action<'a, 'info>( account_infos.push(ctx.accounts.mint_authority.to_account_info()); account_infos.push(ctx.accounts.mint_seed.to_account_info()); account_infos.push(ctx.accounts.payer.to_account_info()); + account_infos.push(ctx.accounts.rent_sponsor.to_account_info()); msg!("mint_action_instruction {:?}", mint_action_instruction); msg!( "account infos pubkeys {:?}", diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs index d5e9efc15e..b73a357002 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs @@ -16,4 +16,7 @@ pub struct PdaCToken<'info> { pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub light_token_cpi_authority: UncheckedAccount<'info>, + /// CHECK: Rent sponsor PDA that receives the mint creation fee. + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info>, } diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index fcae1ce1f7..24c437c087 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -6,6 +6,7 @@ use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ }; use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc, RpcError}; use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +use light_token::instruction::rent_sponsor_pda; use light_token_interface::{ instructions::{ extensions::token_metadata::TokenMetadataInstructionData, @@ -257,6 +258,7 @@ pub async fn create_mint( mint_seed: mint_seed.pubkey(), light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), light_token_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + rent_sponsor: rent_sponsor_pda(), }; let pda_new_address_params = light_sdk::address::NewAddressParamsAssignedPacked { diff --git a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs index 991b44d6b4..6544778c08 100644 --- a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs +++ b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs @@ -12,6 +12,7 @@ use light_sdk::{ instruction::{PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, }; use light_test_utils::RpcError; +use light_token::instruction::rent_sponsor_pda; use light_token_interface::{ instructions::{ mint_action::{MintWithContext, Recipient}, @@ -205,6 +206,7 @@ async fn create_compressed_mint_helper( address_tree_pubkey, output_queue, extensions: None, + rent_sponsor: rent_sponsor_pda(), }) .unwrap(); diff --git a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs index 99e99751e7..a37ed61512 100644 --- a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs +++ b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs @@ -88,6 +88,7 @@ async fn test_compress_full_and_close() { address_tree_pubkey, output_queue, extensions: None, + rent_sponsor: rent_sponsor_pda(), }) .unwrap(); From 64ddc06b2510e4a18ee50b52d835687811b80fc7 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 21 Feb 2026 23:45:12 +0000 Subject: [PATCH 2/5] fix tests Entire-Checkpoint: b5b9ac87e33d --- .../tests/mint/cpi_context.rs | 58 +++++-------------- .../legacy/instructions/mint_action.rs | 37 ++++++++---- .../compressed_token/mint_action/accounts.rs | 11 +++- .../compressed_token/mint_action/processor.rs | 6 +- .../program/tests/mint_action.rs | 13 ++++- .../v2/create_compressed_mint/instruction.rs | 2 +- .../v2/mint_action/account_metas.rs | 5 +- .../token-sdk/src/instruction/create_mint.rs | 1 + .../token-sdk/src/instruction/create_mints.rs | 2 + 9 files changed, 72 insertions(+), 63 deletions(-) diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs index 85b51e8ff0..7fc7136556 100644 --- a/program-tests/compressed-token-test/tests/mint/cpi_context.rs +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -1,6 +1,5 @@ use anchor_lang::InstructionData; use compressed_token_test::ID as WRAPPER_PROGRAM_ID; -use light_client::indexer::Indexer; use light_compressed_account::instruction_data::traits::LightInstructionData; use light_compressed_token_sdk::compressed_token::{ create_compressed_mint::{derive_mint_compressed_address, find_mint_address}, @@ -117,7 +116,7 @@ async fn test_write_to_cpi_context_create_mint() { payer, mint_seed, mint_authority, - compressed_mint_address, + compressed_mint_address: _, cpi_context_pubkey, address_tree, address_tree_index, @@ -181,48 +180,17 @@ async fn test_write_to_cpi_context_create_mint() { 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" - ); + // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context + // is rejected (error 6035: CpiContextSetNotUsable). + let result = rpc + .create_and_send_transaction( + &[wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await; - // 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)" - ); + assert_rpc_error(result, 0, 6035).unwrap(); } #[tokio::test] @@ -455,8 +423,8 @@ async fn test_execute_cpi_context_invalid_tree_index() { mint_seed.pubkey(), Pubkey::new_from_array(MINT_ADDRESS_TREE), output_queue, - ) - .with_rent_sponsor(rent_sponsor); + rent_sponsor, + ); // Set CPI context for execute mode config.cpi_context = Some(cpi_context_pubkey); diff --git a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs index b871247eed..03828ecc4d 100644 --- a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs +++ b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs @@ -324,6 +324,23 @@ pub async fn create_mint_action_instruction( } // Build account metas configuration + // Fetch rent_sponsor early when creating mint (needed as a required parameter). + let create_mint_rent_sponsor = if is_creating_mint { + let config_address = CompressibleConfig::light_token_v1_config_pda(); + let compressible_config: CompressibleConfig = rpc + .get_anchor_account(&config_address) + .await? + .ok_or_else(|| { + RpcError::CustomError(format!( + "CompressibleConfig not found at {}", + config_address + )) + })?; + Some(compressible_config.rent_sponsor) + } else { + None + }; + let mut config = if is_creating_mint { MintActionMetaConfig::new_create_mint( params.payer, @@ -331,6 +348,7 @@ pub async fn create_mint_action_instruction( params.mint_seed, address_tree_pubkey, state_tree_info.queue, + create_mint_rent_sponsor.unwrap(), ) } else { MintActionMetaConfig::new( @@ -354,10 +372,10 @@ pub async fn create_mint_action_instruction( // Add CMint account handling based on operation type: // 1. DecompressMint/CompressAndCloseMint: needs config, cmint, and rent_sponsor - // 2. Plain create_mint (no decompress/compress-and-close): needs only rent_sponsor for fee + // 2. Plain create_mint: rent_sponsor already set via new_create_mint() // 3. Already decompressed (cmint_decompressed): only needs cmint account let (mint_pda, _) = find_mint_address(¶ms.mint_seed); - if is_creating_mint || has_decompress_mint || has_compress_and_close_mint { + if has_decompress_mint || has_compress_and_close_mint { let config_address = CompressibleConfig::light_token_v1_config_pda(); let compressible_config: CompressibleConfig = rpc .get_anchor_account(&config_address) @@ -368,16 +386,11 @@ pub async fn create_mint_action_instruction( config_address )) })?; - if has_decompress_mint || has_compress_and_close_mint { - config = config.with_compressible_mint( - mint_pda, - config_address, - compressible_config.rent_sponsor, - ); - } else { - // Plain create_mint: only rent_sponsor needed, no config or cmint account - config = config.with_rent_sponsor(compressible_config.rent_sponsor); - } + config = config.with_compressible_mint( + mint_pda, + config_address, + compressible_config.rent_sponsor, + ); } else if cmint_decompressed { // For operations on already-decompressed mints, only need the cmint account config = config.with_mint(mint_pda); diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index c9fa2473dd..86576d1cd1 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -20,8 +20,9 @@ pub struct MintActionAccounts<'info> { pub light_system_program: &'info AccountInfo, /// Seed for mint PDA derivation. /// Required only for compressed mint creation. - /// Note: mint_signer is not in executing accounts since create mint - /// is allowed in combination with write to cpi context. + /// Note: mint_signer is not in executing accounts since it is parsed + /// before the executing/cpi-write branch. create_mint is NOT allowed + /// in combination with write to cpi context (rejected in AccountsConfig::new). pub mint_signer: Option<&'info AccountInfo>, pub authority: &'info AccountInfo, /// Required accounts to execute an instruction @@ -450,6 +451,12 @@ impl AccountsConfig { return Err(ErrorCode::CompressAndCloseCMintMustBeOnlyAction.into()); } + // Validation: Cannot combine create_mint with CompressAndCloseCMint + if has_compress_and_close_cmint_action && parsed_instruction_data.create_mint.is_some() { + msg!("Cannot combine create_mint with CompressAndCloseCMint"); + return Err(ErrorCode::CompressAndCloseCMintMustBeOnlyAction.into()); + } + // We need mint signer only if creating a new mint. // CompressAndCloseCMint does NOT need mint_signer - it verifies CMint by compressed_mint.metadata.mint let with_mint_signer = parsed_instruction_data.create_mint.is_some(); diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index 326dc17f1f..21e89d7e05 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -159,9 +159,13 @@ pub fn process_mint_action( &accounts_config, ); - // Check for idempotent early exit - skip CPI and return success + // Check for idempotent early exit - skip CPI and return success. + // create_mint must never use idempotent early exit (fee already charged). if let Err(ref err) = result { if is_idempotent_early_exit(err) { + if accounts_config.create_mint { + return Err(ErrorCode::MintActionMissingExecutingAccounts.into()); + } return Ok(()); } } diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index 1ae6c9f390..494a18bf5e 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -345,10 +345,21 @@ fn check_if_config_should_error(instruction_data: &MintActionCompressedInstructi .metadata .mint_decompressed; + // Check if this is creating a new mint (not from an existing compressed mint) + let is_creating_mint = instruction_data.mint.is_none(); + + // Check if create_mint is set (create_mint + write_to_cpi_context not allowed) + let create_mint_with_cpi_write = instruction_data.create_mint.is_some(); + // Error conditions matching AccountsConfig::new: // 1. has_mint_to_ctoken (MintToCToken actions not allowed) // 2. mint_decompressed && require_token_output_queue (mint decompressed + MintToCompressed not allowed) - has_mint_to_ctoken || (mint_decompressed && require_token_output_queue) + // 3. is_creating_mint (mint creation not allowed when writing to cpi context) + // 4. create_mint_with_cpi_write (create_mint + write_to_cpi_context not allowed) + has_mint_to_ctoken + || (mint_decompressed && require_token_output_queue) + || is_creating_mint + || create_mint_with_cpi_write } else { false } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs index 466250fcfb..836a8d31bf 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs @@ -86,8 +86,8 @@ pub fn create_compressed_mint_cpi( input.mint_signer, input.address_tree_pubkey, input.output_queue, + input.rent_sponsor, ) - .with_rent_sponsor(input.rent_sponsor) }; let account_metas = meta_config.to_account_metas(); diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs index c6d082fd82..b6acfdfff7 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -23,12 +23,15 @@ pub struct MintActionMetaConfig { impl MintActionMetaConfig { /// Create a new MintActionMetaConfig for creating a new compressed mint. + /// `rent_sponsor` is required because mint creation charges a creation fee + /// transferred to the rent sponsor PDA. pub fn new_create_mint( fee_payer: Pubkey, authority: Pubkey, mint_signer: Pubkey, address_tree: Pubkey, output_queue: Pubkey, + rent_sponsor: Pubkey, ) -> Self { Self { fee_payer, @@ -42,7 +45,7 @@ impl MintActionMetaConfig { token_accounts: Vec::new(), mint: None, compressible_config: None, - rent_sponsor: None, + rent_sponsor: Some(rent_sponsor), mint_signer_must_sign: true, } } diff --git a/sdk-libs/token-sdk/src/instruction/create_mint.rs b/sdk-libs/token-sdk/src/instruction/create_mint.rs index 72b49ee513..7c43240d2a 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mint.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mint.rs @@ -165,6 +165,7 @@ impl CreateMint { self.mint_seed_pubkey, self.address_tree_pubkey, self.output_queue, + rent_sponsor_pda(), ) // Always include compressible accounts for Mint creation .with_compressible_mint(self.params.mint, config_pda(), rent_sponsor_pda()); diff --git a/sdk-libs/token-sdk/src/instruction/create_mints.rs b/sdk-libs/token-sdk/src/instruction/create_mints.rs index b29a73a8fb..f065fe5edc 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mints.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mints.rs @@ -265,6 +265,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { *self.mint_seed_accounts[0].key, *self.address_tree.key, *self.output_queue.key, + *self.rent_sponsor.key, ) .with_compressible_mint( *self.mints[0].key, @@ -437,6 +438,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { *self.mint_seed_accounts[last_idx].key, *self.address_tree.key, *self.output_queue.key, + *self.rent_sponsor.key, ) .with_compressible_mint( *self.mints[last_idx].key, From 0edce45aa1d26fc16852cf33e522e81431da560b Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 22 Feb 2026 01:09:57 +0000 Subject: [PATCH 3/5] fix tests Entire-Checkpoint: 75af397ef5ab --- .../tests/mint/cpi_context.rs | 17 ++--- .../legacy/instructions/mint_action.rs | 10 +-- .../compressed_token/mint_action/accounts.rs | 10 +-- .../compressed_token/mint_action/processor.rs | 9 +++ .../v2/create_compressed_mint/instruction.rs | 3 + .../v2/mint_action/account_metas.rs | 4 +- .../v2/mint_action/cpi_accounts.rs | 16 +++++ .../token-sdk/src/instruction/create_mint.rs | 1 + .../token-sdk/src/instruction/create_mints.rs | 2 + .../tests/mint_action_cpi_accounts_tests.rs | 3 + .../sdk-token-test/src/pda_ctoken/mint.rs | 2 + .../sdk-token-test/src/pda_ctoken/mod.rs | 2 + sdk-tests/sdk-token-test/tests/ctoken_pda.rs | 71 +++---------------- sdk-tests/sdk-token-test/tests/pda_ctoken.rs | 3 +- .../sdk-token-test/tests/test_4_transfer2.rs | 3 +- .../tests/test_compress_full_and_close.rs | 1 + .../tests/test_create_two_mints.rs | 48 ++++++++----- 17 files changed, 108 insertions(+), 97 deletions(-) diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs index 7fc7136556..aef143b49f 100644 --- a/program-tests/compressed-token-test/tests/mint/cpi_context.rs +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -8,6 +8,7 @@ use light_compressed_token_sdk::compressed_token::{ MintActionMetaConfigCpiWrite, }, }; +use light_compressible::config::CompressibleConfig; use light_program_test::{utils::assert::assert_rpc_error, LightProgramTest, ProgramTestConfig}; use light_test_utils::Rpc; use light_token_interface::{ @@ -269,7 +270,8 @@ async fn test_write_to_cpi_context_invalid_address_tree() { data: wrapper_ix_data.data(), }; - // Execute wrapper instruction - should fail + // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context + // is rejected (error 6035: CpiContextSetNotUsable) before address tree validation. let result = rpc .create_and_send_transaction( &[wrapper_instruction], @@ -278,9 +280,7 @@ async fn test_write_to_cpi_context_invalid_address_tree() { ) .await; - // Assert that the transaction failed with MintActionInvalidCpiContextAddressTreePubkey error - // Error code 6105 = MintActionInvalidCpiContextAddressTreePubkey - assert_rpc_error(result, 0, 6105).unwrap(); + assert_rpc_error(result, 0, 6035).unwrap(); } #[tokio::test] @@ -363,7 +363,8 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { data: wrapper_ix_data.data(), }; - // Execute wrapper instruction - should fail + // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context + // is rejected (error 6035: CpiContextSetNotUsable) before mint signer validation. let result = rpc .create_and_send_transaction( &[wrapper_instruction], @@ -372,9 +373,7 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { ) .await; - // Assert that the transaction failed with MintActionInvalidMintSigner error - // Error code 6171 = MintActionInvalidMintSigner (mint_signer mismatch is caught before compressed address validation) - assert_rpc_error(result, 0, 6171).unwrap(); + assert_rpc_error(result, 0, 6035).unwrap(); } #[tokio::test] @@ -417,12 +416,14 @@ async fn test_execute_cpi_context_invalid_tree_index() { // Build account metas using regular MintActionMetaConfig for execute mode let rent_sponsor = rpc.test_accounts.funding_pool_config.rent_sponsor_pda; + let compressible_config = CompressibleConfig::light_token_v1_config_pda(); let mut config = MintActionMetaConfig::new_create_mint( payer.pubkey(), mint_authority.pubkey(), mint_seed.pubkey(), Pubkey::new_from_array(MINT_ADDRESS_TREE), output_queue, + compressible_config, rent_sponsor, ); diff --git a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs index 03828ecc4d..d21bc59f9f 100644 --- a/program-tests/utils/src/actions/legacy/instructions/mint_action.rs +++ b/program-tests/utils/src/actions/legacy/instructions/mint_action.rs @@ -324,8 +324,8 @@ pub async fn create_mint_action_instruction( } // Build account metas configuration - // Fetch rent_sponsor early when creating mint (needed as a required parameter). - let create_mint_rent_sponsor = if is_creating_mint { + // Fetch config + rent_sponsor early when creating mint (needed as required parameters). + let create_mint_config = if is_creating_mint { let config_address = CompressibleConfig::light_token_v1_config_pda(); let compressible_config: CompressibleConfig = rpc .get_anchor_account(&config_address) @@ -336,19 +336,21 @@ pub async fn create_mint_action_instruction( config_address )) })?; - Some(compressible_config.rent_sponsor) + Some((config_address, compressible_config.rent_sponsor)) } else { None }; let mut config = if is_creating_mint { + let (config_address, rent_sponsor) = create_mint_config.unwrap(); MintActionMetaConfig::new_create_mint( params.payer, params.authority, params.mint_seed, address_tree_pubkey, state_tree_info.queue, - create_mint_rent_sponsor.unwrap(), + config_address, + rent_sponsor, ) } else { MintActionMetaConfig::new( diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index 86576d1cd1..df7605530a 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -101,8 +101,10 @@ impl<'info> MintActionAccounts<'info> { packed_accounts: ProgramPackedAccounts { accounts: &[] }, }) } else { - // Parse and validate compressible config when creating or closing CMint - let compressible_config = if config.needs_compressible_accounts() { + // Parse and validate compressible config when creating mint (fee validation), + // decompressing, or closing CMint + let compressible_config = if config.create_mint || config.needs_compressible_accounts() + { Some(next_config_account(&mut iter)?) } else { None @@ -213,7 +215,7 @@ impl<'info> MintActionAccounts<'info> { } if let Some(executing) = &self.executing { - // compressible_config (optional) - when creating CMint + // compressible_config (optional) - when creating mint or CMint if executing.compressible_config.is_some() { offset += 1; } @@ -221,7 +223,7 @@ impl<'info> MintActionAccounts<'info> { if executing.cmint.is_some() { offset += 1; } - // rent_sponsor (optional) - when creating CMint + // rent_sponsor (optional) - when creating mint or CMint if executing.rent_sponsor.is_some() { offset += 1; } diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index 21e89d7e05..f961212768 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -10,6 +10,7 @@ use light_token_interface::{ }; use light_zero_copy::{traits::ZeroCopyAt, ZeroCopyNew}; use pinocchio::account_info::AccountInfo; +use spl_pod::solana_msg::msg; use crate::{ compressed_token::mint_action::{ @@ -54,6 +55,14 @@ pub fn process_mint_action( let rent_sponsor = executing .rent_sponsor .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + // Validate rent_sponsor matches config to prevent fee bypass. + let config = executing + .compressible_config + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + if rent_sponsor.key() != &config.rent_sponsor.to_bytes() { + msg!("Rent sponsor account does not match config"); + return Err(ErrorCode::InvalidRentSponsor.into()); + } transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor) .map_err(convert_program_error)?; } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs index 836a8d31bf..6fb4151be7 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs @@ -36,6 +36,8 @@ pub struct CreateMintInputs { pub output_queue: Pubkey, pub extensions: Option>, pub version: u8, + /// CompressibleConfig account for on-chain rent_sponsor validation. + pub compressible_config: Pubkey, /// Rent sponsor PDA that receives the mint creation fee. pub rent_sponsor: Pubkey, } @@ -86,6 +88,7 @@ pub fn create_compressed_mint_cpi( input.mint_signer, input.address_tree_pubkey, input.output_queue, + input.compressible_config, input.rent_sponsor, ) }; diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs index b6acfdfff7..81f953e98f 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -23,6 +23,7 @@ pub struct MintActionMetaConfig { impl MintActionMetaConfig { /// Create a new MintActionMetaConfig for creating a new compressed mint. + /// `compressible_config` is required for on-chain rent_sponsor validation. /// `rent_sponsor` is required because mint creation charges a creation fee /// transferred to the rent sponsor PDA. pub fn new_create_mint( @@ -31,6 +32,7 @@ impl MintActionMetaConfig { mint_signer: Pubkey, address_tree: Pubkey, output_queue: Pubkey, + compressible_config: Pubkey, rent_sponsor: Pubkey, ) -> Self { Self { @@ -44,7 +46,7 @@ impl MintActionMetaConfig { cpi_context: None, token_accounts: Vec::new(), mint: None, - compressible_config: None, + compressible_config: Some(compressible_config), rent_sponsor: Some(rent_sponsor), mint_signer_must_sign: true, } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs index 850657e23a..9d7502f108 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs @@ -45,6 +45,8 @@ pub struct MintActionCpiAccounts<'a, A: AccountInfoTrait + Clone> { pub mint_signer: Option<&'a A>, pub authority: &'a A, + /// CompressibleConfig account โ€” required when creating a new compressed mint (fee validation). + pub compressible_config: Option<&'a A>, /// Rent sponsor PDA โ€” required when creating a new compressed mint (receives the creation fee). pub rent_sponsor: Option<&'a A>, @@ -89,6 +91,7 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { return Err(AccountError::InvalidSigner.into()); } + let compressible_config = iter.next_option("compressible_config", config.create_mint)?; let rent_sponsor = iter.next_option_mut("rent_sponsor", config.create_mint)?; let fee_payer = iter.next_account("fee_payer")?; @@ -158,6 +161,7 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { light_system_program, mint_signer, authority, + compressible_config, rent_sponsor, fee_payer, compressed_token_cpi_authority, @@ -210,6 +214,10 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { accounts.push(self.authority.clone()); + if let Some(config) = self.compressible_config { + accounts.push(config.clone()); + } + if let Some(sponsor) = self.rent_sponsor { accounts.push(sponsor.clone()); } @@ -271,6 +279,14 @@ impl<'a, A: AccountInfoTrait + Clone> MintActionCpiAccounts<'a, A> { is_signer: true, }); + if let Some(config) = self.compressible_config { + metas.push(AccountMeta { + pubkey: config.key().into(), + is_writable: false, + is_signer: false, + }); + } + if let Some(sponsor) = self.rent_sponsor { metas.push(AccountMeta { pubkey: sponsor.key().into(), diff --git a/sdk-libs/token-sdk/src/instruction/create_mint.rs b/sdk-libs/token-sdk/src/instruction/create_mint.rs index 7c43240d2a..7d9f32ef12 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mint.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mint.rs @@ -165,6 +165,7 @@ impl CreateMint { self.mint_seed_pubkey, self.address_tree_pubkey, self.output_queue, + config_pda(), rent_sponsor_pda(), ) // Always include compressible accounts for Mint creation diff --git a/sdk-libs/token-sdk/src/instruction/create_mints.rs b/sdk-libs/token-sdk/src/instruction/create_mints.rs index f065fe5edc..8a370615e1 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mints.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mints.rs @@ -265,6 +265,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { *self.mint_seed_accounts[0].key, *self.address_tree.key, *self.output_queue.key, + *self.compressible_config.key, *self.rent_sponsor.key, ) .with_compressible_mint( @@ -438,6 +439,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { *self.mint_seed_accounts[last_idx].key, *self.address_tree.key, *self.output_queue.key, + *self.compressible_config.key, *self.rent_sponsor.key, ) .with_compressible_mint( diff --git a/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs b/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs index 42ce47cbb1..aab09a9d7c 100644 --- a/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs +++ b/sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs @@ -286,6 +286,8 @@ fn test_successful_create_mint() { create_test_account(mint_signer, [0u8; 32], true, false, false, vec![]), // Authority create_test_account(pubkey_unique(), [0u8; 32], true, false, false, vec![]), + // Compressible config (required for create_mint, read-only) + create_test_account(pubkey_unique(), [0u8; 32], false, false, false, vec![]), // Rent sponsor (required for create_mint, known PDA) create_test_account( LIGHT_TOKEN_RENT_SPONSOR, @@ -351,6 +353,7 @@ fn test_successful_create_mint() { let parsed = result.unwrap(); assert!(parsed.mint_signer.is_some()); + assert!(parsed.compressible_config.is_some()); // Required for create_mint assert!(parsed.rent_sponsor.is_some()); // Required for create_mint assert!(parsed.in_output_queue.is_none()); // Not needed for create_mint } diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 9bc7da619e..a5b7fa3ab9 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -55,6 +55,7 @@ pub fn process_mint_action<'a, 'info>( light_system_program: cpi_accounts.system_program().unwrap(), mint_signer: Some(ctx.accounts.mint_seed.as_ref()), authority: ctx.accounts.mint_authority.as_ref(), + compressible_config: Some(ctx.accounts.compressible_config.as_ref()), rent_sponsor: Some(ctx.accounts.rent_sponsor.as_ref()), fee_payer: ctx.accounts.payer.as_ref(), compressed_token_cpi_authority: ctx.accounts.light_token_cpi_authority.as_ref(), @@ -80,6 +81,7 @@ pub fn process_mint_action<'a, 'info>( account_infos.push(ctx.accounts.mint_authority.to_account_info()); account_infos.push(ctx.accounts.mint_seed.to_account_info()); account_infos.push(ctx.accounts.payer.to_account_info()); + account_infos.push(ctx.accounts.compressible_config.to_account_info()); account_infos.push(ctx.accounts.rent_sponsor.to_account_info()); msg!("mint_action_instruction {:?}", mint_action_instruction); msg!( diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs index b73a357002..4bfda3905e 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs @@ -16,6 +16,8 @@ pub struct PdaCToken<'info> { pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub light_token_cpi_authority: UncheckedAccount<'info>, + /// CHECK: CompressibleConfig account for rent_sponsor validation. + pub compressible_config: UncheckedAccount<'info>, /// CHECK: Rent sponsor PDA that receives the mint creation fee. #[account(mut)] pub rent_sponsor: UncheckedAccount<'info>, diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 69a344e04d..1ae6478a19 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -1,10 +1,12 @@ -use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use anchor_lang::{InstructionData, ToAccountMetas}; use light_client::indexer::Indexer; use light_compressed_account::{address::derive_address, hash_to_bn254_field_size_be}; use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ derive_mint_compressed_address, find_mint_address, }; -use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc, RpcError}; +use light_program_test::{ + utils::assert::assert_rpc_error, LightProgramTest, ProgramTestConfig, Rpc, RpcError, +}; use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; use light_token_interface::{ instructions::{ @@ -60,8 +62,10 @@ async fn test_ctoken_pda() { additional_metadata: Some(additional_metadata), }; - // Create the compressed mint (with chained operations including update mint) - let (compressed_mint_address, _spl_mint) = create_mint( + // create_mint + write_to_cpi_context is rejected because mint creation + // charges a fee requiring the rent_sponsor account, which is not available + // in the CPI context write path (error 6035: CpiContextSetNotUsable). + let result = create_mint( &mut rpc, &mint_seed, decimals, @@ -70,64 +74,9 @@ async fn test_ctoken_pda() { Some(token_metadata), &payer, ) - .await - .unwrap(); - let all_accounts = rpc - .get_compressed_accounts_by_owner(&sdk_token_test::ID, None, None) - .await - .unwrap() - .value; - println!("All accounts: {:?}", all_accounts); - - let mint_account = rpc - .get_compressed_account(compressed_mint_address, None) - .await - .unwrap() - .value - .ok_or("Mint account not found") - .unwrap(); - - // Verify the chained CPI operations worked correctly - println!("๐Ÿงช Verifying chained CPI results..."); - - // 1. Verify compressed mint was created and mint authority was revoked - let compressed_mint = light_token_interface::state::Mint::deserialize( - &mut &mint_account.data.as_ref().unwrap().data[..], - ) - .unwrap(); - - println!("โœ… Compressed mint created:"); - println!(" - SPL mint: {:?}", compressed_mint.metadata.mint); - println!(" - Decimals: {}", compressed_mint.base.decimals); - println!(" - Supply: {}", compressed_mint.base.supply); - println!( - " - Mint authority: {:?}", - compressed_mint.base.mint_authority - ); - println!( - " - Freeze authority: {:?}", - compressed_mint.base.freeze_authority - ); - - // Assert mint authority was revoked (should be None after update) - assert_eq!( - compressed_mint.base.mint_authority, None, - "Mint authority should be revoked (None)" - ); - assert_eq!( - compressed_mint.base.supply, 1000u64, - "Supply should be 1000 after minting" - ); - assert_eq!( - compressed_mint.base.decimals, decimals, - "Decimals should match" - ); + .await; - println!("๐ŸŽ‰ All chained CPI operations completed successfully!"); - println!(" 1. โœ… Created compressed mint with mint authority"); - println!(" 2. โœ… Minted 1000 tokens to payer"); - println!(" 3. โœ… Revoked mint authority (set to None)"); - println!(" 4. โœ… Created escrow PDA"); + assert_rpc_error(result, 0, 6035).unwrap(); } pub async fn create_mint( diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index 24c437c087..91e0f2db9e 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -6,7 +6,7 @@ use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ }; use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc, RpcError}; use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; -use light_token::instruction::rent_sponsor_pda; +use light_token::instruction::{config_pda, rent_sponsor_pda}; use light_token_interface::{ instructions::{ extensions::token_metadata::TokenMetadataInstructionData, @@ -258,6 +258,7 @@ pub async fn create_mint( mint_seed: mint_seed.pubkey(), light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), light_token_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + compressible_config: config_pda(), rent_sponsor: rent_sponsor_pda(), }; diff --git a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs index 6544778c08..d7ef38a08c 100644 --- a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs +++ b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs @@ -12,7 +12,7 @@ use light_sdk::{ instruction::{PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, }; use light_test_utils::RpcError; -use light_token::instruction::rent_sponsor_pda; +use light_token::instruction::{config_pda, rent_sponsor_pda}; use light_token_interface::{ instructions::{ mint_action::{MintWithContext, Recipient}, @@ -206,6 +206,7 @@ async fn create_compressed_mint_helper( address_tree_pubkey, output_queue, extensions: None, + compressible_config: config_pda(), rent_sponsor: rent_sponsor_pda(), }) .unwrap(); diff --git a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs index a37ed61512..18980e42c1 100644 --- a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs +++ b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs @@ -88,6 +88,7 @@ async fn test_compress_full_and_close() { address_tree_pubkey, output_queue, extensions: None, + compressible_config: config_pda(), rent_sponsor: rent_sponsor_pda(), }) .unwrap(); diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs index 3831cac20c..787b2eb759 100644 --- a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -1,5 +1,8 @@ use anchor_lang::InstructionData; -use light_program_test::{AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, Rpc}; +use light_program_test::{ + utils::assert::assert_rpc_error, AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, + Rpc, +}; use light_token::instruction::{ config_pda, derive_mint_compressed_address, find_mint_address, rent_sponsor_pda, SystemAccounts, LIGHT_TOKEN_PROGRAM_ID, @@ -15,11 +18,15 @@ async fn test_create_single_mint() { test_create_mints(1).await; } +/// create_mint + write_to_cpi_context is rejected (error 6035: CpiContextSetNotUsable) +/// because mint creation charges a fee in the processor before CPI context execution. +/// Allowing create_mint in CPI context write path would bypass the fee. #[tokio::test] async fn test_create_two_mints() { test_create_mints(2).await; } +/// Same as test_create_two_mints: create_mint + write_to_cpi_context is rejected. #[tokio::test] async fn test_create_three_mints() { test_create_mints(3).await; @@ -137,21 +144,28 @@ async fn test_create_mints(n: usize) { let mut signers: Vec<&Keypair> = vec![&payer]; signers.extend(mint_signers.iter()); - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) - .await - .unwrap(); - - for (i, (mint_pda, _)) in mint_pdas.iter().enumerate() { - let mint_account = rpc - .get_account(*mint_pda) - .await - .expect("Failed to get mint account") - .unwrap_or_else(|| panic!("Mint PDA {} should exist after decompress", i + 1)); - - assert!( - !mint_account.data.is_empty(), - "Mint {} account should have data", - i + 1 - ); + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) + .await; + + if n > 1 { + // N>1 uses CPI context write for create_mint, which is rejected because + // mint creation charges a fee before CPI context execution. + assert_rpc_error(result, 0, 6035).unwrap(); + } else { + result.unwrap(); + for (i, (mint_pda, _)) in mint_pdas.iter().enumerate() { + let mint_account = rpc + .get_account(*mint_pda) + .await + .expect("Failed to get mint account") + .unwrap_or_else(|| panic!("Mint PDA {} should exist after decompress", i + 1)); + + assert!( + !mint_account.data.is_empty(), + "Mint {} account should have data", + i + 1 + ); + } } } From bb2ac1fcc3390d464334d7a7ebf5628dd7eb0e83 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 22 Feb 2026 22:17:02 +0000 Subject: [PATCH 4/5] feat: allow create_mint in CPI context write mode Removes the restriction that blocked create_mint when write_to_cpi_context is true. This enables multi-mint creation (N-1 write-mode CPI calls + 1 execute-mode call). In write mode, the mint creation fee is charged by validating rent_sponsor against the hardcoded RENT_SPONSOR_V1 constant (no compressible_config account needed). The system program is included as a trailing account to enable the fee transfer CPI. Entire-Checkpoint: 3c0618ad006f --- .../tests/mint/cpi_context.rs | 72 ++++++++++--------- .../compressed_token/mint_action/accounts.rs | 32 ++++++--- .../compressed_token/mint_action/processor.rs | 49 ++++++++----- programs/compressed-token/program/src/lib.rs | 4 ++ .../program/tests/mint_action.rs | 34 +++------ .../v2/create_compressed_mint/instruction.rs | 3 + .../v2/mint_action/account_metas.rs | 16 +++++ .../v2/update_compressed_mint/instruction.rs | 1 + .../src/interface/cpi/create_mints.rs | 67 +++++++++++------ .../token-sdk/src/instruction/create_mints.rs | 11 ++- .../tests/test_create_two_mints.rs | 46 +++++------- 11 files changed, 195 insertions(+), 140 deletions(-) diff --git a/program-tests/compressed-token-test/tests/mint/cpi_context.rs b/program-tests/compressed-token-test/tests/mint/cpi_context.rs index aef143b49f..11df63f38e 100644 --- a/program-tests/compressed-token-test/tests/mint/cpi_context.rs +++ b/program-tests/compressed-token-test/tests/mint/cpi_context.rs @@ -125,6 +125,8 @@ async fn test_write_to_cpi_context_create_mint() { output_queue_index, } = test_setup().await; + let rent_sponsor = rpc.test_accounts.funding_pool_config.rent_sponsor_pda; + // Build instruction data using new builder API let instruction_data = MintActionCompressedInstructionData::new_mint( compressed_mint_inputs.root_index, @@ -148,6 +150,7 @@ async fn test_write_to_cpi_context_create_mint() { fee_payer: payer.pubkey(), mint_signer: Some(mint_seed.pubkey()), authority: mint_authority.pubkey(), + rent_sponsor: Some(rent_sponsor), cpi_context: cpi_context_pubkey, }; @@ -181,22 +184,20 @@ async fn test_write_to_cpi_context_create_mint() { data: wrapper_ix_data.data(), }; - // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context - // is rejected (error 6035: CpiContextSetNotUsable). - let result = rpc - .create_and_send_transaction( - &[wrapper_instruction], - &payer.pubkey(), - &[&payer, &mint_seed, &mint_authority], - ) - .await; - - assert_rpc_error(result, 0, 6035).unwrap(); + // create_mint + write_to_cpi_context is allowed. The mint creation fee is charged + // in write mode against the hardcoded RENT_SPONSOR_V1 constant. + rpc.create_and_send_transaction( + &[wrapper_instruction], + &payer.pubkey(), + &[&payer, &mint_seed, &mint_authority], + ) + .await + .expect("create_mint + write_to_cpi_context should succeed"); } #[tokio::test] #[serial] -async fn test_write_to_cpi_context_invalid_address_tree() { +async fn test_write_to_cpi_context_create_mint_invalid_rent_sponsor() { let TestSetup { mut rpc, compressed_mint_inputs, @@ -205,16 +206,16 @@ async fn test_write_to_cpi_context_invalid_address_tree() { mint_authority, compressed_mint_address: _, cpi_context_pubkey, - address_tree: _, + address_tree, address_tree_index, output_queue: _, output_queue_index, } = test_setup().await; - // Swap the address tree pubkey to a random one (this should fail validation) - let invalid_address_tree = Pubkey::new_unique(); + // Use a random pubkey as rent_sponsor (not the valid RENT_SPONSOR_V1) + let invalid_rent_sponsor = Pubkey::new_unique(); - // Build instruction data with invalid address tree + // Build instruction data let instruction_data = MintActionCompressedInstructionData::new_mint( compressed_mint_inputs.root_index, CompressedProof::default(), @@ -229,14 +230,15 @@ async fn test_write_to_cpi_context_invalid_address_tree() { token_out_queue_index: 0, assigned_account_index: 0, read_only_address_trees: [0; 4], - address_tree_pubkey: invalid_address_tree.to_bytes(), + address_tree_pubkey: address_tree.to_bytes(), }); - // Build account metas using helper + // Build account metas with invalid rent_sponsor let config = MintActionMetaConfigCpiWrite { fee_payer: payer.pubkey(), mint_signer: Some(mint_seed.pubkey()), authority: mint_authority.pubkey(), + rent_sponsor: Some(invalid_rent_sponsor), cpi_context: cpi_context_pubkey, }; @@ -270,8 +272,8 @@ async fn test_write_to_cpi_context_invalid_address_tree() { data: wrapper_ix_data.data(), }; - // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context - // is rejected (error 6035: CpiContextSetNotUsable) before address tree validation. + // Should fail with InvalidRentSponsor because rent_sponsor doesn't match RENT_SPONSOR_V1 + // Error code 6100 = InvalidRentSponsor let result = rpc .create_and_send_transaction( &[wrapper_instruction], @@ -280,12 +282,12 @@ async fn test_write_to_cpi_context_invalid_address_tree() { ) .await; - assert_rpc_error(result, 0, 6035).unwrap(); + assert_rpc_error(result, 0, 6099).unwrap(); } #[tokio::test] #[serial] -async fn test_write_to_cpi_context_invalid_compressed_address() { +async fn test_write_to_cpi_context_create_mint_missing_rent_sponsor() { let TestSetup { mut rpc, compressed_mint_inputs, @@ -300,18 +302,11 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { output_queue_index, } = test_setup().await; - // Swap the mint_signer to an invalid one (this should fail validation) - // The compressed address will be derived from the invalid mint_signer - let invalid_mint_signer = [42u8; 32]; - - // Build instruction data with invalid mint_signer in metadata - let mut invalid_mint = compressed_mint_inputs.mint.clone().unwrap(); - invalid_mint.metadata.mint_signer = invalid_mint_signer; - + // Build instruction data with create_mint let instruction_data = MintActionCompressedInstructionData::new_mint( compressed_mint_inputs.root_index, CompressedProof::default(), - invalid_mint, + compressed_mint_inputs.mint.clone().unwrap(), ) .with_cpi_context(CpiContext { set_context: false, @@ -325,11 +320,14 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { address_tree_pubkey: address_tree.to_bytes(), }); - // Build account metas using helper + // Build account metas WITHOUT rent_sponsor (None). + // The program expects rent_sponsor when create_mint is true in write mode, + // so not providing it will cause the account iterator to misparse accounts. let config = MintActionMetaConfigCpiWrite { fee_payer: payer.pubkey(), mint_signer: Some(mint_seed.pubkey()), authority: mint_authority.pubkey(), + rent_sponsor: None, cpi_context: cpi_context_pubkey, }; @@ -363,8 +361,9 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { data: wrapper_ix_data.data(), }; - // Execute wrapper instruction - should fail because create_mint + write_to_cpi_context - // is rejected (error 6035: CpiContextSetNotUsable) before mint signer validation. + // Should fail - when rent_sponsor is missing, the account iterator shifts: + // fee_payer is parsed as rent_sponsor, then CpiContextLightSystemAccounts + // runs out of accounts. Error 20009 is from the account iterator. let result = rpc .create_and_send_transaction( &[wrapper_instruction], @@ -373,7 +372,7 @@ async fn test_write_to_cpi_context_invalid_compressed_address() { ) .await; - assert_rpc_error(result, 0, 6035).unwrap(); + assert_rpc_error(result, 0, 20009).unwrap(); } #[tokio::test] @@ -519,6 +518,7 @@ async fn test_write_to_cpi_context_decompressed_mint_fails() { fee_payer: payer.pubkey(), mint_signer: None, authority: mint_authority.pubkey(), + rent_sponsor: None, cpi_context: cpi_context_pubkey, }; @@ -611,6 +611,7 @@ async fn test_write_to_cpi_context_mint_to_ctoken_fails() { fee_payer: payer.pubkey(), mint_signer: Some(mint_seed.pubkey()), authority: mint_authority.pubkey(), + rent_sponsor: None, cpi_context: cpi_context_pubkey, }; @@ -703,6 +704,7 @@ async fn test_write_to_cpi_context_decompress_mint_action_fails() { fee_payer: payer.pubkey(), mint_signer: Some(mint_seed.pubkey()), authority: mint_authority.pubkey(), + rent_sponsor: None, cpi_context: cpi_context_pubkey, }; diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index df7605530a..6fd5561c43 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -21,8 +21,7 @@ pub struct MintActionAccounts<'info> { /// Seed for mint PDA derivation. /// Required only for compressed mint creation. /// Note: mint_signer is not in executing accounts since it is parsed - /// before the executing/cpi-write branch. create_mint is NOT allowed - /// in combination with write to cpi context (rejected in AccountsConfig::new). + /// before the executing/cpi-write branch. pub mint_signer: Option<&'info AccountInfo>, pub authority: &'info AccountInfo, /// Required accounts to execute an instruction @@ -32,6 +31,9 @@ pub struct MintActionAccounts<'info> { /// Required accounts to write into a cpi context account. /// - executing is None pub write_to_cpi_context_system: Option>, + /// Rent sponsor account in write mode (when create_mint + write_to_cpi_context). + /// Validated against hardcoded RENT_SPONSOR_V1 constant. + pub write_mode_rent_sponsor: Option<&'info AccountInfo>, /// Packed accounts contain /// [ /// ..tree_accounts, @@ -86,7 +88,18 @@ impl<'info> MintActionAccounts<'info> { // Authority is always required to sign let authority = iter.next_signer("authority")?; if config.write_to_cpi_context { + let write_mode_rent_sponsor = if config.create_mint { + Some(iter.next_account("rent_sponsor")?) + } else { + None + }; let write_to_cpi_context_system = CpiContextLightSystemAccounts::new(&mut iter)?; + // System program is needed for the fee transfer CPI when creating mint in write mode. + // It's placed after all parsed accounts - the account iterator consumes it here, + // but it's available for the system program CPI via the transaction accounts. + if config.create_mint { + let _system_program = iter.next_account("system_program")?; + } if !iter.iterator_is_empty() { msg!("Too many accounts for write to cpi context."); @@ -98,6 +111,7 @@ impl<'info> MintActionAccounts<'info> { authority, executing: None, write_to_cpi_context_system: Some(write_to_cpi_context_system), + write_mode_rent_sponsor, packed_accounts: ProgramPackedAccounts { accounts: &[] }, }) } else { @@ -156,6 +170,7 @@ impl<'info> MintActionAccounts<'info> { tokens_out_queue, }), write_to_cpi_context_system: None, + write_mode_rent_sponsor: None, packed_accounts: ProgramPackedAccounts { accounts: iter.remaining_unchecked()?, }, @@ -214,6 +229,11 @@ impl<'info> MintActionAccounts<'info> { offset += 1; } + // write_mode_rent_sponsor (optional) - when create_mint + write_to_cpi_context + if self.write_mode_rent_sponsor.is_some() { + offset += 1; + } + if let Some(executing) = &self.executing { // compressible_config (optional) - when creating mint or CMint if executing.compressible_config.is_some() { @@ -467,14 +487,6 @@ impl AccountsConfig { let cmint_decompressed = parsed_instruction_data.mint.is_none(); if write_to_cpi_context { - // Cannot create a compressed mint when writing to CPI context. - // Mint creation charges the creation fee and requires the rent_sponsor account, - // which is not available in the CPI context write path. - if parsed_instruction_data.create_mint.is_some() { - msg!("Compressed mint creation not allowed when writing to cpi context"); - return Err(ErrorCode::CpiContextSetNotUsable.into()); - } - // Must not have any MintToCToken actions let has_mint_to_ctoken_actions = parsed_instruction_data .actions diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index f961212768..e0baf915a1 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -45,26 +45,39 @@ pub fn process_mint_action( let validated_accounts = MintActionAccounts::validate_and_parse(accounts, &accounts_config, cmint_pubkey.as_ref())?; - // Charge mint creation fee. create_mint is rejected in CPI context write path - // (validated in AccountsConfig::new), so executing and rent_sponsor are always present here. + // Charge mint creation fee in both execute and write modes. if accounts_config.create_mint { - let executing = validated_accounts - .executing - .as_ref() - .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; - let rent_sponsor = executing - .rent_sponsor - .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; - // Validate rent_sponsor matches config to prevent fee bypass. - let config = executing - .compressible_config - .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; - if rent_sponsor.key() != &config.rent_sponsor.to_bytes() { - msg!("Rent sponsor account does not match config"); - return Err(ErrorCode::InvalidRentSponsor.into()); + if let Some(executing) = validated_accounts.executing.as_ref() { + // Execute mode: validate rent_sponsor against compressible_config. + let rent_sponsor = executing + .rent_sponsor + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + let config = executing + .compressible_config + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + if rent_sponsor.key() != &config.rent_sponsor.to_bytes() { + msg!("Rent sponsor account does not match config"); + return Err(ErrorCode::InvalidRentSponsor.into()); + } + transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor) + .map_err(convert_program_error)?; + } else { + // Write mode: validate rent_sponsor against hardcoded RENT_SPONSOR_V1. + let rent_sponsor = validated_accounts + .write_mode_rent_sponsor + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)?; + if rent_sponsor.key() != &crate::RENT_SPONSOR_V1 { + msg!("Rent sponsor account does not match RENT_SPONSOR_V1"); + return Err(ErrorCode::InvalidRentSponsor.into()); + } + let fee_payer = validated_accounts + .write_to_cpi_context_system + .as_ref() + .ok_or(ErrorCode::MintActionMissingExecutingAccounts)? + .fee_payer; + transfer_lamports_via_cpi(MINT_CREATION_FEE, fee_payer, rent_sponsor) + .map_err(convert_program_error)?; } - transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor) - .map_err(convert_program_error)?; } // Get mint data based on source: diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index ec62bf0610..23cc4bdd27 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -37,6 +37,10 @@ pub const LIGHT_CPI_SIGNER: CpiSigner = pub const MAX_ACCOUNTS: usize = 30; pub const MINT_CREATION_FEE: u64 = 50_000; +/// Hardcoded rent sponsor PDA for write-mode mint creation fee validation. +/// Same value as LIGHT_TOKEN_RENT_SPONSOR in sdk-types/src/constants.rs. +pub const RENT_SPONSOR_V1: pinocchio::pubkey::Pubkey = + light_macros::pubkey_array!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40; /// Maximum number of compression operations per instruction. /// Used for compression_to_input lookup array sizing. diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index 494a18bf5e..cfe0a2b6de 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -325,41 +325,27 @@ fn check_if_config_should_error(instruction_data: &MintActionCompressedInstructi .unwrap_or_default(); if write_to_cpi_context { - // Check for MintToCToken actions + // Check for MintToCToken actions (not allowed in write mode) let has_mint_to_ctoken = instruction_data .actions .iter() .any(|action| matches!(action, Action::MintTo(_))); - // Check for MintToCompressed actions - let require_token_output_queue = instruction_data + // Check for DecompressMint actions (not allowed in write mode) + let has_decompress_mint = instruction_data .actions .iter() - .any(|action| matches!(action, Action::MintToCompressed(_))); + .any(|action| matches!(action, Action::DecompressMint(_))); - // mint_decompressed is only from metadata flag (matches AccountsConfig::new) - let mint_decompressed = instruction_data - .mint - .as_ref() - .unwrap() - .metadata - .mint_decompressed; - - // Check if this is creating a new mint (not from an existing compressed mint) - let is_creating_mint = instruction_data.mint.is_none(); - - // Check if create_mint is set (create_mint + write_to_cpi_context not allowed) - let create_mint_with_cpi_write = instruction_data.create_mint.is_some(); + // cmint_decompressed (mint.is_none()) not allowed in write mode + let cmint_decompressed = instruction_data.mint.is_none(); // Error conditions matching AccountsConfig::new: // 1. has_mint_to_ctoken (MintToCToken actions not allowed) - // 2. mint_decompressed && require_token_output_queue (mint decompressed + MintToCompressed not allowed) - // 3. is_creating_mint (mint creation not allowed when writing to cpi context) - // 4. create_mint_with_cpi_write (create_mint + write_to_cpi_context not allowed) - has_mint_to_ctoken - || (mint_decompressed && require_token_output_queue) - || is_creating_mint - || create_mint_with_cpi_write + // 2. has_decompress_mint (DecompressMint not allowed) + // 3. cmint_decompressed (decompressed mint not allowed) + // Note: create_mint IS allowed in write mode (fee charged to rent_sponsor) + has_mint_to_ctoken || has_decompress_mint || cmint_decompressed } else { false } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs index 6fb4151be7..f12342d3bf 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs @@ -121,6 +121,8 @@ pub struct CreateMintInputsCpiWrite { pub cpi_context_pubkey: Pubkey, pub extensions: Option>, pub version: u8, + /// Rent sponsor PDA for mint creation fee in write mode. + pub rent_sponsor: Pubkey, } pub fn create_compressed_mint_cpi_write(input: CreateMintInputsCpiWrite) -> Result { @@ -158,6 +160,7 @@ pub fn create_compressed_mint_cpi_write(input: CreateMintInputsCpiWrite) -> Resu fee_payer: input.payer, mint_signer: Some(input.mint_signer), authority: input.mint_authority, + rent_sponsor: Some(input.rent_sponsor), cpi_context: input.cpi_context_pubkey, }; diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs index 81f953e98f..f9cac98cf8 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -247,6 +247,7 @@ pub struct MintActionMetaConfigCpiWrite { pub fee_payer: Pubkey, pub mint_signer: Option, // Optional - only when creating mint pub authority: Pubkey, + pub rent_sponsor: Option, // Optional - only when creating mint (write mode) pub cpi_context: Pubkey, } @@ -270,6 +271,11 @@ pub fn get_mint_action_instruction_account_metas_cpi_write( metas.push(AccountMeta::new_readonly(config.authority, true)); + // rent_sponsor (optional) - when creating mint in write mode + if let Some(rent_sponsor) = config.rent_sponsor { + metas.push(AccountMeta::new(rent_sponsor, false)); + } + metas.push(AccountMeta::new(config.fee_payer, true)); metas.push(AccountMeta::new_readonly( @@ -279,5 +285,15 @@ pub fn get_mint_action_instruction_account_metas_cpi_write( metas.push(AccountMeta::new(config.cpi_context, false)); + // System program needed for fee transfer CPI when creating mint in write mode. + // Placed after all parsed accounts - the account iterator won't consume it, + // but it's available for the system program CPI. + if config.rent_sponsor.is_some() { + metas.push(AccountMeta::new_readonly( + default_pubkeys.system_program, + false, + )); + } + metas } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/update_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/update_compressed_mint/instruction.rs index b330fa5b3e..e10cd363a8 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/update_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/update_compressed_mint/instruction.rs @@ -136,6 +136,7 @@ pub fn create_update_compressed_mint_cpi_write( fee_payer: inputs.payer, mint_signer: None, // Not needed for authority updates authority: inputs.authority, + rent_sponsor: None, // Not creating mint cpi_context: inputs.cpi_context_pubkey, }; diff --git a/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs b/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs index 59c36d41d8..b0d83b04e9 100644 --- a/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs +++ b/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs @@ -328,10 +328,11 @@ impl<'a, AI: AccountInfoTrait + Clone> CreateMintsCpi<'a, AI> { // [0]: light_system_program // [1]: mint_signer // [2]: authority (payer) - // [3]: fee_payer (payer) - // [4]: cpi_authority_pda - // [5]: cpi_context - let metas = vec![ + // [3]: rent_sponsor (writable) + // [4]: fee_payer (payer) + // [5]: cpi_authority_pda + // [6]: cpi_context + let mut metas = vec![ CpiMeta { pubkey: self.light_system_program.key(), is_signer: false, @@ -347,32 +348,52 @@ impl<'a, AI: AccountInfoTrait + Clone> CreateMintsCpi<'a, AI> { is_signer: true, is_writable: false, }, - CpiMeta { - pubkey: self.payer.key(), - is_signer: true, - is_writable: true, - }, - CpiMeta { - pubkey: self.cpi_authority_pda.key(), - is_signer: false, - is_writable: false, - }, - CpiMeta { - pubkey: self.cpi_context_account.key(), - is_signer: false, - is_writable: true, - }, ]; - let account_infos = vec![ + let mut account_infos = vec![ self.light_system_program.clone(), self.mint_seed_accounts[index].clone(), self.payer.clone(), - self.payer.clone(), - self.cpi_authority_pda.clone(), - self.cpi_context_account.clone(), ]; + // rent_sponsor (writable) - for mint creation fee in write mode + metas.push(CpiMeta { + pubkey: self.rent_sponsor.key(), + is_signer: false, + is_writable: true, + }); + account_infos.push(self.rent_sponsor.clone()); + + // fee_payer, cpi_authority_pda, cpi_context + metas.push(CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: true, + }); + account_infos.push(self.payer.clone()); + + metas.push(CpiMeta { + pubkey: self.cpi_authority_pda.key(), + is_signer: false, + is_writable: false, + }); + account_infos.push(self.cpi_authority_pda.clone()); + + metas.push(CpiMeta { + pubkey: self.cpi_context_account.key(), + is_signer: false, + is_writable: true, + }); + account_infos.push(self.cpi_context_account.clone()); + + // System program needed for fee transfer CPI + metas.push(CpiMeta { + pubkey: self.system_program.key(), + is_signer: false, + is_writable: false, + }); + account_infos.push(self.system_program.clone()); + self.invoke_mint_action_raw(&ix_data, &account_infos, &metas, index) } diff --git a/sdk-libs/token-sdk/src/instruction/create_mints.rs b/sdk-libs/token-sdk/src/instruction/create_mints.rs index 8a370615e1..adbf99cfa2 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mints.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mints.rs @@ -348,6 +348,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { fee_payer: *self.payer.key, mint_signer: Some(*self.mint_seed_accounts[index].key), authority: *self.payer.key, + rent_sponsor: Some(*self.rent_sponsor.key), cpi_context: *self.cpi_context_account.key, }; @@ -360,16 +361,20 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { // [0]: light_system_program // [1]: mint_signer (optional, when present) // [2]: authority - // [3]: fee_payer - // [4]: cpi_authority_pda - // [5]: cpi_context + // [3]: rent_sponsor (writable) + // [4]: fee_payer + // [5]: cpi_authority_pda + // [6]: cpi_context + // [7]: system_program (for fee transfer CPI) let account_infos = [ self.system_accounts.light_system_program.clone(), self.mint_seed_accounts[index].clone(), self.payer.clone(), + self.rent_sponsor.clone(), self.payer.clone(), self.system_accounts.cpi_authority_pda.clone(), self.cpi_context_account.clone(), + self.system_accounts.system_program.clone(), ]; let instruction = Instruction { program_id: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs index 787b2eb759..944ef6c294 100644 --- a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -1,8 +1,5 @@ use anchor_lang::InstructionData; -use light_program_test::{ - utils::assert::assert_rpc_error, AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, - Rpc, -}; +use light_program_test::{AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, Rpc}; use light_token::instruction::{ config_pda, derive_mint_compressed_address, find_mint_address, rent_sponsor_pda, SystemAccounts, LIGHT_TOKEN_PROGRAM_ID, @@ -18,15 +15,16 @@ async fn test_create_single_mint() { test_create_mints(1).await; } -/// create_mint + write_to_cpi_context is rejected (error 6035: CpiContextSetNotUsable) -/// because mint creation charges a fee in the processor before CPI context execution. -/// Allowing create_mint in CPI context write path would bypass the fee. +/// Test creating 2 mints in a single instruction using CPI context write mode. +/// The first mint uses write_to_cpi_context (write mode) and the second uses execute mode. +/// Both charge the mint creation fee - write mode validates against hardcoded RENT_SPONSOR_V1. #[tokio::test] async fn test_create_two_mints() { test_create_mints(2).await; } -/// Same as test_create_two_mints: create_mint + write_to_cpi_context is rejected. +/// Test creating 3 mints in a single instruction using CPI context write mode. +/// Mints 1..N-1 use write_to_cpi_context and the last one uses execute mode. #[tokio::test] async fn test_create_three_mints() { test_create_mints(3).await; @@ -148,24 +146,18 @@ async fn test_create_mints(n: usize) { .create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) .await; - if n > 1 { - // N>1 uses CPI context write for create_mint, which is rejected because - // mint creation charges a fee before CPI context execution. - assert_rpc_error(result, 0, 6035).unwrap(); - } else { - result.unwrap(); - for (i, (mint_pda, _)) in mint_pdas.iter().enumerate() { - let mint_account = rpc - .get_account(*mint_pda) - .await - .expect("Failed to get mint account") - .unwrap_or_else(|| panic!("Mint PDA {} should exist after decompress", i + 1)); - - assert!( - !mint_account.data.is_empty(), - "Mint {} account should have data", - i + 1 - ); - } + result.unwrap(); + for (i, (mint_pda, _)) in mint_pdas.iter().enumerate() { + let mint_account = rpc + .get_account(*mint_pda) + .await + .expect("Failed to get mint account") + .unwrap_or_else(|| panic!("Mint PDA {} should exist after decompress", i + 1)); + + assert!( + !mint_account.data.is_empty(), + "Mint {} account should have data", + i + 1 + ); } } From b85bb3f0e1860d39f03fd670cca869a6f4b20360 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 22 Feb 2026 22:39:23 +0000 Subject: [PATCH 5/5] fix: update ctoken_pda test expected error code The wrapper program doesn't include rent_sponsor in its CPI, so the error changes from 6035 (CpiContextSetNotUsable) to 20009 (account iterator parse failure). --- sdk-tests/sdk-token-test/tests/ctoken_pda.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 1ae6478a19..3722e071ae 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -62,9 +62,9 @@ async fn test_ctoken_pda() { additional_metadata: Some(additional_metadata), }; - // create_mint + write_to_cpi_context is rejected because mint creation - // charges a fee requiring the rent_sponsor account, which is not available - // in the CPI context write path (error 6035: CpiContextSetNotUsable). + // create_mint + write_to_cpi_context now works, but the wrapper program + // doesn't include rent_sponsor in its CPI accounts. Without rent_sponsor, + // the account iterator shifts and fails parsing (error 20009). let result = create_mint( &mut rpc, &mint_seed, @@ -76,7 +76,7 @@ async fn test_ctoken_pda() { ) .await; - assert_rpc_error(result, 0, 6035).unwrap(); + assert_rpc_error(result, 0, 20009).unwrap(); } pub async fn create_mint(