Skip to content

fix: cmint instruction data & cleanup#2164

Merged
ananas-block merged 38 commits intomainfrom
jorrit/fix-cmint-instruction-data
Jan 12, 2026
Merged

fix: cmint instruction data & cleanup#2164
ananas-block merged 38 commits intomainfrom
jorrit/fix-cmint-instruction-data

Conversation

@ananas-block
Copy link
Contributor

@ananas-block ananas-block commented Jan 7, 2026

Bug Fixes

  1. Compression index bounds check - Added validation preventing array index out-of-bounds in transfer2 token inputs
  2. Decompress amount validation - Added check that ATA/compress_to_pubkey decompress amount matches input token data amount
  3. MAX_COMPRESSIONS boundary fix - Changed >= to > to allow exactly MAX_COMPRESSIONS items in transfer2
  4. Explicit ATA derivation error - Added InvalidAtaDerivation error instead of silent fallback when ATA derivation fails or mismatches
  5. Rent exemption stability - Made rent calculations read CompressionInfo directly from bytes rather than relying on sysvar
  6. CompressedMintMetadata change - Added compressed_address field (32 bytes) directly to mint struct, reduced reserved from 49 to 17 bytes; unifies compressed address source

Error Handling

  1. New error types - Added 6 errors to CTokenError: MissingPayer, BorrowFailed, InvalidCTokenOwner, DecompressAmountMismatch, CompressionIndexOutOfBounds, InvalidAtaDerivation
  2. SPL Token error mapping - Changed from raw SPL Token error codes to Anchor ErrorCode mappings (+6000 offset)

Refactoring

  1. Program folder restructure - Reorganized compressed-token/program/src/ into compressed_token/ (mint_action, transfer2), compressible/ (claim, withdraw), and ctoken/ (approve, burn, close, create, freeze_thaw, mint_to, transfer) subdirectories
  2. File renaming - Removed ctoken_ prefix from files (e.g., ctoken_burn.rs -> ctoken/burn.rs, create_associated_token_account.rs -> ctoken/create_ata.rs)
  3. Added top-up modules - New top_up.rs files for ctoken and mint in ctoken-interface with optimized lamports calculation from account bytes
  4. TypeScript layout alignment - Moved compressedAddress from instruction-level to mint.metadata, removed tokenPoolBump and tokenPoolIndex fields, added rentExemptionPaid and reserved fields to CompressionInfo (88 -> 96 bytes)
  5. Removed CreateSplMint action - Deleted CreateSplMint action type and related code from both Rust and TypeScript

Documentation

  1. Documentation restructure - Moved docs from flat instructions/ to categorized ctoken/, compressed_token/, compressible/ directories; added new INSTRUCTIONS.md with discriminator reference table

Tests

  1. New test coverage - Added ata_decompress.rs (532 lines), check_extensions.rs (744 lines), mint_validation.rs (358 lines), create_ata.rs, create.rs, failing.rs for comprehensive validation testing

Summary by CodeRabbit

  • New Features

    • Added rent exemption tracking in compressed accounts.
    • Introduced ATA (Associated Token Account) support for compressed-only extensions.
    • Added compress-and-close functionality for managing compressed token lifecycles.
  • Bug Fixes

    • Improved error handling with new error codes for better diagnostics.
    • Fixed account validation for compressed account operations.
  • Documentation

    • Reorganized architecture documentation with clearer instruction references and examples.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add CalculateTopUp trait for generic top-up calculations
- Implement trait for CompressionInfo, ZCompressionInfo, ZCompressionInfoMut
- Unify process_compression_top_up to use shared implementation
- Remove duplicate function from compress_or_decompress_ctokens.rs
- Fix extension ordering in assert_create_token_account (insert at index 0)
- Replace redundant .get().ok_or() with direct indexing after length validation
- Add SAFETY comments documenting the length invariants
- Make payer optional - only required when top-up transfer is needed
- Remove redundant process_extensions function in transfer/default.rs
- Use CTokenError::MissingPayer consistently when payer is required
- Remove CreateSplMint enum variant and CreateSplMintAction struct (never activated)
- Remove create_spl_mint.rs file from ctoken-interface
- Add MAX_COMPRESSIONS constant (32) with meaningful error message
- Remove dead no_output_compressed_accounts field from Transfer2Config
- Update documentation (MINT_ACTION.md, CLAUDE.md, lib.rs)
- Update JS layout to remove CreateSplMint
- Refactor check_extensions tests for better coverage
- Add config state validation (active required) during account parsing
- Change compressible_config field to store parsed CompressibleConfig
- Remove redundant config parsing in decompress_mint action
- Refactor mint_output into serialize_decompressed_mint and serialize_compressed_mint helpers
- Use cmint_decompressed flag directly after process_actions
Change validation from >= to > to fix off-by-one error that prevented
using exactly MAX_COMPRESSIONS compression operations.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Reorganizes the compressed-token program into new modules, refactors CompressionInfo to store rent_exemption_paid and derive rent internally, removes CreateSplMint, adds optimized CMint/CToken top-up helpers and centralized error converters, updates many call sites, and expands docs and tests to match the new layouts and validations.

Changes

