Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion program-tests/compressed-token-test/tests/mint/failing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CreateMint> - Configuration for creating new compressed mint (None for existing mint operations)
- `actions`: Vec<Action> - Ordered list of actions to execute
- `proof`: Option<CompressedProof> - ZK proof for compressed account validation (required unless prove_by_index=true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Compression> - 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
Expand Down
8 changes: 5 additions & 3 deletions programs/compressed-token/program/docs/ctoken/APPROVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM! Documentation accurately reflects the unit scaling change.

The documentation correctly explains that max_top_up is now interpreted as units of 1,000 lamports, with accurate examples and consistent code snippets. The math checks out: max_top_up=65535 → 65,535,000 lamports, which addresses the insufficient range issue mentioned in the PR objectives.

The use of saturating_mul(1000) is appropriate for defensive programming, and the "0 = no limit" semantics are clearly documented.

Optional enhancement: Add audit context

Consider adding a brief note explaining the rationale for this unit interpretation. For example, after line 23:

 - 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.
+
+  **Note**: The 1,000-lamport unit interpretation extends the effective range to ~65.5M lamports, addressing the limitation where raw u16 values (max 65,535 lamports) were insufficient for typical rent requirements (e.g., ~2.5M lamports for a 270-byte account).

This would help readers understand why the scaling was introduced.

Also applies to: 116-118, 137-139

🤖 Prompt for AI Agents
In `@programs/compressed-token/program/docs/ctoken/APPROVE.md` at line 23, Add a
brief audit/context note explaining why `max_top_up` is interpreted in units of
1,000 lamports (e.g., to extend range while preserving compatibility and reduce
overflow risk) immediately after the "Bytes 8-9 (optional): `max_top_up`" line
in APPROVE.md and replicate that same explanatory sentence near the
corresponding occurrences around the other documented uses of `max_top_up` (the
sections currently at the equivalents of lines 116-118 and 137-139) so readers
understand the rationale for the scaling change.


**Accounts:**
1. source
Expand Down Expand Up @@ -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)?;
Expand All @@ -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());
}
```
Expand Down
2 changes: 1 addition & 1 deletion programs/compressed-token/program/docs/ctoken/BURN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion programs/compressed-token/program/docs/ctoken/MINT_TO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion programs/compressed-token/program/docs/ctoken/REVOKE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 52 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stale comment on line 52 — it still references the old formula.

Line 52 says +1 allows exact match (total == max_top_up) which is the pre-change wording. Lines 54-55 already carry the correct updated explanation. Line 52 is now both redundant and misleading.

📝 Remove or update the stale comment
     // Array to accumulate transfer amounts by account index
     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();
     // 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);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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);
let max_top_up: u16 = parsed_instruction_data.max_top_up.get();
// 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);
🤖 Prompt for AI Agents
In
`@programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs`
around lines 52 - 56, The comment above the lamports_budget calculation is stale
and misleading: update or remove the line that says "+1 allows exact match
(total == max_top_up)" so the comment matches the actual math. Locate the code
around parsed_instruction_data.max_top_up and the variable lamports_budget (the
computation using (max_top_up as u64).saturating_mul(1000).saturating_add(1))
and either remove the redundant/incorrect sentence or reword it to reflect that
max_top_up is in units of 1,000 lamports and the +1 allows an exact match
against max_top_up * 1000.


// Start metadata authority with same value as mint authority
for action in parsed_instruction_data.actions.iter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ fn process_compressible_top_up<const BASE_LEN: usize, const PAYER_IDX: usize>(
};

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)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down