diff --git a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs index 71febc7dc1..cd1bb64d41 100644 --- a/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/light_token/approve_revoke.rs @@ -161,7 +161,7 @@ async fn test_approve_fails() { delegate.pubkey(), &owner, 100, - Some(1), // max_top_up too low + Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) "max_topup_exceeded", 18043, // TokenError::MaxTopUpExceeded ) @@ -327,7 +327,7 @@ async fn test_revoke_fails() { &mut context, token_account, &owner, - Some(1), // max_top_up too low + Some(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) "max_topup_exceeded", 18043, // TokenError::MaxTopUpExceeded ) diff --git a/program-tests/compressed-token-test/tests/light_token/transfer.rs b/program-tests/compressed-token-test/tests/light_token/transfer.rs index d2c1499414..5ac4b2d370 100644 --- a/program-tests/compressed-token-test/tests/light_token/transfer.rs +++ b/program-tests/compressed-token-test/tests/light_token/transfer.rs @@ -564,7 +564,7 @@ async fn test_ctoken_transfer_max_top_up_exceeded() { destination, 100, owner_keypair.pubkey(), - 1, // max_top_up = 1 lamport (way too low for any rent top-up) + 1, // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) ); // Execute transfer expecting failure diff --git a/program-tests/compressed-token-test/tests/mint/failing.rs b/program-tests/compressed-token-test/tests/mint/failing.rs index 48b5246ec8..f5be6b1dc1 100644 --- a/program-tests/compressed-token-test/tests/mint/failing.rs +++ b/program-tests/compressed-token-test/tests/mint/failing.rs @@ -926,7 +926,7 @@ async fn test_mint_to_ctoken_max_top_up_exceeded() { account_index: 0, amount: 1000u64, }) - .with_max_top_up(1); // max_top_up = 1 lamport (way too low) + .with_max_top_up(1); // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) // Build account metas let config = MintActionMetaConfig::new( diff --git a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs index e6229ffb35..3a6fc3ff7b 100644 --- a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs @@ -686,7 +686,7 @@ async fn test_compression_max_top_up_exceeded() -> Result<(), RpcError> { validity_proof: ValidityProof::default(), transfer_config: Transfer2Config::default() .filter_zero_amount_outputs() - .with_max_top_up(1), // max_top_up = 1 lamport (way too low) + .with_max_top_up(1), // max_top_up = 1 (1,000 lamports budget, still too low for rent top-up) meta_config: Transfer2AccountsMetaConfig::new(payer.pubkey(), account_metas), in_lamports: None, out_lamports: None, diff --git a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md index 937e1ee620..c496920ad7 100644 --- a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md +++ b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md @@ -36,7 +36,7 @@ Key concepts integrated: - `leaf_index`: u32 - Merkle tree leaf index of existing compressed mint (only used if create_mint is None) - `prove_by_index`: bool - Use proof-by-index for existing mint validation (only used if create_mint is None) - `root_index`: u16 - Root index for address proof (create) or validity proof (update). Not used if proof by index. - - `max_top_up`: u16 - Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit) + - `max_top_up`: u16 - Maximum lamports for rent and top-up combined, in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). Transaction fails if exceeded. (0 = no limit) - `create_mint`: Option - Configuration for creating new compressed mint (None for existing mint operations) - `actions`: Vec - Ordered list of actions to execute - `proof`: Option - ZK proof for compressed account validation (required unless prove_by_index=true) diff --git a/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md index 8a19ca7a20..1d21fce21b 100644 --- a/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md +++ b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md @@ -47,7 +47,7 @@ - `lamports_change_account_merkle_tree_index`: u8 - Merkle tree index for lamport change account (placeholder, unimplemented) - `lamports_change_account_owner_index`: u8 - Owner index for lamport change account (placeholder, unimplemented) - `output_queue`: u8 - Output queue index for compressed account outputs - - `max_top_up`: u16 - Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit) + - `max_top_up`: u16 - Maximum lamports for rent and top-up combined, in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). Transaction fails if exceeded. (0 = no limit) - `cpi_context`: Optional CompressedCpiContext - Required for CPI operations; write mode: set either first_set_context or set_context (not both); execute mode: provide with all flags false - `compressions`: Optional Vec - Compress/decompress operations - `proof`: Optional CompressedProof - Required for ZK validation of compressed inputs; not needed for proof by index or when no compressed inputs exist diff --git a/programs/compressed-token/program/docs/ctoken/APPROVE.md b/programs/compressed-token/program/docs/ctoken/APPROVE.md index 9c315abff4..c0bbca6e77 100644 --- a/programs/compressed-token/program/docs/ctoken/APPROVE.md +++ b/programs/compressed-token/program/docs/ctoken/APPROVE.md @@ -20,7 +20,7 @@ Delegates a specified amount to a delegate authority on a decompressed ctoken ac Path: programs/compressed-token/program/src/ctoken/approve_revoke.rs (lines 14-15, 98-106) - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to delegate -- Bytes 8-9 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up (0 = no limit, default for legacy format) +- Bytes 8-9 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit, default for legacy format. **Accounts:** 1. source @@ -113,7 +113,8 @@ let transfer_amount = top_up_lamports_from_account_info_unchecked(account, &mut .unwrap_or(0); if transfer_amount > 0 { - if max_top_up > 0 && transfer_amount > max_top_up as u64 { + // max_top_up is in units of 1,000 lamports (max ~65.5M lamports). + if max_top_up > 0 && transfer_amount > (max_top_up as u64).saturating_mul(1000) { return Err(CTokenError::MaxTopUpExceeded.into()); } let payer = payer.ok_or(CTokenError::MissingPayer)?; @@ -133,7 +134,8 @@ Extended instruction data format (10 bytes total): **Enforcement**: ```rust -if max_top_up > 0 && transfer_amount > max_top_up as u64 { +// max_top_up is in units of 1,000 lamports (max ~65.5M lamports). +if max_top_up > 0 && transfer_amount > (max_top_up as u64).saturating_mul(1000) { return Err(CTokenError::MaxTopUpExceeded.into()); } ``` diff --git a/programs/compressed-token/program/docs/ctoken/BURN.md b/programs/compressed-token/program/docs/ctoken/BURN.md index 1e3c0846c9..528dabac32 100644 --- a/programs/compressed-token/program/docs/ctoken/BURN.md +++ b/programs/compressed-token/program/docs/ctoken/BURN.md @@ -15,7 +15,7 @@ Format 1 (8 bytes, legacy): Format 2 (10 bytes): - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to burn -- Bytes 8-9: `max_top_up` (u16, little-endian) - Maximum lamports for combined CMint + CToken top-ups (0 = no limit) +- Bytes 8-9: `max_top_up` (u16, little-endian) - Maximum lamports for combined CMint + CToken top-ups in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. **Accounts:** 1. source CToken diff --git a/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md b/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md index b1919c10c9..0e47c0cd83 100644 --- a/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md @@ -17,7 +17,7 @@ Format 1 (9 bytes, legacy): Format 2 (11 bytes): - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to burn - Byte 8: `decimals` (u8) - Expected token decimals -- Bytes 9-10: `max_top_up` (u16, little-endian) - Maximum lamports for combined CMint + CToken top-ups (0 = no limit) +- Bytes 9-10: `max_top_up` (u16, little-endian) - Maximum lamports for combined CMint + CToken top-ups in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. **Accounts:** 1. source CToken diff --git a/programs/compressed-token/program/docs/ctoken/MINT_TO.md b/programs/compressed-token/program/docs/ctoken/MINT_TO.md index 2fa45c1a69..0b59e50cb8 100644 --- a/programs/compressed-token/program/docs/ctoken/MINT_TO.md +++ b/programs/compressed-token/program/docs/ctoken/MINT_TO.md @@ -17,7 +17,7 @@ Path: programs/compressed-token/program/src/ctoken/mint_to.rs (see `process_ctok Byte layout: - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to mint -- Bytes 8-9: `max_top_up` (u16, little-endian, optional) - Maximum lamports for top-ups combined, 0 = no limit +- Bytes 8-9: `max_top_up` (u16, little-endian, optional) - Maximum lamports for top-ups in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. Format variants: - 8-byte format: amount only, no max_top_up enforcement diff --git a/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md b/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md index 60c8c7d32a..99041cbe14 100644 --- a/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md @@ -19,7 +19,7 @@ Shared implementation: programs/compressed-token/program/src/ctoken/burn.rs (fun Byte layout: - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to mint - Byte 8: `decimals` (u8) - Expected token decimals -- Bytes 9-10: `max_top_up` (u16, little-endian, optional) - Maximum lamports for top-ups combined, 0 = no limit +- Bytes 9-10: `max_top_up` (u16, little-endian, optional) - Maximum lamports for top-ups in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. Format variants: - 9 bytes: amount + decimals (legacy, no max_top_up enforcement) diff --git a/programs/compressed-token/program/docs/ctoken/REVOKE.md b/programs/compressed-token/program/docs/ctoken/REVOKE.md index 6d0674f176..850c20b400 100644 --- a/programs/compressed-token/program/docs/ctoken/REVOKE.md +++ b/programs/compressed-token/program/docs/ctoken/REVOKE.md @@ -21,7 +21,7 @@ Revokes any previously granted delegation on a decompressed ctoken account (acco Path: programs/compressed-token/program/src/ctoken/approve_revoke.rs (lines 49-59 for revoke, lines 86-124 for top-up processing) - Empty (0 bytes): legacy format, no max_top_up enforcement (max_top_up = 0, no limit) -- Bytes 0-1 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up (0 = no limit) +- Bytes 0-1 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. **Accounts:** 1. source diff --git a/programs/compressed-token/program/docs/ctoken/TRANSFER.md b/programs/compressed-token/program/docs/ctoken/TRANSFER.md index 55a29f01f4..788dd31619 100644 --- a/programs/compressed-token/program/docs/ctoken/TRANSFER.md +++ b/programs/compressed-token/program/docs/ctoken/TRANSFER.md @@ -32,7 +32,7 @@ After discriminator byte, the following formats are supported: - **8 bytes (legacy):** amount (u64) - No max_top_up enforcement - **10 bytes (extended):** amount (u64) + max_top_up (u16) - `amount`: u64 - Number of tokens to transfer - - `max_top_up`: u16 - Maximum lamports for top-up (0 = no limit) + - `max_top_up`: u16 - Maximum lamports for top-up in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. **Accounts:** 1. source diff --git a/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md b/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md index a890cbfdf0..7dd29c875e 100644 --- a/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md @@ -23,7 +23,7 @@ Transfers tokens between decompressed ctoken solana accounts with mint decimals **Instruction data:** - **9 bytes (legacy):** amount (u64) + decimals (u8) - **11 bytes (with max_top_up):** amount (u64) + decimals (u8) + max_top_up (u16) - - max_top_up: Maximum lamports for top-up operations (0 = no limit) + - max_top_up: Maximum lamports for top-up in units of 1,000 lamports (e.g., max_top_up=1 means 1,000 lamports, max_top_up=65535 means ~65.5M lamports). 0 = no limit. **Accounts:** 1. source diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs index 7803aa539e..58ea675a01 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs @@ -51,7 +51,9 @@ pub fn process_actions<'a>( let mut transfer_map = [0u64; MAX_PACKED_ACCOUNTS]; // Initialize budget: +1 allows exact match (total == max_top_up) let max_top_up: u16 = parsed_instruction_data.max_top_up.get(); - let mut lamports_budget = (max_top_up as u64).saturating_add(1); + // max_top_up is in units of 1,000 lamports (max ~65.5M lamports). + // +1 allows exact match (total == max_top_up * 1000). + let mut lamports_budget = (max_top_up as u64).saturating_mul(1000).saturating_add(1); // Start metadata authority with same value as mint authority for action in parsed_instruction_data.actions.iter() { diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs index cc25afc11e..95e1beb9cf 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs @@ -59,8 +59,9 @@ pub fn process_token_compression<'a>( return Err(ErrorCode::TooManyCompressionTransfers.into()); } let mut transfer_map = [0u64; MAX_PACKED_ACCOUNTS]; - // Initialize budget: +1 allows exact match (total == max_top_up) - let mut lamports_budget = (max_top_up as u64).saturating_add(1); + // Initialize budget: max_top_up is in units of 1,000 lamports (max ~65.5M lamports). + // +1 allows exact match (total == max_top_up * 1000). + let mut lamports_budget = (max_top_up as u64).saturating_mul(1000).saturating_add(1); for (compression_index, compression) in compressions.iter().enumerate() { let account_index = compression.source_or_recipient as usize; diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index c6296f3758..261ee344a9 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -111,7 +111,8 @@ fn process_compressible_top_up( }; if transfer_amount > 0 { - if max_top_up > 0 && transfer_amount > max_top_up as u64 { + // max_top_up is in units of 1,000 lamports (max ~65.5M lamports). + if max_top_up > 0 && transfer_amount > (max_top_up as u64).saturating_mul(1000) { return Err(TokenError::MaxTopUpExceeded.into()); } let payer = payer.ok_or(TokenError::MissingPayer)?; diff --git a/programs/compressed-token/program/src/ctoken/transfer/shared.rs b/programs/compressed-token/program/src/ctoken/transfer/shared.rs index 06aee4b40d..ad577fed38 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/shared.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/shared.rs @@ -129,8 +129,9 @@ fn transfer_top_up( ) -> Result<(), ProgramError> { if sender_top_up > 0 || recipient_top_up > 0 { // Check budget if max_top_up is set (non-zero) + // max_top_up is in units of 1,000 lamports (max ~65.5M lamports). let total_top_up = sender_top_up.saturating_add(recipient_top_up); - if max_top_up != 0 && total_top_up > max_top_up as u64 { + if max_top_up != 0 && total_top_up > (max_top_up as u64).saturating_mul(1000) { return Err(TokenError::MaxTopUpExceeded.into()); } diff --git a/programs/compressed-token/program/src/shared/compressible_top_up.rs b/programs/compressed-token/program/src/shared/compressible_top_up.rs index 6797766f96..b66767693f 100644 --- a/programs/compressed-token/program/src/shared/compressible_top_up.rs +++ b/programs/compressed-token/program/src/shared/compressible_top_up.rs @@ -45,8 +45,9 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( let mut current_slot = 0; - // Initialize budget: +1 allows exact match (total == max_top_up) - let mut lamports_budget = (max_top_up as u64).saturating_add(1); + // Initialize budget: max_top_up is in units of 1,000 lamports (max ~65.5M lamports). + // +1 allows exact match (total == max_top_up * 1000). + let mut lamports_budget = (max_top_up as u64).saturating_mul(1000).saturating_add(1); // Calculate CMint top-up using optimized function (owner check inside) #[cfg(target_os = "solana")]