Cohort / File(s) Summary
CompressionInfo Core
program-libs/compressible/src/compression_info.rs, program-libs/compressible/tests/*
Added public fields rent_exemption_paid: u32 and _reserved: u32; removed external rent lookup; is_compressible, calculate_top_up_lamports, and claim now derive rent from stored field; tests updated.
CToken Error & Top-up Helpers
program-libs/ctoken-interface/src/error.rs, program-libs/ctoken-interface/src/state/ctoken/top_up.rs, program-libs/ctoken-interface/src/state/mint/top_up.rs, program-libs/ctoken-interface/tests/*
Added new error variants (e.g., MissingPayer) and mappings; introduced optimized top-up helpers for CMint/CToken (*_top_up_lamports_from_slice / *_from_account_info) to compute top-ups without full deserialization.
MintAction Data & Builder
program-libs/ctoken-interface/src/instructions/mint_action/*, sdk-libs/*, programs/compressed-token/program/src/compressed_token/mint_action/*, programs/compressed-token/program/tests/mint*
Removed CreateSplMint and related exports; moved compressed_address into CompressedMintMetadata; removed compressed_address/token_pool_* from instruction payloads; added new_mint/new_mint_write_to_cpi_context builders and updated input/output plumbing and tests.
Zero-copy & Deserialization Guards
program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs, program-libs/ctoken-interface/src/state/mint/zero_copy.rs
Added zero-copy initialization guards and MemoryNotZeroed checks, from_account_info_checked/from_account_info_mut_checked helpers, is_uninitialized accessors; reduced reserved sizes (49→17) and propagated layout changes.
Shared Utilities & CPI/Error Conversion
programs/compressed-token/program/src/shared/convert_program_error.rs, programs/compressed-token/program/src/shared/config_account.rs, programs/compressed-token/program/src/shared/initialize_ctoken_account.rs
Added centralized converters (convert_program_error, convert_pinocchio_token_error, convert_token_error), config-account helpers, create_compressible_account, is_valid_mint, and threaded rent_exemption_paid into compressible init flows.
Compressible Instruction Flow
programs/compressed-token/program/src/compressible/*
Added claim and withdraw_funding_pool modules; claim now distinguishes CToken vs CMint storage, validates per-account, updates last_claimed_slot, updates RentConfig, and derives rent from rent_exemption_paid.
CToken Processors
programs/compressed-token/program/src/ctoken/*
Split CToken processors into modular files (approve_revoke, burn, create, create_ata, mint_to, close, transfer); integrated compressible top-up logic, centralized error conversion, and hot-path optimizations for plain (165-byte) accounts.
Mint Action Processing
programs/compressed-token/program/src/compressed_token/mint_action/*
Reorganized under compressed_token; Accounts parsing now uses CompressibleConfig type, removed token pool params and create_spl_mint handling; added new mint output processing and updated validation semantics.
Transfer2 / Compression
programs/compressed-token/program/src/compressed_token/transfer2/*
Reorganized Transfer2 under compressed_token; introduced MAX_COMPRESSIONS and replaced fixed [Option;32] with [Option<u8>; MAX_COMPRESSIONS]; added compress_and_close and validate_and_apply_compressed_only decompression flow; tightened extension checks.
Program Layout & Routing
programs/compressed-token/program/src/lib.rs, programs/compressed-token/program/src/compressed_token/mod.rs, programs/compressed-token/program/src/compressible/mod.rs, programs/compressed-token/program/src/ctoken/mod.rs
Major module reorganization and new public modules (compressed_token, compressible, ctoken); updated instruction dispatch and added MAX_COMPRESSIONS constant.
Extensions & ATA-awareness
program-libs/ctoken-interface/src/instructions/extensions/*, program-libs/ctoken-interface/src/state/extensions/*
Added CompressedOnly instruction/state updates (includes ATA metadata such as is_ata), find_compressed_only helper, and is_ata() accessor on compressible zero-copy mut types to support ATA-derived owner checks.
Removed / Replaced Artifacts
programs/compressed-token/program/src/create_token_account.rs, programs/compressed-token/program/src/mint_action/mint_output.rs, plus removed exports
Deleted large inlined account-creation and previous mint-output implementations in favor of shared helpers (create_compressible_account) and new modular flows.
Docs & Tests
programs/compressed-token/program/docs/**, programs/compressed-token/program/tests/**, program-libs/ctoken-interface/tests/**, program-libs/compressible/tests/**
Extensive documentation reorganization and many updated/added tests for top-up helpers, zero-copy guards, extension validation, mint validation, and updated struct layouts (reserved sizes, new fields).

Sequence diagram(s)

sequenceDiagram
  participant Caller
  participant Program
  participant Account
  participant RentConfig
  participant SysvarClock
  Caller->>Program: invoke Claim instruction
  Program->>Account: read account bytes (determine CToken vs CMint)
  alt CMint
    Program->>Account: zero-copy parse CompressionInfo.rent_exemption_paid
  else CToken
    Program->>Account: read Compressible extension CompressionInfo.rent_exemption_paid (bytemuck)
  end
  Program->>SysvarClock: lazy fetch current_slot (if needed)
  Program->>Program: calculate_top_up_lamports (derive rent from rent_exemption_paid)
  Program->>RentConfig: update rent state
  Program->>Account: update last_claimed_slot and persist
  Program->>Caller: return result (may transfer lamports via CPI if needed)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

✨ A module garden trimmed and grown,

Rent tucked in fields now owned and shown,
Mint actions lean, helpers race,
Errors routed to one place,
Tests applaud the sturdier stone.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix: cmint instruction data & cleanup' is vague and uses generic language that fails to convey the substantial refactoring and feature additions present in this changeset. Replace with a more descriptive title capturing the primary change, such as 'refactor: reorganize program modules and fix compressed mint handling' or 'feat: add compression bounds checking and reorganize program structure'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jorrit/fix-cmint-instruction-data

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
programs/compressed-token/program/tests/queue_indices.rs (1)

28-278: Consider adding write mode test coverage in the future.

The comprehensive test suite thoroughly covers execute mode (write_to_cpi_context=false) and no-CPI scenarios. However, all CPI context test cases use first_set_context=false and set_context=false. Consider adding test cases for write mode (where either flag is true) to ensure complete coverage of the QueueIndices behavior in both execution paths.

This is a pre-existing gap outside the scope of this PR's refactoring, but worth noting for future enhancement.

programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs (1)

88-95: TODO: SPL Token-2022 compatibility incomplete.

The commented code indicates that adding new metadata keys is not yet supported. This limits SPL Token-2022 compatibility as users can only update existing keys.

Do you want me to help implement this feature or open a new issue to track it?

programs/compressed-token/program/docs/ctoken/CLOSE.md (1)

147-156: Edge case documentation is clear, but known limitation should be highlighted.

Line 150 notes that SetAuthority is unimplemented and close_authority is always None. This is important context, but it might be worth making more prominent in the main description (perhaps early in the "Accounts" section) so users understand this constraint before implementing against the API.

The note about using CompressAndClose mode in Transfer2 (line 151) is good guidance for handling non-zero balances—this correctly directs users to the right instruction.

Consider restructuring the edge cases section or adding a Limitations section upfront that clarifies:

  • SetAuthority is not implemented; close_authority is always None
  • Users needing to close accounts with non-zero balances must use Transfer2's CompressAndClose mode instead
  • Only 3 accounts required for non-compressible accounts (vs. 4 for compressible)

This would help users understand constraints earlier when designing their workflows.

programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md (2)

66-97: Fix CMint deserialization function name: use zero_copy_at_checked, not zero_copy_at.

The documentation at line 74 states CompressedMint::zero_copy_at, but the actual implementation in programs/compressed-token/program/src/shared/compressible_top_up.rs (line 53) uses CompressedMint::zero_copy_at_checked. All other function names, parameters, and logic flow match correctly.


98-116: Fix ArithmeticOverflow error code in documentation.

The error code for ArithmeticOverflow is documented as (24), but the actual CTokenError implementation maps it to (18003). Update the documentation to reflect the correct code, as developers debugging this error will search for 18003, not 24, in the error logs.

All other CTokenError codes (18002, 18036, 18053, 18020, 18043, 18047, 18056) have been verified and match the actual enum definitions.

programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs (1)

140-155: Consider documenting the magic bytes for the account-compression program check.

The heuristic at line 151 uses raw bytes [9, 44, 54, 236, 34, 245, 23, 131] to identify tree/queue accounts by matching the first 8 bytes of the account owner. While this works, a named constant or brief comment explaining these are the first 8 bytes of the account-compression program ID would improve maintainability.

📝 Suggested documentation improvement
+/// First 8 bytes of the Light account-compression program ID
+/// Used as a heuristic to identify tree/queue accounts
+const ACCOUNT_COMPRESSION_PROGRAM_PREFIX: [u8; 8] = [9, 44, 54, 236, 34, 245, 23, 131];
+
 /// Extract tree accounts by checking account owner matches account-compression program
 #[profile]
 #[inline(always)]
 pub fn extract_tree_accounts<'info>(
     packed_accounts: &'info ProgramPackedAccounts<'info, AccountInfo>,
 ) -> Vec<&'info Pubkey> {
     let mut tree_accounts = Vec::with_capacity(8);
     for account_info in packed_accounts.accounts {
         // As heuristic which accounts are tree or queue accounts we
         // check that the first 8 bytes of the account compression program
         // equal the first 8 bytes of the account owner.
-        if account_info.owner()[0..8] == [9, 44, 54, 236, 34, 245, 23, 131] {
+        if account_info.owner()[0..8] == ACCOUNT_COMPRESSION_PROGRAM_PREFIX {
             tree_accounts.push(account_info.key());
         }
     }
     tree_accounts
 }
programs/compressed-token/program/docs/compressed_token/TRANSFER2.md (1)

344-373: Fix maximum transfer limit in TooManyCompressionTransfers documentation.

The error code documentation is accurate, but one description contains an outdated limit. The TooManyCompressionTransfers error (6095) states "Maximum 40 transfers allowed" in the documentation, but the actual implementation enforces a limit of 32 transfers. Update the documentation to match:

- `ErrorCode::TooManyCompressionTransfers` (error code: 6095) - Too many compression transfers. Maximum 32 transfers allowed per instruction

All other error codes and their numeric values are correct and match the ErrorCode enum in programs/compressed-token/anchor/src/lib.rs.

programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs (1)

60-72: Fix the config validation for decompress operations—using wrong strictness level.

The config is validated upstream in accounts.rs via next_config_account(), which calls validate_active(). However, this is overly restrictive for decompress operations.

Per the documented error requirements: DecompressMint and CompressAndCloseCMint should validate that config is not INACTIVE (allowing both ACTIVE and DEPRECATED states), not ACTIVE-only. Create token account requires ACTIVE-only.

The current code reuses next_config_account() for both operation types through needs_compressible_accounts(), which lumps decompress/compress operations together with account creation validation. This blocks deprecated configs from decompressing mints when they should be allowed.

Required fix: Either create a separate next_config_account_not_inactive() function for decompress/compress operations, or branch the validation in the account parsing based on operation type.

🤖 Fix all issues with AI agents
In @programs/compressed-token/program/docs/CLAUDE.md:
- Line 11: Add a blank line after the list item "**`INSTRUCTIONS.md`** - Full
instruction reference and discriminator table" so the heading that follows has a
proper separating newline (fixes MD022); simply insert an empty line immediately
after that line in the document.

In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md:
- Around line 225-227: Add a blank line immediately before the "### Spl mint
migration" heading to satisfy MD022 (blanks-around-headings); update the
MINT_ACTION.md content so there is an empty line above that heading line (the
heading text "Spl mint migration") to fix the markdown lint error.

In @programs/compressed-token/program/docs/ctoken/CREATE.md:
- Around line 89-91: Reword the awkward phrase "compression_only is only set" in
the CREATE.md validation bullet to remove the repeated adverb; for example
change it to "compression_only may only be set for mints with restricted
extensions" or "compression_only is set exclusively for mints with restricted
extensions" so the validation reads clearly and concisely.

In @programs/compressed-token/program/docs/EXTENSIONS.md:
- Line 139: The EXTENSIONS.md entry for validate_permanent_delegate points to
the call site instead of the actual function definition; update the documented
reference for validate_permanent_delegate so it points to the function
definition (not the surrounding transfer logic/call) in the source and leave the
other two entries unchanged.

In @programs/compressed-token/program/docs/INSTRUCTIONS.md:
- Around line 84-106: The markdown headings for "Compressed Token Operations
(`compressed_token/`)", "CToken Operations (`ctoken/`)", "Compressible
Operations (`compressible/`)" and "Token Pool Operations (root)" need
surrounding blank lines and the lists under each heading should be sequentially
numbered starting at 1 (or converted to unordered lists with explicit
instruction IDs if those numbers are meant to be global); update the
INSTRUCTIONS.md section so each heading has a blank line above and below it and
renumber the items under each heading (e.g., Transfer2/MintAction/Freeze/Thaw
under compressed_token/ start at 1, Create/Close/Transfer/... under ctoken/
start at 1, Claim/Withdraw under compressible/ start at 1, Create Token Pool/Add
Token Pool under Token Pool Operations start at 1) or change them to bullet
lists with parenthetical IDs to preserve global IDs.

In @programs/compressed-token/program/src/compressed_token/mod.rs:
- Around line 1-2: Add public re-exports for commonly used items from the child
modules to improve ergonomics: after the existing `pub mod mint_action;` and
`pub mod transfer2;` declarations, add `pub use` statements to re-export the
public types/functions from `mint_action` and `transfer2` (e.g., `pub use
mint_action::*;` and `pub use transfer2::*;`) so consumers can import those
instruction/data/processor items directly from the parent module.

In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Line 118: Fix the grammar in the inline comment that currently reads "// -
Decompress: no check it restores existing state" by changing it to a correct
phrasing such as "// - Decompress: now check it restores existing state" (or "//
- Decompress: check that it restores existing state") in check_extensions.rs so
the intent is clear; locate the comment near the Decompress bullet in the
transfer2/check_extensions.rs diff and update the text accordingly.

In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 92-97: The non-compressible token account case relies on implicit
ownership validation but initialize_ctoken_account currently does not perform
explicit checks; call check_token_program_owner(token_account_info) near the
start of initialize_ctoken_account (before any writes) to validate the account
is owned by a supported token program (SPL Token, Token-2022, or CToken) and
return an appropriate error if it fails; ensure you import/use the utility from
src/shared/owner_validation.rs and place the check early so subsequent logic
assumes a validated owner.

In @programs/compressed-token/program/src/ctoken/mod.rs:
- Around line 1-21: The close module is exposing its function via a nested path
(close::processor::process_close_token_account) which is inconsistent with other
modules; fix this by changing the close module to re-export the processor symbol
at the close module root (e.g., add in close/mod.rs: pub use
processor::process_close_token_account) so this file can simply use pub use
close::process_close_token_account to match the flat exports of the other
modules (or alternatively move the function to close::process_close module root
and update references accordingly).

In @programs/compressed-token/program/src/shared/config_account.rs:
- Around line 13-30: Define a module-level constant for the hard-coded program
id (e.g., CONFIG_PROGRAM_ID or PROGRAM_ID) and replace the inline pubkey!
invocation inside parse_config_account's call to check_owner with that constant;
specifically, add the constant near the top of the file and update
parse_config_account to call check_owner(&CONFIG_PROGRAM_ID.to_bytes(),
config_account) so the program id is centralized (affects the pubkey! usage in
parse_config_account and keeps CompressibleConfig checks unchanged).

In @programs/compressed-token/program/src/shared/owner_validation.rs:
- Line 106: Update the docstring above the owner validation logic to remove the
incorrect mention of "account delegate" and accurately state the supported
authorization paths: owner or permanent delegate from the mint; replace the
existing docstring lines with a concise description such as "Verify and update
token account authority using zero-copy compressed token format. Allows owner or
permanent delegate (from mint) to authorize compression operations." and ensure
this text sits immediately above the owner validation function in
owner_validation.rs so it matches the implemented checks.

In @programs/compressed-token/program/tests/mint_validation.rs:
- Around line 38-62: Add a short inline comment explaining why byte index 165 is
used in MintData::WithAccountType within the to_bytes() implementation (e.g.,
that Token-2022 layout places AccountType at offset 165: 82 bytes base + 83
bytes padding), so future readers understand the magic number; update the
comment near the assignment to account_type (data[165] = *account_type) and keep
it concise referencing MintData::WithAccountType and to_bytes().

Comment on lines +89 to +91
4.3.1. Validate compression_only is only set for mints with restricted extensions:
- If compression_only != 0 and mint has no restricted extensions
- Error: `ErrorCode::CompressionOnlyNotAllowed`
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

Minor style: adverb repetition.

Line 89 has "compression_only is only set" which reads awkwardly. Consider rephrasing to "compression_only is set exclusively for mints with restricted extensions" or "compression_only may only be set for mints with restricted extensions".

📝 Suggested wording
-    4.3.1. Validate compression_only is only set for mints with restricted extensions:
+    4.3.1. Validate compression_only is set exclusively for mints with restricted extensions:
🧰 Tools
🪛 LanguageTool

[style] ~89-~89: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ... 4.3.1. Validate compression_only is only set for mints with restricted extension...

(ADVERB_REPETITION_PREMIUM)

🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/ctoken/CREATE.md around lines 89 -
91, Reword the awkward phrase "compression_only is only set" in the CREATE.md
validation bullet to remove the repeated adverb; for example change it to
"compression_only may only be set for mints with restricted extensions" or
"compression_only is set exclusively for mints with restricted extensions" so
the validation reads clearly and concisely.

// Compress/Decompress don't need additional validation here:
// - Compress: blocked by check_mint_extensions when outputs exist
// - Decompress: bypassed (restoring existing state)
// - Decompress: no check it restores existing state
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

Fix comment grammar.

The comment has a minor grammatical issue.

📝 Suggested fix
-                // - Decompress: no check it restores existing state
+                // - Decompress: no check as it restores existing state
📝 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
// - Decompress: no check it restores existing state
// - Decompress: no check as it restores existing state
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
at line 118, Fix the grammar in the inline comment that currently reads "// -
Decompress: no check it restores existing state" by changing it to a correct
phrasing such as "// - Decompress: now check it restores existing state" (or "//
- Decompress: check that it restores existing state") in check_extensions.rs so
the intent is clear; locate the comment near the Decompress bullet in the
transfer2/check_extensions.rs diff and update the text accordingly.

Comment on lines +1 to +21
pub mod approve_revoke;
pub mod burn;
pub mod close;
pub mod create;
pub mod create_ata;
pub mod freeze_thaw;
pub mod mint_to;
pub mod transfer;

pub use approve_revoke::{
process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke,
};
pub use burn::{process_ctoken_burn, process_ctoken_burn_checked};
pub use close::processor::process_close_token_account;
pub use create::process_create_token_account;
pub use create_ata::{
process_create_associated_token_account, process_create_associated_token_account_idempotent,
};
pub use freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account};
pub use mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked};
pub use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked};
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

Clean module organization centralizing ctoken operations.

This new module file properly declares and re-exports all ctoken submodules, following the coding guidelines for module organization. The structure aligns with the PR's goal of restructuring the module hierarchy into organized tiers (compressed_token, ctoken, compressible).

One minor note: the close module uses a nested processor path (line 14: close::processor::process_close_token_account) while other modules export functions directly. This inconsistency is minor but worth noting for future cleanup.

🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/ctoken/mod.rs around lines 1 - 21, The
close module is exposing its function via a nested path
(close::processor::process_close_token_account) which is inconsistent with other
modules; fix this by changing the close module to re-export the processor symbol
at the close module root (e.g., add in close/mod.rs: pub use
processor::process_close_token_account) so this file can simply use pub use
close::process_close_token_account to match the flat exports of the other
modules (or alternatively move the function to close::process_close module root
and update references accordingly).

Comment on lines +13 to +30
pub fn parse_config_account(
config_account: &AccountInfo,
) -> Result<&CompressibleConfig, ProgramError> {
// Validate config account owner
check_owner(
&pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
config_account,
)?;
// Parse config data
let data = unsafe { config_account.borrow_data_unchecked() };
check_discriminator::<CompressibleConfig>(data)?;
let config = bytemuck::pod_from_bytes::<CompressibleConfig>(&data[8..]).map_err(|e| {
msg!("Failed to deserialize CompressibleConfig: {:?}", e);
ProgramError::InvalidAccountData
})?;

Ok(config)
}
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

Extract hard-coded program ID to a constant.

The program ID Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX is hard-coded on line 18. This should be defined as a module-level constant for maintainability and to prevent typos.

♻️ Suggested refactor

At the top of the file (after imports):

+pub const COMPRESSIBLE_PROGRAM_ID: Pubkey = pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX");
+
 #[profile]
 #[inline(always)]
 pub fn parse_config_account(

Then update the usage:

     check_owner(
-        &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
+        &COMPRESSIBLE_PROGRAM_ID.to_bytes(),
         config_account,
     )?;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/config_account.rs around lines
13 - 30, Define a module-level constant for the hard-coded program id (e.g.,
CONFIG_PROGRAM_ID or PROGRAM_ID) and replace the inline pubkey! invocation
inside parse_config_account's call to check_owner with that constant;
specifically, add the constant near the top of the file and update
parse_config_account to call check_owner(&CONFIG_PROGRAM_ID.to_bytes(),
config_account) so the program id is centralized (affects the pubkey! usage in
parse_config_account and keeps CompressibleConfig checks unchanged).

if let Some(permanent_delegate) = &checks.permanent_delegate {
if pubkey_eq(authority_key, permanent_delegate) {
return Ok(()); // Permanent delegate can compress any account of this mint
return Ok(()); // Permanent delegate can (de)compress any account of this mint
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, read the owner_validation.rs file around the mentioned lines
echo "=== Reading owner_validation.rs ===" 
head -130 programs/compressed-token/program/src/shared/owner_validation.rs | tail -50

echo -e "\n=== Full check_ctoken_owner function ===" 
grep -n "fn check_ctoken_owner" -A 30 programs/compressed-token/program/src/shared/owner_validation.rs

echo -e "\n=== MintExtensionChecks struct definition ===" 
rg "struct MintExtensionChecks" -A 10 --type=rs programs/compressed-token/program/src

Repository: Lightprotocol/light-protocol

Length of output: 3388


🏁 Script executed:

#!/bin/bash

# Search for check_ctoken_owner usage in compression contexts
echo "=== check_ctoken_owner usage in transfer2 ===" 
rg "check_ctoken_owner" -B 3 -A 5 --type=rs programs/compressed-token/program/src/transfer2

echo -e "\n=== Delegate handling in compression ===" 
rg "permanent_delegate|account.*delegate" -i -B 2 -A 2 --type=rs programs/compressed-token/program/src/transfer2 | head -60

Repository: Lightprotocol/light-protocol

Length of output: 230


🏁 Script executed:

#!/bin/bash

# Check CLAUDE.md for compression/decompression delegate requirements
echo "=== CLAUDE.md compression requirements ===" 
cat programs/compressed-token/program/CLAUDE.md | grep -i -A 5 -B 5 "compress\|delegate" | head -100

Repository: Lightprotocol/light-protocol

Length of output: 6022


Update docstring to accurately reflect supported authorization paths.

The docstring at lines 83-84 claims support for "account delegate" but the implementation only authorizes owner and permanent delegate from mint. Update the docstring to remove the reference to account delegate authorization.

/// Verify and update token account authority using zero-copy compressed token format.
/// Allows owner or permanent delegate (from mint) to authorize compression operations.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/owner_validation.rs at line
106, Update the docstring above the owner validation logic to remove the
incorrect mention of "account delegate" and accurately state the supported
authorization paths: owner or permanent delegate from the mint; replace the
existing docstring lines with a concise description such as "Verify and update
token account authority using zero-copy compressed token format. Allows owner or
permanent delegate (from mint) to authorize compression operations." and ensure
this text sits immediately above the owner validation function in
owner_validation.rs so it matches the implemented checks.

Comment on lines +38 to +62
/// Data configurations for testing
#[derive(Debug, Clone)]
enum MintData {
Empty,
TooSmall(usize), // < 82 bytes
ExactSplSize, // 82 bytes (valid for all)
BetweenSizes(usize), // 83-165 bytes
WithAccountType(u8), // 166+ bytes with specific AccountType
}

impl MintData {
fn to_bytes(&self) -> Vec<u8> {
match self {
MintData::Empty => vec![],
MintData::TooSmall(size) => vec![0u8; *size],
MintData::ExactSplSize => vec![0u8; 82],
MintData::BetweenSizes(size) => vec![0u8; *size],
MintData::WithAccountType(account_type) => {
let mut data = vec![0u8; 170];
data[165] = *account_type;
data
}
}
}
}
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

Consider documenting the magic number 165 for AccountType offset.

The WithAccountType variant places the account type at byte offset 165. While this is correct per Token-2022's layout (82 bytes base + 83 bytes padding to reach the AccountType field at 165), a brief comment explaining this would help future readers understand the test data construction.

📝 Suggested documentation
 impl MintData {
     fn to_bytes(&self) -> Vec<u8> {
         match self {
             MintData::Empty => vec![],
             MintData::TooSmall(size) => vec![0u8; *size],
             MintData::ExactSplSize => vec![0u8; 82],
             MintData::BetweenSizes(size) => vec![0u8; *size],
             MintData::WithAccountType(account_type) => {
+                // Token-2022 extended mint layout: 82 base + padding, AccountType at offset 165
                 let mut data = vec![0u8; 170];
                 data[165] = *account_type;
                 data
             }
         }
     }
 }
📝 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
/// Data configurations for testing
#[derive(Debug, Clone)]
enum MintData {
Empty,
TooSmall(usize), // < 82 bytes
ExactSplSize, // 82 bytes (valid for all)
BetweenSizes(usize), // 83-165 bytes
WithAccountType(u8), // 166+ bytes with specific AccountType
}
impl MintData {
fn to_bytes(&self) -> Vec<u8> {
match self {
MintData::Empty => vec![],
MintData::TooSmall(size) => vec![0u8; *size],
MintData::ExactSplSize => vec![0u8; 82],
MintData::BetweenSizes(size) => vec![0u8; *size],
MintData::WithAccountType(account_type) => {
let mut data = vec![0u8; 170];
data[165] = *account_type;
data
}
}
}
}
/// Data configurations for testing
#[derive(Debug, Clone)]
enum MintData {
Empty,
TooSmall(usize), // < 82 bytes
ExactSplSize, // 82 bytes (valid for all)
BetweenSizes(usize), // 83-165 bytes
WithAccountType(u8), // 166+ bytes with specific AccountType
}
impl MintData {
fn to_bytes(&self) -> Vec<u8> {
match self {
MintData::Empty => vec![],
MintData::TooSmall(size) => vec![0u8; *size],
MintData::ExactSplSize => vec![0u8; 82],
MintData::BetweenSizes(size) => vec![0u8; *size],
MintData::WithAccountType(account_type) => {
// Token-2022 extended mint layout: 82 base + padding, AccountType at offset 165
let mut data = vec![0u8; 170];
data[165] = *account_type;
data
}
}
}
}
🤖 Prompt for AI Agents
In @programs/compressed-token/program/tests/mint_validation.rs around lines 38 -
62, Add a short inline comment explaining why byte index 165 is used in
MintData::WithAccountType within the to_bytes() implementation (e.g., that
Token-2022 layout places AccountType at offset 165: 82 bytes base + 83 bytes
padding), so future readers understand the magic number; update the comment near
the assignment to account_type (data[165] = *account_type) and keep it concise
referencing MintData::WithAccountType and to_bytes().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
programs/compressed-token/program/src/ctoken/close/processor.rs (1)

46-46: Clarify terminology: "rent authority" should be "compression_authority".

The comment states "The rent authority can close the account" but the actual validation at lines 94-98 checks compression.info.compression_authority, not a "rent authority". This terminology inconsistency can confuse developers, especially given that there is a separate rent_sponsor field in the compressible extension.

📝 Suggested clarification
-/// Validates that a ctoken solana account is ready to be closed.
-/// The rent authority can close the account.
+/// Validates that a ctoken solana account is ready to be closed via compress-and-close.
+/// Only the compression_authority can compress and close the account.
programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs (3)

26-26: Remove debug logging or gate behind feature flag.

This debug log statement appears to be leftover development code. Logging consumes compute units and should either be removed or conditionally compiled behind a debug feature flag.

🧹 Proposed cleanup
-    anchor_lang::solana_program::log::msg!("inputs {:?}", inputs);

Or gate it:

+    #[cfg(feature = "debug")]
     anchor_lang::solana_program::log::msg!("inputs {:?}", inputs);

29-33: Clarify the flag logic and field naming.

The field name with_lamports_change_account_merkle_tree_index suggests a boolean presence flag (following the with_* naming pattern), but it's being compared to 0 as an integer. This makes the logic harder to understand at first glance.

If this is indeed a boolean-like flag, consider:

  1. Using a more explicit name like has_lamports_change_account_merkle_tree_index
  2. Or documenting why it's compared to 0 instead of using a boolean

70-80: Replace magic numbers with named constants or enum.

The values 0 and 1 for is_compress are magic numbers that reduce code readability. Consider defining named constants or using an enum to make the intent clearer.

♻️ Proposed refactor

At the module level:

const IS_DECOMPRESS: u8 = 1;
const IS_COMPRESS: u8 = 0;

Then update the code:

         let (change_lamports, is_compress) = if total_input_lamports > total_output_lamports {
             (
                 total_input_lamports.saturating_sub(total_output_lamports),
-                0,
+                IS_COMPRESS,
             )
         } else {
             (
                 total_output_lamports.saturating_sub(total_input_lamports),
-                1,
+                IS_DECOMPRESS,
             )
         };

Or if the field type supports it, use a boolean directly.

programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (1)

73-80: New rent parameter for caching optimization.

The &mut None argument is for the optional Rent sysvar cache. Looking at the shared implementation, when None is passed, Rent::get() is called if needed. Since this function may call process_compression_top_up twice (Compress path then potentially another branch), passing &mut None each time means the rent sysvar could be fetched twice.

Consider caching the rent at the function level if both compress and decompress branches with compressible extensions are possible in the same call:

♻️ Potential optimization
 pub fn compress_or_decompress_ctokens(
     inputs: CTokenCompressionInputs,
     transfer_amount: &mut u64,
     lamports_budget: &mut u64,
 ) -> Result<(), ProgramError> {
     // ... existing setup code ...
     
     let mut current_slot = 0;
+    let mut rent_cache: Option<Rent> = None;
     // Calculate new balance using effective amount
     match mode {
         ZCompressionMode::Compress => {
             // ... existing code ...
             if let Some(compression) = ctoken.get_compressible_extension() {
                 process_compression_top_up(
                     &compression.info,
                     token_account_info,
                     &mut current_slot,
                     transfer_amount,
                     lamports_budget,
-                    &mut None,
+                    &mut rent_cache,
                 )?;
             }

However, since only one branch executes per call (Compress XOR Decompress XOR CompressAndClose), this optimization provides no benefit in practice. The current implementation is correct.

programs/compressed-token/program/src/ctoken/mint_to.rs (1)

28-34: Clarify the validation logic for optional payer.

The validation checks accounts.len() < 3 and returns NotEnoughAccountKeys, stating "expected at least 3 accounts". However, line 59 uses accounts.get(2) which returns Option, and the called function calculate_and_execute_compressible_top_ups accepts Option<&AccountInfo> for the payer.

This creates confusion:

  • If the payer (index 2) is truly optional, the validation should be accounts.len() < 2 and the error message should say "expected at least 2 accounts"
  • If the payer is required, then use &accounts[2] directly instead of .get(2)

Looking at the relevant code snippet, calculate_and_execute_compressible_top_ups returns CTokenError::MissingPayer when payer is None, suggesting the payer becomes required only when top-up is needed. However, the current validation requires 3 accounts upfront, making the Option wrapper unnecessary.

The same issue exists in process_ctoken_mint_to_checked at lines 80-86 and 109-112.

Also applies to: 56-59

programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs (1)

140-153: Use full 32-byte program ID comparison instead of first 8 bytes for account ownership validation.

The hardcoded bytes [9, 44, 54, 236, 34, 245, 23, 131] represent a heuristic that compares only the first 8 bytes of the account compression program ID. Solana's official guidance and best practices recommend comparing the full 32-byte program ID, not a prefix. While the collision probability for this heuristic is astronomically small (~1 in 2^64), using a partial match contradicts standard Solana owner validation patterns and makes the code fragile.

Either:

  1. Change the check to compare the full 32-byte program ID, or
  2. If the heuristic is intentional for performance, add a detailed comment explaining: which program this is, why the first 8 bytes are sufficient in this specific context, and document the collision trade-off.

Reference: Per Solana docs, proper ownership checks should use full Pubkey equality or find_program_address() for PDAs.

programs/compressed-token/program/src/lib.rs (1)

220-222: Consider adding discriminator conflict test.

The TODO at line 221 notes the need for a test verifying no discriminator conflicts with Anchor instructions. This is important since the fallback path at line 222 forwards unmatched discriminators to Anchor.

Would you like me to help create an issue to track adding a test that verifies InstructionType discriminators don't conflict with Anchor instruction discriminators?

🤖 Fix all issues with AI agents
In @programs/compressed-token/program/CLAUDE.md:
- Around line 118-139: The fenced code block containing the ASCII directory tree
in CLAUDE.md is missing a language specifier, so update the opening fence for
that block from ``` to ```text (or ```plaintext) to ensure proper rendering of
the tree; locate the block that starts with "src/" and change its opening fence
only, leaving the block contents and closing fence unchanged.
- Around line 141-168: The Markdown headings "Compressed Token Operations
(`compressed_token/`)", "CToken Operations (`ctoken/`)", and "Rent Management"
need blank lines before and after them for consistent formatting; update the
README/CLAUDE.md by inserting an empty line above each of those heading lines
and an empty line after each heading so the subsequent paragraph (e.g.,
"Operations on compressed accounts stored in Merkle trees.") is separated from
the heading.

In @programs/compressed-token/program/docs/CLAUDE.md:
- Around line 38-42: There is no blank line before the "## Navigation Tips"
heading, which violates markdownlint rules; add a single blank line between the
preceding THAW_ACCOUNT.md entry and the "## Navigation Tips" heading so headings
are surrounded by blank lines, ensuring the file's section separator is a single
empty line above the "Navigation Tips" heading to satisfy markdownlint.

In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md:
- Line 73: Fix the inconsistent unordered list indentation in MINT_ACTION.md by
aligning the "- (signer) - required if create_mint is Some or DecompressMint
action present" list item with the surrounding list items (adjust leading spaces
to match the other bullet lines in the file so it is a sibling list item rather
than mis-indented). Ensure the hyphen and text use the same indentation level as
the other bullets in the "MINT_ACTION" section for consistent markdown
rendering.
- Around line 225-227: Add a blank line immediately before the heading "### Spl
mint migration" in MINT_ACTION.md so the heading is separated from the preceding
paragraph and complies with Markdown rules; locate the section where the two
lines "- cmint to spl mint migration..." and "- A way to support it..." follow
the heading and insert a single empty line above "### Spl mint migration".

In @programs/compressed-token/program/docs/ctoken/CREATE.md:
- Around line 89-91: Reword the validation bullet so it avoids adverb repetition
while keeping the same logic and symbols; e.g., clarify that if compression_only
!= 0 and the mint lacks restricted extensions, return
ErrorCode::CompressionOnlyNotAllowed — update the phrase "compression_only is
only set for mints with restricted extensions" to a more concise form like
"compression_only may be set only on mints with restricted extensions" or
"disallow setting compression_only on mints without restricted extensions."

In @programs/compressed-token/program/docs/INSTRUCTIONS.md:
- Line 106: Add a single trailing newline to the end of the INSTRUCTIONS.md file
so it ends with one newline character; place it immediately after the last line
containing "16. **Add Token Pool** - Add additional token pools for a mint (up
to 5 per mint)" and ensure there are no extra blank lines after it.
- Around line 84-89: Documentation inconsistency: INSTRUCTIONS.md lists
MintAction as "9 actions" while CLAUDE.md counts "10 action types" because
CreateCompressedMint is invoked via the create_mint field rather than an Action
enum variant. Update docs to use consistent terminology: either state "9 Action
enum variants + create_mint (CreateCompressedMint)" or change both files to "10
action types" if you want to count create_mint as an action; reference the
Action enum, MintAction, create_mint field, and CreateCompressedMint when
updating the text so both INSTRUCTIONS.md and CLAUDE.md convey the same
breakdown.

In
@programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs:
- Around line 56-69: The function split_mint_and_token_accounts assumes at least
one element and will panic on empty slices; change its signature to return
Result<(_, _), E> (or a suitable error type) and at the top check if
output_compressed_accounts.is_empty() and return Err(descriptive_error) before
calling split_at_mut(1), then update callers to propagate the error;
alternatively, if the caller truly guarantees non-empty input, add a
debug_assert!(!output_compressed_accounts.is_empty(), "expected at least one
output_compressed_account") at the start to make the contract explicit.

In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Line 118: Update the unclear inline comment in check_extensions.rs that
currently reads "- Decompress: no check it restores existing state" to a
grammatically correct version, e.g. "- Decompress: no check that it restores the
existing state" (or "- Decompress: does not check that it restores the existing
state") so the intent is clear; locate the comment near the Decompress
test/section in the file (the line showing "- Decompress: no check it restores
existing state") and replace it with the corrected wording.

In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 92-97: The non-compressible branch currently assumes token_account
is pre-existing and owned by the CToken program but doesn't enforce it; add an
explicit ownership check in the None branch of the create logic: verify
token_account.owner equals the CToken program id (use the same program id
constant/validator used elsewhere in create.rs) and return a clear error if not,
or if you prefer to keep it voluntary, expand the comment on the None branch to
explicitly describe the security model and why ownership is validated later
(referencing token_account, CToken program, and the SPL initialize_account3
contrast).

In @programs/compressed-token/program/src/shared/config_account.rs:
- Around line 17-20: The hardcoded program id passed into check_owner (the
pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes()) should be
extracted to a named constant (suggest LIGHT_REGISTRY_PROGRAM_ID) to improve
readability and maintainability; define LIGHT_REGISTRY_PROGRAM_ID as the
appropriate type (e.g., a Pubkey or raw byte array using the existing pubkey!
macro) near the top of the module and replace the inline pubkey! call in the
check_owner invocation with a reference to that constant (e.g.,
LIGHT_REGISTRY_PROGRAM_ID.to_bytes() or LIGHT_REGISTRY_PROGRAM_ID as needed).

In @programs/compressed-token/program/src/shared/mint_to_token_pool.rs:
- Line 10: Summary: Centralize error conversion by using convert_program_error
in mint_to_compressed processors and update the doc comment; remove any
now-unused imports. Replace prior ad-hoc error mapping in the mint_to_token_pool
logic with a call to convert_program_error (ensure convert_program_error is
imported and invoked where previous manual conversions occurred), update the doc
comment above the processor to state it uses convert_program_error for
mint_to_compressed processors, and if LIGHT_CPI_SIGNER is no longer used after
this change remove that import to avoid an unused-import warning; apply the same
replacement to the other occurrence of the manual conversion in this file so
both locations use convert_program_error.

In @programs/compressed-token/program/tests/mint_validation.rs:
- Around line 322-358: The test currently executes each case twice (first loop
calls is_valid_mint for reporting, then run_test_case runs them again);
consolidate by running each case only once: in the first loop call
run_test_case(tc) or call is_valid_mint(tc.owner.pubkey(), &tc.data.to_bytes())
and capture the Result alongside tc (e.g., a Vec of (tc, result)), use that
captured result for printing and for assertions so you can remove the second
loop; update references to is_valid_mint, run_test_case, test_cases, and
Expected to use the single collected outcome and fail the test if any captured
result indicates failure.

Comment on lines +118 to +139
```
src/
├── compressed_token/ # Operations on compressed accounts (in Merkle trees)
│ ├── mint_action/ # MintAction instruction (103)
│ └── transfer2/ # Transfer2 instruction (101)
├── compressible/ # Rent management
│ ├── claim.rs # Claim instruction (104)
│ └── withdraw_funding_pool.rs # WithdrawFundingPool instruction (105)
├── ctoken/ # Operations on CToken Solana accounts (decompressed)
│ ├── approve_revoke.rs # CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13)
│ ├── burn.rs # CTokenBurn (8), CTokenBurnChecked (15)
│ ├── close/ # CloseTokenAccount instruction (9)
│ ├── create.rs # CreateTokenAccount instruction (18)
│ ├── create_ata.rs # CreateAssociatedCTokenAccount (100, 102)
│ ├── freeze_thaw.rs # CTokenFreezeAccount (10), CTokenThawAccount (11)
│ ├── mint_to.rs # CTokenMintTo (7), CTokenMintToChecked (14)
│ └── transfer/ # CTokenTransfer (3), CTokenTransferChecked (12)
├── extensions/ # Extension handling
├── shared/ # Common utilities
├── convert_account_infos.rs
└── lib.rs # Entry point and instruction dispatch
```
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

Add language specifier to fenced code block.

The directory structure block should have a language specifier for proper rendering. Use text or plaintext for ASCII tree structures.

Proposed fix
-```
+```text
 src/
 ├── compressed_token/       # Operations on compressed accounts (in Merkle trees)
📝 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
```
src/
├── compressed_token/ # Operations on compressed accounts (in Merkle trees)
│ ├── mint_action/ # MintAction instruction (103)
│ └── transfer2/ # Transfer2 instruction (101)
├── compressible/ # Rent management
│ ├── claim.rs # Claim instruction (104)
│ └── withdraw_funding_pool.rs # WithdrawFundingPool instruction (105)
├── ctoken/ # Operations on CToken Solana accounts (decompressed)
│ ├── approve_revoke.rs # CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13)
│ ├── burn.rs # CTokenBurn (8), CTokenBurnChecked (15)
│ ├── close/ # CloseTokenAccount instruction (9)
│ ├── create.rs # CreateTokenAccount instruction (18)
│ ├── create_ata.rs # CreateAssociatedCTokenAccount (100, 102)
│ ├── freeze_thaw.rs # CTokenFreezeAccount (10), CTokenThawAccount (11)
│ ├── mint_to.rs # CTokenMintTo (7), CTokenMintToChecked (14)
│ └── transfer/ # CTokenTransfer (3), CTokenTransferChecked (12)
├── extensions/ # Extension handling
├── shared/ # Common utilities
├── convert_account_infos.rs
└── lib.rs # Entry point and instruction dispatch
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

118-118: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @programs/compressed-token/program/CLAUDE.md around lines 118 - 139, The
fenced code block containing the ASCII directory tree in CLAUDE.md is missing a
language specifier, so update the opening fence for that block from ``` to
```text (or ```plaintext) to ensure proper rendering of the tree; locate the
block that starts with "src/" and change its opening fence only, leaving the
block contents and closing fence unchanged.

Comment on lines +141 to +168
## Compressed Token Operations (`compressed_token/`)
Operations on compressed accounts stored in Merkle trees.

- **`mint_action/`** - MintAction instruction for compressed mint management
- `processor.rs` - Main instruction processor
- `accounts.rs` - Account validation and parsing
- `actions/` - Individual action handlers (create_mint, mint_to, decompress_mint, etc.)
- **`transfer2/`** - Unified transfer instruction supporting multiple modes
- `compression/` - Compress & decompress functionality
- `ctoken/` - CToken-specific compression (compress_and_close.rs, decompress.rs, etc.)
- `spl.rs` - SPL token compression
- `processor.rs` - Main instruction processor
- `accounts.rs` - Account validation and parsing
- **`mint_action/`** - Mint tokens to compressed/decompressed accounts
- **`ctoken_approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13)
- **`ctoken_mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14)
- **`ctoken_burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15)
- **`ctoken_freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11)

## Rent Management
## CToken Operations (`ctoken/`)
Operations on CToken Solana accounts (decompressed compressed tokens).

- **`create.rs`** - Create regular ctoken accounts with optional compressible extension
- **`create_ata.rs`** - Create deterministic ATA accounts
- **`close/`** - Close ctoken accounts, handle rent distribution
- **`transfer/`** - SPL-compatible transfers between decompressed accounts
- `default.rs` - CTokenTransfer (discriminator: 3)
- `checked.rs` - CTokenTransferChecked (discriminator: 12)
- `shared.rs` - Common transfer utilities
- **`approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13)
- **`mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14)
- **`burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15)
- **`freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11)
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

Minor: Add blank lines around headings for consistent markdown formatting.

Per markdown best practices, headings should be surrounded by blank lines. The headings at "Compressed Token Operations", "CToken Operations", and "Rent Management" are missing the preceding blank line.

Proposed fix (example for line 141)

Compressed Token Operations (compressed_token/)

-Operations on compressed accounts stored in Merkle trees.
+
+Operations on compressed accounts stored in Merkle trees.

</details>


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

141-141: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

155-155: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @programs/compressed-token/program/CLAUDE.md around lines 141 - 168, The
Markdown headings "Compressed Token Operations (compressed_token/)", "CToken
Operations (ctoken/)", and "Rent Management" need blank lines before and after
them for consistent formatting; update the README/CLAUDE.md by inserting an
empty line above each of those heading lines and an empty line after each
heading so the subsequent paragraph (e.g., "Operations on compressed accounts
stored in Merkle trees.") is separated from the heading.


</details>

<!-- fingerprinting:phantom:medusa:ocelot -->

<!-- This is an auto-generated comment by CodeRabbit -->

Optional accounts (based on configuration):
2. mint_signer
- (signer) - required if create_mint is Some or CreateSplMint action present
- (signer) - required if create_mint is Some or DecompressMint action present
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

Minor markdown formatting issue.

Static analysis flagged unordered list indentation at this line. Consider adjusting indentation for consistency with the rest of the document.

🔧 Suggested fix
 2. mint_signer
-   - (signer) - required if create_mint is Some or DecompressMint action present
+- (signer) - required if create_mint is Some or DecompressMint action present
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

73-73: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)

🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md at
line 73, Fix the inconsistent unordered list indentation in MINT_ACTION.md by
aligning the "- (signer) - required if create_mint is Some or DecompressMint
action present" list item with the surrounding list items (adjust leading spaces
to match the other bullet lines in the file so it is a sibling list item rather
than mis-indented). Ensure the hyphen and text use the same indentation level as
the other bullets in the "MINT_ACTION" section for consistent markdown
rendering.

// Compress/Decompress don't need additional validation here:
// - Compress: blocked by check_mint_extensions when outputs exist
// - Decompress: bypassed (restoring existing state)
// - Decompress: no check it restores existing state
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

Minor comment grammar issue.

The comment reads "no check it restores existing state" which is slightly unclear.

📝 Suggested fix
-                // - Decompress: no check it restores existing state
+                // - Decompress: no check - it restores existing state
📝 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
// - Decompress: no check it restores existing state
// - Decompress: no check - it restores existing state
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
at line 118, Update the unclear inline comment in check_extensions.rs that
currently reads "- Decompress: no check it restores existing state" to a
grammatically correct version, e.g. "- Decompress: no check that it restores the
existing state" (or "- Decompress: does not check that it restores the existing
state") so the intent is clear; locate the comment near the Decompress
test/section in the file (the line showing "- Decompress: no check it restores
existing state") and replace it with the corrected wording.

Comment on lines +17 to +20
check_owner(
&pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
config_account,
)?;
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

Consider extracting the hardcoded pubkey to a named constant.

The Light registry program ID Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX appears inline. Extracting it to a named constant (e.g., LIGHT_REGISTRY_PROGRAM_ID) would improve readability and make it easier to update if needed.

💡 Suggested improvement
+const LIGHT_REGISTRY_PROGRAM_ID: [u8; 32] = 
+    pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes();
+
 #[profile]
 #[inline(always)]
 pub fn parse_config_account(
     config_account: &AccountInfo,
 ) -> Result<&CompressibleConfig, ProgramError> {
     // Validate config account owner
     check_owner(
-        &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
+        &LIGHT_REGISTRY_PROGRAM_ID,
         config_account,
     )?;
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/config_account.rs around lines
17 - 20, The hardcoded program id passed into check_owner (the
pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes()) should be
extracted to a named constant (suggest LIGHT_REGISTRY_PROGRAM_ID) to improve
readability and maintainability; define LIGHT_REGISTRY_PROGRAM_ID as the
appropriate type (e.g., a Pubkey or raw byte array using the existing pubkey!
macro) near the top of the module and replace the inline pubkey! call in the
check_owner invocation with a reference to that constant (e.g.,
LIGHT_REGISTRY_PROGRAM_ID.to_bytes() or LIGHT_REGISTRY_PROGRAM_ID as needed).

Comment on lines +29 to +55
/// Internal: Map SPL Token error code (0-18) to ErrorCode.
fn convert_spl_token_error_code(code: u32) -> anchor_lang::prelude::ProgramError {
let error_code = match code {
0 => ErrorCode::NotRentExempt,
1 => ErrorCode::InsufficientFunds,
2 => ErrorCode::InvalidMint,
3 => ErrorCode::MintMismatch,
4 => ErrorCode::OwnerMismatch,
5 => ErrorCode::FixedSupply,
6 => ErrorCode::AlreadyInUse,
7 => ErrorCode::InvalidNumberOfProvidedSigners,
8 => ErrorCode::InvalidNumberOfRequiredSigners,
9 => ErrorCode::UninitializedState,
10 => ErrorCode::NativeNotSupported,
11 => ErrorCode::NonNativeHasBalance,
12 => ErrorCode::InvalidInstruction,
13 => ErrorCode::InvalidState,
14 => ErrorCode::Overflow,
15 => ErrorCode::AuthorityTypeNotSupported,
16 => ErrorCode::MintHasNoFreezeAuthority,
17 => ErrorCode::AccountFrozen,
18 => ErrorCode::MintDecimalsMismatch,
// Pass through unknown/higher codes with standard +6900 offset
_ => return anchor_lang::prelude::ProgramError::Custom(code + 6900),
};
error_code.into()
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and examine the ErrorCode enum definition
echo "=== Looking for ErrorCode enum in programs/compressed-token/anchor/src/lib.rs ==="
cat programs/compressed-token/anchor/src/lib.rs | head -200

echo ""
echo "=== Searching for ErrorCode enum definition ==="
rg -A 100 "^pub enum ErrorCode" programs/compressed-token/anchor/src/lib.rs

Repository: Lightprotocol/light-protocol

Length of output: 303


🌐 Web query:

Light Protocol anchor_compressed_token ErrorCode enum definition github

💡 Result:

Found the enum definition (from the Light Protocol compressed-token crate). Variants with their repr(u32) values:

PublicKeyAmountMissmatch = 0
ComputeInputSumFailed = 1
ComputeOutputSumFailed = 2
ComputeCompressSumFailed = 3
ComputeDecompressSumFailed = 4
SumCheckFailed = 5
DecompressRecipientUndefinedForDecompress = 6
CompressedPdaUndefinedForDecompress = 7
DeCompressAmountUndefinedForDecompress = 8
CompressedPdaUndefinedForCompress = 9
DeCompressAmountUndefinedForCompress = 10
DelegateSignerCheckFailed = 11
MintTooLarge = 12
SplTokenSupplyMismatch = 13
HeapMemoryCheckFailed = 14
InstructionNotCallable = 15
ArithmeticUnderflow = 16
HashToFieldError = 17
InvalidAuthorityMint = 18
InvalidFreezeAuthority = 19
InvalidDelegateIndex = 20
TokenPoolPdaUndefined = 21
IsTokenPoolPda = 22
InvalidTokenPoolPda = 23
NoInputTokenAccountsProvided = 24
NoInputsProvided = 25
MintHasNoFreezeAuthority = 26
MintWithInvalidExtension = 27
InsufficientTokenAccountBalance = 28
InvalidTokenPoolBump = 29
FailedToDecompress = 30
FailedToBurnSplTokensFromTokenPool = 31
NoMatchingBumpFound = 32
NoAmount = 33
AmountsAndAmountProvided = 34

Source: docs for the light_compressed_token crate (enum ErrorCode). [1]
Repository: Light Protocol GitHub. [2]

References:
[1] docs.rs — light_compressed_token enum ErrorCode.
[2] Lightprotocol / light-protocol GitHub.


Fix non-existent ErrorCode variant references causing compilation failure.

The convert_spl_token_error_code function references ErrorCode variants (NotRentExempt, InsufficientFunds, InvalidMint, MintMismatch, OwnerMismatch, FixedSupply, AlreadyInUse, InvalidNumberOfProvidedSigners, InvalidNumberOfRequiredSigners, UninitializedState, NativeNotSupported, NonNativeHasBalance, InvalidInstruction, InvalidState, Overflow, AuthorityTypeNotSupported, MintHasNoFreezeAuthority, AccountFrozen, MintDecimalsMismatch) that do not exist in the anchor_compressed_token::ErrorCode enum. The actual enum contains completely different variants specific to the compressed-token program (PublicKeyAmountMissmatch, ComputeInputSumFailed, ComputeOutputSumFailed, etc. with codes 0-34+). This mapping logic cannot compile. Either map to existing ErrorCode variants or clarify the intent of this SPL Token error conversion.

};

use crate::LIGHT_CPI_SIGNER;
use crate::{shared::convert_program_error, LIGHT_CPI_SIGNER};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Good refactoring to centralize error conversion.

Using the shared convert_program_error utility improves consistency across the codebase. The doc comment update at line 13 correctly reflects that this is now used specifically by mint_to_compressed processors.

Also applies to: 58-58

🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/mint_to_token_pool.rs at line
10, Summary: Centralize error conversion by using convert_program_error in
mint_to_compressed processors and update the doc comment; remove any now-unused
imports. Replace prior ad-hoc error mapping in the mint_to_token_pool logic with
a call to convert_program_error (ensure convert_program_error is imported and
invoked where previous manual conversions occurred), update the doc comment
above the processor to state it uses convert_program_error for
mint_to_compressed processors, and if LIGHT_CPI_SIGNER is no longer used after
this change remove that import to avoid an unused-import warning; apply the same
replacement to the other occurrence of the manual conversion in this file so
both locations use convert_program_error.

Comment on lines +322 to +358
println!(
"\nRunning {} test cases for is_valid_mint:\n",
test_cases.len()
);

let mut passed = 0;
let mut failed = 0;

for tc in &test_cases {
print!(" {:60} ... ", tc.description);
let data = tc.data.to_bytes();
let result = is_valid_mint(tc.owner.pubkey(), &data);

let success = match tc.expected {
Expected::Valid => result.as_ref().map(|v| *v).unwrap_or(false),
Expected::Invalid => result.as_ref().map(|v| !*v).unwrap_or(false),
Expected::IncorrectProgramId => {
result.as_ref().err() == Some(&ProgramError::IncorrectProgramId)
}
};

if success {
println!("ok");
passed += 1;
} else {
println!("FAILED (got {:?})", result);
failed += 1;
}
}

println!("\nResults: {} passed, {} failed\n", passed, failed);

// Now run assertions to fail the test if any failed
for tc in &test_cases {
run_test_case(tc);
}
}
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

Minor optimization: Consider removing duplicate test execution.

The test iterates through test cases twice - once for status reporting (lines 330-350) and again for assertions (lines 355-357). While this provides nice output, it doubles execution time unnecessarily.

♻️ Suggested consolidation
-    for tc in &test_cases {
-        print!("  {:60} ... ", tc.description);
-        let data = tc.data.to_bytes();
-        let result = is_valid_mint(tc.owner.pubkey(), &data);
-
-        let success = match tc.expected {
-            Expected::Valid => result.as_ref().map(|v| *v).unwrap_or(false),
-            Expected::Invalid => result.as_ref().map(|v| !*v).unwrap_or(false),
-            Expected::IncorrectProgramId => {
-                result.as_ref().err() == Some(&ProgramError::IncorrectProgramId)
-            }
-        };
-
-        if success {
-            println!("ok");
-            passed += 1;
-        } else {
-            println!("FAILED (got {:?})", result);
-            failed += 1;
-        }
-    }
-
-    println!("\nResults: {} passed, {} failed\n", passed, failed);
-
-    // Now run assertions to fail the test if any failed
-    for tc in &test_cases {
-        run_test_case(tc);
-    }
+    for tc in &test_cases {
+        print!("  {:60} ... ", tc.description);
+        // Use std::panic::catch_unwind or check inline
+        let result = std::panic::catch_unwind(|| run_test_case(tc));
+        if result.is_ok() {
+            println!("ok");
+            passed += 1;
+        } else {
+            println!("FAILED");
+            failed += 1;
+        }
+    }
+
+    println!("\nResults: {} passed, {} failed\n", passed, failed);
+    assert_eq!(failed, 0, "Some test cases failed");
🤖 Prompt for AI Agents
In @programs/compressed-token/program/tests/mint_validation.rs around lines 322
- 358, The test currently executes each case twice (first loop calls
is_valid_mint for reporting, then run_test_case runs them again); consolidate by
running each case only once: in the first loop call run_test_case(tc) or call
is_valid_mint(tc.owner.pubkey(), &tc.data.to_bytes()) and capture the Result
alongside tc (e.g., a Vec of (tc, result)), use that captured result for
printing and for assertions so you can remove the second loop; update references
to is_valid_mint, run_test_case, test_cases, and Expected to use the single
collected outcome and fail the test if any captured result indicates failure.

ananas-block and others added 2 commits January 8, 2026 00:06
- Move compressedAddress from top-level to mint.metadata
- Remove tokenPoolBump and tokenPoolIndex from instruction data
- Update all instruction builders and tests accordingly
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs (1)

118-145: Replace panic with proper error or remove unreachable code.

After a successful cache.insert() on line 115 (which returns early on failure), the mint should always be found in the cache on line 118. The else branch on lines 141-144 appears unreachable, yet contains a panic with a TODO comment indicating uncertainty.

Two issues:

  1. Panic is inappropriate: If this code path is somehow reachable, it should return a proper ProgramError instead of panicking
  2. Dead code: If the branch is truly unreachable, it should be removed or replaced with an assertion in debug builds
🔧 Suggested fix

If the branch is unreachable, remove it:

-            if let Some(checks) = cache.get_by_key(&mint_index) {
+            let checks = cache
+                .get_by_key(&mint_index)
+                .expect("mint must exist in cache after successful insert");
                 // CompressAndClose with restricted extensions requires CompressedOnly output.
                 // Compress/Decompress don't need additional validation here:
                 // - Compress: blocked by check_mint_extensions when outputs exist
-                // - Decompress: no check it restores existing state
+                // - Decompress: no check, it restores existing state
                 if checks.has_restricted_extensions && compression.mode.is_compress_and_close() {
                     let output_idx = compression.get_compressed_token_account_index()?;
                     let has_compressed_only = inputs
                         .out_tlv
                         .as_ref()
                         .and_then(|tlvs| tlvs.get(output_idx as usize))
                         .map(|tlv| {
                             tlv.iter()
                                 .any(|e| matches!(e, ZExtensionInstructionData::CompressedOnly(_)))
                         })
                         .unwrap_or(false);
                     if !has_compressed_only {
                         msg!("Mint has restricted extensions - CompressedOnly output required");
                         return Err(
                             ErrorCode::CompressAndCloseMissingCompressedOnlyExtension.into()
                         );
                     }
                 }
-            } else {
-                // TODO: double check.
-                panic!("Mint cache: compression: mint index not found");
-            }

Or if there's genuine uncertainty, return an error:

             } else {
-                // TODO: double check.
-                panic!("Mint cache: compression: mint index not found");
+                msg!("Mint cache: compression: mint index not found after insert");
+                return Err(ErrorCode::MintCacheCapacityExceeded.into());
             }

Based on coding guidelines requiring proper error handling in Solana programs.

programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs (1)

43-47: TODO flagged: Idempotent behavior needs test coverage.

The TODO on line 43 indicates missing test coverage for the idempotent exit path. This is a valid concern—silent success without observable side effects can mask bugs if not properly tested.

Would you like me to open an issue to track adding a test case that verifies the instruction exits silently when is_idempotent() is true and cmint_decompressed is false?

programs/compressed-token/program/src/ctoken/close/processor.rs (1)

279-287: Clarify the magic number in error conversion.

The + 6000 offset for the error code lacks documentation. While I understand this likely aligns with Anchor's custom error offset convention, a brief comment would help future maintainers understand why this specific value is used.

📝 Suggested documentation
 fn finalize_account_closure(accounts: &CloseTokenAccountAccounts<'_>) -> Result<(), ProgramError> {
     unsafe {
         accounts.token_account.assign(&[0u8; 32]);
     }
     match accounts.token_account.resize(0) {
         Ok(()) => Ok(()),
-        Err(e) => Err(ProgramError::Custom(u64::from(e) as u32 + 6000)),
+        // 6000 offset aligns with Anchor's custom error code base
+        Err(e) => Err(ProgramError::Custom(u64::from(e) as u32 + 6000)),
     }
 }
🤖 Fix all issues with AI agents
In @program-libs/compressible/src/rent/config.rs:
- Around line 9-12: The rent constants in compressible::rent::config are 10x too
low; update the constant values so BASE_RENT = 1_220 (u16), RENT_PER_BYTE = 10
(u8), and SLOTS_PER_EPOCH = 432_000 (u64) in the file where those symbols are
defined (BASE_RENT, RENT_PER_BYTE, SLOTS_PER_EPOCH), and remove or update the
"TODO: multiply by 10" comments and any misleading epoch-duration comment to
reflect the corrected values.

In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs:
- Line 86: Implement the TODO in amount_from_account_info: first validate the
AccountInfo data length and check the account_type byte (offset 165 when token
extensions are present) equals ACCOUNT_TYPE_TOKEN_ACCOUNT before calling
amount_from_slice; if the data is too short or the account_type mismatches,
return an appropriate error or None (matching the function's error convention)
instead of extracting the amount. Use the existing symbols
amount_from_account_info, amount_from_slice, and ACCOUNT_TYPE_TOKEN_ACCOUNT to
locate where to add the checks and ensure you handle both missing extensions
(insufficient length) and wrong account_type cases consistently with surrounding
error handling.
- Around line 83-85: The borrow failure from account_info.try_borrow_data() is
being mapped to ZeroCopyError::Size which is incorrect; change the error mapping
to a dedicated borrow-failure variant instead of Size — either add
ZeroCopyError::BorrowFailed to the ZeroCopyError enum (mirroring patterns like
AccountError::BorrowAccountDataFailed) and return that on map_err, or
propagate/convert the error to the existing CTokenError::CMintBorrowFailed (as
used elsewhere, e.g., compressed_mint.rs) so try_borrow_data() failures are
represented semantically rather than as Size.

In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs:
- Around line 188-190: The deserializer currently accepts any account_type byte
(bytes[0]) and skips validating it against ACCOUNT_TYPE_TOKEN_ACCOUNT; change
the logic in zero_copy.rs so that when bytes is not empty and bytes[0] !=
ACCOUNT_TYPE_TOKEN_ACCOUNT you return a clear error (add or use an
InvalidAccountType variant on ZeroCopyError if missing) instead of proceeding,
e.g., validate account_type before using it and return
ZeroCopyError::InvalidAccountType with context when the check fails to prevent
type confusion and layout misinterpretation.
- Around line 229-231: The mutable zero-copy deserialization path reads
account_type from bytes[0] but does not validate it (same missing check as the
immutable path), so add the same account_type validation: in the mutable
deserializer (the function performing zero-copy mutable deserialization), check
that bytes is non-empty and that bytes[0] equals ACCOUNT_TYPE_TOKEN_ACCOUNT and
return the appropriate error (or result::Err) if it does not, before proceeding
to construct the mutable view; reference the bytes[0] read and the
ACCOUNT_TYPE_TOKEN_ACCOUNT constant and mirror the validation logic used in the
immutable deserialization path.
- Line 113: new_zero_copy is missing a check that prevents overwriting an
already-initialized account; before writing the base struct, read the state byte
at offset 108 from the account data and return a
ZeroCopyError::AlreadyInitialized if that byte != 0. Add an AlreadyInitialized
variant to the ZeroCopyError enum (if absent) and use it in new_zero_copy to
abort initialization when the state byte indicates prior initialization,
ensuring you perform the read using the same account data slice used later for
writing.
- Around line 161-166: Remove the misleading TODO that says "remaining bytes
should be checked to be zero" and replace it with a short clarifying comment
indicating that the code enforces exact buffer sizing by requiring remaining to
be empty; update the comment near the ACCOUNT_TYPE_TOKEN_ACCOUNT branch and the
`if !remaining.is_empty() { return
Err(light_zero_copy::errors::ZeroCopyError::Size); }` check to state that any
extra bytes are invalid and must not exist rather than being zero-checked.

In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the core::panic import and the panic!() call in
check_extensions.rs and replace it with proper error propagation: return a
Result::Err containing an appropriate Solana program error (e.g.,
Err(ProgramError::InvalidArgument) or a domain-specific error variant such as
CompressedTokenError::InvalidExtension converted via Into<ProgramError>), and
optionally log context with msg!("...") before returning; ensure the function
signature returns a Result so callers can handle the error instead of the
program aborting.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs:
- Around line 119-124: The TODO asking to double-check the decompress check
should be resolved: confirm and keep the validation that when
compression.mode.is_decompress() and
compression_to_input[compression_index].is_some() the code returns
ErrorCode::CompressedOnlyRequiresCTokenDecompress (i.e., CompressedOnly inputs
must decompress to CToken accounts), remove the inline TODO comment from the if
condition in mod.rs, and add a unit test that constructs a CompressedOnly input
where compression.mode.is_decompress() is true and
compression_to_input[compression_index] is Some(...) to assert the function
returns the CompressedOnlyRequiresCTokenDecompress error; reference the
compression.mode.is_decompress() call, compression_to_input array access,
CompressedOnly concept, and ErrorCode::CompressedOnlyRequiresCTokenDecompress
when locating code to change and tests to add.

In
@programs/compressed-token/program/src/compressed_token/transfer2/processor.rs:
- Around line 143-147: The version check currently verifies all
inputs.out_token_data have version == 3 but returns the wrong error; update the
branch that returns CTokenError::CompressedTokenAccountTlvUnimplemented to
instead return ErrorCode::TlvRequiresVersion3 (mirror the behavior in
check_extensions.rs) so that failing the version validation for TLV outputs uses
the correct error variant; leave the allowed calculation
(inputs.out_token_data.iter().all(|c| c.version == 3)) intact and only change
the returned error value.

In @programs/compressed-token/program/src/ctoken/close/processor.rs:
- Around line 85-87: The guard checking accounts.token_account.key() ==
accounts.destination.key() inside the COMPRESS_AND_CLOSE branch is redundant
because the same check (token_account == destination) is already performed
before branching; remove the duplicate condition and its
Err(ProgramError::InvalidAccountData) return from the COMPRESS_AND_CLOSE
handling (i.e., delete the if checking accounts.token_account.key() ==
accounts.destination.key() and its error path) so the code relies on the earlier
validation.
- Line 195: The code uses light_account_checks::check_owner for validating
LIGHT_CPI_SIGNER.owner; replace this with the shared utility
check_token_program_owner() from shared/owner_validation.rs (update the import
and call in processor.rs and the matching usage in accounts.rs) so owner
validation follows the centralized pattern; if you need stricter
program-id-specific checks for LIGHT_CPI_SIGNER, implement a small wrapper
function in shared/owner_validation.rs (e.g., check_light_cpi_signer_owner())
that performs the extra program_id comparison and use that wrapper instead of
calling light_account_checks::check_owner directly.

In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 91-96: Before calling CToken::new_zero_copy in
initialize_ctoken_account, verify the target account is uninitialized: read the
zero-copy CToken account header (or use the existing is_initialized() helper)
and ensure state == Uninitialized (0); if it is already initialized return an
appropriate error instead of proceeding. Apply this check in both the
compressible-path branch and the non-compressible-path branch (the code that
currently calls CToken::new_zero_copy without validation), so you never call
new_zero_copy on an initialized account.

In @programs/compressed-token/program/src/shared/compressible_top_up.rs:
- Line 13: The import ordering is wrong for the symbol LIGHT_CPI_SIGNER; move
the line `use crate::LIGHT_CPI_SIGNER;` to the crate-level import group (after
external crate imports and before super:: or self:: imports) so it follows the
project's import order convention and removes the lint warning; ensure it is
grouped with other `crate::` imports if present and adjust surrounding use
statements accordingly.
- Line 71: The inline TODO comment "// TODO: add from account info" next to the
check_owner(&LIGHT_CPI_SIGNER.program_id, ctoken)? call is ambiguous; either
remove it if there's no planned change, or replace it with a clear note
describing the intended work (e.g., "TODO: extract owner validation into
from_account_info-based constructor to accept AccountInfo instead of program_id
and ctoken"). Update the comment to reference the exact target (check_owner,
LIGHT_CPI_SIGNER, and ctoken) and the expected API change or timeline, or simply
delete the comment if it's stale.

Comment on lines 83 to 85
let data = account_info
.try_borrow_data()
.map_err(|_| ZeroCopyError::Size)?;
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the ZeroCopyError definition
find . -type f -name "*.rs" | xargs grep -l "enum ZeroCopyError" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 110


🏁 Script executed:

# Search for the specific file mentioned in the review
find . -type f -path "*/ctoken-interface/src/state/ctoken/ctoken_struct.rs"

