Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 37 additions & 64 deletions program-tests/compressed-token-test/tests/mint/cpi_context.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -9,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::{
Expand Down Expand Up @@ -117,14 +117,16 @@ 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,
output_queue: _,
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,
Expand All @@ -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,
};

Expand Down Expand Up @@ -181,53 +184,20 @@ async fn test_write_to_cpi_context_create_mint() {
data: wrapper_ix_data.data(),
};

// Execute wrapper instruction
// 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("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)"
);
.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,
Expand All @@ -236,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(),
Expand All @@ -260,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,
};

Expand Down Expand Up @@ -301,7 +272,8 @@ async fn test_write_to_cpi_context_invalid_address_tree() {
data: wrapper_ix_data.data(),
};

// Execute wrapper instruction - should fail
// 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],
Expand All @@ -310,14 +282,12 @@ 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, 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,
Expand All @@ -332,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,
Expand All @@ -357,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,
};

Expand Down Expand Up @@ -395,7 +361,9 @@ async fn test_write_to_cpi_context_invalid_compressed_address() {
data: wrapper_ix_data.data(),
};

// Execute wrapper instruction - should fail
// 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],
Expand All @@ -404,9 +372,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, 20009).unwrap();
}

#[tokio::test]
Expand Down Expand Up @@ -448,12 +414,16 @@ 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 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,
);

// Set CPI context for execute mode
Expand Down Expand Up @@ -548,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,
};

Expand Down Expand Up @@ -640,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,
};

Expand Down Expand Up @@ -732,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,
};

Expand Down
69 changes: 69 additions & 0 deletions program-tests/compressed-token-test/tests/mint/failing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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,
};
Expand Down Expand Up @@ -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,
);
}
24 changes: 22 additions & 2 deletions program-tests/utils/src/actions/legacy/instructions/mint_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,33 @@ pub async fn create_mint_action_instruction<R: Rpc + Indexer>(
}

// Build account metas configuration
// 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)
.await?
.ok_or_else(|| {
RpcError::CustomError(format!(
"CompressibleConfig not found at {}",
config_address
))
})?;
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,
config_address,
rent_sponsor,
)
} else {
MintActionMetaConfig::new(
Expand All @@ -354,10 +374,10 @@ pub async fn create_mint_action_instruction<R: Rpc + Indexer>(

// 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: rent_sponsor already set via new_create_mint()
// 3. Already decompressed (cmint_decompressed): only needs cmint account
let (mint_pda, _) = find_mint_address(&params.mint_seed);
if has_decompress_mint || has_compress_and_close_mint {
// Get config and rent_sponsor from v1 config PDA
let config_address = CompressibleConfig::light_token_v1_config_pda();
let compressible_config: CompressibleConfig = rpc
.get_anchor_account(&config_address)
Expand Down
Loading