Repository: Lightprotocol/light-protocol

Length of output: 137


🏁 Script executed:

# Find error definitions in the zero-copy module
find . -type f -path "*zero-copy*" -name "*.rs" | grep -E "(error|lib)" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 602


🏁 Script executed:

# Examine the ZeroCopyError enum definition
cat -n ./program-libs/zero-copy/src/errors.rs

Repository: Lightprotocol/light-protocol

Length of output: 5775


🏁 Script executed:

# Examine the specific code in ctoken_struct.rs around lines 83-85
cat -n ./program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs | head -120 | tail -50

Repository: Lightprotocol/light-protocol

Length of output: 1847


🏁 Script executed:

# Check if there are other usages of try_borrow_data in the codebase and how errors are handled
rg "try_borrow_data\(\)" -A 2 -B 2 program-libs/ | head -100

Repository: Lightprotocol/light-protocol

Length of output: 4238


🏁 Script executed:

# Check if CTokenError exists and what it looks like
find . -type f -name "*.rs" | xargs grep -l "enum CTokenError\|struct CTokenError" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 116


🏁 Script executed:

# Check the ctoken-interface module structure
find ./program-libs/ctoken-interface -type f -name "*.rs" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 1551


🏁 Script executed:

# Look for error handling patterns in ctoken-interface
find ./program-libs/ctoken-interface -type f -name "*.rs" | xargs grep -l "Error\|error" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 754


🏁 Script executed:

# Check the CTokenError definition
cat -n ./program-libs/ctoken-interface/src/error.rs

Repository: Lightprotocol/light-protocol

Length of output: 12351


Map borrow failures to a dedicated error variant, not Size.

The ZeroCopyError::Size variant (error code 15010) semantically represents data that's malformed or too small—not access failures. Mapping BorrowError from try_borrow_data() to Size conflates two distinct failure modes: the data cannot be accessed (already borrowed mutably elsewhere) versus the data structure is invalid. This masks the true cause during debugging.

The codebase establishes a clear pattern: dedicated borrow failure variants exist elsewhere (CTokenError::CMintBorrowFailed, AccountError::BorrowAccountDataFailed). Add a BorrowFailed variant to ZeroCopyError (consistent with the error hierarchy in this crate), or return CTokenError which already has CMintBorrowFailed (error code 18046) for exactly this scenario. See how compressed_mint.rs handles the same operation correctly.

🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs around lines
83 - 85, The borrow failure from account_info.try_borrow_data() is being mapped
to ZeroCopyError::Size which is incorrect; change the error mapping to a
dedicated borrow-failure variant instead of Size — either add
ZeroCopyError::BorrowFailed to the ZeroCopyError enum (mirroring patterns like
AccountError::BorrowAccountDataFailed) and return that on map_err, or
propagate/convert the error to the existing CTokenError::CMintBorrowFailed (as
used elsewhere, e.g., compressed_mint.rs) so try_borrow_data() failures are
represented semantically rather than as Size.

bytes: &'a mut [u8],
config: Self::ZeroCopyConfig,
) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> {
// TODO: check that this function fails if the account is already initialized
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 | 🔴 Critical

Implement the initialization check to prevent data corruption.

The TODO indicates a critical missing validation. Without checking if the account is already initialized, new_zero_copy could overwrite an existing account's data, leading to state corruption or loss of funds.

🔒 Suggested implementation

Before writing the base struct, verify the account is uninitialized by checking that the state byte (at offset 108) is zero:

 fn new_zero_copy(
     bytes: &'a mut [u8],
     config: Self::ZeroCopyConfig,
 ) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> {
-    // TODO: check that this function fails if the account is already initialized
+    // Check account is uninitialized (state byte at offset 108 must be 0)
+    if bytes.len() > 108 && bytes[108] != 0 {
+        return Err(light_zero_copy::errors::ZeroCopyError::AlreadyInitialized);
+    }
     // Use derived new_zero_copy for base struct (config type is () for fixed-size struct)

Note: You may need to add AlreadyInitialized variant to ZeroCopyError if it doesn't exist.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs at line 113,
new_zero_copy is missing a check that prevents overwriting an
already-initialized account; before writing the base struct, read the state byte
at offset 108 from the account data and return a
ZeroCopyError::AlreadyInitialized if that byte != 0. Add an AlreadyInitialized
variant to the ZeroCopyError enum (if absent) and use it in new_zero_copy to
abort initialization when the state byte indicates prior initialization,
ensuring you perform the read using the same account data slice used later for
writing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
programs/compressed-token/anchor/src/process_transfer.rs (1)

645-652: Clarify the discriminator override comment to reflect version-specific behavior.

The comment "We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version" is imprecise. The discriminator is only overridden for ShaFlat and V2 versions (lines 307–320). For V1 (the default path when version is None), the discriminator set here is not overridden—it's used as-is.

Suggested improvement:

-            discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version
+            discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // V1 default; overridden for ShaFlat/V2 in add_data_hash_to_input_compressed_accounts_with_version

This clarification removes ambiguity for future maintainers and aligns with best practices for documenting discriminator usage.

programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs (1)

255-263: Normalize frozen state comparison to boolean values.

The direct comparison ctoken.state != compression_only_extension.is_frozen compares incompatible type encodings. ctoken.state is an AccountState enum (0, 1, or 2), where 2 represents frozen. is_frozen is a bool where 1 (true) represents frozen. When an account is frozen, the comparison 2 != 1 incorrectly triggers a mismatch error despite both values representing the same frozen state.

Normalize both values to boolean before comparison:

Fix
let ctoken_frozen = ctoken.state == 2;
let ext_frozen = compression_only_extension.is_frozen;
if ctoken_frozen != ext_frozen {
    msg!(
        "is_frozen mismatch: ctoken {} != extension {}",
        ctoken_frozen,
        ext_frozen
    );
    return Err(ErrorCode::CompressAndCloseFrozenMismatch.into());
}
programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs (1)

54-61: Add bounds validation for compression_index before array access.

The TODO on line 56 correctly identifies a critical issue. Since compression_index is a u8 provided by untrusted instruction data, any value ≥ 32 will panic when accessing the array on line 58. Add validation before the duplicate check:

                if let ZExtensionInstructionData::CompressedOnly(co) = ext {
                    let idx = co.compression_index as usize;
-                   // TODO check that it is not out of bounds
+                   if idx >= MAX_COMPRESSIONS {
+                       return Err(CTokenError::InvalidCompressionIndex.into());
+                   }
                    // Check uniqueness - error if compression_index already used
                    if compression_to_input[idx].is_some() {
                        return Err(CTokenError::DuplicateCompressionIndex.into());
                    }
                    compression_to_input[idx] = Some(i as u8);

Add InvalidCompressionIndex to CTokenError in program-libs/ctoken-interface/src/error.rs with the next error code (18062).

programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs (1)

36-115: Critical: Variables used outside their defining scope cause compilation failure.

The block from lines 36-95 defines input_owner_bytes, derived_ata, and wallet_owner_bytes, but lines 97-115 reference these variables after the block closes at line 95. This is the root cause of the pipeline failures you're seeing.

Looking at the logic, the checks at lines 97-104 are also redundant—lines 75-82 already verify derived_ata == destination_account.key(), and lines 87-94 verify input_owner_bytes == derived_ata. By transitivity, lines 97-104 add no new validation.

Similarly, line 108's check using wallet_owner_bytes duplicates what should be verified within the block.

🔧 Proposed fix: Remove dead code outside the block
             if !pubkey_eq(&input_owner_bytes, &derived_ata) {
                 msg!(
                     "Decompress ATA: compressed owner {:?} != derived ATA {:?}",
                     solana_pubkey::Pubkey::new_from_array(input_owner_bytes),
                     solana_pubkey::Pubkey::new_from_array(derived_ata)
                 );
                 return Err(CTokenError::DecompressDestinationMismatch.into());
             }
-        }
 
-        if !pubkey_eq(&input_owner_bytes, destination_account.key()) {
-            msg!(
-                "Decompress ATA mismatch: derived {:?} != destination {:?}",
-                solana_pubkey::Pubkey::new_from_array(derived_ata),
-                solana_pubkey::Pubkey::new_from_array(*destination_account.key())
-            );
-            return Err(CTokenError::DecompressDestinationMismatch.into());
-        }
-
-        // Also verify destination CToken owner matches wallet_owner
-        // (destination should be wallet's ATA, owned by wallet)
-        if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) {
-            msg!(
-                "Decompress ATA: wallet owner {:?} != destination owner {:?}",
-                solana_pubkey::Pubkey::new_from_array(*wallet_owner_bytes),
-                solana_pubkey::Pubkey::new_from_array(ctoken.base.owner.to_bytes())
-            );
-            return Err(CTokenError::DecompressDestinationMismatch.into());
+            // Also verify destination CToken owner matches wallet_owner
+            // (destination should be wallet's ATA, owned by wallet)
+            if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) {
+                msg!(
+                    "Decompress ATA: wallet owner {:?} != destination owner {:?}",
+                    solana_pubkey::Pubkey::new_from_array(*wallet_owner_bytes),
+                    solana_pubkey::Pubkey::new_from_array(ctoken.base.owner.to_bytes())
+                );
+                return Err(CTokenError::DecompressDestinationMismatch.into());
+            }
         }
programs/compressed-token/program/src/ctoken/approve_revoke.rs (2)

29-43: Update misleading documentation.

The comment on line 30 states "Handles compressible extension top-up before delegating to pinocchio," but the code actually calls process_approve first (line 43), then handles the top-up afterward (lines 49-61). While this order may be intentional to ensure rent exemption is maintained after the operation, the comment should accurately reflect the implementation.

📝 Suggested fix
-/// Process CToken approve instruction.
-/// Handles compressible extension top-up before delegating to pinocchio.
-///
+/// Process CToken approve instruction.
+/// Delegates to pinocchio for approval, then handles compressible extension top-up.
+///

64-79: Update misleading documentation.

Same issue as process_ctoken_approve: the comment states top-up happens "before delegating to pinocchio," but the code calls process_revoke first (line 79), then handles top-up afterward.

📝 Suggested fix
-/// Process CToken revoke instruction.
-/// Handles compressible extension top-up before delegating to pinocchio.
-///
+/// Process CToken revoke instruction.
+/// Delegates to pinocchio for revocation, then handles compressible extension top-up.
+///
🤖 Fix all issues with AI agents
In @programs/compressed-token/anchor/src/process_transfer.rs:
- Around line 659-662: Uncomment the TLV presence check so TLV is validated
fail-fast during input validation: restore the conditional using
input_token_data.tlv.is_some() (the commented block with unimplemented!("Tlv is
unimplemented.")) so the code returns the unimplemented/error immediately
instead of letting TLV slip through to the runtime path referenced by the later
unimplemented! at line 98; mirror the existing FROZEN_INPUTS-style gating if you
need to toggle behavior, or if leaving it commented is intentional open an issue
to track adding a const-generic flag similar to FROZEN_INPUTS.

In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the import "use core::panic;" and replace any panic!
calls in this module (the panic! invocation referenced in check_extensions.rs)
with proper error returns; specifically, change panic!("<msg>") to returning a
Result::Err with a Solana program error (e.g.,
Err(ProgramError::Custom(<unique_code>)) or a defined program error type) and
optionally emit a diagnostic via msg!("<msg>") before returning; ensure every
occurrence of panic! in check_extensions.rs is converted to an Err return so the
program fails gracefully without consuming remaining compute units.
- Around line 144-147: Replace the panic!("Mint cache: compression: mint index
not found") with a proper error return: detect the surrounding function's error
type (e.g., Result<..., ProgramError> or a crate-specific error enum) and return
an appropriate error (for example ProgramError::InvalidAccountData or a
CompressedTokenError::MintNotFound converted into the function's error type).
Locate the preceding insert and the get_by_key call and either (a) remove the
redundant lookup and use the inserted value directly, or (b) use
get_by_key().ok_or_else(|| <appropriate error>) so the code defensively returns
an error instead of panicking.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 97-98: Remove the stale inline question comment "// what about is
ata?" that follows the condition check for compress_to_pubkey || is_ata in the
compress_and_close logic; the code already handles the is_ata branch so simply
delete that leftover comment near the if (compress_to_pubkey || is_ata) to clean
up the file compress_and_close.rs.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 188-190: Replace the unchecked addition of delegated amounts on
the ctoken (ctoken.base.delegated_amount.set(current + delegated_amount)) with a
checked addition using checked_add; if checked_add returns None, return
Err(ProgramError::ArithmeticOverflow). Mirror the error handling pattern used in
compress_or_decompress_ctokens.rs so you call checked_add on the current value
and delegated_amount, set the field only on Some(sum), and propagate
ProgramError::ArithmeticOverflow on overflow.

In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Around line 122-134: The max_top_up validation is inconsistent: instead of
passing &mut 0 for lamports_budget and manually checking max_top_up after
calling process_compression_top_up, follow the pattern used in
process_ctoken_approve_checked by computing a lamports_budget value from
max_top_up (e.g., let mut lamports_budget = if max_top_up > 0 { max_top_up as
u64 } else { 0 };) pass &mut lamports_budget into process_compression_top_up,
and remove the manual if-check that returns CTokenError::MaxTopUpExceeded so the
budget validation is handled inside process_compression_top_up.

Comment on lines +1 to +2
use core::panic;

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

Avoid using panic! in on-chain programs.

Importing core::panic solely to use panic! in on-chain code is a red flag. Panics in Solana programs consume all remaining compute units and provide poor error messages to users. This import hints at the problematic usage on line 146.

🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
around lines 1 - 2, Remove the import "use core::panic;" and replace any panic!
calls in this module (the panic! invocation referenced in check_extensions.rs)
with proper error returns; specifically, change panic!("<msg>") to returning a
Result::Err with a Solana program error (e.g.,
Err(ProgramError::Custom(<unique_code>)) or a defined program error type) and
optionally emit a diagnostic via msg!("<msg>") before returning; ensure every
occurrence of panic! in check_extensions.rs is converted to an Err return so the
program fails gracefully without consuming remaining compute units.

Comment on lines +144 to +147
} else {
// TODO: double check.
panic!("Mint cache: compression: mint index not found");
}
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 | 🔴 Critical

Replace panic! with a proper error return.

On-chain programs should never use panic!. Even if this code path is believed to be unreachable, a panic will consume all compute units and give users an opaque error. The TODO comment suggests uncertainty about this logic.

Since you just inserted the mint into the cache on lines 114-118, the get_by_key on line 121 should always succeed. However, defensive programming dictates returning an error rather than panicking.

🐛 Proposed fix to replace panic with error
             if let Some(checks) = cache.get_by_key(&mint_index) {
                 // CompressAndClose with restricted extensions requires CompressedOnly output.
                 // ...validation logic...
             } else {
-                // TODO: double check.
-                panic!("Mint cache: compression: mint index not found");
+                // This should be unreachable since we just inserted the mint above,
+                // but return an error instead of panicking for safety.
+                return Err(ErrorCode::MintCacheCapacityExceeded.into());
             }

Consider whether this else branch is even necessary - if you're confident the insert succeeded, you could use unwrap_or_else with a proper error, or restructure to avoid the redundant lookup entirely.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
around lines 144 - 147, Replace the panic!("Mint cache: compression: mint index
not found") with a proper error return: detect the surrounding function's error
type (e.g., Result<..., ProgramError> or a crate-specific error enum) and return
an appropriate error (for example ProgramError::InvalidAccountData or a
CompressedTokenError::MintNotFound converted into the function's error type).
Locate the preceding insert and the get_by_key call and either (a) remove the
redundant lookup and use the inserted value directly, or (b) use
get_by_key().ok_or_else(|| <appropriate error>) so the code defensively returns
an error instead of panicking.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs (1)

97-115: Critical: Remove duplicate validation logic.

Lines 97-104 duplicate the validation already performed at lines 84-94. Since you've already verified:

  1. derived_ata == destination_account (lines 74-82)
  2. input_owner == derived_ata (lines 84-94)

The transitive property proves input_owner == destination_account, making lines 97-104 redundant.

Additionally, once the inner block is removed, these variables will be in scope, but the logic should still be deleted to avoid redundant checks.

🗑️ Proposed fix

Remove the duplicate validation:

-        if !pubkey_eq(&input_owner_bytes, destination_account.key()) {
-            msg!(
-                "Decompress ATA mismatch: derived {:?} != destination {:?}",
-                solana_pubkey::Pubkey::new_from_array(derived_ata),
-                solana_pubkey::Pubkey::new_from_array(*destination_account.key())
-            );
-            return Err(CTokenError::DecompressDestinationMismatch.into());
-        }
-
         // Also verify destination CToken owner matches wallet_owner
         // (destination should be wallet's ATA, owned by wallet)
         if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) {
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs (1)

256-263: Critical type mismatch in frozen state validation—comparing incompatible values.

The comparison ctoken.state != compression_only_extension.is_frozen is incorrect. ctoken.state uses the full AccountState enum (Uninitialized=0, Initialized=1, Frozen=2), while is_frozen is a boolean (0/1). When an account is frozen, ctoken.state == 2 but is_frozen == 1, causing the comparison to incorrectly fail (2 != 1).

Use boolean comparison instead: (ctoken.state == 2) != compression_only_extension.is_frozen

Reference: AccountState enum and current code
        if ctoken.state != compression_only_extension.is_frozen {
            msg!(
                "is_frozen mismatch: ctoken {} != extension {}",
                ctoken.state,
                compression_only_extension.is_frozen
            );
            return Err(ErrorCode::CompressAndCloseFrozenMismatch.into());
        }
programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs (1)

151-162: LGTM!

The new zero-amount output handling correctly ensures that mints for zero-amount outputs undergo full validation with restricted extension checks, which is appropriate since these represent new state being created.

Minor note: Line 158 hardcodes true for deny_restricted_extensions. While correct (zero-amount outputs imply out_token_data is non-empty, so deny_restricted_extensions would be true), consider using the variable from line 88 for consistency and clarity.

programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs (1)

55-62: Add bounds check for compression_index to prevent panic.

The compression_index from instruction data is used directly as an array index without validation (line 55-58). Since this value comes from untrusted user input, an attacker could provide a value >= MAX_COMPRESSIONS to cause a panic, resulting in a denial-of-service attack.

🔒 Proposed fix
                 if let ZExtensionInstructionData::CompressedOnly(co) = ext {
                     let idx = co.compression_index as usize;
-                    // TODO check that it is not out of bounds
+                    // Validate compression_index is within bounds
+                    if idx >= MAX_COMPRESSIONS {
+                        return Err(CTokenError::CompressionIndexOutOfBounds.into());
+                    }
                     // Check uniqueness - error if compression_index already used
                     if compression_to_input[idx].is_some() {
                         return Err(CTokenError::DuplicateCompressionIndex.into());
                     }

Note: This assumes CTokenError::CompressionIndexOutOfBounds exists. If not, you'll need to add it to the error enum.

programs/compressed-token/program/src/ctoken/approve_revoke.rs (1)

223-240: Inconsistent budget enforcement pattern compared to process_compressible_top_up.

This function uses lamports_budget tracking (lines 223-227, 234, 238-240) to enforce max_top_up, while process_compressible_top_up (lines 119-133) bypasses the budget parameter and checks max_top_up < transfer_amount directly.

Both approaches work, but the inconsistency makes the code harder to maintain. Consider unifying the budget enforcement approach across both functions—either always use lamports_budget tracking or always use post-hoc comparison.

🤖 Fix all issues with AI agents
In @programs/compressed-token/anchor/src/process_transfer.rs:
- Line 647: The comment saying the discriminator will be overridden is
misleading because the override in
add_data_hash_to_input_compressed_accounts_with_version is conditional; update
the comment near TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR to state that this value
acts as the V1/default discriminator set by get_input_compressed_accounts and
will only be replaced when
add_data_hash_to_input_compressed_accounts_with_version is called with
TokenDataVersion::ShaFlat or TokenDataVersion::V2 (calls where version is Some);
when version is None the initial discriminator remains.

In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the unnecessary explicit import `use core::panic;`
from the top of check_extensions.rs; the `panic!()` macro used later (line ~146)
is in the Rust prelude and works without this import, so delete that use
statement to clean up unused imports.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 97-99: The inline comment "what about is ata?" in the if condition
checking compress_to_pubkey || is_ata should be removed or replaced with a clear
explanatory comment; update the comment near the if (compress_to_pubkey ||
is_ata) in compress_and_close.rs to state that ATA (associated token accounts)
follow the same owner-validation path as pubkey-targeted compression so owner
must match the token account pubkey when compressing to either a pubkey or an
ATA, or simply delete the question-style remark.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 187-190: Replace the unchecked addition on
ctoken.base.delegated_amount with checked arithmetic: call
current.checked_add(delegated_amount) and handle the None case (overflow) by
returning an appropriate error or aborting the operation; update the call on the
ctoken.base.delegated_amount.set(...) site to use the checked sum when Some(sum)
and propagate or convert the overflow None into the function's error path
instead of performing unchecked addition.
- Around line 36-95: Remove the unnecessary inner scope (the extra pair of curly
braces) that wraps the ATA-decompress logic so variables declared
there—wallet_owner_bytes, derived_ata, input_owner_bytes (and
bump_seed/ata_seeds)—remain in scope for the subsequent code; simply delete the
opening '{' before "let wallet_owner = wallet_owner.ok_or_else..." and the
matching closing '}' after the input_owner_bytes check so the variables are
available later.

In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Line 164: The TODO on process_ctoken_approve_checked must be resolved: either
delete the function and remove all references (call sites, exports, tests) if
it's unused, or keep it and replace the inline TODO with a tracked issue
reference (create an issue describing why/when to remove, add issue ID or URL)
and add a comment explaining why it remains and planned timeline; update any
public API docs or changelog entries accordingly and ensure tests/build pass
after the change.

In @programs/compressed-token/program/src/shared/token_input.rs:
- Line 86: Consolidate the ATA derivation and signer checks by moving the
associated token account (ATA) pubkey derivation logic into the token input
validation function in token_input.rs (the function responsible for input
validation, e.g., validate/validate_inputs), compute the ATA deterministically
from owner and mint there, verify the provided ATA/account matches the derived
pubkey and that required signers are present, remove duplicate ATA derivation
and signer checks from call sites, and update any tests to reflect the
centralized validation and failure messages.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
programs/compressed-token/program/src/ctoken/close/processor.rs (3)

61-61: Fix inconsistent terminology in error message.

The error message says "rent recipient mismatch" but the variable being validated is rent_sponsor. Use consistent terminology throughout the codebase.

📝 Proposed fix
-            msg!("rent recipient mismatch");
+            msg!("rent sponsor mismatch");

65-65: Remove outdated comment referencing removed COMPRESS_AND_CLOSE path.

The comment mentions "For regular close (!COMPRESS_AND_CLOSE)" but the COMPRESS_AND_CLOSE code path was removed in this PR. This comment is now misleading.

♻️ Proposed fix
-    // For regular close (!COMPRESS_AND_CLOSE): fall through to owner check

67-74: Update misleading comment about state validation scope.

The comment says "only for regular close" but since the COMPRESS_AND_CLOSE path was removed, this validation always applies. The comment is now redundant and potentially confusing.

♻️ Proposed fix
-    // Check account state - reject frozen and uninitialized (only for regular close)
+    // Check account state - reject frozen and uninitialized
programs/compressed-token/program/src/shared/compressible_top_up.rs (1)

20-36: Document when payer can be None.

The function signature changed payer from a required &AccountInfo to an optional Option<&AccountInfo>, but the documentation doesn't explain when None is acceptable. Since line 87 returns CTokenError::MissingPayer when None, callers need clarity on when they can omit the payer.

Update the documentation to explain:

  • Under what conditions can payer be None?
  • What happens when payer is None and top-ups are needed?
📝 Suggested documentation improvement
 /// # Arguments
 /// * `cmint` - The CMint account (may or may not have Compressible extension)
 /// * `ctoken` - The CToken account (may or may not have Compressible extension)
-/// * `payer` - The fee payer for top-ups
+/// * `payer` - Optional fee payer for top-ups. Pass None only when no top-ups are expected (max_top_up=0 or accounts already funded). Returns MissingPayer error if None when top-ups are needed.
 /// * `max_top_up` - Maximum lamports for top-ups combined (0 = no limit)
programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs (1)

55-59: Keep rent_payment validation consistent with the doc-comment contract (0 or >=2).

Right now you only special-case == 1. If rent_payment is user-provided and can be large, consider adding an upper-bound (or overflow-safe handling) so rent math can’t overflow downstream.

programs/compressed-token/program/src/ctoken/transfer/shared.rs (2)

18-44: has_default_account_state is compared but (likely) never set → mismatch check may be ineffective or transfers may start failing

AccountExtensionInfo::check_t22_extensions now compares flags.has_default_account_state (Line 37), but process_account_extensions doesn’t appear to ever set info.flags.has_default_account_state. If a token account can carry a DefaultAccountState marker, you either need to (a) parse and set this flag, or (b) remove the comparison because it can’t be derived here.

Also, the comment on flags (Line 22) should be updated to include default_account_state since it’s now part of equality.


54-74: Docs say “mint not provided” for CTokenTransfer, but behavior currently fails with MintRequiredForTransfer rather than a “restricted extensions denied” error

The doc update (Lines 55-56) is directionally correct, but the actual failure mode for restricted extensions when mint is None is ErrorCode::MintRequiredForTransfer (via validate_sender). If the intent is “restricted extensions are denied”, consider short-circuiting to a more specific error when deny_restricted_extensions == true, instead of first requiring a mint that the instruction can’t supply.

This is mostly about making failures self-explanatory (and avoiding users thinking they passed the wrong accounts for an instruction that cannot accept them).

🤖 Fix all issues with AI agents
In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Line 87: Remove the unnecessary #[allow(unused)] attribute applied above the
approve_revoke function in approve_revoke.rs (the function that's called from
line 78); delete that attribute so the function has normal compiler warning
behavior, then rebuild (cargo build) to ensure no other unused attributes are
present or needed elsewhere.

In @programs/compressed-token/program/src/ctoken/mint_to.rs:
- Around line 16-18: Update the instruction data comment in mint_to.rs to remove
the confusing reference to CTokenTransfer and explicitly describe the binary
layout: state that the format is an 8-byte amount followed optionally by a
2-byte max_top_up (u16, where 0 = no limit), and clarify this only describes the
shared binary layout (not semantic behavior) between mint_to and transfer;
replace the existing multi-line comment with this clearer description near the
mint_to instruction handling.

In @programs/compressed-token/program/src/shared/compressible_top_up.rs:
- Around line 53-62: The current call to cmint_top_up_lamports_from_account_info
silently ignores ownership failure by treating None as "no top-up"; change this
to an explicit validation: call cmint_top_up_lamports_from_account_info and
handle the None case explicitly (e.g., convert it into a clear error return or a
processLogger/error log) instead of silently skipping; you can either (A)
perform an explicit owner check before calling using the same owner logic and
return Err(...) if the owner is invalid, or (B) change the if let Some(...)
usage to a match and on None return an Err indicating invalid CMint owner
(propagating the error up) while keeping the existing transfers[0].amount and
lamports_budget updates in the Some branch; reference
cmint_top_up_lamports_from_account_info, cmint, current_slot, LIGHT_CPI_SIGNER,
transfers, and lamports_budget when making the change.
- Around line 92-123: Add rich doc comments to the public helper
process_compression_top_up: describe the T: CalculateTopUp trait and expected
implementations, when callers should use this helper vs
calculate_and_execute_compressible_top_ups, and explicitly document side-effects
on mutable parameters current_slot, transfer_amount, and lamports_budget
(how/when they are updated). Also change the error mapping for the
calculate_top_up_lamports call to return a more specific error (e.g.,
CTokenError::ArithmeticOverflow or a new CTokenError::TopUpCalculationFailed)
instead of always mapping into CTokenError::InvalidAccountData so calculation
failures are reported accurately. Ensure references to function names
calculate_top_up_lamports and calculate_and_execute_compressible_top_ups are
used in the docs to guide maintainers.
- Around line 71-72: The current suppression line using let _ = (cmint, ctoken,
&mut current_slot); is unconventional; instead mark the parameters as
intentionally unused by renaming them to start with an underscore (e.g., _cmint,
_ctoken, _current_slot) in the function signature that declares cmint, ctoken
and current_slot and then remove the let _ = ... line; this communicates intent
at the parameter level and avoids the temporary tuple binding.

In @programs/compressed-token/program/src/shared/initialize_ctoken_account.rs:
- Around line 331-351: The function is_valid_mint has inconsistent error
handling: for known owners it returns Ok(false) when the mint data is invalid
but for unknown owners it returns Err(ProgramError::IncorrectProgramId); change
the unknown-owner branch in is_valid_mint to return Ok(false) (and keep or
update the msg! call for diagnostics) so callers get a consistent Ok<boolean>
result for any owner, referencing symbols SPL_TOKEN_ID, SPL_TOKEN_2022_ID,
CTOKEN_PROGRAM_ID and ProgramError::IncorrectProgramId to locate and update the
logic.

Comment on lines 130 to +139
let account_size = borsh::to_vec(compressed_mint)
.map_err(|_| ErrorCode::MintActionOutputSerializationFailed)?
.len();

// 11. Calculate Light Protocol rent (base_rent + bytes * lamports_per_byte * epochs + compression_cost)
// 7a.1. Store rent exemption at creation (only query Rent sysvar here, never again)
let rent_exemption_paid = Rent::get()
.map_err(|_| ProgramError::UnsupportedSysvar)?
.minimum_balance(account_size) as u32;
compressed_mint.compression.rent_exemption_paid = rent_exemption_paid;

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 | 🟠 Major

Guard the u64 -> u32 truncation for rent_exemption_paid (and align “query Rent sysvar once” with reality).

Rent::minimum_balance() returns u64; casting to u32 can silently truncate. Even if sizes are small today, it’s a brittle assumption.

Also, note that create_pda_account(...) likely queries Rent again internally (to fund rent exemption), so the “only query Rent sysvar here, never again” comment is misleading unless create_pda_account is guaranteed not to.

Proposed hardening (no new custom error codes)
-    let rent_exemption_paid = Rent::get()
-        .map_err(|_| ProgramError::UnsupportedSysvar)?
-        .minimum_balance(account_size) as u32;
+    let rent_exemption_paid: u32 = Rent::get()
+        .map_err(|_| ProgramError::UnsupportedSysvar)?
+        .minimum_balance(account_size)
+        .try_into()
+        .map_err(|_| ProgramError::InvalidArgument)?;
     compressed_mint.compression.rent_exemption_paid = rent_exemption_paid;

Comment on lines +53 to 62
// Calculate CMint top-up using optimized function (owner check inside)
#[cfg(target_os = "solana")]
if let Some(amount) = cmint_top_up_lamports_from_account_info(
cmint,
&mut current_slot,
&LIGHT_CPI_SIGNER.program_id,
) {
transfers[0].amount = amount;
lamports_budget = lamports_budget.saturating_sub(amount);
}
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

Consider explicit error when owner check fails.

The cmint_top_up_lamports_from_account_info function includes an owner check and returns None if it fails (as noted in the comment at line 53). However, this means an invalid CMint owner will silently skip the top-up without raising an error.

While this might be intentional for flexibility, it could mask bugs where the wrong account is passed. Consider whether an explicit ownership validation error would be more appropriate, or add a comment explaining why silent skipping is the desired behavior.

🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/compressible_top_up.rs around
lines 53 - 62, The current call to cmint_top_up_lamports_from_account_info
silently ignores ownership failure by treating None as "no top-up"; change this
to an explicit validation: call cmint_top_up_lamports_from_account_info and
handle the None case explicitly (e.g., convert it into a clear error return or a
processLogger/error log) instead of silently skipping; you can either (A)
perform an explicit owner check before calling using the same owner logic and
return Err(...) if the owner is invalid, or (B) change the if let Some(...)
usage to a match and on None return an Err indicating invalid CMint owner
(propagating the error up) while keeping the existing transfers[0].amount and
lamports_budget updates in the Some branch; reference
cmint_top_up_lamports_from_account_info, cmint, current_slot, LIGHT_CPI_SIGNER,
transfers, and lamports_budget when making the change.

Comment on lines +331 to +351
#[inline(always)]
pub fn is_valid_mint(owner: &Pubkey, mint_data: &[u8]) -> Result<bool, ProgramError> {
if *owner == SPL_TOKEN_ID {
// SPL Token: mint must be exactly 82 bytes
Ok(mint_data.len() == SPL_MINT_LEN)
} else if *owner == SPL_TOKEN_2022_ID {
// Token-2022: Either exactly 82 bytes (no extensions) or
// check AccountType marker at offset 165 (with extensions)
// Layout with extensions: 82 bytes mint + 83 bytes padding + AccountType
Ok(mint_data.len() == SPL_MINT_LEN
|| (mint_data.len() > T22_ACCOUNT_TYPE_OFFSET
&& mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT))
} else if *owner == CTOKEN_PROGRAM_ID {
// CToken: Always has extensions, must be >165 bytes with AccountType=Mint
Ok(mint_data.len() > T22_ACCOUNT_TYPE_OFFSET
&& mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT)
} else {
msg!("Invalid mint owner");
Err(ProgramError::IncorrectProgramId)
}
}
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

Inconsistent error handling in is_valid_mint.

For SPL Token and Token-2022, the function returns Ok(false) when the mint is invalid. However, for an unknown owner (line 348-349), it returns Err(ProgramError::IncorrectProgramId). This asymmetry can be confusing—callers expecting Ok(false) for invalid mints won't catch the error case.

Consider returning Ok(false) for unknown owners to maintain consistency, or document that unknown owners are considered a program error rather than an invalid mint.

Proposed fix for consistent behavior
     } else if *owner == CTOKEN_PROGRAM_ID {
         // CToken: Always has extensions, must be >165 bytes with AccountType=Mint
         Ok(mint_data.len() > T22_ACCOUNT_TYPE_OFFSET
             && mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT)
     } else {
-        msg!("Invalid mint owner");
-        Err(ProgramError::IncorrectProgramId)
+        // Unknown owner - not a valid mint
+        Ok(false)
     }
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/initialize_ctoken_account.rs
around lines 331 - 351, The function is_valid_mint has inconsistent error
handling: for known owners it returns Ok(false) when the mint data is invalid
but for unknown owners it returns Err(ProgramError::IncorrectProgramId); change
the unknown-owner branch in is_valid_mint to return Ok(false) (and keep or
update the msg! call for diagnostics) so callers get a consistent Ok<boolean>
result for any owner, referencing symbols SPL_TOKEN_ID, SPL_TOKEN_2022_ID,
CTOKEN_PROGRAM_ID and ProgramError::IncorrectProgramId to locate and update the
logic.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (1)

61-63: Redundant initialization check — consider removing for clarity.

The validate_ctoken function (line 142) already rejects uninitialized accounts, and for Compress mode, frozen accounts are also rejected (line 148). This means by the time we reach line 61, the account state is guaranteed to be initialized.

If this is intentional defensive coding, a brief comment would help future readers understand the rationale. Otherwise, consider removing this redundant check.

🤖 Fix all issues with AI agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 134-137: The addition to ctoken.base.delegated_amount uses
unchecked addition and can overflow; replace the plain current +
delegated_amount with current.checked_add(delegated_amount) and handle the None
case (overflow) by returning an appropriate error (or otherwise aborting safely)
instead of wrapping; update the code around
ctoken.base.delegated_amount.get()/set() in decompress.rs to use checked_add and
propagate/convert the overflow into a clear error.

- Rename apply_decompress_extension_state to validate_and_apply_compressed_only
- Add InvalidAtaDerivation error code (18066) for explicit ATA validation failures
- Extract resolve_ata_signer cold function with explicit error handling
- Extract convert_tlv_to_extension_structs cold function out of hot path
- Fix test_ata_decompress_with_mismatched_amount_fails to use correct owner setup

Previously, ATA derivation failures silently fell back to owner_account,
relying on the implicit property that ATAs cannot sign. Now returns
explicit InvalidAtaDerivation error for better debugging and security.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (20)
programs/compressed-token/program/docs/compressible/WITHDRAW_FUNDING_POOL.md (4)

20-48: Add serialization examples and discriminator details for CompressibleConfig.

Per coding guidelines for account documentation, serialization examples should specify the deserialization method. Line 46 mentions the owner and validation, but the documentation should include:

  • CompressibleConfig discriminator value
  • State layout/serialization example showing how to deserialize with bytemuck (as mentioned in line 61)
  • Exact layout of version field and how it's extracted as little-endian u16 bytes

Example addition:

**CompressibleConfig State Layout:**
- Discriminator: [8 bytes]
- version: u16 (little-endian)
- compression_authority: Pubkey [32 bytes]
- rent_sponsor: Pubkey [32 bytes]
- [remaining fields...]

Deserialization: Use `bytemuck::from_bytes::<CompressibleConfig>(&data)` after verifying discriminator.

50-76: Add concrete code examples showing instruction construction and PDA seed derivation.

Per coding guidelines, documentation should include working code snippets showing correct usage. The current documentation describes the logic well but lacks examples for:

  1. Instruction construction and invocation - How to build the instruction data (8-byte amount) and call it
  2. PDA derivation example - How to construct seeds [b"rent_sponsor", version_bytes, bump] before calling invoke_signed
  3. Account validation pattern - The AccountIterator usage mentioned in line 57 should include a code snippet

Example structure to add:

// Derive rent_sponsor PDA
let version_bytes = version.to_le_bytes(); // u16 -> [u8; 2]
let (rent_sponsor_pda, bump) = Pubkey::find_program_address(
    &[b"rent_sponsor", &version_bytes],
    &registry_program_id,
);

// Build and invoke instruction
let instruction = // ... construct with amount in first 8 bytes
let accounts = [rent_sponsor, compression_authority, destination, system_program, config];

// Execute with PDA signer
invoke_signed(
    &instruction,
    &accounts,
    &[&[b"rent_sponsor", &version_bytes, &[bump]]],
)?;

Also clarify: Is the registry_program_id the constant for the registry program, and does it match the owner check in line 46?


78-87: Map error codes to specific logic steps for clarity.

The error codes are listed, but their relationship to the logic steps (lines 50-76) could be clearer. Add annotations showing which checks produce which errors:

  • Step 1 (data parsing): InvalidInstructionData (3)
  • Step 2 (account validation): InvalidAccountData (4), InvalidSeeds (14), NotEnoughAccountKeys (20014), InvalidSigner (20009), AccountNotMutable (20002), InvalidState (19002)
  • Step 3 (funds check): InsufficientFunds (6)

Consider restructuring the error section to reference these step numbers, e.g., "Error code 3: Returned from Step 1 if..."


1-14: Documentation is accurate; however, the underlying implementation violates the learning requirement for safe lamport transfers.

The path exists and is correct. The documentation in lines 73–76 accurately describes the current implementation, which uses pinocchio_system::instructions::Transfer with invoke_signed(). However, the learning requirement states that "Safe lamport transfer operations must use helpers from src/shared/transfer_lamports.rs." The implementation should be refactored to use transfer_lamports_via_cpi() from the canonical module instead of directly invoking pinocchio system transfer. The documentation itself is not inaccurate—it faithfully describes what the code does—but the code itself requires remediation to align with established patterns.

programs/compressed-token/program/docs/compressed_token/CREATE_TOKEN_POOL.md (4)

11-20: Add references to associated instructions and clarify the token pool's role in related operations.

The documentation describes what CREATE_TOKEN_POOL does but doesn't link to related instructions that interact with or depend on the token pool (e.g., compress, decompress, transfer operations). Per the coding guidelines for account documentation, include a section on associated instructions—or add a cross-reference if this documentation exists elsewhere.

Additionally, consider adding a brief note on the token pool account layout or what state it holds (empty upon creation, then accumulates tokens as users compress/decompress).


59-63: Expand CPI documentation with account mapping and data details.

The CPI section is too brief. Per the documentation guidelines for CPI calls, include:

  1. Account mapping: Show how the listed accounts map to the target instruction parameters (e.g., which is the token account, which is the mint, etc.)
  2. Data passthrough: Clarify what instruction data (if any) is passed to initialize_account3
  3. Example code: Show the actual CPI invocation so readers understand the exact parameter order

This helps consumers of the documentation understand the account semantics and troubleshoot mapping errors.


65-71: Provide remediation guidance for common error scenarios.

The error section lists error codes but doesn't explain how developers should respond. For example:

  • MintWithInvalidExtension: List which extensions are allowed, or point to the allowlist
  • NonZeroTransferFeeNotSupported: Explain that transfer fees must be zero for compression support
  • TransferHookNotSupported: Clarify why and how users can remove the hook if needed

Consider adding a "Troubleshooting" section with example scenarios and their solutions.


1-72: Add working code examples and cross-reference associated pool instructions.

The documentation is factually accurate but lacks supporting details per the coding guidelines. Add a working code example showing how to call create_token_pool, reference the ADD_TOKEN_POOL instruction (already documented in CLAUDE.md and INSTRUCTIONS.md) for creating pools at indices 1-4, and either include or cross-reference the token_pool_pda account structure (it's a standard SPL token account initialized with the mint's variable-length extensions).

programs/compressed-token/program/docs/ctoken/CLOSE.md (4)

1-20: Add serialization details and state layout reference per documentation guidelines.

Per coding guidelines, account operation documentation should reference the state layout and serialization methods. Consider adding:

  1. State Layout: Expand line 9 reference with concrete field descriptions or link to CToken serialization example
  2. Serialization Method: Specify whether CToken deserialization uses Borsh (BorshDeserialize) or zero-copy (ZeroCopyAt) pattern
  3. Extension Handling: Document how Compressible extension serialization and deserialization works within CToken
  4. Associated Instructions: Cross-reference to create and optional update instructions for context

This ensures readers understand both the account structure being modified and the data formats involved.


155-163: Clarify that SetAuthority is permanently unsupported, not a planned feature.

Line 158 states "SetAuthority instruction to set close_authority is currently unimplemented," implying future support. However, SetAuthority is not listed in the instruction set (see INSTRUCTIONS.md discriminator table), and there are no TODO markers indicating planned implementation. The design is permanent: close_authority exists for SPL Token compatibility but is intentionally read-only and always None. Update the wording to: "SetAuthority is not supported; close_authority is always None on token accounts" to set correct user expectations.


129-163: Fix AccountError code mappings—these reference the wrong range entirely.

The error codes for AccountError variants are incorrect throughout the error section. They reference codes in the 12xxx range, but the actual enum definitions use 20000-20015:

  • AccountError::AccountOwnedByWrongProgram is error code 20001 (not 12007)
  • AccountError::AccountNotMutable is error code 20002 (not 12008)
  • AccountError::InvalidSigner is error code 20009 (not 12015)
  • AccountError::NotEnoughAccountKeys is error code 20014 (not 12020)

The CTokenError codes (18002, 18036, 18053, 18062, 18063) and ErrorCode codes (6074, 6075, 6076) documented are accurate.


86-113: Clarify that distribute_lamports is internally reused by CompressAndClose, not just CloseTokenAccount.

The documented logic is correct but misleading. The distribute_lamports function is called not only by the CloseTokenAccount instruction but also internally by Transfer2's CompressAndClose mode (close_for_compress_and_close in compress_and_close.rs). When CompressAndClose invokes close_token_account, it passes the compression_authority as the authority parameter, making the check at lines 111-113 reachable.

The note at line 90 stating "For CompressAndClose mode in Transfer2, compression_authority validation is done separately" is confusing because the validation is not separate—it's handled within the same distribute_lamports function via the check at line 151.

Suggestion: Clarify in section 4.3 that when distribute_lamports is called from CompressAndClose, the authority parameter will be the compression_authority, and explain how this differs from the regular CloseTokenAccount instruction flow described in section 3.5.

programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md (3)

1-5: Use h1 (top-level) heading for consistency with other instruction documentation.

Following markdown standard (MD041), change ## CToken MintToChecked to # CToken MintToChecked for consistency.

📝 Proposed fix
-## CToken MintToChecked
+# CToken MintToChecked

19-26: Fix byte range notation inconsistency in documentation.

The documentation correctly specifies "Bytes 9-10" for the max_top_up field in the byte layout table (line 22), but line 58 incorrectly states "bytes 9-11". Since max_top_up is a u16 (2 bytes), it occupies indices 9 and 10, not 9-11. The implementation confirms this: instruction_data[BASE_LEN..BASE_LEN + 2] where BASE_LEN=9 parses bytes at positions 9-10.

Update line 58 from:

Parse max_top_up from bytes 9-11 if present (11-byte format)

To:

Parse max_top_up from bytes 9-10 if present (11-byte format)

This maintains consistency with the byte layout specification and clarifies that the field occupies exactly 2 bytes, not 3.


1-110: Update documentation paths to use correct directory structure.

The documented account structure paths contain inaccuracies:

  • program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs should reference crates/light-ctoken-interface/src/state/ctoken.rs
  • program-libs/ctoken-interface/src/state/mint/compressed_mint.rs should reference the corresponding path in crates/light-ctoken-interface/src/state/mint/
  • program-libs/compressible/src/compression_info.rs should reference the actual path in crates/light-compressible/

The codebase uses crates/ as the root directory for these libraries, not program-libs/. Verify and correct all three paths to ensure developers can navigate to the actual struct definitions. Also verify that field names mentioned in the documentation (mint, mint_authority, supply, decimals) match the current struct implementations.

programs/compressed-token/program/docs/T22_VS_CTOKEN_COMPARISON.md (1)

59-107: Fix markdown formatting: missing blank lines and list numbering inconsistencies.

Several headings lack blank lines before them (e.g., lines 59, 64, 68), and ordered list item numbering is inconsistent throughout. While this doesn't affect technical correctness, it violates the markdown style guide and reduces readability.

📝 Markdown formatting fixes

Apply blank lines before headings and normalize list numbering:

# (After section 5. Pausable on line 163)

-### CompressAndClose/Decompress Bypass (CToken-specific)
+### CompressAndClose/Decompress Bypass (CToken-specific)

And normalize the table section to proper markdown (around line 221):

-| Extension           | T22 Adds Marker     | CToken Adds Marker                 |
-|---------------------|---------------------|------------------------------------|
+| Extension | T22 Adds Marker | CToken Adds Marker |
+|---|---|---|

More systematically: add blank lines before all headers and ensure list items follow a consistent numbering pattern (1, 2, 3 rather than 1, 3, 5, etc.).

programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md (2)

210-241: Error codes are comprehensively documented with clear descriptions.

The error table provides good coverage of failure scenarios, from proof validation (MintActionProofMissing: 6055) to operation constraints (CompressAndCloseCMintMustBeOnlyAction: 6169). Error codes are mapped to descriptive names that communicate intent.

One suggestion: Consider grouping errors by category (validation errors, state errors, permission errors, constraint errors) in the documentation to help developers quickly locate relevant error handling code.

Group errors by category to improve error handling documentation organization and developer experience.


6-64: Markdown formatting violations: missing blank lines and inconsistent list numbering.

Lines 6, 64, 81, 87-93, and 102-103 have formatting issues:

  • Line 6: Heading immediately after description (missing blank line)
  • Lines 88-93: List item numbering jumps from 1 to 5-10 (should be 2-7)
  • Line 102: Table lacks blank line before it (MD058)

Add blank lines before all section headings and tables, and normalize list item numbering to sequential order (1, 2, 3...) rather than (5, 6, 7...).

programs/compressed-token/program/src/shared/token_input.rs (1)

80-95: ATA signer resolution: good security posture, but make the “TLV required for ATA” behavior explicit.
Right now, ATA verification only runs when tlv_data is Some(_); if an ATA input ever reaches this path with tlv_data=None, it will attempt to authorize using owner_account (the ATA pubkey) and fail in a non-obvious way. Consider returning a targeted error when ATA is expected but TLV is missing (or add an invariant comment where this function is called).
As per coding guidelines, keeping auth/validation behavior explicit in shared utilities will reduce foot-guns.

programs/compressed-token/program/docs/ctoken/APPROVE.md (1)

7-14: Account documentation is incomplete: system_program account is missing for rent top-up operations.

The APPROVE.md and REVOKE.md account lists (lines 25-40 for APPROVE, lines 26-36 for REVOKE) omit the system_program account, which is required when the CToken account has a compressible extension and needs a rent top-up.

Your code calls transfer_lamports_via_cpi() on line 40 (APPROVE) and line 58 (REVOKE), which invokes pinocchio_system::instructions::Transfer. The WITHDRAW_FUNDING_POOL.md documentation (lines 38-41) correctly shows this pattern—it explicitly lists system_program as a required account and documents it as "Required for pinocchio_system Transfer instruction."

Impact on the SPL compatibility claim (lines 7-14): When no top-up is required, the instruction does work with just three accounts (source, delegate, owner for APPROVE). However, when compressible accounts need top-up, the system program account must be included. The compatibility scenarios should clarify that the system program requirement applies specifically to top-up paths, and callers need to include it when building transactions for compressible accounts that may need funding.

Add system_program to the accounts list and clarify its presence in the top-up path.

🤖 Fix all issues with AI agents
In @programs/compressed-token/program/CLAUDE.md:
- Around line 59-68: Add a blank line before each section heading that directly
follows list content in the document (e.g., insert a blank line before "### Rent
Management" and "### Token Operations" and any other "##" / "###" headings that
come immediately after list items like "3. **Claim**" or "4. **Withdraw Funding
Pool**") so every heading has an empty line above it and the file complies with
MD022 (blanks-around-headings).
- Around line 46-107: The markdown uses a single continuous numbered list across
sections; update the ordered lists so each section restarts numbering (Account
Management, Rent Management, Token Operations) — e.g., make the items under
"Account Management" 1..n, under "Rent Management" 1..n (Claim, Withdraw Funding
Pool), and under "Token Operations" 1..n (Transfer2, MintAction, CTokenTransfer,
etc.), keeping the instruction names and discriminators
(InstructionType::CreateTokenAccount, ::Claim, ::Transfer2, ::MintAction, etc.)
intact so reviewers can find each entry.

In @programs/compressed-token/program/docs/ACCOUNTS.md:
- Around line 38-40: The markdown heading "**serialization example**" is missing
a blank line after the ordered instruction list (the list ending with
"WithdrawFundingPool `105`"); insert a single empty line between the end of that
list and the "**serialization example**" heading so the heading is rendered
properly as a separate block.

In @programs/compressed-token/program/docs/CLAUDE.md:
- Around line 38-41: Add a blank line immediately before the "## Navigation
Tips" heading so the heading is separated from the previous paragraph (ensure
the line containing "## Navigation Tips" has an empty line above it to satisfy
MD022 markdown style).

In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md:
- Around line 112-121: Clarify account semantics for the two paths: state
whether compressions_only_cpi_authority_pda is the same PDA as cpi_authority_pda
or a distinct PDA (update text around the two identifiers), explicitly document
that when no_compressed_accounts=true the Path B accounts replace the original
system accounts and the original system accounts may be omitted (or must be
passed as empty placeholders) so callers know whether to include them, add
guidance on path selection logic showing that no_compressed_accounts is read
from instruction data and which branch ignores/uses which accounts, add
error-handling guidance to mention existing CompressionsOnlyMissingFeePayer and
CompressionsOnlyMissingCpiAuthority and add symmetrical errors for when
compressions-only accounts are provided erroneously in Path A, and include the
requested comparison table mapping each account (cpi_authority_pda,
compressions_only_cpi_authority_pda, light_system_program, etc.) to Path A vs
Path B requirements.
- Line 136: The docs mix two error enums and should explicitly annotate which
enum each referenced error code belongs to: update references to error codes
(e.g., 6038, 6039, 6095-6097, 6106-6107, 6139, 6149, 6168) to indicate they come
from anchor_compressed_token::ErrorCode and annotate codes (e.g., 18038, 18043,
18048) as from light_ctoken_interface::CTokenError; when first mentioning
specific named errors like TooManyInputAccounts, include the enum name
(anchor_compressed_token::ErrorCode::TooManyInputAccounts) or a parenthetical
note so readers know the source enum, and apply this convention consistently
throughout TRANSFER2.md.

In
@programs/compressed-token/program/docs/compressible/WITHDRAW_FUNDING_POOL.md:
- Around line 72-76: The code currently performs a direct pinocchio_system
Transfer in withdraw_funding_pool.rs; replace that block (lines ~110-118) with a
call to transfer_lamports_via_cpi(amount, accounts.rent_sponsor,
accounts.destination).map_err(convert_program_error)?; followed by Ok(()). Also
update the documentation section in WITHDRAW_FUNDING_POOL.md (lines ~72-76) to
describe using transfer_lamports_via_cpi with PDA signer via the helper instead
of a direct system Transfer to keep transfers consistent and safe across the
codebase.

In @programs/compressed-token/program/docs/ctoken/CLOSE.md:
- Around line 12-13: The doc wording is misleading: update lines 12–13 to state
that calculate_close_distribution (via get_unused_lamports) returns to_user the
unutilized lamports after preserving rent_exemption_paid and subtracting
required_epochs * rent_per_epoch, while to_rent_sponsor receives the remainder
(which includes the preserved rent exemption plus required rent epochs), i.e.,
the rent_sponsor gets all lamports except the true excess beyond current/future
rent obligations; reference calculate_close_distribution, get_unused_lamports,
rent_exemption_paid, to_user, and to_rent_sponsor when rewriting the two lines.

In @programs/compressed-token/program/docs/ctoken/CREATE.md:
- Line 87: Edit the sentence referencing compression_only to remove the awkward
double "is only" phrasing; replace "Validate compression_only is only set for
mints with restricted extensions" with a clearer variant such as "Validate
compression_only is set only for mints with restricted extensions" or "Validate
compression_only applies only to mints with restricted extensions", referring to
the same compression_only/mints/restricted extensions wording in CREATE.md.
- Around line 109-112: Update the CREATE.md validation order to match the actual
execution in initialize_ctoken_account: list the write_top_up <=
config.rent_config.max_top_up check (error:
CTokenError::WriteTopUpExceedsMaximum) before the token_account_version == 3
check (error: ProgramError::InvalidInstructionData), so the documentation
mirrors the code path executed in the function that performs the checks.
- Around line 84-93: The CREATE.md doc's restricted-extensions list is missing
DefaultAccountState and uses an inconsistent name for TransferFee; update the
documentation (around the restriction list in CREATE.md) to enumerate all five
restricted extensions exactly as implemented by is_restricted_extension():
Pausable, PermanentDelegate, TransferFeeConfig, TransferHook, and
DefaultAccountState, and replace any occurrence of "TransferFee" with the exact
type name "TransferFeeConfig" so docs match the create.rs validation logic and
token_2022_extensions implementation.

In @programs/compressed-token/program/docs/ctoken/REVOKE.md:
- Around line 1-5: The top-level heading in the REVOKE documentation uses an h2
("## CToken Revoke"); update that to an h1 by replacing "## CToken Revoke" with
"# CToken Revoke" to satisfy MD041 (first line must be a top-level heading).

In @programs/compressed-token/program/docs/ctoken/THAW_ACCOUNT.md:
- Line 1: The document title uses a level-2 heading; update the first line of
THAW_ACCOUNT.md (the "CToken Thaw Account" heading) from "## CToken Thaw
Account" to a top-level heading by replacing "##" with "#" so the file begins
with a level-1 heading.

In @programs/compressed-token/program/docs/ctoken/TRANSFER.md:
- Around line 63-77: Create a single shared constant named
SPL_ACCOUNT_SIZE_WITHOUT_EXTENSIONS: usize = 165 in a common module (e.g., a new
or existing constants module) and export it (pub(crate) or pub as needed), then
replace the hardcoded 165 in the transfer implementation (where the magic number
is used in transfer/default.rs) with this constant and update the other local
constants T22_ACCOUNT_TYPE_OFFSET and ACCOUNT_TYPE_OFFSET in the claim and
initialize logic to import and reuse SPL_ACCOUNT_SIZE_WITHOUT_EXTENSIONS
(removing their duplicated 165 values) so all modules reference the same single
source of truth.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 72-106: In validate_destination, don’t trust input_owner_key as
the ATA address; instead derive the expected ATA and compare to
destination.key(): fetch the wallet owner via
packed_accounts.get_u8(ext_data.owner_index), use ctoken.base.mint and
ext_data.bump with the validate_ata_derivation utility to derive the ATA for
(wallet_owner, mint, program_id, bump), then compare that derived ATA to
destination.key() and return CTokenError::DecompressDestinationMismatch if they
differ (replace the current direct comparison of destination.key() vs
input_owner_key).

In @programs/compressed-token/program/src/shared/token_input.rs:
- Around line 163-166: The functions that accept slices of
ZExtensionInstructionData are using the type without its lifetime parameter;
make the functions generic over a lifetime and use ZExtensionInstructionData<'a>
in the parameter types (e.g. change fn convert_tlv_to_extension_structs(...) to
fn convert_tlv_to_extension_structs<'a>(exts: &[ZExtensionInstructionData<'a>])
-> Result<..., ProgramError>) and apply the same lifetime annotation to the
other helper at lines ~191-196 so the code compiles with the existing call sites
that use ZExtensionInstructionData<'a>.
- Around line 213-230: Update the inline comment above the ATA derivation to
explicitly list all four seeds and state this is the Light PDA variant (not an
SPL ATA): mention seeds owner (wallet_owner.key()),
crate::LIGHT_CPI_SIGNER.program_id, mint_account.key(), and data.bump, and
clarify the derived address is a compressed-token program PDA used throughout
create_ata.rs and related code; leave the derivation and pubkey check
(derived_ata, data.bump, wallet_owner, owner_account) unchanged.

@ananas-block ananas-block force-pushed the jorrit/fix-cmint-instruction-data branch from 7592de2 to 15425e5 Compare January 11, 2026 20:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs (1)

306-337: TODO comment warrants attention for future cleanup.

The TODO at Line 315 suggests the CMint validation logic could be simplified or moved. The current implementation is correct—it validates CMint pubkey matches when provided—but consider tracking this as technical debt.

Would you like me to open an issue to track the TODO for simplifying or relocating the CMint validation logic?

program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs (1)

500-505: Bug: Frozen accounts are incorrectly rejected despite docstring claiming they're allowed.

The docstring at line 492 states: "Allows both Initialized (1) and Frozen (2) states." However, is_initialized() returns true only when state == 1. A frozen account (state == 2) fails this check and returns InvalidAccountState.

You likely want to check for "not uninitialized" rather than "is initialized":

Proposed fix
-        if !ctoken.is_initialized() {
+        if ctoken.is_uninitialized() {
             return Err(crate::error::CTokenError::InvalidAccountState);
         }

Apply the same fix to zero_copy_at_mut_checked at line 521.

🤖 Fix all issues with AI agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs:
- Around line 232-233: Remove the orphaned inline comment inside the if
!bytes.is_empty() block that reads "&& bytes[0] == ACCOUNT_TYPE_TOKEN_ACCOUNT 
should throw an error"; mirror the cleanup done in the immutable version by
deleting that confusing comment (or replace it with a clear TODO or proper error
check) so only meaningful code/comments remain around the
bytes/ACCOUNT_TYPE_TOKEN_ACCOUNT check in zero_copy.rs.
- Around line 191-192: The orphaned inline comment in zero_copy.rs inside the
block starting with "if !bytes.is_empty()" is unclear and should be removed or
replaced with an explicit note; either delete the stray "// && bytes[0] ==
ACCOUNT_TYPE_TOKEN_ACCOUNT  should throw an error" or convert it into a clear,
actionable comment or TODO that states the intended behavior (e.g., "TODO:
validate bytes[0] == ACCOUNT_TYPE_TOKEN_ACCOUNT here" or "By design we do not
validate account_type here because ..."). Update the comment near the bytes
handling so it documents the decision and references ACCOUNT_TYPE_TOKEN_ACCOUNT
and the surrounding deserialization/validation logic to avoid future confusion.
- Around line 552-558: Update the safety documentation around the zero-copy
slice creation (the `try_borrow_data()` use and the `data_slice: &'a [u8] =
unsafe { core::slice::from_raw_parts(...) }` return) to explicitly state that
pinocchio's borrow guards are released when dropped and that this function
intentionally drops its borrow guard before returning a reference with an
extended lifetime; clearly document that the extended lifetime is only safe if
the caller guarantees no subsequent borrows that would mutate or re-borrow the
account (e.g., no calls to `try_borrow_mut_data()` or other access that could
alias) while the returned reference is live, and add a short note pointing
reviewers to the matching drop sites (the local borrow guard around lines using
`try_borrow_data()` and the analogous `try_borrow_mut_data()` behavior) so the
aliasing contract is unambiguous.

In @programs/compressed-token/anchor/src/process_transfer.rs:
- Around line 662-664: The unimplemented! call inside the CHECK_TLV branch is
vague; replace it with a clear input-validation error that includes context
about the failing field (input_token_data.tlv) and actionable guidance (e.g.,
TLV handling not supported here — remove TLV or implement TLV parsing/enable the
CHECK_TLV handler). Instead of panicking with unimplemented!, return or
propagate a typed error (or at minimum update the panic message) referencing
CHECK_TLV and input_token_data.tlv so callers and logs can understand this is an
input validation failure and what to do next.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 287-333: The validation uses generic
ProgramError::InvalidAccountData for all failure cases in
validate_ctoken_account (checks around compressible,
compression.info.rent_sponsor, compression.info.compression_authority, and
compression.info.is_compressible); replace these generic returns with distinct
error values (e.g., a custom enum or specific ProgramError variants) so callers
can distinguish "missing compressible extension", "rent sponsor mismatch",
"invalid compression authority", and "account not compressible"—update the error
creation sites where compressible.ok_or_else, the rent_sponsor check, the
compression_authority check, and the is_compressible mapping occur to return the
new specific errors and map/convert them to ProgramError only at the top-level
if needed.

@ananas-block ananas-block force-pushed the jorrit/fix-cmint-instruction-data branch from 919da0e to 39ccd3f Compare January 11, 2026 20:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs:
- Around line 113-117: The current check inspects only the single state byte at
STATE_OFFSET (108) and returns MemoryNotZeroed, which is semantically
misleading; either (A) broaden the validation to assert all critical
initialization bytes are zero (e.g., check a range like
bytes[STATE_OFFSET..STATE_OFFSET+N].iter().all(|b| *b == 0) or include other
specific offsets) and keep MemoryNotZeroed, or (B) introduce a more specific
error variant (e.g.,
light_zero_copy::errors::ZeroCopyError::AccountAlreadyInitialized) and return
that when bytes.len() > STATE_OFFSET && bytes[STATE_OFFSET] != 0; update the
match/propagation sites accordingly to use the new error if option B is chosen.

In
@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs:
- Around line 44-69: The code duplicates mint-data retrieval for the create path
and the existing compressed-mint-with-data path; consolidate by using
accounts_config.cmint_decompressed as the discriminant: if cmint_decompressed
use
CompressedMint::from_account_info_checked(validated_accounts.get_cmint()...)?
(ensuring get_cmint() is present or return
ErrorCode::MintActionMissingCMintAccount), otherwise read
parsed_instruction_data.mint.as_ref().ok_or(ErrorCode::MintDataRequired)? and
call CompressedMint::try_from(...)?, removing the duplicated block while
preserving the cmint_decompressed branch behavior.

Comment on lines +113 to +117
// Check that the account is not already initialized (state byte at offset 108)
const STATE_OFFSET: usize = 108;
if bytes.len() > STATE_OFFSET && bytes[STATE_OFFSET] != 0 {
return Err(light_zero_copy::errors::ZeroCopyError::MemoryNotZeroed);
}
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

Consider renaming the error or expanding the check for semantic clarity.

The check only validates that the state byte (offset 108) is zero, but returns MemoryNotZeroed which implies a broader validation. Either expand the check to validate multiple critical bytes are zeroed, or consider a more specific error like AccountAlreadyInitialized.

🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs around lines 113
- 117, The current check inspects only the single state byte at STATE_OFFSET
(108) and returns MemoryNotZeroed, which is semantically misleading; either (A)
broaden the validation to assert all critical initialization bytes are zero
(e.g., check a range like bytes[STATE_OFFSET..STATE_OFFSET+N].iter().all(|b| *b
== 0) or include other specific offsets) and keep MemoryNotZeroed, or (B)
introduce a more specific error variant (e.g.,
light_zero_copy::errors::ZeroCopyError::AccountAlreadyInitialized) and return
that when bytes.len() > STATE_OFFSET && bytes[STATE_OFFSET] != 0; update the
match/propagation sites accordingly to use the new error if option B is chosen.

Comment on lines +541 to +600
pub fn from_account_info_checked<'a>(
account_info: &pinocchio::account_info::AccountInfo,
) -> Result<ZCToken<'a>, crate::error::CTokenError> {
// 1. Check program ownership
if !account_info.is_owned_by(&crate::CTOKEN_PROGRAM_ID) {
return Err(crate::error::CTokenError::InvalidCTokenOwner);
}

let data = account_info
.try_borrow_data()
.map_err(|_| crate::error::CTokenError::BorrowFailed)?;

// Extend lifetime to 'a - safe because account data lives for transaction duration
let data_slice: &'a [u8] =
unsafe { core::slice::from_raw_parts(data.as_ptr(), data.len()) };

let (ctoken, remaining) = CToken::zero_copy_at_checked(data_slice)?;

// 4. Check no trailing bytes
if !remaining.is_empty() {
return Err(crate::error::CTokenError::InvalidAccountData);
}

Ok(ctoken)
}

/// Mutable version of from_account_info_checked.
/// Deserialize a CToken from account info with validation using zero-copy.
///
/// Checks:
/// 1. Account is owned by the CTOKEN program
/// 2. Account is initialized (state != 0)
/// 3. Account type is ACCOUNT_TYPE_TOKEN_ACCOUNT (byte 165 == 2)
/// 4. No trailing bytes after the CToken structure
#[inline(always)]
pub fn from_account_info_mut_checked<'a>(
account_info: &pinocchio::account_info::AccountInfo,
) -> Result<ZCTokenMut<'a>, crate::error::CTokenError> {
// 1. Check program ownership
if !account_info.is_owned_by(&crate::CTOKEN_PROGRAM_ID) {
return Err(crate::error::CTokenError::InvalidCTokenOwner);
}

let mut data = account_info
.try_borrow_mut_data()
.map_err(|_| crate::error::CTokenError::BorrowFailed)?;

// Extend lifetime to 'a - safe because account data lives for transaction duration
let data_slice: &'a mut [u8] =
unsafe { core::slice::from_raw_parts_mut(data.as_mut_ptr(), data.len()) };

let (ctoken, remaining) = CToken::zero_copy_at_mut_checked(data_slice)?;

// 4. Check no trailing bytes
if !remaining.is_empty() {
return Err(crate::error::CTokenError::InvalidAccountData);
}

Ok(ctoken)
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check CTOKEN_PROGRAM_ID definition
rg -n "CTOKEN_PROGRAM_ID" program-libs/ctoken-interface/src/ -A 2 -B 2

Repository: Lightprotocol/light-protocol

Length of output: 4881


🏁 Script executed:

#!/bin/bash
# Check the file structure and imports for zero_copy.rs
head -50 program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 1857


🏁 Script executed:

#!/bin/bash
# Check if this module is feature-gated for pinocchio
grep -n "feature" program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs | head -20

Repository: Lightprotocol/light-protocol

Length of output: 139


🏁 Script executed:

#!/bin/bash
# Look for AccountInfoTrait usage in ctoken-interface
rg -n "AccountInfoTrait" program-libs/ctoken-interface/ -A 3

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check the CLAUDE.md file for guidance on this crate
find program-libs -name "CLAUDE.md" -exec cat {} \;

Repository: Lightprotocol/light-protocol

Length of output: 17655


🏁 Script executed:

#!/bin/bash
# Examine the broader context of where these methods are implemented
sed -n '520,610p' program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 3598


🏁 Script executed:

#!/bin/bash
# Check if there's a feature gate on the entire zero_copy.rs module
head -20 program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs | grep -E "^#\[cfg"

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check how compressed_mint.rs handles AccountInfo to understand the pattern
sed -n '95,110p' program-libs/ctoken-interface/src/state/mint/compressed_mint.rs

Repository: Lightprotocol/light-protocol

Length of output: 814


🏁 Script executed:

#!/bin/bash
# Check what AccountInfo type is imported in zero_copy.rs
grep -n "use.*AccountInfo\|use.*account_info" program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check if AccountInfoTrait is available in ctoken-interface
rg -n "AccountInfoTrait" program-libs/ctoken-interface/src/ -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check the module path context - is this module pinocchio-specific?
head -1 program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Check if compressed_mint.rs also uses from_account_info_checked and how it's generic
sed -n '100,115p' program-libs/ctoken-interface/src/state/mint/compressed_mint.rs

Repository: Lightprotocol/light-protocol

Length of output: 795


🏁 Script executed:

#!/bin/bash
# Check the full imports in zero_copy.rs to understand the AccountInfo type
sed -n '1,30p' program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 1068


🏁 Script executed:

#!/bin/bash
# Look for other from_account_info methods in the crate to see if there's a pattern
rg -n "from_account_info" program-libs/ctoken-interface/src/ --type rs

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Look for other from_account_info methods in the crate
rg -n "from_account_info" program-libs/ctoken-interface/src/

Repository: Lightprotocol/light-protocol

Length of output: 891


🏁 Script executed:

#!/bin/bash
# Find where AccountInfo is imported from
rg -n "^use.*AccountInfo|^pub use.*AccountInfo" program-libs/ctoken-interface/src/

Repository: Lightprotocol/light-protocol

Length of output: 379


🏁 Script executed:

#!/bin/bash
# Check the ctoken-interface lib.rs to see what's being re-exported
cat program-libs/ctoken-interface/src/lib.rs | head -50

Repository: Lightprotocol/light-protocol

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Check if there's a cfg feature gate on the module where these methods are
grep -B 5 "pub fn from_account_info_checked" program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs | head -20

Repository: Lightprotocol/light-protocol

Length of output: 376


🏁 Script executed:

#!/bin/bash
# Check full imports of zero_copy.rs
sed -n '1,50p' program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs

Repository: Lightprotocol/light-protocol

Length of output: 1857


🏁 Script executed:

#!/bin/bash
# Check if ctoken-interface has feature flags that gate pinocchio usage
grep -n "feature\|pinocchio\|solana" program-libs/ctoken-interface/Cargo.toml

Repository: Lightprotocol/light-protocol

Length of output: 1091


🏁 Script executed:

#!/bin/bash
# Check the parent state module structure
ls -la program-libs/ctoken-interface/src/state/ctoken/

Repository: Lightprotocol/light-protocol

Length of output: 549


🏁 Script executed:

#!/bin/bash
# Check if ctoken-interface is intended to be pinocchio-only or if it has SDK abstraction needs
grep -r "AccountInfoTrait" program-libs/ctoken-interface/

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Look at the structure of compressed_mint.rs to see if it's also pinocchio-specific
sed -n '1,20p' program-libs/ctoken-interface/src/state/mint/compressed_mint.rs

Repository: Lightprotocol/light-protocol

Length of output: 727


🏁 Script executed:

#!/bin/bash
# Check the crate's readme or documentation for SDK requirements
find program-libs/ctoken-interface -name "README.md" -o -name "CLAUDE.md" | xargs cat 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Verify that CTOKEN_PROGRAM_ID is exported from crate root (it's in constants.rs)
rg -n "pub.*CTOKEN_PROGRAM_ID\|pub use.*CTOKEN_PROGRAM_ID" program-libs/ctoken-interface/src/ -A 1

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Search for usages of these new methods to understand the intended API
rg -n "from_account_info_checked|from_account_info_mut_checked" program-libs/ --max-count 20

Repository: Lightprotocol/light-protocol

Length of output: 569


🏁 Script executed:

#!/bin/bash
# Check if ctoken-interface is used in any instruction handlers
rg -n "use.*ctoken_interface" program-libs/ --max-count 20

Repository: Lightprotocol/light-protocol

Length of output: 1845


🏁 Script executed:

#!/bin/bash
# Look at the parent state/mod.rs to understand module organization
cat program-libs/ctoken-interface/src/state/mod.rs

Repository: Lightprotocol/light-protocol

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Check the crate dependencies - is pinocchio required or optional?
sed -n '8,50p' program-libs/ctoken-interface/Cargo.toml

Repository: Lightprotocol/light-protocol

Length of output: 1667


🏁 Script executed:

#!/bin/bash
# Look at how light-account-checks uses AccountInfoTrait - is it a dependency of ctoken-interface?
grep -n "light-account-checks\|AccountInfoTrait" program-libs/ctoken-interface/Cargo.toml

Repository: Lightprotocol/light-protocol

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Check if other ctoken methods in the same file also use pinocchio directly
rg -n "pinocchio::account_info" program-libs/ctoken-interface/src/state/ctoken/ -B 5 | head -40

Repository: Lightprotocol/light-protocol

Length of output: 2090


🏁 Script executed:

#!/bin/bash
# Examine the actual behavior of the unsafe lifetime extension - what does try_borrow_data return?
rg -n "fn try_borrow_data\|fn try_borrow_mut_data" program-libs/ -A 3 | head -50

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Find the definition of try_borrow_data in pinocchio package
rg -n "fn try_borrow_data\|fn try_borrow_mut_data" . -A 5 | grep -A 5 "pinocchio"

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check the pinocchio crate's account_info implementation
fd "account_info" program-libs -type f | xargs grep -l "try_borrow_data" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Since pinocchio is external, check if ctoken-interface has any tests that use these new methods
rg -n "from_account_info_checked\|from_account_info_mut_checked" program-libs/ctoken-interface/tests/

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check the SDK-specific implementation requirements in the learnings - look at the actual definition
find program-libs/account-checks -name "*.rs" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 369


🏁 Script executed:

#!/bin/bash
# Check the AccountInfoTrait definition
cat program-libs/account-checks/src/account_info/account_info_trait.rs | head -100

Repository: Lightprotocol/light-protocol

Length of output: 1745


🏁 Script executed:

#!/bin/bash
# Check the pinocchio implementation of AccountInfoTrait
cat program-libs/account-checks/src/account_info/pinocchio.rs | head -100

Repository: Lightprotocol/light-protocol

Length of output: 3413


🏁 Script executed:

#!/bin/bash
# Check if the pinocchio borrow guard drops at function end (understanding the lifetime issue)
rg -n "Ref\|RefMut" program-libs/account-checks/src/account_info/pinocchio.rs -A 2 | head -50

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Check how other crates in program-libs handle AccountInfo abstraction
rg -n "from_account_info" program-libs/ | grep -v ctoken-interface | head -20

Repository: Lightprotocol/light-protocol

Length of output: 2641


🏁 Script executed:

#!/bin/bash
# Check if ctoken-interface is documented in program-libs CLAUDE.md
find program-libs -name "CLAUDE.md" -exec grep -l "ctoken-interface\|ctoken" {} \;

Repository: Lightprotocol/light-protocol

Length of output: 145


🏁 Script executed:

#!/bin/bash
# Check the crate's design by looking at its README
find program-libs/ctoken-interface -name "README.md" -o -name "DESIGN.md" | xargs cat 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Confirm that light-account-checks is ONLY in dev-dependencies
sed -n '44,56p' program-libs/ctoken-interface/Cargo.toml

Repository: Lightprotocol/light-protocol

Length of output: 550


🏁 Script executed:

#!/bin/bash
# Check if ctoken-interface is meant to be SDK-specific or SDK-agnostic by looking at how it's used
rg -n "use light_ctoken_interface\|ctoken_interface::" program-libs/ | grep -v test | head -20

Repository: Lightprotocol/light-protocol

Length of output: 54


Critical unsound lifetime extension in new account-info methods.

The unsafe lifetime extension at lines 554-555 and 589-590 is a genuine safety issue. Both methods borrow account data via try_borrow_data() / try_borrow_mut_data(), which returns a pinocchio::account_info::Ref/RefMut borrow guard. You then extend this borrowed slice's lifetime to 'a using unsafe, allowing the reference to outlive the guard. When the borrow guard drops at function end, RefCell tracking ends, but your reference persists—bypassing Rust's borrow-checker guarantees. The safety comment claims account data is stable for the transaction duration, but that doesn't justify the unsound borrow pattern. If other code later attempts to access the same account data, aliasing violations become possible.

Suggested fix: Either (1) return a wrapper struct that holds the borrow guard alongside the slice, ensuring the guard lives as long as the reference, or (2) document and require that callers ensure exclusive access, accepting the transaction-level invariant as part of the API contract.

CTOKEN_PROGRAM_ID is properly defined in constants.rs and exported.

Comment on lines 44 to 69
// 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)
// 3. CMint is source of truth: read from CMint account (cmint_decompressed = true)
let mint = if parsed_instruction_data.create_mint.is_some() {
// 3. CMint is decompressed: read from CMint account (cmint_decompressed = true)
let mint = if accounts_config.create_mint {
// Creating new mint - mint data required in instruction
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else if let Some(mint_data) = parsed_instruction_data.mint.as_ref() {
} else if !accounts_config.cmint_decompressed {
// Existing compressed mint with data in instruction
// In case that cmint is not actually compressed proof verification will fail.
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else {
// CMint is source of truth - read from CMint account
// CMint is decompressed - read from CMint account
let cmint_account = validated_accounts
.get_cmint()
.ok_or(ErrorCode::MintActionMissingCMintAccount)?;
CompressedMint::from_account_info_checked(
&crate::LIGHT_CPI_SIGNER.program_id,
cmint_account,
)?
CompressedMint::from_account_info_checked(cmint_account)?
};
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

Consider consolidating duplicate mint data retrieval paths.

The logic for cases 1 (creating new mint) and 2 (existing compressed mint with data in instruction) are identical:

let mint_data = parsed_instruction_data
    .mint
    .as_ref()
    .ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?

These could be combined into a single branch:

let mint = if accounts_config.cmint_decompressed {
    // CMint is decompressed - read from CMint account
    CompressedMint::from_account_info_checked(validated_accounts.get_cmint()...)?
} else {
    // Creating new mint OR existing compressed mint - data from instruction
    let mint_data = parsed_instruction_data.mint.as_ref()
        .ok_or(ErrorCode::MintDataRequired)?;
    CompressedMint::try_from(mint_data)?
};

However, keeping them separate does make the three logical paths explicit, which aids readability. This is a minor style preference.

🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs
around lines 44 - 69, The code duplicates mint-data retrieval for the create
path and the existing compressed-mint-with-data path; consolidate by using
accounts_config.cmint_decompressed as the discriminant: if cmint_decompressed
use
CompressedMint::from_account_info_checked(validated_accounts.get_cmint()...)?
(ensuring get_cmint() is present or return
ErrorCode::MintActionMissingCMintAccount), otherwise read
parsed_instruction_data.mint.as_ref().ok_or(ErrorCode::MintDataRequired)? and
call CompressedMint::try_from(...)?, removing the duplicated block while
preserving the cmint_decompressed branch behavior.

Comment on lines +51 to 64
// Calculate CMint top-up using optimized function (owner check inside)
#[cfg(target_os = "solana")]
if let Some(amount) = cmint_top_up_lamports_from_account_info(cmint, &mut current_slot) {
transfers[0].amount = amount;
lamports_budget = lamports_budget.saturating_sub(amount);
}

// Calculate CToken top-up (only if not 165 bytes - 165 means no extensions)
if ctoken.data_len() != 165 {
let account_data = ctoken.try_borrow_data().map_err(convert_program_error)?;
let (token, _) = CToken::zero_copy_at_checked(&account_data)?;
// Check for Compressible extension
let compressible = token
.get_compressible_extension()
.ok_or::<ProgramError>(CTokenError::MissingCompressibleExtension.into())?;
if current_slot == 0 {
current_slot = Clock::get()
.map_err(|_| CTokenError::SysvarAccessError)?
.slot;
rent = Some(Rent::get().map_err(|_| CTokenError::SysvarAccessError)?);
}
let rent_exemption = rent.as_ref().unwrap().minimum_balance(ctoken.data_len());
transfers[1].amount = compressible
.info
.calculate_top_up_lamports(
ctoken.data_len() as u64,
current_slot,
ctoken.lamports(),
rent_exemption,
)
.map_err(|_| CTokenError::InvalidAccountData)?;
lamports_budget = lamports_budget.saturating_sub(transfers[1].amount);
// Calculate CToken top-up using optimized function
// Returns None if no Compressible extension (165 bytes or missing extension)
#[cfg(target_os = "solana")]
if let Some(amount) = top_up_lamports_from_account_info_unchecked(ctoken, &mut current_slot) {
transfers[1].amount = amount;
lamports_budget = lamports_budget.saturating_sub(amount);
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full function to understand its structure and the #[allow(unused)] placement
cd programs/compressed-token/program/src/shared
head -80 compressible_top_up.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 3467


🏁 Script executed:

#!/bin/bash
# Also check if there are any cfg attributes and how current_slot is used
rg -n "current_slot|#\[allow\(unused" programs/compressed-token/program/src/shared/compressible_top_up.rs -A 2 -B 2

Repository: Lightprotocol/light-protocol

Length of output: 1750


🏁 Script executed:

#!/bin/bash
# Search for how this function is called to understand intended usage contexts
rg -n "calculate_and_execute_compressible_top_ups" programs/compressed-token/program/src/ -A 5 -B 2

Repository: Lightprotocol/light-protocol

Length of output: 1926


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation in CLAUDE.md about compressible_top_up.rs
find programs/compressed-token -name "CLAUDE.md" -o -name "*README*" | xargs grep -l "compressible_top_up" 2>/dev/null || echo "No specific docs found"

Repository: Lightprotocol/light-protocol

Length of output: 115


🏁 Script executed:

#!/bin/bash
# Check the CLAUDE.md for guidance on compressible_top_up.rs
cat programs/compressed-token/program/CLAUDE.md | head -100

Repository: Lightprotocol/light-protocol

Length of output: 5527


🏁 Script executed:

#!/bin/bash
# Search for any test files that test this function
fd -e rs "test" programs/compressed-token/program/src/ --type f | xargs grep -l "calculate_and_execute_compressible_top_ups" 2>/dev/null || echo "No test files found for this function"

Repository: Lightprotocol/light-protocol

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Check if the function is tested in integration tests
find programs/compressed-token -name "*test*" -type f | xargs grep -l "calculate_and_execute_compressible_top_ups" 2>/dev/null || echo "No integration tests found"

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Check documentation about Compressible extension and top-ups
find programs/compressed-token/program/docs -name "*.md" | xargs grep -l "top.up\|Compressible" 2>/dev/null | head -5

Repository: Lightprotocol/light-protocol

Length of output: 378


🏁 Script executed:

#!/bin/bash
# Check if there's specific documentation about rent top-ups
cat programs/compressed-token/program/docs/compressible/CLAIM.md | head -80

Repository: Lightprotocol/light-protocol

Length of output: 3967


🏁 Script executed:

#!/bin/bash
# Check how the function behaves with the profile macro and inline attributes
rg -n "profile|inline" programs/compressed-token/program/src/shared/compressible_top_up.rs | head -10

Repository: Lightprotocol/light-protocol

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Verify the function is actually exported and used (not dead code itself)
rg -n "pub.*fn calculate_and_execute_compressible_top_ups|use.*calculate_and_execute_compressible_top_ups" programs/compressed-token/program/src/ --type rust

Repository: Lightprotocol/light-protocol

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Check if there are other usages of this function we might have missed
rg "calculate_and_execute_compressible_top_ups" programs/compressed-token/program/ --type rust

Repository: Lightprotocol/light-protocol

Length of output: 481


Consider documenting the on-chain-only behavior of this function.

The #[cfg(target_os = "solana")] gates confirm that on non-Solana targets (e.g., tests, WASM), the top-up calculations are completely skipped—cmint and ctoken parameters remain unused, current_slot stays at 0, and the function returns Ok(()) at line 68. While the existing doc comment is good, adding a note about this platform-specific behavior would clarify why #[allow(unused)] is necessary and help maintainers understand that this is intentional on-chain-only logic.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs (1)

315-322: Address or remove TODO comment.

The TODO at line 315 suggests this validation might be simplified or moved to the decompress action processor. While the current implementation is correct, leaving TODOs in the codebase creates technical debt.

Can you clarify whether this validation is correctly placed, or should it be moved to the decompress action processor as the TODO suggests?

🤖 Fix all issues with AI agents
In @program-libs/ctoken-interface/src/state/mint/top_up.rs:
- Around line 11-23: Add a compile-time/test assertion to ensure the hard-coded
layout constants (CMINT_MIN_SIZE_WITH_COMPRESSION, COMPRESSION_INFO_OFFSET,
COMPRESSION_INFO_SIZE, ACCOUNT_TYPE_OFFSET) remain correct vs the actual struct
definitions; specifically verify COMPRESSION_INFO_SIZE equals
std::mem::size_of::<light_compressible::compression_info::CompressionInfo>() and
verify COMPRESSION_INFO_OFFSET (and ACCOUNT_TYPE_OFFSET if applicable) using an
offset check (e.g., offset_of or computing field offsets) against the real
CMint/CompressedMintMetadata structs in a #[cfg(test)] test or a const_assert so
changes to struct fields surface as test/compile failures.

In @programs/compressed-token/anchor/src/process_transfer.rs:
- Around line 662-664: Replace the panicking unimplemented!() calls guarded by
CHECK_TLV (the checks on input_token_data.tlv) with a proper Anchor error
return: add a new error variant TlvNotSupported to your ErrorCode enum (e.g.,
#[msg("TLV extensions are not yet supported for this instruction")]
TlvNotSupported) and change the checks to return
Err(ErrorCode::TlvNotSupported.into()) instead of calling unimplemented!();
update both occurrences (the one at the shown diff and the similar check around
lines 97-99) so the function returns a controlled, user-friendly error rather
than panicking.
- Around line 687-704: The comment for
get_input_compressed_accounts_with_merkle_context_and_check_signer_for_freeze is
too terse about why TLV validation is skipped; update the doc comment to state
that this freeze/thaw variant intentionally passes CHECK_TLV=false to
get_input_compressed_accounts_with_merkle_context_and_check_signer_inner::<IS_FROZEN,
false> so TLV parsing/validation is deferred to the higher-level freeze/thaw
flow (which handles TLV atomically with state transitions and avoids duplicate
checks), and note any safety assumptions (e.g., caller must perform TLV
validation before finalizing) so maintainers understand the delegation and
responsibilities.

In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 305-309: The log message is inconsistent: the code compares
compression.info.rent_sponsor to *rent_sponsor.key() but prints "rent recipient
mismatch"; change the message to accurately reflect the check (e.g., replace
msg!("rent recipient mismatch") with msg!("rent sponsor mismatch") so
compression.info.rent_sponsor and rent_sponsor.key() are correctly described).

In @programs/compressed-token/program/src/shared/compressible_top_up.rs:
- Around line 51-64: The Solana-only top-up calls
(cmint_top_up_lamports_from_account_info and
top_up_lamports_from_account_info_unchecked) are conditionally compiled with
#[cfg(target_os = "solana")], leaving current_slot at 0 and transfers[*].amount
as 0 on non-Solana targets which causes an implicit early exit; update the file
by either adding a clear comment above these blocks stating that top-up
calculations are intentionally skipped on non-Solana targets for off-chain
testing, or add an explicit #[cfg(not(target_os = "solana"))] stub block that
documents this behavior (referencing current_slot and transfers) so the intent
is explicit.

Comment on lines +11 to +23
/// Minimum size for CMint with CompressionInfo.
/// 166 (offset to CompressionInfo) + 96 (CompressionInfo size) = 262
pub const CMINT_MIN_SIZE_WITH_COMPRESSION: usize = COMPRESSION_INFO_OFFSET + COMPRESSION_INFO_SIZE;

/// Offset to CompressionInfo in CMint.
/// 82 (BaseMint) + 66 (metadata) + 17 (reserved) + 1 (account_type) = 166
const COMPRESSION_INFO_OFFSET: usize = 166;

/// Size of CompressionInfo struct (96 bytes).
const COMPRESSION_INFO_SIZE: usize = 96;

/// Offset to account_type field.
const ACCOUNT_TYPE_OFFSET: usize = 165;
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

Well-documented constants for CMint layout navigation.

The constants are correctly calculated based on the updated struct layouts:

  • COMPRESSION_INFO_OFFSET = 166: BaseMint (82) + CompressedMintMetadata (66) + reserved (17) + account_type (1)
  • COMPRESSION_INFO_SIZE = 96: The updated CompressionInfo size including rent_exemption_paid and _reserved
  • CMINT_MIN_SIZE_WITH_COMPRESSION = 262: Offset + Size

Consider adding a static assertion or test to verify these offsets match the actual struct layout, preventing silent breakage if struct fields change.

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn verify_compression_info_offset() {
        // Verify offset matches actual struct layout
        assert_eq!(COMPRESSION_INFO_SIZE, std::mem::size_of::<light_compressible::compression_info::CompressionInfo>());
    }
}
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/mint/top_up.rs around lines 11 - 23,
Add a compile-time/test assertion to ensure the hard-coded layout constants
(CMINT_MIN_SIZE_WITH_COMPRESSION, COMPRESSION_INFO_OFFSET,
COMPRESSION_INFO_SIZE, ACCOUNT_TYPE_OFFSET) remain correct vs the actual struct
definitions; specifically verify COMPRESSION_INFO_SIZE equals
std::mem::size_of::<light_compressible::compression_info::CompressionInfo>() and
verify COMPRESSION_INFO_OFFSET (and ACCOUNT_TYPE_OFFSET if applicable) using an
offset check (e.g., offset_of or computing field offsets) against the real
CMint/CompressedMintMetadata structs in a #[cfg(test)] test or a const_assert so
changes to struct fields surface as test/compile failures.

Comment on lines +662 to +664
if CHECK_TLV && input_token_data.tlv.is_some() {
unimplemented!("Tlv is unimplemented.");
}
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

**Consider proper error handling instead of unimplemented!().

While gating the TLV check with CHECK_TLV is correct for differentiating freeze from transfer flows, using unimplemented!() will cause a panic with a stack trace rather than returning a user-friendly error. This also applies to the similar check at lines 97-99.

♻️ Proposed fix for better error handling
-        if CHECK_TLV && input_token_data.tlv.is_some() {
-            unimplemented!("Tlv is unimplemented.");
-        }
+        if CHECK_TLV && input_token_data.tlv.is_some() {
+            return err!(crate::ErrorCode::TlvNotSupported);
+        }

This requires adding a new error variant:

#[error_code]
pub enum ErrorCode {
    // ... existing variants ...
    #[msg("TLV extensions are not yet supported for this instruction")]
    TlvNotSupported,
}
🤖 Prompt for AI Agents
In @programs/compressed-token/anchor/src/process_transfer.rs around lines 662 -
664, Replace the panicking unimplemented!() calls guarded by CHECK_TLV (the
checks on input_token_data.tlv) with a proper Anchor error return: add a new
error variant TlvNotSupported to your ErrorCode enum (e.g., #[msg("TLV
extensions are not yet supported for this instruction")] TlvNotSupported) and
change the checks to return Err(ErrorCode::TlvNotSupported.into()) instead of
calling unimplemented!(); update both occurrences (the one at the shown diff and
the similar check around lines 97-99) so the function returns a controlled,
user-friendly error rather than panicking.

Comment on lines +687 to +704
/// Get input compressed accounts - for freeze/thaw (skips TLV check, handles it separately)
pub fn get_input_compressed_accounts_with_merkle_context_and_check_signer_for_freeze<
const IS_FROZEN: bool,
>(
signer: &Pubkey,
signer_is_delegate: &Option<DelegatedTransfer>,
remaining_accounts: &[AccountInfo<'_>],
input_token_data_with_context: &[InputTokenDataWithContext],
mint: &Pubkey,
) -> Result<(Vec<InAccount>, Vec<TokenData>, u64)> {
get_input_compressed_accounts_with_merkle_context_and_check_signer_inner::<IS_FROZEN, false>(
signer,
signer_is_delegate,
remaining_accounts,
input_token_data_with_context,
mint,
)
}
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

Consider enhancing documentation for the freeze-specific variant.

The function correctly delegates to the inner implementation with CHECK_TLV=false, allowing freeze/thaw operations to handle TLV validation at a higher level. However, the comment could better explain why TLV checking is skipped here.

📝 Suggested documentation enhancement
-/// Get input compressed accounts - for freeze/thaw (skips TLV check, handles it separately)
+/// Get input compressed accounts for freeze/thaw operations.
+///
+/// This variant skips the inner TLV validation check because freeze/thaw operations
+/// perform TLV validation at a higher level (see freeze.rs lines 115-122), where
+/// version compatibility is checked before processing.
 pub fn get_input_compressed_accounts_with_merkle_context_and_check_signer_for_freeze<
     const IS_FROZEN: bool,
 >(
🤖 Prompt for AI Agents
In @programs/compressed-token/anchor/src/process_transfer.rs around lines 687 -
704, The comment for
get_input_compressed_accounts_with_merkle_context_and_check_signer_for_freeze is
too terse about why TLV validation is skipped; update the doc comment to state
that this freeze/thaw variant intentionally passes CHECK_TLV=false to
get_input_compressed_accounts_with_merkle_context_and_check_signer_inner::<IS_FROZEN,
false> so TLV parsing/validation is deferred to the higher-level freeze/thaw
flow (which handles TLV atomically with state transitions and avoids duplicate
checks), and note any safety assumptions (e.g., caller must perform TLV
validation before finalizing) so maintainers understand the delegation and
responsibilities.

Comment on lines +48 to 69
let mint = if accounts_config.create_mint {
// Creating new mint - mint data required in instruction
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else if let Some(mint_data) = parsed_instruction_data.mint.as_ref() {
} else if !accounts_config.cmint_decompressed {
// Existing compressed mint with data in instruction
// In case that cmint is not actually compressed proof verification will fail.
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else {
// CMint is source of truth - read from CMint account
// CMint is decompressed - read from CMint account
let cmint_account = validated_accounts
.get_cmint()
.ok_or(ErrorCode::MintActionMissingCMintAccount)?;
CompressedMint::from_account_info_checked(
&crate::LIGHT_CPI_SIGNER.program_id,
cmint_account,
)?
CompressedMint::from_account_info_checked(cmint_account)?
};
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

Clarify the comment at line 57.

The logic for determining the mint data source is correct, but the comment at line 57 is confusing:

"In case that cmint is not actually compressed proof verification will fail."

This branch handles the case where the CMint IS compressed (not decompressed), and we're reading mint data from the instruction. The comment seems to describe an error case but phrases it in a way that suggests the opposite of the branch condition.

📝 Suggested comment improvement
 } else if !accounts_config.cmint_decompressed {
     // Existing compressed mint with data in instruction
-    // In case that cmint is not actually compressed proof verification will fail.
+    // Note: If caller incorrectly provides cmint_decompressed=false when mint is actually decompressed,
+    // proof verification will fail due to hash mismatch.
     let mint_data = parsed_instruction_data
         .mint
         .as_ref()
         .ok_or(ErrorCode::MintDataRequired)?;
     CompressedMint::try_from(mint_data)?

Alternatively, remove the comment entirely if it's not adding value.

📝 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
let mint = if accounts_config.create_mint {
// Creating new mint - mint data required in instruction
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else if let Some(mint_data) = parsed_instruction_data.mint.as_ref() {
} else if !accounts_config.cmint_decompressed {
// Existing compressed mint with data in instruction
// In case that cmint is not actually compressed proof verification will fail.
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else {
// CMint is source of truth - read from CMint account
// CMint is decompressed - read from CMint account
let cmint_account = validated_accounts
.get_cmint()
.ok_or(ErrorCode::MintActionMissingCMintAccount)?;
CompressedMint::from_account_info_checked(
&crate::LIGHT_CPI_SIGNER.program_id,
cmint_account,
)?
CompressedMint::from_account_info_checked(cmint_account)?
};
let mint = if accounts_config.create_mint {
// Creating new mint - mint data required in instruction
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else if !accounts_config.cmint_decompressed {
// Existing compressed mint with data in instruction
// Note: If caller incorrectly provides cmint_decompressed=false when mint is actually decompressed,
// proof verification will fail due to hash mismatch.
let mint_data = parsed_instruction_data
.mint
.as_ref()
.ok_or(ErrorCode::MintDataRequired)?;
CompressedMint::try_from(mint_data)?
} else {
// CMint is decompressed - read from CMint account
let cmint_account = validated_accounts
.get_cmint()
.ok_or(ErrorCode::MintActionMissingCMintAccount)?;
CompressedMint::from_account_info_checked(cmint_account)?
};

Comment on lines +305 to +309
// Validate rent_sponsor matches
if compression.info.rent_sponsor != *rent_sponsor.key() {
msg!("rent recipient mismatch");
return Err(ProgramError::InvalidAccountData);
}
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

Error message terminology inconsistency.

The error message says "rent recipient mismatch" but the code validates rent_sponsor. This inconsistency could confuse debugging.

Suggested fix
     // Validate rent_sponsor matches
     if compression.info.rent_sponsor != *rent_sponsor.key() {
-        msg!("rent recipient mismatch");
+        msg!("rent sponsor mismatch");
         return Err(ProgramError::InvalidAccountData);
     }
📝 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
// Validate rent_sponsor matches
if compression.info.rent_sponsor != *rent_sponsor.key() {
msg!("rent recipient mismatch");
return Err(ProgramError::InvalidAccountData);
}
// Validate rent_sponsor matches
if compression.info.rent_sponsor != *rent_sponsor.key() {
msg!("rent sponsor mismatch");
return Err(ProgramError::InvalidAccountData);
}
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
around lines 305 - 309, The log message is inconsistent: the code compares
compression.info.rent_sponsor to *rent_sponsor.key() but prints "rent recipient
mismatch"; change the message to accurately reflect the check (e.g., replace
msg!("rent recipient mismatch") with msg!("rent sponsor mismatch") so
compression.info.rent_sponsor and rent_sponsor.key() are correctly described).

if u64::from(ctoken.amount) != 0 {
return Err(ErrorCode::NonNativeHasBalance.into());
}
// Note: Non-zero transfer fees are not yet supported. If fees != 0 support is added:
Copy link
Contributor

Choose a reason for hiding this comment

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

is this inline comment needed? Implementation detail for code that does not exist

MintData::BetweenSizes(size) => vec![0u8; *size],
MintData::WithAccountType(account_type) => {
let mut data = vec![0u8; 170];
data[165] = *account_type;
Copy link
Contributor

Choose a reason for hiding this comment

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

consider using the const here


// Validate rent_sponsor matches
if compression.info.rent_sponsor != *rent_sponsor.key() {
msg!("rent recipient mismatch");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
msg!("rent recipient mismatch");
msg!("rent sponsor mismatch");

// 7a.1. Store rent exemption at creation (only query Rent sysvar here, never again)
let rent_exemption_paid = Rent::get()
.map_err(|_| ProgramError::UnsupportedSysvar)?
.minimum_balance(account_size) as u32;
Copy link
Contributor

Choose a reason for hiding this comment

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

consider

#2164 (comment)

.try_borrow_mut_data()
.map_err(|_| crate::error::CTokenError::BorrowFailed)?;

// Extend lifetime to 'a - safe because account data lives for transaction duration
Copy link
Contributor

Choose a reason for hiding this comment

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

consider sanity checking:

#2164 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its fine

@ananas-block ananas-block changed the title fix: cmint instruction data fix: cmint instruction data & cleanup Jan 12, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/compressed-token/program/docs/compressed_token/TRANSFER2.md (1)

78-112: Restructure account list to fix numbering.

The account list has a structural issue: items 1-2 appear before the "System accounts" heading (lines 73-82), then items 3-10 appear after it (lines 83-112). Markdown interpreters see this as a broken list and expect the numbering to restart after the heading.

This creates confusion about the account ordering and makes the documentation harder to parse programmatically.

♻️ Suggested restructure

Option 1: Use a single continuous list without the intermediate heading:

**Accounts:**
1. light_system_program
   - non-mutable
   - Light Protocol system program for compressed account operations
   - Optional if no_compressed_accounts (only decompressed operations)

2. fee_payer (signer, mutable)
   - Pays transaction fees and rent for new compressed accounts

3. cpi_authority_pda
   - PDA for CPI calls to light system program
   - Seeds: [CPI_AUTHORITY_SEED]
...

Option 2: Separate into two distinct lists with clear context:

**Accounts:**

**Required for all operations:**
1. light_system_program (optional if no_compressed_accounts)
2. fee_payer (signer, mutable)

**System accounts (when compressed accounts involved):**
1. cpi_authority_pda
2. registered_program_pda
...
🤖 Fix all issues with AI agents
In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md:
- Around line 118-120: The fenced code block for computing
no_compressed_accounts is missing surrounding blank lines and a language
identifier; update the markdown so there's a blank line before and after the
fenced block and add the Rust language tag (```rust) to the fence, preserving
the expression using the existing identifiers no_compressed_accounts,
in_token_data, and out_token_data.
- Around line 371-373: Remove the internal implementation detail that says
"Collects all transfers into a 40-element array indexed by packed account index"
and any mention of internal buffer/indexing; instead keep the user-facing
behavior: state that transfers to the same account are deduplicated and
combined, and that there is a hard limit of 32 compressions per instruction
(error 6095). Update the bullets in TRANSFER2.md to remove the "40-element
array" and indexing wording and ensure the remaining text clearly notes
deduplication and the 32-compression limit (include the error code 6095).
- Around line 353-360: Update the phrasing for the is_ata validation: replace
the ambiguous reference to `compressible_extension.is_ata()` with a clear
mention that the check compares the compression instruction data's `is_ata`
field to the output CompressedOnly extension's `is_ata()` field (i.e., "Must
match between the compression instruction data's `is_ata` and
`extension.is_ata()`"). Also add a brief rationale stating that CompressedOnly
is required for `is_ata=true` accounts because the extension preserves `bump`
and `owner_index` needed to reconstruct the ATA PDA during decompression.
- Around line 7-14: The nav table links point to anchors that don’t exist
because several sections are written as bold/unformatted text instead of
headers; locate the existing content for Path B (around the current Path B
layout), SPL token compression (SPL compression), CToken compression (CToken
compression), CPI context write/execute (CPI context write path and CPI context
support for cross-program invocations), CompressAndClose (for-compressandclose)
and Errors (error reference) and convert those lines into proper markdown
headers (use ### followed by the exact anchor text used in the nav table) so the
links (#path-b-with-compressed-accounts-full-transfer-operations,
#spl-token-compressiondecompression,
#ctoken-compressiondecompression-srctransfer2compressionctoken,
#cpi-context-write-path, #cpi-context-support-for-cross-program-invocations,
#for-compressandclose, #errors) resolve correctly.

Comment on lines +7 to +14
| Understand which accounts to pass | → [Path Selection and Account Requirements](#path-selection-and-account-requirements) |
| Transfer compressed tokens | → [Path B](#path-b-with-compressed-accounts-full-transfer-operations) + [System accounts](#system-accounts-when-compressed-accounts-involved) |
| Only compress/decompress (no transfers) | → [Path A](#path-a-no-compressed-accounts-compressions-only-operations) + [Compressions-only accounts](#compressions-only-accounts-path-a-when-no_compressed_accountstrue) |
| Compress SPL tokens | → [SPL compression](#spl-token-compressiondecompression) |
| Compress CToken accounts | → [CToken compression](#ctoken-compressiondecompression-srctransfer2compressionctoken) |
| Close compressible account (forester) | → [CompressAndClose](#for-compressandclose) - compression_authority only |
| Use CPI context | → [Write mode](#cpi-context-write-path) or [Execute mode](#cpi-context-support-for-cross-program-invocations) |
| Debug errors | → [Error reference](#errors) |
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Extract all section headers and generate expected anchors
echo "=== Section Headers in TRANSFER2.md ==="
grep -n "^##" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -30

echo ""
echo "=== Extracting anchor links from navigation table (lines 7-14) ==="
sed -n '7,14p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Function to generate GitHub anchor from header ==="
python3 << 'PYTHON_SCRIPT'
import re

# List of headers to check
headers_and_anchors = [
    ("Path Selection and Account Requirements", "#path-selection-and-account-requirements"),
    ("Path B - With Compressed Accounts (Full Transfer Operations)", "#path-b-with-compressed-accounts-full-transfer-operations"),
    ("System Accounts - When Compressed Accounts Involved", "#system-accounts-when-compressed-accounts-involved"),
    ("Path A - No Compressed Accounts (Compressions-Only Operations)", "#path-a-no-compressed-accounts-compressions-only-operations"),
    ("Compressions-Only Accounts - Path A (when no_compressed_accounts=true)", "#compressions-only-accounts-path-a-when-no_compressed_accountstrue"),
    ("SPL Token Compression/Decompression", "#spl-token-compressiondecompression"),
    ("CToken Compression/Decompression", "#ctoken-compressiondecompression-srctransfer2compressionctoken"),
    ("CompressAndClose", "#for-compressandclose"),
    ("CPI Context - Write Path", "#cpi-context-write-path"),
    ("CPI Context - Support for Cross-Program Invocations", "#cpi-context-support-for-cross-program-invocations"),
    ("Errors", "#errors"),
]

def generate_anchor(header):
    # Convert to lowercase
    anchor = header.lower()
    # Replace spaces with hyphens
    anchor = re.sub(r'\s+', '-', anchor)
    # Remove special characters except hyphens
    anchor = re.sub(r'[^a-z0-9-]', '', anchor)
    # Remove multiple consecutive hyphens
    anchor = re.sub(r'-+', '-', anchor)
    # Remove leading/trailing hyphens
    anchor = anchor.strip('-')
    return f"#{anchor}"

print("Header vs Expected Anchor (GitHub auto-generated)")
print("-" * 80)
for header, nav_anchor in headers_and_anchors:
    expected = generate_anchor(header)
    match = "✓" if expected == nav_anchor else "✗"
    print(f"{match} '{header}'")
    print(f"  Expected: {expected}")
    print(f"  Nav link: {nav_anchor}")
    if expected != nav_anchor:
        print(f"  MISMATCH!")
    print()
PYTHON_SCRIPT

Repository: Lightprotocol/light-protocol

Length of output: 158


🏁 Script executed:

#!/bin/bash

# First, just get the headers from the file
echo "=== All Section Headers in TRANSFER2.md ==="
grep -n "^## " programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Navigation table links (lines 7-14) ==="
sed -n '7,14p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

Repository: Lightprotocol/light-protocol

Length of output: 1191


🏁 Script executed:

#!/bin/bash

# Check for all levels of headers
echo "=== All Headers (## and ###) in TRANSFER2.md ==="
grep -n "^#" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -50

echo ""
echo "=== Total line count ==="
wc -l programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

Repository: Lightprotocol/light-protocol

Length of output: 377


🏁 Script executed:

#!/bin/bash

# Check if there are HTML anchor tags that might not show up as headers
echo "=== Looking for HTML anchors ==="
grep -n "<a id=" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Check section structure around line 115-150 ==="
sed -n '110,160p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -40

Repository: Lightprotocol/light-protocol

Length of output: 2293


🏁 Script executed:

#!/bin/bash

# Search for the referenced section names in the document
echo "=== Searching for section terms mentioned in navigation ==="
echo "Looking for 'Path B' references:"
grep -n "Path B" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -5

echo ""
echo "Looking for 'Errors' references:"
grep -n "Errors" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -5

echo ""
echo "Looking for 'CPI' references:"
grep -n "CPI" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -5

echo ""
echo "Looking for 'Compress' related references:"
grep -n "Compress" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -5

echo ""
echo "=== Full header list with line numbers ==="
grep -n "^###" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

Repository: Lightprotocol/light-protocol

Length of output: 2424


🏁 Script executed:

#!/bin/bash

# Let's see the full structure - check what's between headers
echo "=== Document structure overview ==="
grep -n "^###" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | cat -n
echo ""
echo "Lines 375-429 (end of document) to see if Errors section exists:"
tail -60 programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | head -40

Repository: Lightprotocol/light-protocol

Length of output: 4452


🏁 Script executed:

#!/bin/bash

# Let's check if the referenced sections exist as content but without proper headers
echo "=== Searching for 'Path B' content (should be full section) ==="
sed -n '120,180p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Searching around 'SPL compression' term ==="
grep -n "SPL\|CToken\|CPI Context" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md | grep -E "(###|SPL Token|CToken|CPI Context)"

Repository: Lightprotocol/light-protocol

Length of output: 6315


🏁 Script executed:

#!/bin/bash

# Check the complete structure - see what's at line 261, 286, 296 where content exists
echo "=== Content around line 261 (CPI Context) ==="
sed -n '258,265p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Content around line 286-296 (SPL/CToken compression) ==="
sed -n '283,300p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

Repository: Lightprotocol/light-protocol

Length of output: 1787


🏁 Script executed:

#!/bin/bash

# Get ALL headers in the document
echo "=== All headers (any level) ==="
grep -n "^#" programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

echo ""
echo "=== Check line 375 - is 'Errors' a header? ==="
sed -n '375p' programs/compressed-token/program/docs/compressed_token/TRANSFER2.md

Repository: Lightprotocol/light-protocol

Length of output: 321


Add missing section headers to support the navigation table.

The document references multiple sections in the navigation table (lines 7-14) via anchor links, but most lack corresponding markdown headers. Only 2 of approximately 11 navigation links would work:

  • #path-selection-and-account-requirements → ✓ Header exists (line 115)
  • #compressions-only-accounts-path-a-when-no_compressed_accountstrue → ✓ Header exists (line 150)
  • All others (#path-b-with-compressed-accounts-full-transfer-operations, #spl-token-compressiondecompression, #ctoken-compressiondecompression-srctransfer2compressionctoken, #cpi-context-write-path, #cpi-context-support-for-cross-program-invocations, #errors, etc.) → ✗ No corresponding ### headers

The content for these sections exists in the document (Path B layout around lines 120–148, SPL compression at line 286, CToken compression at line 296, CPI context at line 261, and errors at line 375), but they need to be converted from bold text or unnumbered content into proper markdown headers (###) so the navigation links resolve correctly.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

8-8: Link fragments should be valid

(MD051, link-fragments)


8-8: Link fragments should be valid

(MD051, link-fragments)


9-9: Link fragments should be valid

(MD051, link-fragments)


10-10: Link fragments should be valid

(MD051, link-fragments)


11-11: Link fragments should be valid

(MD051, link-fragments)


12-12: Link fragments should be valid

(MD051, link-fragments)


13-13: Link fragments should be valid

(MD051, link-fragments)


13-13: Link fragments should be valid

(MD051, link-fragments)


14-14: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md around
lines 7 - 14, The nav table links point to anchors that don’t exist because
several sections are written as bold/unformatted text instead of headers; locate
the existing content for Path B (around the current Path B layout), SPL token
compression (SPL compression), CToken compression (CToken compression), CPI
context write/execute (CPI context write path and CPI context support for
cross-program invocations), CompressAndClose (for-compressandclose) and Errors
(error reference) and convert those lines into proper markdown headers (use ###
followed by the exact anchor text used in the nav table) so the links
(#path-b-with-compressed-accounts-full-transfer-operations,
#spl-token-compressiondecompression,
#ctoken-compressiondecompression-srctransfer2compressionctoken,
#cpi-context-write-path, #cpi-context-support-for-cross-program-invocations,
#for-compressandclose, #errors) resolve correctly.

- `pool_account_index`: u8 - For SPL: pool account index; For CompressAndClose: rent_sponsor_index
- `pool_index`: u8 - For SPL: pool index; For CompressAndClose: compressed_account_index
- `bump`: u8 - For SPL: pool PDA bump; For CompressAndClose: destination_index
- `decimals`: u8 - For SPL: decimals for transfer_checked; For CompressAndClose: rent_sponsor_is_signer flag (non-zero = true)
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

Clarify dual-purpose semantics of the decimals field.

The decimals field in the Compression struct serves different purposes depending on the compression mode:

  • SPL tokens: Contains decimals for transfer_checked validation
  • CompressAndClose: Repurposed as a rent_sponsor_is_signer flag (non-zero = true)

While this field reuse is space-efficient, the semantics are non-obvious and could lead to errors. Consider adding a warning or example showing correct usage for each mode.

📝 Suggested documentation enhancement

Add an example section:

**Examples:**
- Compress SPL tokens with 6 decimals: `decimals: 6`
- Decompress SPL tokens with 9 decimals: `decimals: 9`
- CompressAndClose with signer rent sponsor: `decimals: 1` (or any non-zero value)
- CompressAndClose with non-signer rent sponsor: `decimals: 0`

Comment on lines +118 to +120
```
no_compressed_accounts = in_token_data.is_empty() && out_token_data.is_empty()
```
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

Fix code block formatting.

The code block showing the no_compressed_accounts calculation has two formatting issues flagged by markdownlint:

  1. Missing blank lines before and after the fenced code block
  2. Missing language identifier for syntax highlighting
🔧 Proposed fix
 
-**Path selection logic:** The instruction determines which path to execute based on the `no_compressed_accounts` flag computed from instruction data:
-```
-no_compressed_accounts = in_token_data.is_empty() && out_token_data.is_empty()
-```
-
+**Path selection logic:** The instruction determines which path to execute based on the `no_compressed_accounts` flag computed from instruction data:
+
+```rust
+no_compressed_accounts = in_token_data.is_empty() && out_token_data.is_empty()
+```
+
 When `no_compressed_accounts=true`, the instruction executes **Path A** (compressions-only).
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

118-118: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


118-118: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md around
lines 118 - 120, The fenced code block for computing no_compressed_accounts is
missing surrounding blank lines and a language identifier; update the markdown
so there's a blank line before and after the fenced block and add the Rust
language tag (```rust) to the fence, preserving the expression using the
existing identifiers no_compressed_accounts, in_token_data, and out_token_data.

Comment on lines +353 to +360
- If account has `compression_only` flag set (restricted mint) or `is_ata` flag set (ATA accounts), CompressedOnly extension is REQUIRED in output TLV
- CompressedOnly extension preserves: `is_frozen`, `delegated_amount`, `delegate` (in token_data), `withheld_transfer_fee`, `is_ata`
- Delegate: Must match between ctoken.delegate and compressed output delegate
- Delegated amount: Must match between ctoken.delegated_amount and extension.delegated_amount
- Frozen state: Must match between ctoken.state==2 and extension.is_frozen
- Withheld fee: Must match between ctoken TransferFeeAccount.withheld_amount and extension.withheld_transfer_fee
- Error: `CompressAndCloseDelegatedAmountMismatch`, `CompressAndCloseInvalidDelegate`, `CompressAndCloseFrozenMismatch`, `CompressAndCloseWithheldFeeMismatch`
- is_ata: Must match between compressible_extension.is_ata() and extension.is_ata()
- Error: `CompressAndCloseDelegatedAmountMismatch`, `CompressAndCloseInvalidDelegate`, `CompressAndCloseFrozenMismatch`, `CompressAndCloseWithheldFeeMismatch`, `CompressAndCloseIsAtaMismatch`
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for is_ata implementation details

echo "=== 1. CompressedOnly struct definition ==="
rg -n "struct CompressedOnly" programs/compressed-token/ -A 20

echo ""
echo "=== 2. is_ata method implementations ==="
rg -n "fn is_ata" programs/compressed-token/ -A 5

echo ""
echo "=== 3. CompressAndCloseIsAtaMismatch error handling ==="
rg -n "CompressAndCloseIsAtaMismatch" programs/compressed-token/ -B 3 -A 3

echo ""
echo "=== 4. is_ata flag usage in validation ==="
rg -n "is_ata()" programs/compressed-token/ -B 2 -A 2 | head -40

echo ""
echo "=== 5. Extension TLV handling ==="
rg -n "CompressedOnly" programs/compressed-token/ -B 2 -A 2 | grep -E "(TLV|extension|Extension)" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 16481


🏁 Script executed:

# Quick verification of is_ata determination at creation
echo "=== Checking how is_ata is determined during account creation ==="
rg -n "is_ata.*true" programs/compressed-token/program/docs/ctoken/CREATE.md -B 5 -A 5

Repository: Lightprotocol/light-protocol

Length of output: 1103


Clarify terminology for is_ata validation between compression data and extension output.

Lines 353–360 document is_ata validation correctly and the error handling matches the implementation (ErrorCode::CompressAndCloseIsAtaMismatch at error code 6168). However, the phrasing at line 359 needs adjustment:

Line 359 states: "is_ata: Must match between compressible_extension.is_ata() and extension.is_ata()"

The code validates between the compression instruction data's is_ata field and the output CompressedOnly extension's is_ata field. The term "compressible_extension" is ambiguous here—it should specify that this is the is_ata value supplied in the compression operation data.

Also add rationale: CompressedOnly is required for is_ata=true accounts because the extension preserves the bump and owner_index fields needed to reconstruct the ATA PDA during decompression. Without these fields in the extension, the original ATA derivation path would be lost.

🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md around
lines 353 - 360, Update the phrasing for the is_ata validation: replace the
ambiguous reference to `compressible_extension.is_ata()` with a clear mention
that the check compares the compression instruction data's `is_ata` field to the
output CompressedOnly extension's `is_ata()` field (i.e., "Must match between
the compression instruction data's `is_ata` and `extension.is_ata()`"). Also add
a brief rationale stating that CompressedOnly is required for `is_ata=true`
accounts because the extension preserves `bump` and `owner_index` needed to
reconstruct the ATA PDA during decompression.

Comment on lines +371 to +373
- Collects all transfers into a 40-element array indexed by packed account index
- Deduplicates transfers to same account by summing amounts
- Executes single `multi_transfer_lamports` CPI with deduplicated transfers (max 40, error: TooManyCompressionTransfers)
- Executes single `multi_transfer_lamports` CPI with deduplicated transfers (max 32 compressions per instruction, error: TooManyCompressionTransfers)
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

Remove implementation detail about array size from user documentation.

Lines 371-373 expose internal implementation details:

  • "Collects all transfers into a 40-element array indexed by packed account index"
  • "max 32 compressions per instruction"

The "40-element array" is an internal buffer size that:

  1. Could change in future implementations without affecting user-facing behavior
  2. Creates confusion about why the array is 40 when the compression limit is 32
  3. Is too low-level for user-facing documentation

Users need to know:

  • ✅ Maximum 32 compressions allowed per instruction (error 6095)
  • ✅ Transfers to the same account are deduplicated and combined
  • ❌ Internal array size and indexing strategy
📝 Suggested revision
    - Calculate compressible extension top-up if present (returns Option<u64>)
    - **Transfer deduplication optimization:**
-     - Collects all transfers into a 40-element array indexed by packed account index
-     - Deduplicates transfers to same account by summing amounts
-     - Executes single `multi_transfer_lamports` CPI with deduplicated transfers (max 32 compressions per instruction, error: TooManyCompressionTransfers)
+     - Deduplicates transfers to the same account by summing amounts
+     - Executes single `multi_transfer_lamports` CPI with deduplicated transfers
+     - **Limit:** Maximum 32 compressions per instruction (error 6095: TooManyCompressionTransfers)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/TRANSFER2.md around
lines 371 - 373, Remove the internal implementation detail that says "Collects
all transfers into a 40-element array indexed by packed account index" and any
mention of internal buffer/indexing; instead keep the user-facing behavior:
state that transfers to the same account are deduplicated and combined, and that
there is a hard limit of 32 compressions per instruction (error 6095). Update
the bullets in TRANSFER2.md to remove the "40-element array" and indexing
wording and ensure the remaining text clearly notes deduplication and the
32-compression limit (include the error code 6095).

@ananas-block ananas-block merged commit bced0c7 into main Jan 12, 2026
32 checks passed
@ananas-block ananas-block deleted the jorrit/fix-cmint-instruction-data branch January 12, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants