Skip to content

Conversation

@ananas-block
Copy link
Contributor

@ananas-block ananas-block commented Dec 18, 2025

Typescript changes:

  1. src/v3/layout/layout-transfer2.ts - Added required decimals: number parameter to createCompressSpl() and createDecompressSpl()
  2. src/v3/instructions/wrap.ts - Added decimals: number parameter to createWrapInstruction()
  3. src/v3/instructions/unwrap.ts - Added decimals: number parameter to createUnwrapInstruction()
  4. src/v3/instructions/create-decompress-interface-instruction.ts - Added decimals: number parameter to createDecompressInterfaceInstruction(), changed splInterfaceInfo to SplInterfaceInfo | undefined
  5. src/v3/actions/wrap.ts - Added getMint import, fetch mint decimals, pass to createWrapInstruction()
  6. src/v3/actions/unwrap.ts - Added getMint import, fetch mint decimals, pass to createUnwrapInstruction()
  7. src/v3/actions/load-ata.ts - Added getMint import, fetch decimals, pass to 4x createDecompressInterfaceInstruction() and 2x createWrapInstruction()
  8. src/v3/actions/transfer-interface.ts - Added getMint import, fetch decimals, pass to createDecompressInterfaceInstruction() and 2x createWrapInstruction()
  9. src/v3/actions/decompress-interface.ts - Added getMint import, fetch decimals for SPL destinations, pass to createDecompressInterfaceInstruction()

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for SPL Token-2022 extensions (pausable, permanent delegate, transfer fee, transfer hook).
    • Implemented token freezing and thawing operations for compressed accounts.
    • Added token approval and revocation functionality.
    • Introduced transfer validation with decimal support for SPL token compatibility.
    • Added account type discriminators to improve account validation.
    • Implemented delegate support for compressed token accounts.
    • Added TLV extension support for storing extension metadata in compressed accounts.
  • Improvements

    • Enhanced compression and rent-exemption handling with embedded metadata.
    • Improved mint extension detection and validation at runtime.
    • Better error handling for restricted and unsupported extension types.
    • Streamlined token account creation with built-in extension awareness.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

This PR comprehensively refactors the compressed token (ctoken) architecture by embedding compression metadata directly within token accounts instead of relying on extensions, introducing TLV-based extension handling for Token 2022 compatibility (pausable, permanent delegate, transfer fee, transfer hook), redesigning the zero-copy token model, and replacing Compressible extension patterns with embedded CompressionInfo fields throughout the codebase.

Changes

Cohort / File(s) Summary
Core CToken State & Zero-Copy Redesign
program-libs/ctoken-interface/src/state/ctoken/{ctoken_struct, zero_copy, borsh}.rs
Replaced extension-based compression with embedded CompressionInfo field; introduced CTokenZeroCopyMeta and ZCToken/ZCTokenMut zero-copy wrappers; added account_type discriminator at byte 165; refactored BorshSerialize/Deserialize to handle new fields (decimals, compression_only, compression); added checked accessors and getters/setters for zero-copy operations.
Extension Type & Structure Overhaul
program-libs/ctoken-interface/src/state/extensions/{extension_type, extension_struct, compressed_only, pausable, permanent_delegate, transfer_fee, transfer_hook}.rs
program-libs/ctoken-interface/src/instructions/extensions/{compressed_only, pausable, permanent_delegate}.rs
Replaced Placeholder variants with named extension types (PausableAccount=27, PermanentDelegateAccount=28, TransferFeeAccount=29, TransferHookAccount=30, CompressedOnly=31); removed Compressible variant; added new marker extension instruction data types and state structs; updated zero-copy deserialization to map discriminants to new variants.
CompressedMint & CMint State
program-libs/ctoken-interface/src/state/mint/{compressed_mint, zero_copy}.rs
Added CompressionInfo embedding, account_type discriminator (ACCOUNT_TYPE_MINT=1), reserved fields; introduced zero-copy model with ZCompressedMint/ZCompressedMintMut wrappers; removed ZCompressedMintMut mutable API in favor of config-driven initialization; added checked accessor helpers.
Transfer2 & Token 2022 Integration
program-libs/ctoken-interface/src/instructions/transfer2/{instruction_data, compression}.rs
Added decimals field to Compression struct; added TLV-aware transfer instruction data with in_tlv/out_tlv carrying ExtensionInstructionData; removed ZeroCopyMut derive; added new fields (with_lamports_change_account_merkle_tree_index, cpi_context, compressions, proof, token_data fields).
Instruction Data Refactoring
program-libs/ctoken-interface/src/instructions/{create_ctoken_account, create_associated_token_account}.rs
Added token_account_version, rent_payment, compression_only, write_top_up fields; replaced CompressibleExtensionInstructionData with CompressToPubkey struct; added derive_address PDA derivation function and seeds validation.
Error Variants & Constants
program-libs/ctoken-interface/src/{error, constants, token_2022_extensions}.rs
Replaced TokenDataTlvUnimplemented with UnsupportedTlvExtensionType; added new error variants (CompressedOnlyBlocksTransfer, TlvExtensionLengthMismatch, InvalidAccountType, etc.); updated EXTENSION_METADATA from 7→4; added new extension constants and MintExtensionFlags tracking.
Compressed Token Program Core
programs/compressed-token/program/src/{lib, claim}.rs
programs/compressed-token/program/src/close_token_account/{processor}.rs
Added new error variants to ErrorCode enum; replaced ZCompressedTokenMut with ZCTokenMut; updated compression handling to use embedded compression_info instead of extensions; added check_cpi_context helper; simplified account validation logic.
Account Creation & Initialization
programs/compressed-token/program/src/{create_token_account, create_associated_token_account, shared/initialize_ctoken_account}.rs
Introduced CTokenInitConfig and CompressionInstructionData structures; refactored account initialization to use zero-copy CToken::new_zero_copy; added mint extension handling via MintExtensionFlags; integrated dynamic account sizing based on extensions.
Extension & Mint Validation
programs/compressed-token/program/src/extensions/check_mint_extensions.rs
New module providing zero-copy mint extension validation; introduced MintExtensionChecks and MintExtensionFlags structs; added check_mint_extensions and has_mint_extensions functions for runtime extension detection and restricted-extension enforcement.
Transfer2 Processing & Compression
programs/compressed-token/program/src/transfer2/{check_extensions, processor, token_inputs, token_outputs, compression/{mod, ctoken/*, spl}}.rs
Added MintExtensionCache for per-mint caching; introduced TLV validation and frozen-state derivation; refactored input/output processing to thread TLV extension data; updated SPL transfer to use transfer_checked with decimals and mint; added delegate handling; embedded compression info usage.
Compressed Token Operations
programs/compressed-token/program/src/transfer2/compression/ctoken/{compress_and_close, compress_or_decompress_ctokens, inputs}.rs
Added TLV support to CompressAndCloseInputs and CTokenCompressionInputs; updated validate_compressed_token_account to accept and validate CompressedOnly TLV extensions; refactored decompress to apply extension state transfer; added process_compression_top_up helper.
Approve, Revoke, Freeze, Thaw Operations
programs/compressed-token/program/src/{ctoken_approve_revoke, ctoken_freeze_thaw}.rs
programs/compressed-token/program/src/transfer/{mod, checked, default, shared}.rs
New wrapper processors for approve/revoke with pre-step top-up; new freeze/thaw passthrough functions; introduced new transfer module with checked (transfer_checked) and default (transfer) implementations; added shared extension processing helpers.
Mint Action Refactoring
programs/compressed-token/program/src/mint_action/actions/{mint_to, decompress_mint, compress_and_close_cmint}.rs
programs/compressed-token/program/src/mint_action/{mint_output, zero_copy_config}.rs
Replaced extension-based compression retrieval with embedded compression_info; removed Compressible extension serialization; refactored CMint output to serialize CMint-as-source-of-truth paths; updated CompressedMintConfig to use Option instead of tuple.
Compressible Top-Up & Rent Management
programs/compressed-token/program/src/shared/{compressible_top_up, owner_validation}.rs
Simplified top-up logic to use embedded compression metadata directly; removed per-extension iteration; added check_token_program_owner validation; extended verify_owner_or_delegate_signer with permanent_delegate support via MintExtensionCache.
Forester & State Management
forester/src/compressible/{compressor, state, bootstrap, subscriber}.rs
Replaced compressible extension retrieval with embedded compression info; updated COMPRESSIBLE_TOKEN_ACCOUNT_SIZE → BASE_TOKEN_ACCOUNT_SIZE in RPC filters; added delegate_index tracking in CompressAndCloseIndices.
Registry Compress & Close
programs/registry/src/compressible/compressed_token/compress_and_close.rs
Added delegate_index field to CompressAndCloseIndices; integrated TLV extension support; added per-output TLV container and CompressedOnly extension building.
CToken SDK Builders
sdk-libs/ctoken-sdk/src/ctoken/{create_associated_token_account, create, decompress, transfer_ctoken_spl, transfer_spl_ctoken, transfer_interface, transfer_ctoken_checked, approve, revoke, freeze, thaw, decompress_cmint}.rs
Added comprehensive instruction builders for ATA/compressible variants, approve, revoke, freeze, thaw, transfer_checked, decompress_cmint; extended CPI wrappers; propagated decimals and compression_only fields; added account-info collection helpers.
Compressed Token SDK Operations
sdk-libs/ctoken-sdk/src/compressed_token/v2/{account2, decompress_full, transfer2/instruction}.rs
sdk-libs/ctoken-sdk/src/ctoken/compressible.rs
Added decimals parameter to compress_spl/decompress_spl; introduced TLV support in DecompressFullIndices; added in_tlv threading to Transfer2Inputs; added compression_only field to CompressibleParams.
Transfer2 Instruction Building
sdk-libs/token-client/src/instructions/transfer2.rs
sdk-libs/token-client/src/actions/transfer2/{compress, decompress, spl_to_ctoken, ctoken_to_spl}.rs
Extended DecompressInput/CompressInput with decimals field; added in_tlv support with TLV data collection; updated instruction construction to thread TLV extension data; refactored compress/decompress paths to use new Compression fields.
Program Test Infrastructure
sdk-libs/program-test/src/compressible.rs
Added CompressibleAccountStore and StoredCompressibleAccount types; replaced COMPRESSIBLE_TOKEN_ACCOUNT_SIZE with account.data.len(); removed debug logging.
Test Files
programs/compressed-token/program/tests/{compress_and_close, mint_action, multi_sum_check, token_input, token_output, check_authority, print_error_codes}.rs
program-libs/ctoken-interface/tests/{ctoken/*, compressed_mint, cross_deserialization, mint_borsh_zero_copy, allocation_test, exact_allocation_test, mint}.rs
Updated test data to use new Compression decimals field; added ANCHOR_ERROR_OFFSET handling; introduced V3 discriminator and TLV test paths; refactored account setup to initialize CompressionInfo; added pausable extension test coverage; updated zero-copy/allocation tests.
Client & Compatibility
sdk-libs/client/src/indexer/types.rs
sdk-libs/ctoken-sdk/src/pack.rs
sdk-libs/token-client/src/actions/create_compressible_token_account.rs
sdk-libs/token-client/Cargo.toml
Updated TLV deserialization to parse ExtensionStruct; changed TokenData.tlv type from Vec to Vec; added compression_only logic to determine restricted-extension mode; added solana-system-interface dependency.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

This is a large, deeply interconnected architectural refactor affecting core compressed token state representation, compression metadata storage, extension handling, and transfer logic. The changes span ~100+ files across program logic, SDK, and tests with significant logic density in state deserialization, compression info embedding, TLV handling, and function signature updates. While individual changes are often straightforward (field additions, type replacements), their interdependencies demand careful verification that all callsites properly thread new fields, that zero-copy layouts remain correct, and that error paths align with the new extension model.

Possibly related PRs

  • #2085: Modifies ZCToken PartialEq logic for compression field comparison
  • #2090: Related changes to compress_and_close and ctoken compression processing
  • #2093: Threads max_top_up/lamports_budget through compression and transfer2 paths
  • #2140: Refactors compression data model with nested .info representation

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Key Technical Observations

Reading through this refactor, I can identify the core architectural shift: compression metadata is being lifted out of optional extensions and embedded directly into the token account structure. This is a substantial quality-of-life improvement because:

  1. Compression becomes mandatory, not optional: Every CToken now carries compression: CompressionInfo, eliminating the need to iterate through extensions searching for compressibility.

  2. TLV extensions are now type-safe: The new ExtensionInstructionData enum replaces raw byte arrays, making it harder to accidentally misinterpret extension data. The CompressedOnly marker extension elegantly carries delegated_amount and withheld_transfer_fee without bloating the base token layout.

  3. Token 2022 compatibility is explicit: Rather than treating extensions as opaque, the code now explicitly recognizes pausable, permanent delegate, transfer fee, and transfer hook extensions—including their validation constraints (zero fees, nil program IDs).

  4. The account_type discriminator at byte 165 is a clever safeguard: it prevents accidental deserialization of a CMint as a CToken or vice versa, which is a silent-corruption risk in zero-copy systems.

What to watch during review:

  • Serialization consistency: All BorshSerialize/BorshDeserialize and zero-copy implementations must agree on field order and sizing. The fixed discriminator position is critical.
  • Extension iteration removal: Several code paths now assume compression_info exists directly rather than scanning extensions. Verify that no fallback paths remain that might mask serialization errors.
  • TLV length validation: The new validate_tlv_and_get_frozen and output-count-matching logic in transfer2 processor must handle empty TLV gracefully.
  • Delegate indexing: The new delegate_index field in CompressAndCloseIndices must match packed account indices precisely.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: more ctoken extensions rebased' is vague and does not clearly convey the primary changes in this large refactoring. Consider a more descriptive title such as 'feat: embed compression metadata in ctoken accounts and refactor extension handling' to reflect the major architectural changes.
✅ 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%.
✨ 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/feat-more-ctoken-extensions2

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: 36

Caution

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

⚠️ Outside diff range comments (11)
sdk-libs/ctoken-sdk/src/compressed_token/v2/account2.rs (1)

252-284: Missing #[allow(clippy::too_many_arguments)] on decompress_spl.

The compress_spl method received the clippy annotation (line 193), but decompress_spl also has 7 parameters now and will trigger the same warning. Consider adding the annotation here for consistency.

🔎 Suggested fix:
 #[profile]
+#[allow(clippy::too_many_arguments)]
 pub fn decompress_spl(
     &mut self,
     amount: u64,
     source_index: u8,
     pool_account_index: u8,
     pool_index: u8,
     bump: u8,
     decimals: u8,
 ) -> Result<(), CTokenSdkError> {
sdk-libs/program-test/src/forester/compress_and_close_forester.rs (2)

50-50: Hardcoded epoch value may not reflect actual on-chain state.

The current_epoch is hardcoded to 0, which will not work correctly if the forester epoch has progressed beyond 0. Consider retrieving the current epoch from on-chain data or accepting it as a function parameter.


56-62: Remove unused config variable with default values.

The config variable is created with default pubkeys but never meaningfully used. The compression_authority extracted from it (line 61) is immediately overridden by the authority found in the account loop (lines 130-135). This creates confusion about the actual authority source.

🔎 Remove the unused config variable:
-    let config = CompressibleConfig::ctoken_v1(Pubkey::default(), Pubkey::default());
-
     let compressible_config = CompressibleConfig::derive_v1_config_pda(&registry_program_id).0;
 
-    // Derive compression_authority PDA (uses u16 version)
-    let compression_authority = config.compression_authority;
-    println!("config compression_authority {:?}", compression_authority);
-
sdk-libs/token-client/src/actions/transfer2/compress.rs (1)

25-37: Consider decimals validation consistency across the Transfer2 API.

The compress function accepts decimals as a parameter without validating it against the mint's actual decimals. While the TransferChecked instruction checks the token mint and decimals value by the caller, this is a design choice that appears intentional—all Transfer2 operations (compress, decompress, spl_to_ctoken, ctoken_to_spl) follow this pattern, delegating validation to the caller. If you want to add validation here, ensure it's done consistently across all similar operations in the Transfer2 API, not just compress. This would likely require an architectural discussion about whether the SDK should query the mint account for validation or continue delegating this responsibility to callers.

sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (1)

17-25: Add decimals validation by querying the mint account.

The decimals parameter is passed directly to the SPL transfer_checked operation without verification against the mint's actual decimals. The transfer_checked instruction asserts the token mint and decimals, so mismatched values cause runtime failures that are difficult to debug. Since the mint account is already available, extract and verify its decimals field before constructing the instruction—similar validation patterns should also be applied to compress.rs and transfer_ctoken_to_spl.rs for consistency.

sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (1)

15-24: Decimals parameter addition is correct, but consider future refactoring.

The addition of the decimals parameter is necessary for proper token transfer handling. While 8 parameters triggers a Clippy warning, the current design is acceptable. However, for future iterations, consider grouping related parameters (e.g., source account, destination account, amount, decimals) into a TransferParams struct to improve maintainability.

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

66-96: Account delegate authorization appears to be removed from compression checks.

The function now only authorizes the owner and permanent delegate from mint, but the original account-level delegate (compressed_token.delegate) is no longer checked. Was this intentional?

Previously, if a user delegated tokens to someone, that delegate could authorize operations. Now only the permanent delegate (a mint-level authority) can act alongside the owner.

If this is intentional (permanent delegate replaces account-level delegate for compression), please confirm. Otherwise, you may need:

🔎 Restore account delegate check
     // Check if authority is the owner
     if *authority_key == owner_key {
         return Ok(()); // Owner can always compress
     }
 
+    // Check if authority is the account's delegate
+    if let Some(delegate) = compressed_token.delegate.as_deref() {
+        if *authority_key == delegate.to_bytes() {
+            return Ok(()); // Account delegate can compress delegated amount
+        }
+    }
+
     // Check if authority is the permanent delegate from the mint

Also, _compression_amount is unused—was this parameter intended for checking delegated amount limits?

programs/compressed-token/program/src/transfer2/compression/spl.rs (1)

70-73: unimplemented!() will panic at runtime.

If CompressAndClose is a valid mode that can be passed to this function, the unimplemented!() macro will cause a runtime panic rather than returning a graceful error. Consider returning a ProgramError instead to avoid unexpected program crashes.

🔎 Suggested fix:
         ZCompressionMode::CompressAndClose => {
-            msg!("CompressAndClose is unimplemented for spl token accounts");
-            unimplemented!()
+            msg!("CompressAndClose is unimplemented for spl token accounts");
+            return Err(ProgramError::InvalidArgument);
         }
sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (2)

40-52: Documentation example is stale.

The doc example at lines 17-39 doesn't include the new decimals field. Callers following the example will get compilation errors.

🔎 Update the doc example
 /// let instruction = TransferSplToCtoken {
 ///     amount: 100,
 ///     spl_interface_pda_bump: 255,
+///     decimals: 9,
 ///     source_spl_token_account,
 ///     destination_ctoken_account,

66-80: CPI struct documentation example is also stale.

The TransferSplToCtokenCpi doc example (lines 54-79) is missing both the new decimals and system_program fields.

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

75-82: Potential issue: None passed for mint_account prevents extension detection.

When process_create_associated_token_account_inner is called from the non-idempotent path, mint_account is None, causing has_mint_extensions to return default (all false). If the mint actually has extensions like Pausable or PermanentDelegate, the created token account will be undersized.

The comment says "No mint account available in create_ata (owner/mint passed as bytes)" - but mint is right there at owner_and_mint[1]. Should this pass Some(mint) instead?

🔎 Consider passing the mint account for extension detection:
     process_create_associated_token_account_inner::<IDEMPOTENT>(
         remaining_accounts,
         owner.key(),
         mint.key(),
         instruction_inputs.bump,
         instruction_inputs.compressible_config,
-        None, // No mint account available in create_ata (owner/mint passed as bytes)
+        Some(mint), // Pass mint for extension detection
     )

Comment on lines 167 to 222
32 => {
// Compressible variant (index 32 to avoid Token-2022 overlap)
let (compressible_ext, remaining_bytes) =
CompressionInfo::zero_copy_at_mut(remaining_data)?;
CompressibleExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::Compressible(compressible_ext),
remaining_bytes,
))
}
27 => {
// PausableAccount variant (marker extension, no data)
let (pausable_ext, remaining_bytes) =
PausableAccountExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::PausableAccount(pausable_ext),
remaining_bytes,
))
}
28 => {
// PermanentDelegateAccount variant (marker extension, no data)
let (permanent_delegate_ext, remaining_bytes) =
PermanentDelegateAccountExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::PermanentDelegateAccount(permanent_delegate_ext),
remaining_bytes,
))
}
29 => {
// TransferFeeAccount variant
let (transfer_fee_ext, remaining_bytes) =
TransferFeeAccountExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::TransferFeeAccount(transfer_fee_ext),
remaining_bytes,
))
}
30 => {
// TransferHookAccount variant
let (transfer_hook_ext, remaining_bytes) =
TransferHookAccountExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::TransferHookAccount(transfer_hook_ext),
remaining_bytes,
))
}
31 => {
// CompressedOnly variant
let (compressed_only_ext, remaining_bytes) =
CompressedOnlyExtension::zero_copy_at_mut(remaining_data)?;
Ok((
ZExtensionStructMut::CompressedOnly(compressed_only_ext),
remaining_bytes,
))
}
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
# Search for discriminant-related code and constants in ctoken-interface
find program-libs/ctoken-interface -type f -name "*.rs" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 1518


🏁 Script executed:

#!/bin/bash
# Look for the extension_struct.rs file and surrounding context
find program-libs -name "extension_struct.rs" -o -name "CLAUDE.md" -o -name "DISCRIMINATOR.md" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 398


🏁 Script executed:

#!/bin/bash
# Check for discriminant or enum values in the file
rg "enum.*Extension|discriminant|DISCRIMINATOR" program-libs/ctoken-interface/src -B 3 -A 3 --type rust | head -100

Repository: Lightprotocol/light-protocol

Length of output: 10251


🏁 Script executed:

#!/bin/bash
# Find and examine the extension_struct.rs file
find program-libs/ctoken-interface -name "extension_struct.rs" -exec cat {} \; | head -300

Repository: Lightprotocol/light-protocol

Length of output: 11264


🏁 Script executed:

#!/bin/bash
# Look for CLAUDE.md documentation files
find program-libs/ctoken-interface -name "CLAUDE.md" -o -name "*.md" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Search for discriminant constants and enum definitions in ctoken-interface
rg "const.*DISCRIMIN|enum.*Extension" program-libs/ctoken-interface -B 2 -A 5 --type rust | head -150

Repository: Lightprotocol/light-protocol

Length of output: 4655


🏁 Script executed:

#!/bin/bash
# Search for discriminant constants in ctoken-interface
rg "const.*27|const.*28|const.*29|const.*30|const.*31|const.*32" program-libs/ctoken-interface/src --type rust | head -50

Repository: Lightprotocol/light-protocol

Length of output: 917


🏁 Script executed:

#!/bin/bash
# Check if extension discriminants should be extracted to constants
grep -n "bytes\[0\] = " program-libs/ctoken-interface/src/state/extensions/extension_struct.rs

Repository: Lightprotocol/light-protocol

Length of output: 330


🏁 Script executed:

#!/bin/bash
# Look for any documentation or README in ctoken-interface
find program-libs/ctoken-interface -name "README.md" -o -name "*.md" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 54


Extract discriminants to named constants and document the serialization contract.

The discriminant mapping is intentionally decoupled from enum indices for Token-2022 compatibility (extensions 26-31 are reserved). However, the discriminant values (19, 27-32) are currently hardcoded as literals in multiple match arms and write operations, violating DRY principles. Extract these to named constants (e.g., const TOKEN_METADATA_DISCRIMINANT: u8 = 19;) and create a CLAUDE.md file documenting the complete discriminant-to-extension mapping as part of the serialization contract.

🤖 Prompt for AI Agents
In program-libs/ctoken-interface/src/state/extensions/extension_struct.rs around
lines 167 to 220, hardcoded discriminant literals (e.g., 19, 27..32) are used
directly in match arms and write operations; extract each discriminant into
clearly named u8 constants (for example const TOKEN_METADATA_DISCRIMINANT: u8 =
19;, const PAUSABLE_ACCOUNT_DISCRIMINANT: u8 = 27;, etc.), replace all literal
usages in match arms, serialization writes and any other places with those
constants to avoid duplication, and add a new CLAUDE.md at the repo root that
documents the complete discriminant -> extension mapping and the serialization
contract so future changes remain consistent and discoverable.

Comment on lines 100 to 102
has_top_up: 1,
compression_only: if config.compression_only { 1 } else { 0 },
write_top_up: config.lamports_per_write.unwrap_or(0),
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

# Find and read the create_ata.rs file
echo "=== File: sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs ==="
find . -name "create_ata.rs" -type f | head -5

# Search for CompressibleExtensionInstructionData struct definition
echo -e "\n=== Searching for CompressibleExtensionInstructionData struct ==="
rg "struct CompressibleExtensionInstructionData" -A 10 --type rust

# Search for has_top_up field definition/usage
echo -e "\n=== Searching for has_top_up field ==="
rg "has_top_up" -B 2 -A 2 --type rust | head -80

# Look for CLAUDE.md or documentation about compressible extension
echo -e "\n=== Looking for documentation ==="
find . -name "*.md" -path "*compressible*" -o -name "CLAUDE.md" -path "*ctoken*" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 4889


🏁 Script executed:

#!/bin/bash
# Read the create_ata.rs file content around lines 95-105
wc -l sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs
head -250 sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs | tail -160

Repository: Lightprotocol/light-protocol

Length of output: 6017


🏁 Script executed:

#!/bin/bash

# Read the create_ata.rs file 
cat -n sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs | sed -n '85,110p'

# Find the CompressibleExtensionInstructionData struct
echo -e "\n\n=== CompressibleExtensionInstructionData struct ==="
rg -A 15 "struct CompressibleExtensionInstructionData" --type rust

# Find documentation about the fields
echo -e "\n\n=== Documentation about has_top_up and write_top_up ==="
rg -B 3 -A 3 "has_top_up|write_top_up" --type rust | head -100

Repository: Lightprotocol/light-protocol

Length of output: 10711


Make has_top_up conditional on write_top_up being provided.

Currently, has_top_up is hardcoded to 1 when a compressible config is provided, even when write_top_up is 0 (via unwrap_or(0) when config.lamports_per_write is None). This deviates from the pattern in create_associated_token_account.rs and create.rs, which only set has_top_up: 1 when lamports_per_write.is_some(). To stay consistent with those builders, change line 100 to:

has_top_up: if config.lamports_per_write.is_some() { 1 } else { 0 },
🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs around lines 100 to 102,
has_top_up is currently hardcoded to 1 even when config.lamports_per_write is
None (because write_top_up uses unwrap_or(0)); change it so has_top_up is
conditional on lamports_per_write being Some by setting has_top_up to 1 when
config.lamports_per_write.is_some() and 0 otherwise, matching the pattern used
in create_associated_token_account.rs and create.rs.

Comment on lines +41 to 63
#[allow(clippy::too_many_arguments)]
pub fn new(
amount: u64,
decimals: u8,
source_account: AccountInfo<'info>,
destination_account: AccountInfo<'info>,
authority: AccountInfo<'info>,
payer: AccountInfo<'info>,
compressed_token_program_authority: AccountInfo<'info>,
system_program: AccountInfo<'info>,
) -> Self {
Self {
source_account,
destination_account,
authority,
amount,
decimals,
payer,
compressed_token_program_authority,
spl_interface: None,
system_program,
}
}
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 builder pattern for constructor ergonomics.

With 8 parameters, the constructor is becoming unwieldy. The with_spl_interface method already demonstrates a builder-like approach. Consider extending this pattern for optional parameters like system_program to improve call-site readability:

TransferInterfaceCpi::new(amount, decimals, source, dest, authority, payer, ctp_authority)
    .with_system_program(system_program)
    .with_spl_interface(...)

This is a minor ergonomic improvement—the current approach is functional.

Comment on lines +170 to +175
// System program (index 6) - needed for compressible account lamport top-ups
AccountMeta::new_readonly(Pubkey::default(), false),
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
# Find and display the transfer_spl_ctoken.rs file content around lines 170-171
fd -t f transfer_spl_ctoken.rs --exec head -n 200 {} \; | tail -n +150

Repository: Lightprotocol/light-protocol

Length of output: 2141


🏁 Script executed:

#!/bin/bash
# Get more lines from the file to see how packed_accounts is used
fd -t f transfer_spl_ctoken.rs --exec head -n 230 {} \; | tail -n +160

Repository: Lightprotocol/light-protocol

Length of output: 2426


🏁 Script executed:

#!/bin/bash
# Search for Transfer2 instruction processing and system program handling
rg -n "Transfer2" sdk-libs/ctoken-sdk/src --type rust -A 3 -B 1 | head -50

Repository: Lightprotocol/light-protocol

Length of output: 4604


🏁 Script executed:

#!/bin/bash
# Search for Transfer2 processing in program-libs (on-chain code)
fd -t f -path "*/program-libs/*" | grep -i transfer | head -20

Repository: Lightprotocol/light-protocol

Length of output: 240


🏁 Script executed:

#!/bin/bash
# Search for Transfer2 processing and system program usage
find program-libs -name "*.rs" -path "*transfer*" 2>/dev/null | head -20

Repository: Lightprotocol/light-protocol

Length of output: 491


🏁 Script executed:

#!/bin/bash
# Check how Transfer2 processes accounts, especially for lamport transfers
rg -n "system_program|System.*Program|Pubkey::default" program-libs/ctoken-interface/src/instructions/transfer2/ --type rust -B 2 -A 2 | head -80

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Search in the ctoken program itself for transfer2 processing
rg -n "transfer2|Transfer2" programs/compressed-token/program/src --type rust -l | head -5

Repository: Lightprotocol/light-protocol

Length of output: 369


🏁 Script executed:

#!/bin/bash
# Check the transfer2 mod.rs file to see how accounts are processed
head -n 300 programs/compressed-token/program/src/transfer2/mod.rs | tail -n 150

Repository: Lightprotocol/light-protocol

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Check transfer2 processor to understand account handling
rg -n "packed_accounts|system_program|Pubkey::default" programs/compressed-token/program/src/transfer2 --type rust -B 3 -A 3 | head -100

Repository: Lightprotocol/light-protocol

Length of output: 10049


🏁 Script executed:

#!/bin/bash
# Search for lamport processing in transfer2
rg -n "lamport|invoke|cpi" programs/compressed-token/program/src/transfer2/processor.rs --type rust -B 2 -A 2 | head -120

Repository: Lightprotocol/light-protocol

Length of output: 4682


🏁 Script executed:

#!/bin/bash
# Check what directory transfer_spl_ctoken.rs is in to confirm it's sdk-libs
find . -name "transfer_spl_ctoken.rs" -type f 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Verify that in_lamports and out_lamports are indeed unimplemented in all transfer2 code
rg -n "in_lamports|out_lamports" programs/compressed-token/program/src/transfer2 --type rust | head -30

Repository: Lightprotocol/light-protocol

Length of output: 692


🏁 Script executed:

#!/bin/bash
# Check if there's a SYSTEM_PROGRAM_ID constant defined
rg -n "SYSTEM_PROGRAM_ID|system_program::ID" --type rust | head -10

Repository: Lightprotocol/light-protocol

Length of output: 1223


Replace Pubkey::default() with the actual System Program ID if lamport transfers will be implemented, or remove this account if it's unused.

The comment indicates this should be the System Program for lamport top-ups, but the System Program address is 11111111111111111111111111111111, not the zero address. Additionally, on-chain Transfer2 processing currently rejects any lamport inputs or outputs as unimplemented. Either wire up the correct System Program ID (available via solana_program::system_program::ID) if this feature is planned, or omit this account and update the documentation.

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs around lines 170-171,
the AccountMeta is using Pubkey::default() for the System Program placeholder
which is incorrect for lamport transfers; either replace Pubkey::default() with
the real System Program ID (solana_program::system_program::ID) if you intend to
support lamport top-ups, or remove this AccountMeta entry entirely and adjust
the surrounding comment/docs to reflect that lamport transfers are not
supported. Ensure the chosen action matches on-chain Transfer2 behavior (wire up
the proper system program ID when implementing lamport transfers, otherwise
remove the unused account and update the comment).

// Get delegate if present
let delegate_index = if let Some(delegate_bytes) = ctoken_account.delegate.as_ref() {
let delegate_pubkey = Pubkey::from(delegate_bytes.to_bytes());
packed_accounts.insert_or_get(delegate_pubkey)
} else {
0 // 0 means no delegate
};
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

Sentinel value 0 for "no delegate" can collide with actual index 0.

Using 0 to represent "no delegate" is problematic because if a delegate account is inserted as the first account in packed_accounts, it will receive index 0, creating ambiguity. The instruction handler won't be able to distinguish between "no delegate" and "delegate at index 0".

Consider one of these approaches:

  1. Use Option<usize> for delegate_index in CompressAndCloseIndices (preferred)
  2. Use a sentinel value that cannot collide, such as usize::MAX
  3. Reserve index 0 by inserting a dummy account first (not recommended)
🔎 Suggested approach using a non-colliding sentinel:
     // Get delegate if present
-    let delegate_index = if let Some(delegate_bytes) = ctoken_account.delegate.as_ref() {
+    let delegate_index = if let Some(delegate_bytes) = ctoken_account.delegate.as_ref() {
         let delegate_pubkey = Pubkey::from(delegate_bytes.to_bytes());
         packed_accounts.insert_or_get(delegate_pubkey)
     } else {
-        0 // 0 means no delegate
+        usize::MAX // usize::MAX means no delegate
     };

Note: This assumes CompressAndCloseIndices and the instruction handler recognize usize::MAX as the sentinel. Verify this convention is documented and consistently applied across the codebase.

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

🤖 Prompt for AI Agents
In sdk-libs/program-test/src/forester/compress_and_close_forester.rs around
lines 148 to 154, using 0 as a sentinel for "no delegate" collides with a real
delegate that could be at index 0; change the representation so it cannot
collide—preferably make delegate_index an Option<usize> in
CompressAndCloseIndices (update struct/type and all call sites to use
Some(index)/None), or if changing the type is infeasible use a guaranteed
impossible sentinel like usize::MAX and ensure CompressAndCloseIndices and the
instruction handler treat usize::MAX as "no delegate" consistently across the
codebase.

mint_index,
owner_index,
rent_sponsor_index,
delegate_index,
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 CompressAndCloseIndices struct definition and delegate_index usage

echo "=== Finding CompressAndCloseIndices struct definition ==="
fd -e rs | xargs rg "struct CompressAndCloseIndices" -B2 -A10

echo -e "\n=== Finding delegate_index field usage ==="
fd -e rs | xargs rg "delegate_index" -B2 -A2

echo -e "\n=== Checking file path context ==="
fd "compress_and_close_forester.rs"

Repository: Lightprotocol/light-protocol

Length of output: 21957


🏁 Script executed:

#!/bin/bash
# Get more context around line 161 in the forester file and look for the else branch

echo "=== Full function context around delegate_index in compress_and_close_forester.rs ==="
sed -n '140,180p' sdk-libs/program-test/src/forester/compress_and_close_forester.rs

echo -e "\n=== Check what happens when delegate is None ==="
sed -n '150,175p' sdk-libs/program-test/src/forester/compress_and_close_forester.rs

echo -e "\n=== Look for how delegate_index is used after assignment ==="
rg -A5 "let delegate_index =" sdk-libs/program-test/src/forester/compress_and_close_forester.rs

Repository: Lightprotocol/light-protocol

Length of output: 2952


🏁 Script executed:

#!/bin/bash
# Check how the instruction handler uses delegate_index in the registry program

echo "=== Handler code using delegate_index from CompressAndCloseIndices ==="
sed -n '80,140p' programs/registry/src/compressible/compressed_token/compress_and_close.rs

echo -e "\n=== Check if there's any validation of sentinel value ==="
rg -B3 -A3 "delegate.*idx.delegate_index" programs/registry/src/compressible/compressed_token/compress_and_close.rs

Repository: Lightprotocol/light-protocol

Length of output: 3161


Address sentinel value collision in delegate_index.

The current implementation uses 0 as a sentinel to indicate "no delegate" in CompressAndCloseIndices, but packed_accounts.insert_or_get() can also return 0 if the delegate account happens to be the first inserted. This creates ambiguity: a delegate_index of 0 could mean either "no delegate" or "delegate is the first packed account."

While the handler mitigates this by reading has_delegate from the actual CToken account data (not the instruction), this indirect safety mechanism masks a design flaw. Align with the pattern used elsewhere in the codebase (in transfer.rs, freeze.rs, etc.) by changing delegate_index to Option<u8> to make the intent explicit and type-safe.

🤖 Prompt for AI Agents
In sdk-libs/program-test/src/forester/compress_and_close_forester.rs around line
161, change the CompressAndCloseIndices.delegate_index from a raw u8 sentinel to
Option<u8> so presence/absence of a delegate is explicit and type-safe; update
the instruction struct, its serialization/deserialization (encode/decode) to
represent Option<u8> (e.g., write a presence flag then the value), update all
call sites that build the instruction to convert
packed_accounts.insert_or_get(...) into Some(index as u8) when a delegate exists
or None when not, and update any unpacking/handler logic to use the Option
directly (and update tests/mocks accordingly).

@ananas-block ananas-block force-pushed the jorrit/feat-more-ctoken-extensions2 branch from 87b7a1e to 7113d51 Compare December 18, 2025 01:33
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: 13

Caution

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

⚠️ Outside diff range comments (10)
sdk-libs/ctoken-sdk/src/compressed_token/v2/account2.rs (1)

252-284: Add clippy suppression for consistency with compress_spl.

This method now has 7 arguments (clippy's default threshold), and compress_spl already has #[allow(clippy::too_many_arguments)]. For consistency and to prevent future lint noise if another parameter is added, consider adding the same annotation here.

🔎 Suggested fix
 #[profile]
+#[allow(clippy::too_many_arguments)]
 pub fn decompress_spl(
     &mut self,
     amount: u64,
     source_index: u8,
     pool_account_index: u8,
     pool_index: u8,
     bump: u8,
     decimals: u8,
 ) -> Result<(), CTokenSdkError> {
sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_spl.rs (3)

17-52: Update the documentation example to include the new decimals field.

The example at lines 28-38 will fail to compile because it doesn't initialize the new required decimals field. Users copying this example will encounter a compilation error.

🔎 Apply this diff to fix the documentation example:
 let instruction = TransferCTokenToSpl {
     source_ctoken_account,
     destination_spl_token_account,
     amount: 100,
     authority,
     mint,
     payer,
     spl_interface_pda,
     spl_interface_pda_bump: 255,
+    decimals: 9, // Token decimals (e.g., 9 for typical SPL tokens)
     spl_token_program,
 }.instruction()?;

54-93: Update the documentation example to include the new decimals field.

The example at lines 66-77 will fail to compile because it doesn't initialize the new required decimals field.

🔎 Apply this diff to fix the documentation example:
 TransferCTokenToSplCpi {
     source_ctoken_account,
     destination_spl_token_account,
     amount: 100,
     authority,
     mint,
     payer,
     spl_interface_pda,
     spl_interface_pda_bump: 255,
+    decimals: 9, // Token decimals (e.g., 9 for typical SPL tokens)
     spl_token_program,
     compressed_token_program_authority,
 }

133-148: Decimals parameter relies on caller accuracy—consider documenting or validating.

The decimals field is accepted as-is from the caller without verification against the mint account. Since the mint account is available in the accounts list, the SDK could optionally validate that the provided decimals matches the mint's actual decimals to catch caller errors early. Alternatively, document explicitly that the caller is responsible for ensuring decimals correctness, as incorrect values would cause decompression to behave incorrectly.

sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (2)

40-52: Doc example is now out of sync with struct fields.

The example at lines 27-37 doesn't include the new decimals field, which will cause compilation errors for anyone copying the example.

🔎 Update the doc example to include decimals:
 /// let instruction = TransferSplToCtoken {
 ///     amount: 100,
 ///     spl_interface_pda_bump: 255,
+///     decimals: 9,
 ///     source_spl_token_account,
 ///     destination_ctoken_account,

81-96: Doc example for CPI struct is incomplete.

The example at lines 66-78 is missing the new decimals and system_program fields required by TransferSplToCtokenCpi.

🔎 Update the CPI doc example:
 /// TransferSplToCtokenCpi {
 ///     amount: 100,
 ///     spl_interface_pda_bump: 255,
+///     decimals: 9,
 ///     source_spl_token_account,
 ///     ...
 ///     compressed_token_program_authority,
+///     system_program,
 /// }
sdk-libs/token-client/src/actions/transfer2/decompress.rs (1)

14-34: Documentation should include the new decimals parameter.

The function signature now includes decimals: u8, but the doc comment doesn't describe this parameter. Consider adding:

/// * `decimals` - The decimal places for the token mint

This helps callers understand what value to provide.

programs/compressed-token/program/src/transfer2/compression/spl.rs (1)

161-170: Consider preserving original CPI error for debugging.

Mapping all CPI errors to ProgramError::InvalidArgument loses diagnostic information. While this is consistent with the existing pattern, preserving the original error would aid debugging:

🔎 Potential improvement
     match signers {
         Some(signers) => {
             pinocchio::cpi::slice_invoke_signed(&instruction, account_infos, signers)
-                .map_err(|_| ProgramError::InvalidArgument)?;
+                .map_err(ProgramError::from)?;
         }
         None => {
             pinocchio::cpi::slice_invoke(&instruction, account_infos)
-                .map_err(|_| ProgramError::InvalidArgument)?;
+                .map_err(ProgramError::from)?;
         }
     }
program-libs/ctoken-interface/src/state/extensions/extension_type.rs (1)

54-68: TryFrom implementation correctly handles new variants.

The TryFrom<u8> implementation properly maps discriminants 27-31 to the new extension types. The error handling for unsupported values returns CTokenError::UnsupportedExtension.

One observation: placeholders 0-26 (except 19) are not handled in TryFrom and will return UnsupportedExtension. This is intentional since they're reserved/unused, but consider adding a comment explaining this design choice for future maintainers.

sdk-libs/token-client/src/instructions/transfer2.rs (1)

197-201: Consider more graceful error handling for RPC calls.

The chain .await.unwrap().value will panic on RPC failures. While this is SDK code (not on-chain), users of this function might prefer to receive a proper error rather than a panic.

🔎 Suggested improvement:
     let rpc_proof_result = rpc
         .get_validity_proof(hashes, vec![], None)
         .await
-        .unwrap()
+        .map_err(|e| CTokenSdkError::RpcError(e.to_string()))?
         .value;

Note: This pattern appears in several other places in this file (lines 275-280, 354-359, 525-529). A consistent error handling approach would improve the SDK's usability.

♻️ Duplicate comments (35)
program-libs/zero-copy-derive/src/shared/utils.rs (1)

24-28: UnitStruct variant addition is well-structured.

The InputType enum extension cleanly separates unit structs from named-field structs. The comment on line 26 clearly documents what this variant represents (struct Foo; syntax).

The documentation mismatch with coding guidelines ("Structs must have at least one field for zero-copy serialization; empty structs are not supported") was already flagged in a previous review. Please ensure the README and guidelines are updated to reflect that unit structs are now intentionally supported.

program-libs/zero-copy-derive/src/zero_copy.rs (1)

303-333: Unit struct immutable zero-copy implementation is correct.

The static instance pattern at line 322 (static UNIT: #name = #name;) is the right approach for immutable access—the static is created once at compile time, and returning &'a UNIT safely provides a reference with arbitrary lifetime since the static lives for the entire program duration.

This contrasts appropriately with the mutable version in zero_copy_mut.rs which must use Box::leak since you can't safely hand out multiple &'static mut references to the same static.

Tests and documentation updates for unit struct support were flagged in a previous review. Please add integration tests under program-libs/zero-copy-derive/tests/ and update the README.

sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)

64-64: Document the compression_only field and its usage here.

This hardcodes compression_only: true without any explanation of why true is the appropriate value for compressible token account creation. The underlying field definition also lacks documentation (see past review comments on sdk-libs/ctoken-sdk/src/ctoken/compressible.rs).

sdk-libs/ctoken-sdk/src/ctoken/compressible.rs (2)

37-37: Documentation for compression_only is still missing.

This new public field was already flagged in previous review comments for lacking documentation. The struct-level docstring and field-level comments still don't explain what compression_only controls or when to use true vs false.

Also applies to: 49-49


96-96: Builder method and documentation still needed.

As noted in previous review comments, the compression_only field lacks documentation and there's no builder method like with_compression_only() to set it fluently.

Also applies to: 114-114

sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs (1)

100-102: Make has_top_up conditional on write_top_up being provided.

Currently, has_top_up is hardcoded to 1 when a compressible config is provided, even when write_top_up is 0 (via unwrap_or(0) on line 102). This deviates from the pattern in create_associated_token_account.rs and create.rs, which only set has_top_up: 1 when lamports_per_write.is_some().

🔎 Apply this diff to make has_top_up conditional:
 has_top_up: if config.lamports_per_write.is_some() { 1 } else { 0 },
 compression_only: if config.compression_only { 1 } else { 0 },
 write_top_up: config.lamports_per_write.unwrap_or(0),
programs/compressed-token/program/src/transfer2/cpi.rs (1)

54-59: Fix TLV boolean flag and use safe type conversion.

Two issues remain from the previous review:

  1. Line 56: The TLV boolean is hardcoded to true even when tlv_config is empty. This causes incorrect byte length calculations. Use !tlv_config.is_empty() to reflect whether TLV data actually exists, matching the pattern in shared/token_output.rs.

  2. Line 59: The unchecked as u32 cast should be replaced with .try_into() for explicit overflow handling and defensive programming.

🔎 Apply this diff to fix both issues:
 let token_config = TokenDataConfig {
     delegate: (has_delegate, ()),
-    tlv: (true, tlv_config),
+    tlv: (!tlv_config.is_empty(), tlv_config),
 };
 TokenData::byte_len(&token_config).map_err(|_| ProgramError::InvalidAccountData)?
-    as u32
+    .try_into()
+    .map_err(|_| ProgramError::InvalidAccountData)?
program-libs/ctoken-interface/src/state/extensions/compressed_only.rs (1)

1-31: COMPRESSED_ONLY_EXTENSION_SIZE constant needs updating to 16 bytes.

The CompressedOnlyExtension struct correctly defines two u64 fields (delegated_amount and withheld_transfer_fee) totaling 16 bytes, and the LEN constant properly uses size_of::<Self>(). However, the COMPRESSED_ONLY_EXTENSION_SIZE constant in constants.rs is outdated at 8 bytes.

Update that constant to 16, or better yet, set it to CompressedOnlyExtension::LEN to keep it automatically in sync with the struct definition. Also update the constant's documentation to reflect the two-field (16-byte) layout.

#!/bin/bash
# Verify the current value of COMPRESSED_ONLY_EXTENSION_SIZE
rg -n "COMPRESSED_ONLY_EXTENSION_SIZE" --type rust -B 2 -A 2
programs/compressed-token/program/src/shared/owner_validation.rs (1)

40-60: Verbose failure logging adds compute overhead.

Multiple msg! calls on authorization failure increase compute cost. This concern was raised in a previous review. Consider consolidating into a single message or gating behind a feature flag if this is a hot path.

sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (1)

170-171: Pubkey::default() is incorrect for the System Program.

Pubkey::default() produces the null address [0; 32], not the System Program ID (11111111111111111111111111111111). This will fail on-chain validation when the program checks the system program account.

🔎 Use the actual System Program ID:
-            // System program (index 6) - needed for compressible account lamport top-ups
-            AccountMeta::new_readonly(Pubkey::default(), false),
+            // System program (index 6) - needed for compressible account lamport top-ups
+            AccountMeta::new_readonly(solana_system_interface::program::ID, false),
sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs (4)

16-20: Discriminators are magic numbers without documentation.

These discriminator values (100, 102, 106, 107) should reference the canonical on-chain instruction enum or include comments explaining their meaning.


206-210: System program pubkey is null, not the actual System Program ID.

Pubkey::new_from_array([0; 32]) creates a null pubkey. The System Program ID is 11111111111111111111111111111111 (base58) or the constant from solana_system_interface::program::ID.

🔎 Fix the system program pubkey:
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false), // system_program
+        solana_instruction::AccountMeta::new_readonly(solana_system_interface::program::ID, false), // system_program

383-389: Same null system program issue in ATA2 instruction builder.

🔎 Apply the same fix:
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false),
+        solana_instruction::AccountMeta::new_readonly(solana_system_interface::program::ID, false),

408-451: CPI wrapper passes authority account but instruction doesn't expect it.

The create_associated_ctoken_account function passes authority in the CPI accounts (line 448), but create_compressible_associated_token_account_with_bump doesn't include authority in its accounts list (lines 206-222). This account mismatch will cause the CPI to fail.

sdk-libs/client/src/indexer/types.rs (1)

1-1: Type evolution is correct, but error handling and duplication concerns remain.

The change from Option<Vec<u8>> to deserializing into Vec<ExtensionStruct> properly reflects the typed extension model introduced in this PR. The TLV data is now strongly typed, which is a good design evolution.

However, the past review correctly identified two improvement opportunities:

  1. Generic error mapping loses debugging context: Both base64 decode and Borsh deserialization errors map to InvalidResponseData, making it difficult to diagnose whether the issue is data corruption (base64) or schema mismatch (Borsh).

  2. Logic duplication: The identical TLV parsing appears in both TokenAccount (lines 890-896) and TokenAccountV2 (931-937) implementations.

The functional change is sound—the TLV data is now properly typed. The error handling and duplication concerns noted in the past review remain valid suggestions for future improvement, but don't block this PR since the core functionality is correct.

Also applies to: 890-896, 931-937

program-libs/ctoken-interface/src/instructions/extensions/compressible.rs (1)

21-25: Consider adding validation or clarifying the design choice.

The addition of has_top_up: u8 creates potential for inconsistency with the existing write_top_up: u32 field. If has_top_up == 0 but write_top_up != 0, which takes precedence?

Options to consider:

  1. Remove the flag: Use write_top_up != 0 as the presence indicator
  2. Add validation: Ensure write_top_up == 0 when has_top_up == 0 at deserialization/construction sites
  3. Document the design: If the flag is required (e.g., to distinguish "no top-up specified" from "explicit zero top-up"), add a comment explaining this

Without validation or documentation, this creates a potential correctness issue where the two fields could contradict each other.

sdk-libs/program-test/src/forester/compress_and_close_forester.rs (1)

148-154: Address sentinel value collision for delegate_index.

Using 0 as a sentinel for "no delegate" creates ambiguity because packed_accounts.insert_or_get() can legitimately return 0 if the delegate account is inserted first. This makes it impossible to distinguish between "no delegate" and "delegate at index 0".

Consider using Option<usize> for delegate_index in CompressAndCloseIndices (update the struct definition, serialization, and all call sites), or use a non-colliding sentinel like usize::MAX if changing the type is not feasible.

forester/src/compressible/compressor.rs (1)

155-163: Address sentinel value collision for delegate_index.

Using 0 as the default when no delegate is present creates the same ambiguity as in the compress_and_close_forester: if the delegate is the first account inserted into packed_accounts, it receives index 0, making it indistinguishable from "no delegate."

Change delegate_index to Option<usize> in CompressAndCloseIndices or use usize::MAX as a non-colliding sentinel.

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

115-132: Test correctly adapts to unified API signature.

The mint_cache initialization with MintExtensionChecks::default() at index 0 is appropriate for this test since the token data uses mint: 0 (line 67). The updated set_input_compressed_account call properly passes all required parameters.

As noted in a previous review, tlv_data: None means TLV extension paths remain untested here. That's acceptable for this test's scope (validating basic input account creation), but dedicated TLV tests should exist elsewhere.

sdk-libs/ctoken-sdk/src/ctoken/transfer_interface.rs (1)

41-63: Constructor expansion is reasonable given always-required parameters.

Since decimals and system_program are required for the CPI paths (unlike optional SPL interface), including them in the constructor makes sense. The #[allow(clippy::too_many_arguments)] is appropriate.

The builder pattern suggestion from previous reviews remains valid for future ergonomic improvements, but isn't blocking.

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

217-220: Frozen state restoration uses magic number.

Line 219 uses 2 for AccountState::Frozen. This relates to the earlier suggestion about extracting constants for account states.

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

252-264: Magic number for AccountState::Frozen - already flagged in past review.

This uses *ctoken.state == 2 for frozen state check. A past review comment already flagged this for extraction to a constant.

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

68-93: Consider a more specific error for extension mismatch.

InvalidInstructionData is generic. A dedicated error like ExtensionMismatch would help callers diagnose why the transfer failed.

🔎 Suggested improvement
     fn check_t22_extensions(&self, other: &Self) -> Result<(), ProgramError> {
         if !self.t22_extensions_eq(other) {
-            Err(ProgramError::InvalidInstructionData)
+            Err(CTokenError::ExtensionMismatch.into())
         } else {
             Ok(())
         }
     }

This would require adding ExtensionMismatch to CTokenError.

forester/src/compressible/state.rs (1)

33-34: Using Rent::default() may not reflect actual on-chain rent parameters.

Rent::default() returns hardcoded mainnet values. In test environments or if Solana changes rent parameters, this could produce incorrect compressible slot calculations. Consider accepting the Rent sysvar as a parameter or fetching it via RPC in the calling context for accuracy.

program-libs/ctoken-interface/src/instructions/transfer2/instruction_data.rs (1)

34-37: TLV type change is a breaking serialization change—verify migration strategy.

The fields in_tlv and out_tlv changed from Option<Vec<Vec<u8>>> to Option<Vec<Vec<ExtensionInstructionData>>>. This alters the on-chain serialization format.

A previous review flagged this concern. Ensure that:

  1. SDK versions are coordinated with program deployment
  2. There's a clear migration path for existing transactions
  3. Error messages guide users if format mismatches occur
programs/compressed-token/program/src/create_token_account.rs (1)

177-190: Restricted extension enforcement is correct.

The validation correctly identifies restricted extensions and enforces compression_only mode when present. This prevents creating decompressible accounts for mints with complex extension behaviors.

Consider extracting has_restricted_extensions() as a method on MintExtensionFlags to reduce duplication with similar checks elsewhere.

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

132-133: Extract extension discriminators to named constants.

The discriminators (27, 28, 29, 30, 32) are hardcoded magic numbers. Consider defining them as constants for maintainability and to ensure consistency with ExtensionStruct enum indices:

const PAUSABLE_ACCOUNT_DISCRIMINATOR: u8 = 27;
const PERMANENT_DELEGATE_DISCRIMINATOR: u8 = 28;
const TRANSFER_FEE_DISCRIMINATOR: u8 = 29;
const TRANSFER_HOOK_DISCRIMINATOR: u8 = 30;
const COMPRESSIBLE_DISCRIMINATOR: u8 = 32;

Also applies to: 163-166, 174-177, 185-188, 196-199

programs/compressed-token/program/tests/token_output.rs (1)

124-124: Address the TODO for frozen flag randomization.

The frozen flag is hardcoded to false. For comprehensive test coverage, consider randomizing this similar to other test parameters.

🔎 Suggested fix:
-                        is_frozen: false, // TODO: make random
+                        is_frozen: rng.gen_bool(0.2), // 20% chance of frozen
programs/compressed-token/program/src/create_associated_token_account.rs (1)

118-126: Account sizing depends on extension detection - verify fallback is safe.

When mint_account is None, MintExtensionFlags::default() is used (all extensions false). This means the token account size calculation at line 126 will use the minimum size.

If the mint actually has extensions that require larger accounts (like TransferFee which needs 9 extra bytes per the relevant snippet), accounts created without the mint reference will be too small and potentially fail initialization or later operations.

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

103-106: Empty vec allocation when no TLV config.

When tlv_config is None or empty, an empty vec![] is still created. This is fine for correctness but could be avoided by using a default or sentinel value if performance matters here.

programs/compressed-token/program/src/extensions/check_mint_extensions.rs (2)

91-100: unwrap_or_default() silently swallows extension type enumeration errors.

If get_extension_types() fails, this returns an empty vector and has_restricted_extensions becomes false—potentially allowing restricted mints to bypass validation.


182-183: Same unwrap_or_default() pattern in has_mint_extensions.

This has the same issue as in check_mint_extensions - if get_extension_types() fails, the function silently returns default flags instead of surfacing the error. This could allow mints with parsing errors to be treated as having no extensions.

🔎 Suggested fix:
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;
program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (1)

158-225: Extract discriminant values to named constants.

The discriminant values (19, 27, 28, 29, 30, 31, 32) appear in multiple places:

  • zero_copy_at_mut match arms (lines 161, 169, 178, 187, 196, 205, 214)
  • new_zero_copy write operations (lines 284, 301, 318, 335, 352, 369, 386)

Extracting these to named constants improves maintainability and makes the serialization contract explicit:

🔎 Suggested constant definitions
// Extension discriminant values for serialization
// Reserved ranges: 0-18 (placeholders), 20-26 (placeholders)
// Token-2022 compatible range: 27-31
pub const TOKEN_METADATA_DISCRIMINANT: u8 = 19;
pub const PAUSABLE_ACCOUNT_DISCRIMINANT: u8 = 27;
pub const PERMANENT_DELEGATE_ACCOUNT_DISCRIMINANT: u8 = 28;
pub const TRANSFER_FEE_ACCOUNT_DISCRIMINANT: u8 = 29;
pub const TRANSFER_HOOK_ACCOUNT_DISCRIMINANT: u8 = 30;
pub const COMPRESSED_ONLY_DISCRIMINANT: u8 = 31;
pub const COMPRESSIBLE_DISCRIMINANT: u8 = 32;

Then use these constants in all match arms and write operations.

programs/compressed-token/anchor/src/instructions/create_token_pool.rs (2)

105-135: Add runtime validation for documented extension constraints.

The comments document that ConfidentialTransferMint, ConfidentialMintBurn, and ConfidentialTransferFeeConfig have specific constraints, but assert_mint_extensions() only validates TransferFeeConfig and TransferHook. This creates a gap between documented behavior and actual enforcement.

At minimum, consider validating:

  • ConfidentialTransferFeeConfig: fees must be zero (similar to TransferFeeConfig)
  • DefaultAccountState: document which states are acceptable

137-173: Complete the extension validation logic.

The validation for TransferFeeConfig and TransferHook is solid. However, several documented constraints are not enforced:

  1. ConfidentialTransferFeeConfig: Should validate fees are zero
  2. DefaultAccountState: No validation present despite being in the allowed list
  3. ConfidentialTransferMint/ConfidentialMintBurn: Comments say "initialized but not enabled" but no code enforces this

If these extensions truly don't need runtime checks (e.g., the "initialized but not enabled" state is acceptable), consider clarifying the comments to indicate validation is intentionally deferred or unnecessary.

Comment on lines 1 to 57
use light_ctoken_interface::{
state::calculate_ctoken_account_size, BASE_TOKEN_ACCOUNT_SIZE,
COMPRESSIBLE_PAUSABLE_TOKEN_ACCOUNT_SIZE, COMPRESSIBLE_TOKEN_ACCOUNT_SIZE,
};

#[test]
fn test_ctoken_account_size_calculation() {
// Base only (no extensions)
assert_eq!(
calculate_ctoken_account_size(false, false, false, false, false),
BASE_TOKEN_ACCOUNT_SIZE
);

// With compressible only
assert_eq!(
calculate_ctoken_account_size(true, false, false, false, false),
COMPRESSIBLE_TOKEN_ACCOUNT_SIZE
);

// With compressible + pausable
assert_eq!(
calculate_ctoken_account_size(true, true, false, false, false),
COMPRESSIBLE_PAUSABLE_TOKEN_ACCOUNT_SIZE
);

// With compressible + pausable + permanent_delegate (262 + 1 = 263)
assert_eq!(
calculate_ctoken_account_size(true, true, true, false, false),
263
);

// With pausable only (165 + 1 = 166)
assert_eq!(
calculate_ctoken_account_size(false, true, false, false, false),
166
);

// With permanent_delegate only (165 + 1 = 166)
assert_eq!(
calculate_ctoken_account_size(false, false, true, false, false),
166
);

// With pausable + permanent_delegate (165 + 1 + 1 = 167)
assert_eq!(
calculate_ctoken_account_size(false, true, true, false, false),
167
);

// With compressible + permanent_delegate (261 + 1 = 262)
assert_eq!(
calculate_ctoken_account_size(true, false, true, false, false),
262
);

// With transfer_fee only (165 + 9 = 174)
assert_eq!(
calculate_ctoken_account_size(false, false, false, true, false),
174
);

// With compressible + transfer_fee (261 + 9 = 270)
assert_eq!(
calculate_ctoken_account_size(true, false, false, true, false),
270
);

// With 4 extensions (261 + 1 + 1 + 9 = 272)
assert_eq!(
calculate_ctoken_account_size(true, true, true, true, false),
272
);

// With all 5 extensions (261 + 1 + 1 + 9 + 2 = 274)
assert_eq!(
calculate_ctoken_account_size(true, true, true, true, true),
274
);

// With transfer_hook only (165 + 2 = 167)
assert_eq!(
calculate_ctoken_account_size(false, false, false, false, true),
167
);
}
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

Comprehensive test coverage with room for improvement.

The test thoroughly exercises various extension combinations, which is excellent. However, several assertions use hard-coded numeric values (e.g., 263, 166, 167) while others reference named constants (BASE_TOKEN_ACCOUNT_SIZE, COMPRESSIBLE_TOKEN_ACCOUNT_SIZE).

💡 Consider defining constants for all expected sizes:
+const COMPRESSIBLE_PERMANENT_DELEGATE_SIZE: u64 = 262;
+const PAUSABLE_ONLY_SIZE: u64 = 166;
+const PERMANENT_DELEGATE_ONLY_SIZE: u64 = 166;
+// ... etc.

 // With compressible + pausable + permanent_delegate
 assert_eq!(
     calculate_ctoken_account_size(true, true, true, false, false),
-    263
+    COMPRESSIBLE_PAUSABLE_PERMANENT_DELEGATE_SIZE
 );

This approach:

  • Makes the test intent clearer (e.g., "COMPRESSIBLE_PERMANENT_DELEGATE_SIZE" vs "262")
  • Centralizes size definitions for easier maintenance
  • Reduces risk of typos in hard-coded values

That said, the current implementation works correctly and the hard-coded values do document the exact expected sizes clearly.

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

🤖 Prompt for AI Agents
program-libs/ctoken-interface/tests/ctoken/size.rs lines 1-85: Several
assertions use hard-coded numeric sizes (e.g., 263, 166, 167, 262, 174, 270,
272, 274) instead of named constants; define descriptive constants (or import
existing ones) for each expected size variant (e.g.,
COMPRESSIBLE_PERMANENT_DELEGATE_SIZE, PAUSABLE_SIZE,
PAUSABLE_PERMANENT_DELEGATE_SIZE, COMPRESSIBLE_PERMANENT_DELEGATE_SIZE,
TRANSFER_FEE_SIZE, COMPRESSIBLE_TRANSFER_FEE_SIZE, FOUR_EXTENSIONS_SIZE,
ALL_EXTENSIONS_SIZE, TRANSFER_HOOK_SIZE) by composing the known base constants
(BASE_TOKEN_ACCOUNT_SIZE, COMPRESSIBLE_TOKEN_ACCOUNT_SIZE,
COMPRESSIBLE_PAUSABLE_TOKEN_ACCOUNT_SIZE, and the extension deltas) and replace
the hard-coded literals in the assertions with those named constants so tests
remain clear and maintainable.

Comment on lines +62 to +85
/// Initializes a token account via CPI to the token program.
pub fn initialize_token_account<'info>(
token_account: &AccountInfo<'info>,
mint: &AccountInfo<'info>,
authority: &AccountInfo<'info>,
token_program: &AccountInfo<'info>,
) -> Result<()> {
let ix = spl_token_2022::instruction::initialize_account3(
token_program.key,
token_account.key,
mint.key,
authority.key,
)?;
anchor_lang::solana_program::program::invoke(
&ix,
&[
token_account.clone(),
mint.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
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 validating token_program is a known SPL program.

The CPI to initialize_account3 looks correct. However, token_program.key is used directly without validation that it's either SPL Token or Token-2022. While the Anchor Interface<'info, TokenInterface> constraint should enforce this elsewhere, this helper function could be called with an arbitrary account.

🔎 Optional: Add program ID validation
 pub fn initialize_token_account<'info>(
     token_account: &AccountInfo<'info>,
     mint: &AccountInfo<'info>,
     authority: &AccountInfo<'info>,
     token_program: &AccountInfo<'info>,
 ) -> Result<()> {
+    // Validate token program is SPL Token or Token-2022
+    if *token_program.key != spl_token::id() && *token_program.key != spl_token_2022::id() {
+        return Err(crate::ErrorCode::InvalidTokenProgram.into());
+    }
     let ix = spl_token_2022::instruction::initialize_account3(

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

🤖 Prompt for AI Agents
In programs/compressed-token/anchor/src/instructions/create_token_pool.rs around
lines 62-85, the CPI uses token_program.key directly without verifying it is the
official SPL Token or Token-2022 program; add a guard before building the
instruction that checks token_program.key equals spl_token::id() or
spl_token_2022::id() and return an appropriate error if it does not match.
Implement the check using the canonical id() helpers from those crates and
return a ProgramError::IncorrectProgramId (or convert into the function Result
type) when the key is not one of the expected program IDs, then proceed to
construct and invoke the CPI only after validation succeeds.

Comment on lines 67 to 75
let is_compress_and_close_with_rent_sponsor = mode == ZCompressionMode::CompressAndClose
&& compress_and_close_inputs
.as_ref()
.map(|inputs| inputs.rent_sponsor_is_signer_flag)
.unwrap_or(false);
if *ctoken.state == 2 && !is_compress_and_close_with_rent_sponsor {
msg!("Cannot modify frozen account");
return Err(ErrorCode::AccountFrozen.into());
}
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 magic number 2 for AccountState::Frozen into a constant.

The frozen state check *ctoken.state == 2 appears here and later at line 219. Defining a constant like const ACCOUNT_STATE_FROZEN: u8 = 2; would improve readability and reduce the risk of inconsistencies.

🔎 Suggested improvement:
// At module level or in a constants file
const ACCOUNT_STATE_FROZEN: u8 = 2;
const ACCOUNT_STATE_INITIALIZED: u8 = 1;

Then use these constants instead of magic numbers throughout.

🤖 Prompt for AI Agents
In
programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs
around lines 67–75, the code uses the magic number 2 to check for the frozen
account state; define a module-level constant (e.g., const ACCOUNT_STATE_FROZEN:
u8 = 2;) and replace the literal 2 here and at the other occurrence (around line
219) with that constant; optionally add other state constants (e.g.,
INITIALIZED) in the same place to keep state values consistent and improve
readability.

let mut withheld_transfer_fee: u64 = 0;
let delegated_amount: u64 = (*ctoken.delegated_amount).into();
// AccountState::Frozen = 2 in CToken
let is_frozen = ctoken.state == 2;
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

Verify the frozen state constant.

The code assumes ctoken.state == 2 represents AccountState::Frozen. While this appears correct based on SPL Token conventions, consider adding a constant or comment referencing the state enum to make this more maintainable and discoverable.

🔎 Consider adding clarity:
+        // AccountState: Uninitialized=0, Initialized=1, Frozen=2
         let is_frozen = ctoken.state == 2;

Or define a constant:

const ACCOUNT_STATE_FROZEN: u8 = 2;
// ...
let is_frozen = ctoken.state == ACCOUNT_STATE_FROZEN;
🤖 Prompt for AI Agents
In programs/registry/src/compressible/compressed_token/compress_and_close.rs
around line 111, the code uses a magic number to check frozen state with
`ctoken.state == 2`; replace this with a named constant or a clear comment
referencing the SPL Token AccountState enum to improve readability and
maintainability. Add a module-level constant like ACCOUNT_STATE_FROZEN: u8 = 2
(or import/alias the enum if available) and use it in the comparison, or add a
one-line comment that documents that 2 corresponds to AccountState::Frozen and
reference the SPL Token docs/enum.

Comment on lines +456 to +493
pub fn create_associated_ctoken_account_idempotent<'info>(
payer: AccountInfo<'info>,
associated_token_account: AccountInfo<'info>,
system_program: AccountInfo<'info>,
compressible_config: AccountInfo<'info>,
rent_sponsor: AccountInfo<'info>,
authority: Pubkey,
mint: Pubkey,
bump: u8,
pre_pay_num_epochs: Option<u8>,
lamports_per_write: Option<u32>,
) -> std::result::Result<(), solana_program_error::ProgramError> {
let inputs = CreateCompressibleAssociatedTokenAccountInputs {
payer: *payer.key,
owner: authority,
mint,
compressible_config: *compressible_config.key,
rent_sponsor: *rent_sponsor.key,
pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(2),
lamports_per_write,
token_account_version: TokenDataVersion::ShaFlat,
};

let ix = create_compressible_associated_token_account_with_bump_and_mode::<true>(
inputs,
*associated_token_account.key,
bump,
)?;

solana_cpi::invoke(
&ix,
&[
payer,
associated_token_account,
system_program,
compressible_config,
rent_sponsor,
],
)
}
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

Inconsistent authority handling between CPI wrappers.

create_associated_ctoken_account takes authority: AccountInfo and passes it in invoke, while create_associated_ctoken_account_idempotent takes authority: Pubkey and doesn't include it in the invoke accounts. This inconsistency is confusing and one of them is likely wrong.

If the on-chain program doesn't require authority as a separate account, remove it from both functions. If it does, add it to both consistently.

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs around
lines 456-495, the idempotent wrapper currently accepts authority: Pubkey and
does not include an authority AccountInfo in the CPI account list while the
non-idempotent wrapper accepts authority: AccountInfo and passes it to invoke;
make them consistent by changing this function to accept authority:
AccountInfo<'info>, set inputs.owner = *authority.key (instead of the Pubkey
param), and add the authority AccountInfo to the list passed to
solana_cpi::invoke so the CPI receives the authority account like the other
wrapper.

@ananas-block ananas-block force-pushed the jorrit/feat-more-ctoken-extensions2 branch from 7113d51 to 5c9e4e8 Compare December 18, 2025 17:05
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 (12)
sdk-libs/program-test/src/compressible.rs (1)

32-32: Consider removing redundant pubkey field.

The pubkey is stored both as the HashMap key (line 27) and as a field in the struct. This creates unnecessary data duplication. Since the HashMap already provides the key when iterating, consider removing the pubkey field from the struct.

🔎 Apply this diff to remove the redundant field:
 #[derive(Eq, Hash, PartialEq)]
 pub struct StoredCompressibleAccount {
-    pub pubkey: Pubkey,
     pub last_paid_slot: u64,
     pub account: CToken,
 }

Update the insertion site (around line 113-119):

     stored_compressible_accounts.insert(
         account.0,
         StoredCompressibleAccount {
-            pubkey: account.0,
             last_paid_slot: last_funded_slot,
             account: des_account.clone(),
         },
     );
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)

8-16: Consider making compression_only configurable.

The compression_only flag is currently hardcoded to true in the function body. If there are use cases where callers need to create compressible token accounts that are NOT compression-only, consider adding a compression_only: bool field to this input struct.

🔎 Proposed refactor to make compression_only configurable:
 pub struct CreateCompressibleTokenAccountInputs<'a> {
     pub owner: Pubkey,
     pub mint: Pubkey,
     pub num_prepaid_epochs: u8,
     pub payer: &'a Keypair,
     pub token_account_keypair: Option<&'a Keypair>,
     pub lamports_per_write: Option<u32>,
     pub token_account_version: TokenDataVersion,
+    pub compression_only: bool,
 }

Then update line 64 to use the input parameter:

     let compressible_params = CompressibleParams {
         compressible_config,
         rent_sponsor,
         pre_pay_num_epochs: num_prepaid_epochs,
         lamports_per_write,
         compress_to_account_pubkey: None,
         token_account_version,
-        compression_only: true,
+        compression_only: inputs.compression_only,
     };
forester/src/compressible/compressor.rs (1)

97-103: Hardcoded tree info should be addressed before production.

The FIXME comment and hardcoded TreeInfo values indicate this is temporary. The commented-out code shows the intended pattern using get_latest_active_state_trees(). Ensure this is tracked for resolution before mainnet deployment.

Would you like me to open an issue to track replacing these hardcoded values with dynamic tree lookup?

sdk-libs/ctoken-sdk/src/compressed_token/v2/transfer2/instruction.rs (2)

156-191: Remove commented-out dead code.

This commented-out transfer2 function appears to be legacy code. Dead code reduces readability and can cause confusion during maintenance. Consider removing it entirely or, if it's intended for future use, track it in an issue instead.

🔎 Remove the dead code block
-/*
-/// Create a multi-transfer instruction
-pub fn transfer2(inputs: create_transfer2_instruction) -> Result<Instruction> {
-    let create_transfer2_instruction {
-        fee_payer,
-        authority,
-        validity_proof,
-        token_accounts,
-        tree_pubkeys,
-        config,
-    } = inputs;
-
-    // Validate that no token account has been used
-    for token_account in &token_accounts {
-        if token_account.method_used {
-            return Err(CTokenSdkError::MethodUsed);
-        }
-    }
-
-    let config = config.unwrap_or_default();
-    let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, authority)
-        .with_sol_pool(
-            config.sol_pool_pda.unwrap_or_default(),
-            config.sol_decompression_recipient.unwrap_or_default(),
-        )
-        .with_cpi_context();
-
-    create_transfer2_instruction(
-        token_accounts,
-        validity_proof,
-        config,
-        meta_config,
-        tree_pubkeys,
-    )
-}
-*/

64-79: Improve validation ergonomics for in_tlv field.

The documentation for in_tlv is clear and the Option<Vec<Vec<...>>> design maintains good backward compatibility. However, consider adding SDK-level validation in create_transfer2_instruction() that checks in_tlv.len() matches token_accounts.len() before serialization. While the program validates this and returns CTokenError::InvalidInstructionData with a descriptive message, early SDK-level validation would catch mismatches during instruction building rather than on-chain, providing better developer experience with synchronous error feedback.

sdk-libs/token-client/src/actions/transfer2/decompress.rs (1)

14-34: Update doc comment to include decimals parameter.

The function signature now includes decimals: u8, but the doc comment doesn't document it. For API clarity, add a description of this parameter.

🔎 Add decimals to documentation
 /// # Arguments
 /// * `rpc` - RPC client with indexer capabilities
 /// * `compressed_token_account` - Slice of compressed token accounts to decompress
 /// * `decompress_amount` - Amount of tokens to decompress
 /// * `solana_token_account` - The SPL token account to receive the decompressed tokens
 /// * `authority` - Authority that can spend from the compressed token account
 /// * `payer` - Transaction fee payer keypair
+/// * `decimals` - Token decimals (required for SPL transfer_checked)
program-libs/ctoken-interface/src/constants.rs (1)

1-43: COMPRESSED_ONLY_EXTENSION_SIZE constant is incorrect and needs fixing.

The COMPRESSED_ONLY_EXTENSION_SIZE constant is set to 8 bytes, but the CompressedOnlyExtension struct contains two u64 fields (delegated_amount and withheld_transfer_fee), totaling 16 bytes. The constant should be updated to 16, and the comment should reflect both fields instead of only mentioning delegated_amount.

The other extension constants are correct:

  • TRANSFER_FEE_ACCOUNT_EXTENSION_LEN = 9 (1 discriminant + 8 withheld_amount) ✓
  • TRANSFER_HOOK_ACCOUNT_EXTENSION_LEN = 2 (1 discriminant + 1 transferring) ✓
  • TRANSFER2 = 101 ✓

The removal of COMPRESSIBLE_TOKEN_RENT_EXEMPTION is safe—no code references this constant anywhere in the codebase.

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

66-96: Fix misleading comment that incorrectly describes delegate support.

The comment at line 64 claims this function "Allows owner, account delegate, or permanent delegate" but the code only checks owner and permanent delegate—account delegates are intentionally excluded. The error message at line 95 repeats this inaccuracy. Update the comment to reflect the actual implementation: "Allows owner or permanent delegate (from mint) to authorize compression operations."

The unused _compression_amount parameter is intentional (underscore suppresses warnings), but consider adding a clarifying comment like // TODO: Reserve for future transfer fee calculations to document the design intent for future maintainers.

programs/compressed-token/program/src/transfer2/compression/spl.rs (1)

70-73: Consider returning an error instead of unimplemented!().

While the msg! provides context, unimplemented!() causes a hard panic. Returning a proper error would provide a cleaner failure mode and better error handling for callers.

🔎 Suggested fix:
         ZCompressionMode::CompressAndClose => {
-            msg!("CompressAndClose is unimplemented for spl token accounts");
-            unimplemented!()
+            msg!("CompressAndClose is not supported for SPL token accounts");
+            return Err(ProgramError::InvalidInstructionData);
         }
program-libs/ctoken-interface/src/instructions/extensions/mod.rs (1)

46-55: Fix position misalignment between ExtensionInstructionData and ExtensionStruct enums.

Both enums use #[repr(C)] with the ZeroCopy derive for serialization. Currently, ExtensionInstructionData has variants at positions 27, 28, and 31, but ExtensionStruct places the same variants at positions 33, 34, and 37 respectively. Only position 32 (Compressible) is correctly aligned. This mismatch will cause binary incompatibility and data corruption during serialization/deserialization. Align all variant positions between the two enums.

sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (2)

27-37: Doc example is missing the new decimals field.

The example code won't compile since TransferSplToCtoken now requires decimals: u8. This applies to both doc examples (lines 27-37 and 66-78).

🔎 Apply this diff to fix the doc example:
 let instruction = TransferSplToCtoken {
     amount: 100,
     spl_interface_pda_bump: 255,
+    decimals: 9,
     source_spl_token_account,
     destination_ctoken_account,
     authority,

66-78: Second doc example also missing decimals and system_program.

The TransferSplToCtokenCpi example needs both new fields.

🔎 Apply this diff:
 TransferSplToCtokenCpi {
     amount: 100,
     spl_interface_pda_bump: 255,
+    decimals: 9,
     source_spl_token_account,
     destination_ctoken_account,
     authority,
     mint,
     payer,
     spl_interface_pda,
     spl_token_program,
     compressed_token_program_authority,
+    system_program,
 }
♻️ Duplicate comments (40)
program-libs/zero-copy-derive/src/zero_copy.rs (1)

303-333: Immutable unit-struct path is technically sound.

The approach differs appropriately from the mutable path:

  • Immutable: Returns &'a reference to a static UNIT instance—safe because shared references to statics are allowed.
  • Mutable (in zero_copy_mut.rs): Uses Box::leak because returning &'a mut to a static would allow multiple mutable references, which is unsound.

The initialization static UNIT: #name = #name; correctly instantiates a unit struct (e.g., static UNIT: Foo = Foo;).

A previous review flagged that this unit-struct support contradicts the coding guidelines stating "Structs must have at least one field for zero-copy serialization; empty structs are not supported." That comment was marked as addressed. Ensure the README and guidelines are updated to document unit struct support as intentional for marker/phantom use cases.

program-libs/zero-copy-derive/src/shared/utils.rs (1)

24-28: UnitStruct variant cleanly extends the InputType enum.

Adding UnitStruct as a distinct variant (rather than trying to represent it as Struct with empty fields) is the right design choice. It makes the handling explicit in each derive implementation and avoids ambiguity about whether "no fields" means unit struct or an error.

Per the past review, documentation updates are needed to reflect that unit structs are now intentionally supported. The coding guidelines still state "Structs must have at least one field for zero-copy serialization."

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

64-75: Logic is sound; magic number already flagged.

The frozen account handling correctly allows CompressAndClose with rent sponsor authority—this is the intended behavior for compressing expired frozen accounts. The check on line 72 using *ctoken.state == 2 for frozen state was already noted in a previous review suggesting a named constant.


198-220: LGTM on fee and frozen state handling; magic number already flagged.

The withheld_transfer_fee handling correctly iterates through extensions to find TransferFeeAccount and adds the fee amount. The error path when no extension is found is appropriate since having a fee without the extension is an invalid state.

Line 219 uses *ctoken.state = 2 for frozen—this is the same magic number flagged in the past review comment. When that constant is introduced, update this location as well.

programs/compressed-token/anchor/src/instructions/create_token_pool.rs (1)

62-85: Past suggestion remains unaddressed: validate token_program ID.

The previous review comment (lines 62-85) suggested adding validation that token_program.key matches either spl_token::id() or spl_token_2022::id() before constructing the CPI. While the Interface<'info, TokenInterface> constraint provides some protection at the instruction level, adding explicit validation here would provide defense in depth.

🔎 Apply this diff to add token program validation:
 pub fn initialize_token_account<'info>(
     token_account: &AccountInfo<'info>,
     mint: &AccountInfo<'info>,
     authority: &AccountInfo<'info>,
     token_program: &AccountInfo<'info>,
 ) -> Result<()> {
+    // Validate token program is SPL Token or Token-2022
+    if *token_program.key != spl_token::id() && *token_program.key != spl_token_2022::id() {
+        return Err(crate::ErrorCode::InvalidTokenProgram.into());
+    }
     let ix = spl_token_2022::instruction::initialize_account3(
forester/src/compressible/state.rs (2)

33-34: Rent::default() uses hardcoded mainnet values.

This returns Solana mainnet rent parameters. In test environments or if rent parameters change, this could produce incorrect compressible slot calculations. For production forester code targeting mainnet, this is acceptable, but be aware of this limitation.


127-137: Silent skip on calculation failure may hide issues.

When calculate_compressible_slot fails, the account is silently skipped with only a warning. This prevents one bad account from blocking updates, but these accounts won't be tracked at all.

programs/registry/src/compressible/compressed_token/compress_and_close.rs (2)

100-104: Use a more appropriate error variant for CToken parsing failure.

RegistryError::InvalidSigner doesn't accurately describe a CToken parsing failure. This should use RegistryError::InvalidTokenAccountData for consistency with line 97-98.


111-111: Document the frozen state constant for maintainability.

The magic number 2 represents AccountState::Frozen from SPL Token. Consider adding a comment or constant to make this discoverable.

sdk-libs/program-test/src/forester/compress_and_close_forester.rs (1)

148-154: Sentinel value 0 for "no delegate" can collide with actual index 0.

Using 0 to represent "no delegate" is problematic because if a delegate account is the first inserted into packed_accounts, it will receive index 0, creating ambiguity.

The instruction handler mitigates this by reading has_delegate from actual CToken data, but this indirect safety mechanism masks a design flaw.

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

72-74: Magic number for AccountState::Initialized.

Similar to the existing comment about AccountState::Frozen (line 255, 268), line 74 uses the magic number 1 for AccountState::Initialized. Consider defining constants for both states:

const ACCOUNT_STATE_INITIALIZED: u8 = 1;
const ACCOUNT_STATE_FROZEN: u8 = 2;

Then use these throughout the file for consistency and clarity.

programs/compressed-token/program/tests/multi_sum_check.rs (1)

1-493: Test location concern already flagged in previous review.

A previous review comment already noted that integration tests should be moved to program-tests/ per coding guidelines. This applies to this test file.

sdk-libs/client/src/indexer/types.rs (1)

890-896: TLV deserialization concerns already flagged in previous review.

A previous review comment comprehensively identified issues with this TLV deserialization logic:

  • Duplicated implementation in both TokenAccount and TokenAccountV2
  • Generic error handling loses base64 vs. Borsh failure context
  • Missing integration test coverage in sdk-tests/
  • Should extract into shared helper function

These concerns remain unaddressed.

Also applies to: 931-937

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

86-92: Consider a more specific error for extension mismatch.

ProgramError::InvalidInstructionData is generic. A dedicated error like CTokenError::ExtensionMismatch would help callers diagnose why the transfer failed.

program-libs/ctoken-interface/src/instructions/transfer2/instruction_data.rs (1)

34-37: Breaking serialization change for TLV fields.

This type change from Vec<Vec<u8>> to Vec<Vec<ExtensionInstructionData>> alters the Borsh wire format. Older SDK-generated transactions will fail deserialization.

This was flagged in a previous review. Ensure the SDK and program are deployed atomically, or consider a versioned approach if gradual rollout is needed.

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

131-144: Acknowledge prior suggestion: TLV filtering comment.

A previous review correctly noted that only CompressedOnly is mapped here while other extension variants (Pausable, PermanentDelegate, TransferFee, TransferHook) are intentionally filtered out. These are marker extensions that reference mint-level configuration rather than carrying per-account state. The suggested comment would help future maintainers understand this design choice.

program-libs/ctoken-interface/src/instructions/extensions/compressible.rs (1)

21-25: Acknowledge prior concern: Potential flag/value inconsistency.

A previous review correctly identified that has_top_up (line 21) and write_top_up (line 25) could become inconsistent—e.g., has_top_up=0 but write_top_up>0, or vice versa. This creates two sources of truth for the same semantic information. Consider either:

  1. Removing has_top_up and treating write_top_up != 0 as the presence indicator, OR
  2. Adding validation logic where this struct is created/parsed to ensure (has_top_up == 0) == (write_top_up == 0) and reject inconsistent combinations

Without one of these approaches, consumers must guess which field takes precedence.

sdk-libs/ctoken-sdk/src/ctoken/compressible.rs (2)

37-50: Document the purpose and behavior of compression_only.

This field lacks documentation explaining what behavior it controls, when it should be true vs false, and its implications for compressible accounts. Additionally, the struct's docstring (lines 14-18) lists default values but doesn't include this field.


96-114: Add documentation and consider a builder method.

The same documentation issue applies: this field lacks explanation of its purpose and behavior. Additionally, unlike compress_to_account_pubkey which has a with_compress_to_pubkey builder method (line 118), there's no method to configure compression_only. If users need to set this value, consider adding:

pub fn with_compression_only(mut self, compression_only: bool) -> Self {
    self.compression_only = compression_only;
    self
}
sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs (1)

100-102: Make has_top_up conditional on write_top_up being provided.

Currently, has_top_up is hardcoded to 1 when a compressible config is provided, even when write_top_up is 0 (via unwrap_or(0) when config.lamports_per_write is None). This deviates from the pattern in create_associated_token_account.rs and create.rs, which only set has_top_up: 1 when lamports_per_write.is_some(). To stay consistent:

-                    has_top_up: 1,
+                    has_top_up: if config.lamports_per_write.is_some() { 1 } else { 0 },
programs/compressed-token/program/tests/token_input.rs (1)

115-132: Test correctly adapts to the new unified API signature.

The mint_cache initialization and updated set_input_compressed_account call are correct. Passing the same slice for both packed_accounts and all_accounts is valid for this test scenario where they're identical.

One observation: the test always passes tlv_data: None, so TLV/extension data flows aren't exercised here. Consider adding a follow-up test case that provides Some(tlv_data) with CompressedOnly extension data to validate the TLV population path in ZTokenDataMut::set.

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

27-40: Safe arithmetic with checked operations.

The add_withheld_amount method correctly uses checked_add to prevent overflow, which is critical for fee accumulation. The implementation pattern (get → checked_add → set) is idiomatic for zero-copy mutable references.

The ArithmeticOverflow error type is minimal but sufficient. As noted in a previous review, adding std::fmt::Display would improve error messages when converting to ProgramError, though the current definition works for no_std contexts.

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

54-59: Fix TLV boolean flag and improve type conversion safety.

The tlv tuple in TokenDataConfig has two issues:

  1. The boolean should reflect whether TLV data exists: use !tlv_config.is_empty() instead of hardcoding true. Other parts of the codebase (e.g., shared/token_output.rs and tests) correctly set this conditionally—hardcoding true when the config is empty causes incorrect byte length calculations.

  2. Replace the as u32 cast with .try_into() for explicit overflow handling. While Solana's 10MB account limit makes overflow unlikely, using try_into() makes the conversion bounds explicit and is more defensive.

🔎 Apply these fixes:
 let token_config = TokenDataConfig {
     delegate: (has_delegate, ()),
-    tlv: (true, tlv_config),
+    tlv: (!tlv_config.is_empty(), tlv_config),
 };
-TokenData::byte_len(&token_config).map_err(|_| ProgramError::InvalidAccountData)?
-    as u32
+TokenData::byte_len(&token_config)
+    .map_err(|_| ProgramError::InvalidAccountData)?
+    .try_into()
+    .map_err(|_| ProgramError::InvalidAccountData)?
programs/compressed-token/program/src/shared/owner_validation.rs (1)

40-60: Verbose logging concern already noted.

The multiple msg! calls on authorization failure add compute overhead on the error path. This was flagged in a previous review.

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

96-99: Compress mode validation remains commented out.

The documentation at line 71 states "Compress: NOT allowed (mints with restricted extensions must not be compressed)", but the validation is still commented out. This contradicts the documented design intent and was flagged in a previous review.

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

177-194: Mint extension validation and account sizing are well-implemented.

The logic correctly:

  1. Checks mint extensions once via has_mint_extensions
  2. Enforces compression_only when restricted extensions exist
  3. Calculates account size dynamically based on extension flags

However, as noted in a previous review, the has_restricted_extensions check duplicates logic from check_mint_extensions.rs. Consider extracting MintExtensionFlags::has_restricted_extensions() as a shared helper.

programs/compressed-token/program/src/ctoken_approve_revoke.rs (2)

113-117: Clarify the saturating_add(1) sentinel value pattern.

The +1 creates a sentinel allowing differentiation between "exactly at limit" and "exceeded", but this isn't immediately obvious. Since line 132 performs a direct comparison (transfer_amount > max_top_up), the budget mechanism becomes redundant.

Consider simplifying by removing saturating_add(1) and relying solely on the direct comparison, or adding a comment explaining the sentinel pattern if both checks are intentional.


130-137: Redundant max_top_up enforcement.

The direct comparison at line 132 (transfer_amount > max_top_up as u64) makes the lamports_budget mechanism on lines 113-117 redundant. Consider either:

  1. Using only the direct comparison (simpler), or
  2. Using only the budget mechanism (check lamports_budget == 0 after process_compressible_extension)
programs/compressed-token/program/src/create_associated_token_account.rs (1)

118-126: Account sizing falls back to minimum when mint_account is unavailable.

When mint_account is None (as in the create_ata path at line 81), MintExtensionFlags::default() produces all-false flags. If the mint actually has extensions like TransferFee (which requires extra bytes per the PR), the created account will be undersized.

The comment on lines 94-95 mentions create_ata2 uses this—verify that callers who need extension-aware sizing always provide the mint account.

#!/bin/bash
# Check how create_ata2 calls this function
rg -n "process_create_associated_token_account_inner" --type rust -B 3 -A 10
sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (1)

170-171: Pubkey::default() is not the System Program ID.

The System Program address is 11111111111111111111111111111111 (all ones), not the zero address. If lamport top-ups are intended to work, use solana_program::system_program::ID. If this feature isn't implemented yet, consider removing this account or adding a // TODO: not yet implemented comment.

programs/compressed-token/program/tests/token_output.rs (1)

124-124: TODO: Randomize frozen flag.

The frozen state path should be exercised. Consider implementing as suggested in the past review:

is_frozen: rng.gen_bool(0.2), // 20% chance of frozen
programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs (1)

107-116: Delegate lookup silently swallows errors.

When has_delegate() returns true but packed_accounts.get_u8() fails, the error is silently converted to None via .ok(). This could mask configuration errors. Consider logging or propagating the error.

program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (2)

160-225: Discriminant handling is correct but should use constants.

The discriminant-to-variant mapping (19→TokenMetadata, 27→PausableAccount, 28→PermanentDelegateAccount, 29→TransferFeeAccount, 30→TransferHookAccount, 31→CompressedOnly, 32→Compressible) is intentionally decoupled from enum indices for Token-2022 compatibility.

However, as noted in previous reviews, these magic numbers should be extracted to named constants and documented as part of the serialization contract.

🔎 Define constants at module level:
/// Discriminant values for ExtensionStruct variants
/// These are intentionally decoupled from enum indices for Token-2022 compatibility
pub const TOKEN_METADATA_DISCRIMINANT: u8 = 19;
pub const PAUSABLE_ACCOUNT_DISCRIMINANT: u8 = 27;
pub const PERMANENT_DELEGATE_ACCOUNT_DISCRIMINANT: u8 = 28;
pub const TRANSFER_FEE_ACCOUNT_DISCRIMINANT: u8 = 29;
pub const TRANSFER_HOOK_ACCOUNT_DISCRIMINANT: u8 = 30;
pub const COMPRESSED_ONLY_DISCRIMINANT: u8 = 31;
pub const COMPRESSIBLE_DISCRIMINANT: u8 = 32;

378-394: Inconsistent size validation pattern for CompressedOnly.

The CompressedOnly case validates buffer size explicitly (line 380) before writing, while other cases rely on new_zero_copy to fail if the buffer is too small. This inconsistency was noted in previous reviews.

For consistency, either add explicit pre-checks to all variants or rely uniformly on new_zero_copy for validation.

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

116-133: Extract discriminator values to named constants.

The discriminators (27, 28, 29, 30, 32) are hardcoded magic numbers. As flagged in previous reviews, these should be extracted to named constants for maintainability and to ensure consistency with the ExtensionStruct enum in extension_struct.rs.

🔎 Suggested constants:
// At module level or in a shared constants module
const PAUSABLE_ACCOUNT_DISCRIMINATOR: u8 = 27;
const PERMANENT_DELEGATE_DISCRIMINATOR: u8 = 28;
const TRANSFER_FEE_DISCRIMINATOR: u8 = 29;
const TRANSFER_HOOK_DISCRIMINATOR: u8 = 30;
const COMPRESSIBLE_DISCRIMINATOR: u8 = 32;
programs/compressed-token/program/src/extensions/check_mint_extensions.rs (2)

91-100: unwrap_or_default() silently swallows extension type enumeration errors.

If get_extension_types() fails, this returns an empty vector and has_restricted_extensions becomes false—potentially allowing restricted mints to bypass validation.

🔎 Handle the error explicitly:
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;

182-191: Same unwrap_or_default() issue in has_mint_extensions.

Line 183 has the same silent error swallowing issue. If extension type enumeration fails, the function returns default flags instead of propagating the error, which could lead to accounts being created with incorrect extension configurations.

🔎 Propagate the error:
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;
sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs (3)

206-210: System program pubkey is incorrect - will cause CPI failures.

Using Pubkey::new_from_array([0; 32]) creates a null pubkey (all zeros), not the system program ID. This will cause the instruction to fail on-chain when the program validates the system program account.

🔎 Fix the system program pubkey:
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false), // system_program
+        solana_instruction::AccountMeta::new_readonly(solana_program::system_program::ID, false), // system_program

Or if using solana_system_interface:

+use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
// ...
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false), // system_program
+        solana_instruction::AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), // system_program

383-389: Same system program issue in v2 instruction builder.

The [0; 32] placeholder is used here as well. This needs the same fix to use the actual system program ID.

🔎 Apply the same fix:
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false),
+        solana_instruction::AccountMeta::new_readonly(solana_program::system_program::ID, false),

410-451: Inconsistent authority handling between CPI wrappers.

create_associated_ctoken_account (line 416) takes authority: AccountInfo and passes it in the invoke accounts (line 448), while create_associated_ctoken_account_idempotent (line 462) takes authority: Pubkey and doesn't include it in the invoke accounts.

This inconsistency is confusing and one of them is likely incorrect. If the on-chain program expects the authority as a separate account for validation, both should pass it. If not, neither should.

Additionally, the non-idempotent wrapper passes 6 accounts to invoke but the instruction built by create_compressible_associated_token_account_with_bump only includes 5 accounts in its AccountMeta list (lines 206-222), which will cause a CPI mismatch.

Also applies to: 456-495

Comment on lines +245 to +246
// Single assert comparing full account state
assert_eq!(ctoken, expected_ctoken, "CToken account should match expected");
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 additional test patterns beyond the single assertion approach.

The section demonstrates a comprehensive single-assert pattern, which is excellent for full account validation. However, consider adding guidance for scenarios where tests need to: (a) assert on specific fields only (partial validation), (b) handle variable extension ordering, or (c) test failed account creation paths. A follow-up section with these patterns would strengthen the testing guidance.

🤖 Prompt for AI Agents
In CLAUDE.md around lines 245-246, expand the testing section by adding a
follow-up subsection that covers three patterns: (a) partial validation — show
how to assert specific fields rather than the entire account (e.g., compare
selected fields or use helper matchers to ignore irrelevant fields), (b)
extension-order resilience — describe comparing extensions in an
order-independent way (e.g., sort or compare by key/ID or convert to a map/set
before asserting), and (c) negative/failure paths — demonstrate testing failed
account creation by asserting error kinds/messages and any preserved state. Keep
each pattern short with a one-paragraph explanation and a small example outline
for the assertion strategy and test setup.

Comment on lines +141 to 158
// Build TLV extensions for this output if marker extensions are present
if has_marker_extensions {
out_tlv.push(vec![ExtensionInstructionData::CompressedOnly(
CompressedOnlyExtensionInstructionData {
delegated_amount,
withheld_transfer_fee,
is_frozen,
},
)]);
} else {
out_tlv.push(vec![]);
}

// Create one output account per compression operation
output_accounts.push(MultiTokenTransferOutputData {
owner: idx.owner_index,
amount,
delegate: 0,
delegate: idx.delegate_index,
mint: idx.mint_index,
version: 3, // Shaflat
has_delegate: false,
has_delegate: delegated_amount > 0,
});
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 . -name "compress_and_close.rs" -type f

Repository: Lightprotocol/light-protocol

Length of output: 497


🏁 Script executed:

cat -n ./programs/registry/src/compressible/compressed_token/compress_and_close.rs | sed -n '130,170p'

Repository: Lightprotocol/light-protocol

Length of output: 1867


🏁 Script executed:

cat -n ./programs/registry/src/compressible/compressed_token/compress_and_close.rs | sed -n '100,140p'

Repository: Lightprotocol/light-protocol

Length of output: 2169


🏁 Script executed:

cat -n ./programs/registry/src/compressible/compressed_token/compress_and_close.rs | sed -n '1,50p'

Repository: Lightprotocol/light-protocol

Length of output: 2518


🏁 Script executed:

# Let me check what happens downstream with this data - look for where MultiTokenTransferOutputData is used
rg -n "MultiTokenTransferOutputData" --type rust -A 5 programs/registry/src/ | head -40

Repository: Lightprotocol/light-protocol

Length of output: 1368


🏁 Script executed:

# Check if CompressedOnly extension is the only way to preserve delegated_amount downstream
rg -n "delegated_amount" --type rust -B 2 -A 2 programs/compressed-token/ | head -60

Repository: Lightprotocol/light-protocol

Length of output: 5326


🏁 Script executed:

# Check how the transfer2 instruction processes the TLV and output data
rg -n "transfer2\|Transfer2" --type rust -l programs/compressed-token/program/src/ | head -5

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Let's find how the CompressedOnly extension is processed
rg -n "CompressedOnly" --type rust -B 2 -A 5 programs/compressed-token/program/src/ | head -80

Repository: Lightprotocol/light-protocol

Length of output: 7940


🏁 Script executed:

# Check the ACCOUNTS.md documentation for account layout specifications
cat ./programs/compressed-token/program/docs/ACCOUNTS.md | head -100

Repository: Lightprotocol/light-protocol

Length of output: 3165


🏁 Script executed:

# Let's verify if the MultiTokenTransferOutputData.delegate index is used to reconstruct delegated_amount
rg -n "delegate.*index\|delegate_index" --type rust -B 2 -A 2 programs/compressed-token/program/src/transfer2/token_outputs.rs | head -40

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

cat -n ./programs/compressed-token/program/src/transfer2/token_outputs.rs | sed -n '1,150p'

Repository: Lightprotocol/light-protocol

Length of output: 4526


🏁 Script executed:

cat -n ./programs/compressed-token/program/src/shared/token_output.rs | sed -n '1,150p'

Repository: Lightprotocol/light-protocol

Length of output: 6314


🏁 Script executed:

# Check if delegated_amount can be recovered from anywhere else besides CompressedOnly
rg -n "delegated_amount\|CompressedOnly" --type rust programs/compressed-token/program/src/shared/token_output.rs -A 3 -B 3

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check the TokenData structure and how it handles delegated_amount
rg -n "delegated_amount\|struct TokenData" --type rust -A 5 program-libs/ctoken-types/src/state/ | head -80

Repository: Lightprotocol/light-protocol

Length of output: 148


🏁 Script executed:

find . -name "*token_data*" -type f | grep -E "\.rs$"

Repository: Lightprotocol/light-protocol

Length of output: 601


🏁 Script executed:

rg -n "delegated_amount\|pub struct TokenData" --type rust -B 2 -A 10 program-libs/ctoken-interface/src/state/compressed_token/token_data.rs | head -100

Repository: Lightprotocol/light-protocol

Length of output: 54


Preserve delegated_amount in CompressedOnly extension when delegate exists, even without marker extensions.

The code only creates the CompressedOnly extension when marker extensions are present (frozen state, transfer fees, pausable, permanent delegate, transfer hook, or compressible extension). However, when an account has a delegate with a delegated_amount but none of these marker extensions, the delegated_amount value is lost—the delegate index only stores the delegate's pubkey, not the spending limit.

The compressed output will have has_delegate: true but no way to recover the original delegated_amount because it's only preserved in CompressedOnly. To fix this, create CompressedOnly whenever delegated_amount > 0, not just when marker extensions are present.

🤖 Prompt for AI Agents
In programs/registry/src/compressible/compressed_token/compress_and_close.rs
around lines 141 to 162, the CompressedOnly extension is only added when
has_marker_extensions is true, which drops delegated_amount when a delegate
exists but no marker extensions are present; change the condition to add the
CompressedOnly extension whenever has_marker_extensions || delegated_amount > 0
so delegated_amount is preserved in the TLV, keeping the same
CompressedOnlyExtensionInstructionData fields (delegated_amount,
withheld_transfer_fee, is_frozen) and leaving the output_accounts construction
(including has_delegate: delegated_amount > 0) unchanged.

.unwrap_or(0),
mint: packed_accounts.insert_or_get(token.mint),
version,
version: if tlv.is_some() { 3 } else { 2 },
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 checking for non-empty TLV, not just presence.

The version is set to 3 when tlv.is_some(), but this would also apply if tlv = Some(vec![]). In token_inputs.rs (line 47), the check uses is_some_and(|v| !v.is_empty()) for validation. Consider aligning this logic:

🔎 Suggested fix:
-        version: if tlv.is_some() { 3 } else { 2 },
+        version: if tlv.as_ref().is_some_and(|v| !v.is_empty()) { 3 } else { 2 },
📝 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
version: if tlv.is_some() { 3 } else { 2 },
version: if tlv.as_ref().is_some_and(|v| !v.is_empty()) { 3 } else { 2 },
🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/compressed_token/v2/decompress_full.rs around line
174, the code sets version based on tlv.is_some(), which treats Some(empty_vec)
the same as a non-empty TLV; change the condition to check for a non-empty TLV
(e.g., use is_some_and(|v| !v.is_empty())) so version is set to 3 only when tlv
contains data, matching the validation in token_inputs.rs.

use solana_signer::Signer;

/// Transfer tokens from a compressed token account to an SPL token account
#[allow(clippy::too_many_arguments)]
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

Decimals parameter added; consider builder pattern for parameter-heavy functions.

The decimals parameter is correctly propagated to the TransferCTokenToSpl instruction. The #[allow(clippy::too_many_arguments)] attribute indicates the function now has 8 parameters. While acceptable for this incremental change, consider refactoring to a builder pattern in future work if additional parameters are anticipated.

Also applies to: 24-24, 38-38

🤖 Prompt for AI Agents
In sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs around lines 15,
24 and 38, the function now takes eight parameters (triggering
#[allow(clippy::too_many_arguments)]) after adding decimals; refactor to a
builder pattern: introduce a TransferCTokenToSplParams struct containing all
current parameters (including decimals) and implement a fluent builder (new() +
setter methods for each field and a build() that validates required fields),
change the instruction function signature to accept a single params struct (or
the built params), update all call sites to construct the params via the
builder, remove the clippy allow attribute, and add minimal validation in
build() for required invariants.

@ananas-block ananas-block changed the title feat: more ctoken extensions rebased feat: more ctoken extensions rebased Dec 18, 2025
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: 2

Caution

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

⚠️ Outside diff range comments (1)
sdk-libs/ctoken-sdk/src/compressed_token/v2/decompress_full.rs (1)

63-87: TLV collection logic is correct but could be simplified.

The logic correctly collects TLV data and tracks whether any TLV exists. However, the conditional push could be simplified:

🔎 Optional simplification
-        // Collect TLV data for this input
-        if let Some(tlv) = &idx.tlv {
-            has_any_tlv = true;
-            in_tlv_data.push(tlv.clone());
-        } else {
-            in_tlv_data.push(Vec::new());
-        }
+        // Collect TLV data for this input
+        let tlv = idx.tlv.clone().unwrap_or_default();
+        if !tlv.is_empty() {
+            has_any_tlv = true;
+        }
+        in_tlv_data.push(tlv);
♻️ Duplicate comments (5)
program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (1)

364-380: CompressedOnly has explicit size validation – good defensive programming.

The explicit length check at line 366 before writing the discriminant is more defensive than other variants. As previously noted, consider making this consistent across all variants.

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

65-70: Magic number for Frozen state – extract to constant.

The literal 2 for AccountState::Frozen appears here (and was previously flagged). Consider defining a constant like const ACCOUNT_STATE_FROZEN: u8 = 2; to improve readability and prevent inconsistencies.

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

87-117: Delegate lookup silently swallows errors – previously flagged.

At line 108, .ok() converts any error from packed_accounts.get_u8() to None, which can mask issues when has_delegate() returns true but the delegate index is invalid. Consider propagating or logging the error.

🔎 Suggested improvement:
             let input_delegate = matching_input_index.and_then(|idx| {
                 let input = inputs.in_token_data.get(idx)?;
                 if input.has_delegate() {
-                    packed_accounts
+                    match packed_accounts
                         .get_u8(input.delegate, "input delegate")
-                        .ok()
+                    {
+                        Ok(delegate) => Some(delegate),
+                        Err(e) => {
+                            solana_msg::msg!("Failed to resolve input delegate: {:?}", e);
+                            None
+                        }
+                    }
                 } else {
                     None
                 }
             });
sdk-libs/ctoken-sdk/src/ctoken/compressible.rs (2)

37-37: Documentation still missing for compression_only field.

The compression_only field lacks any doc comment explaining its purpose, when to set it, or its implications for compressible account behavior. The struct-level docstring (lines 14-18) lists default values but omits this field.


96-96: Documentation and builder method still missing.

The compression_only field in CompressibleParamsCpi lacks documentation, and unlike compress_to_account_pubkey (which has a builder method at line 118), there's no way to configure this field through a builder pattern.

Comment on lines 50 to 54
// Reject uninitialized accounts (state == 0)
if *ctoken.state == 0 {
msg!("Account is uninitialized");
return Err(CTokenError::InvalidAccountState.into());
}
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

Redundant uninitialized account check.

This check duplicates the validation already performed by CToken::zero_copy_at_mut_checked at line 48, which returns InvalidAccountState when bytes[108] == 0. The double-check is defensive but adds unnecessary code.

🔎 Consider removing the redundant check:
     let (mut ctoken, _) = CToken::zero_copy_at_mut_checked(&mut token_account_data)?;
 
-    // Reject uninitialized accounts (state == 0)
-    if *ctoken.state == 0 {
-        msg!("Account is uninitialized");
-        return Err(CTokenError::InvalidAccountState.into());
-    }
     if ctoken.mint.to_bytes() != mint {

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

🤖 Prompt for AI Agents
programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs
around lines 50 to 54: the explicit uninitialized-account check (if
*ctoken.state == 0 { ... return Err(...) }) duplicates the validation already
performed by CToken::zero_copy_at_mut_checked at line 48; remove that entire
if-block (lines 50–54) so the function relies on the prior checked constructor,
and run/adjust tests or callers if they depended on the duplicated log message.

@ananas-block ananas-block force-pushed the jorrit/feat-more-ctoken-extensions2 branch from a6748f8 to 1156039 Compare December 18, 2025 22:41
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: 9

Caution

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

⚠️ Outside diff range comments (3)
program-libs/ctoken-interface/src/instructions/extensions/compressible.rs (1)

56-70: Update comment to match actual implementation.

The comment describing modifications (line 59) states that the function returns CTokenError::InvalidAccountData when seeds.len() > MAX_SEEDS, but the actual implementation (line 68-69) checks seeds.len() >= MAX_SEEDS and returns CTokenError::TooManySeeds(MAX_SEEDS - 1).

🔎 Proposed fix
 // Taken from pinocchio 0.9.2.
 // Modifications:
 // -  seeds: &[&[u8]; N], ->  seeds: &[&[u8]],
-// - if seeds.len() > MAX_SEEDS CTokenError::InvalidAccountData
+// - if seeds.len() >= MAX_SEEDS returns CTokenError::TooManySeeds(MAX_SEEDS - 1)
 pub fn derive_address(
sdk-libs/ctoken-sdk/src/compressed_token/v2/account2.rs (2)

253-284: Document the decimals parameter.

Similar to compress_spl, the new decimals: u8 parameter in decompress_spl lacks documentation explaining its purpose and expected values.

Add documentation clarifying the parameter's role in SPL token decompression and its relationship to the token mint's decimal precision.


193-223: Add inline documentation for the decimals parameter in compression/decompression methods.

The decimals: u8 parameter in compress_spl and decompress_spl lacks explanation. While compress_full includes an inline comment clarifying when decimals are used (// Not used for ctoken compression), these methods need similar documentation.

Decimals are passed through to SPL token program's transfer_checked instruction where the on-chain program validates they match the mint's actual decimal count. Add a comment like:

/// decimals - Mint decimal precision for SPL token operations (validated by transfer_checked)

This clarifies the parameter's purpose and documents that validation happens at the SPL program layer, consistent with the pattern already established in compress_full.

♻️ Duplicate comments (7)
sdk-libs/ctoken-sdk/src/ctoken/transfer_spl_ctoken.rs (1)

174-175: Replace Pubkey::default() with actual System Program ID.

Line 175 uses Pubkey::default() (zero address) for the System program placeholder. The actual System Program ID is 11111111111111111111111111111111.

Since the comment indicates this account is "needed for compressible account lamport top-ups," either:

  1. Use the correct System Program ID constant (e.g., from solana_program::system_program::ID or define it locally)
  2. Or remove this account entirely if lamport transfers remain unimplemented

Note: The on-chain Transfer2 processor currently rejects in_lamports and out_lamports as unimplemented (see lines 305-306 in TRANSFER2.md), so this account may not be actively used yet.

🔎 Proposed fix using system program constant
-            // System program (index 6) - needed for compressible account lamport top-ups
-            AccountMeta::new_readonly(Pubkey::default(), false),
+            // System program (index 6) - needed for compressible account lamport top-ups
+            AccountMeta::new_readonly(solana_program::system_program::ID, false),
program-libs/ctoken-interface/src/error.rs (1)

151-152: Consider improving error message clarity.

The error messages could be more specific:

  • Line 151-152: The message "CompressedOnly tokens cannot have compressed outputs - must decompress only" mixes prohibition and directive. Consider: "CompressedOnly extension prohibits compressed outputs" or "CompressedOnly tokens can only be decompressed, not compressed"

  • Line 154-155: The term "Output TLV data count must match number of compressed outputs" doesn't clearly indicate what's mismatched. Consider: "TLV output count mismatch: provided {provided} but expected {expected}" or "Number of TLV entries must match number of compressed outputs"

Clear error messages help developers quickly diagnose issues during debugging.

Also applies to: 154-155

sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs (4)

205-209: System program pubkey is incorrect.

Pubkey::new_from_array([0; 32]) creates a null pubkey, not the system program ID. This will cause on-chain validation to fail when the program checks for the system program account.

🔎 Proposed fix
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false), // system_program
+        solana_instruction::AccountMeta::new_readonly(solana_system_interface::program::ID, false), // system_program

You'll need to add the import:

use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;

381-387: Same system program issue in v2 instruction builder.

This uses the same [0; 32] placeholder which will fail on-chain validation.

🔎 Proposed fix
-        solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false),
+        solana_instruction::AccountMeta::new_readonly(solana_system_interface::program::ID, false),

406-449: CPI wrapper passes authority but instruction doesn't expect it.

The create_associated_ctoken_account function takes authority: AccountInfo (line 414) and passes it in the invoke accounts (line 446), but the instruction built by create_compressible_associated_token_account_with_bump only expects 5 accounts: [payer, ata, system_program, compressible_config, rent_sponsor]. This account count mismatch will cause the CPI to fail.

Either remove authority from the invoke accounts if it's not needed by the on-chain program, or add it to the instruction's account list if it is required.


454-493: Inconsistent authority handling between CPI wrappers.

create_associated_ctoken_account takes authority: AccountInfo and passes it in invoke (though incorrectly), while create_associated_ctoken_account_idempotent takes authority: Pubkey and doesn't include it in invoke accounts. This inconsistency is confusing and one implementation is likely wrong.

Harmonize the signatures and account handling:

  • If authority is needed: both should take AccountInfo and include it in invoke
  • If authority is not needed: both should just use Pubkey for owner derivation only
programs/compressed-token/program/src/transfer2/check_extensions.rs (1)

94-97: Uncomment Compress mode validation for restricted extensions

The documented behavior states "Compress: NOT allowed (mints with restricted extensions must not be compressed)" (line 69), but the validation is commented out, silently allowing restricted mints to compress. CompressAndClose mode has equivalent validation enabled (lines 73-92). Restore the validation to match the documented design intent.

🔎 Restore the commented validation
 ZCompressionMode::Compress => {
-    // msg!("Mints with restricted extensions cannot be compressed");
-    // return Err(ErrorCode::MintHasRestrictedExtensions.into());
+    msg!("Mints with restricted extensions cannot be compressed");
+    return Err(ErrorCode::MintHasRestrictedExtensions.into());
 }

Comment on lines +199 to 209
// This is the compression-only hot path (no compressed inputs/outputs).
// Extension checks are skipped because balance must be restored immediately
// (compress + decompress in same tx) or sum check will fail.
process_token_compression(
fee_payer,
inputs,
&validated_accounts.packed_accounts,
cpi_authority_pda,
inputs.max_top_up.get(),
mint_cache,
)?;
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 clarifying the extension check comment

The comment states "Extension checks are skipped" while still passing mint_cache to process_token_compression. While this is likely correct (the cache is required by the function signature, and specific checks may be relaxed downstream), consider clarifying what "skipped" means—perhaps "certain extension checks are relaxed" or "balance-restoration checks are deferred."

The logic itself is sound for the compression-only hot path.

🤖 Prompt for AI Agents
In programs/compressed-token/program/src/transfer2/processor.rs around lines 199
to 209, the comment "Extension checks are skipped" is ambiguous while the code
still passes mint_cache into process_token_compression; update the comment to
explicitly state that only certain extension checks are relaxed or deferred
(e.g., balance-restoration/extension validation deferred because
compress+decompress occur in the same tx) and note that mint_cache is still
required by the function signature for any remaining validations; keep the
current call and behavior unchanged, just make the comment precise about which
checks are skipped versus which are still performed.

Comment on lines +172 to +188
let compressible_extension = if COMPRESSIBLE {
if let Some((pre_pay_num_epochs, lamports_per_write, _, _, token_account_version)) =
compressible_config
{
Some(CompressibleExtensionInstructionData {
token_account_version: token_account_version as u8,
rent_payment: pre_pay_num_epochs,
compression_only: 0,
write_top_up: lamports_per_write.unwrap_or(0),
compress_to_account_pubkey: None, // Not used for ATA creation
})
} else {
return Err(TokenSdkError::InvalidAccountData);
}
} else {
None
};
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

compression_only is hardcoded to 0, inconsistent with other builders.

In create.rs and create_ata.rs, compression_only is derived from config.compression_only. Here it's always set to 0. If the CreateCompressibleAssociatedTokenAccountInputs struct should support this field, it needs to be added and propagated.

🔎 Proposed fix

Add the field to the inputs struct:

 pub struct CreateCompressibleAssociatedTokenAccountInputs {
     // ... existing fields ...
     pub token_account_version: TokenDataVersion,
+    /// Whether the account is compression-only (cannot be transferred while decompressed)
+    pub compression_only: bool,
 }

Then propagate it in the unified builder:

-                compression_only: 0,
+                compression_only: if compressible_config.5 { 1 } else { 0 },

Note: You'll need to update the tuple type and all call sites accordingly.

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

Comment on lines +230 to +239
pub fn derive_ctoken_ata(owner: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
owner.as_ref(),
light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID.as_ref(),
mint.as_ref(),
],
&Pubkey::from(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID),
)
}
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

derive_ctoken_ata is duplicated from create_ata.rs.

This function is identical to the one in sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs (lines 16-25 per relevant_code_snippets). Consider extracting it to a shared module to avoid drift.

You could move this to a common utilities module (e.g., ctoken/mod.rs or a new ctoken/utils.rs) and re-export it from both locations for backward compatibility.

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs around
lines 230 to 239, the derive_ctoken_ata function is duplicated from
create_ata.rs; extract this helper into a shared module (e.g.,
sdk-libs/ctoken-sdk/src/ctoken/utils.rs or
sdk-libs/ctoken-sdk/src/ctoken/mod.rs), move the derive_ctoken_ata
implementation there, update both create_associated_token_account.rs and
create_ata.rs to import/re-export the function (pub use
crate::ctoken::utils::derive_ctoken_ata or pub use
super::utils::derive_ctoken_ata as appropriate) and remove the duplicate
implementation, then run a build to ensure all references compile.

Comment on lines +352 to +368
let compressible_extension = if COMPRESSIBLE {
if let Some((pre_pay_num_epochs, lamports_per_write, _, _, token_account_version)) =
compressible_config
{
Some(CompressibleExtensionInstructionData {
token_account_version: token_account_version as u8,
rent_payment: pre_pay_num_epochs,
compression_only: 0,
write_top_up: lamports_per_write.unwrap_or(0),
compress_to_account_pubkey: None,
})
} else {
return Err(TokenSdkError::InvalidAccountData);
}
} else {
None
};
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

Same compression_only hardcoding issue in v2 builder.

This also hardcodes compression_only: 0 instead of deriving from config, creating the same inconsistency with create.rs and create_ata.rs.

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/create_associated_token_account.rs around
lines 352 to 368, the CompressibleExtensionInstructionData currently hardcodes
compression_only: 0; update the code to read compression_only from the
compressible_config tuple (the appropriate element in the tuple that represents
compression_only), cast/convert it to the expected u8 type, and assign that
value to compression_only instead of 0 while keeping the same None/error
handling when compressible_config is missing.

Comment on lines +431 to +436
// TODO: switch to wrapper ixn using accounts instead of ixdata.
let ix = create_compressible_associated_token_account_with_bump(
inputs,
*associated_token_account.key,
bump,
)?;
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

Address TODO or track it.

The TODO comment indicates this should switch to a wrapper instruction using accounts instead of instruction data. Consider opening an issue to track this work if it's not being addressed in this PR.

Would you like me to open an issue to track this technical debt?

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: 2

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/anchor/src/lib.rs (1)

543-552: Consider adding doc comments for clarity.

The check_cpi_context function correctly validates that CPI context isn't in write mode (set_context or first_set_context), which would be incompatible with operations like burn, compression, or decompression.

However, adding doc comments would improve maintainability by explaining:

  • Which operations this constraint applies to (burn, compress, decompress)
  • Why these flags are incompatible with those operations
  • What set_context and first_set_context signify
📝 Suggested documentation addition
+/// Checks if CPI context usage is valid for the current instruction.
+///
+/// Returns an error if the CPI context has `set_context` or `first_set_context` set to true,
+/// which indicates the context is being written/modified. This is incompatible with
+/// burn, compression (in transfer), and decompression operations.
+///
+/// # Arguments
+/// * `cpi_context` - Optional CPI context to validate
+///
+/// # Errors
+/// Returns `ErrorCode::CpiContextSetNotUsable` if context write flags are set
 pub fn check_cpi_context(cpi_context: &Option<CompressedCpiContext>) -> Result<()> {
♻️ Duplicate comments (9)
programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (2)

50-54: Redundant uninitialized account check (previously flagged).

This check duplicates the validation already performed by CToken::zero_copy_at_mut_checked at line 48. As noted in the previous review, this defensive double-check adds unnecessary code.


67-67: Magic number for frozen state (previously flagged).

Lines 67 and 211 use the literal 2 to check and set the frozen account state. As noted in the previous review, defining constants like const ACCOUNT_STATE_FROZEN: u8 = 2; would improve readability and maintainability.

Also applies to: 211-211

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

115-132: Test correctly adapts to the expanded API signature.

The mint_cache initialization with MintExtensionChecks::default() for mint index 0 is appropriate since this test focuses on basic input account creation, not extension-specific behavior. The unified call path with the is_frozen parameter simplifies the test structure.

As noted in previous review, the test always passes tlv_data: None, so TLV population paths in ZTokenDataMut::set remain unexercised. Consider adding a dedicated test case with Some(tlv_data) containing CompressedOnly extension data to cover that code path.

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

95-108: Add explanatory comment for TLV extension filtering.

The conversion correctly filters to only CompressedOnly extensions since other variants (Pausable, PermanentDelegate, TransferHook) are marker extensions referencing mint configuration rather than account-specific state data. A brief comment would help future readers understand this intentional filtering.

🔎 Suggested comment addition
+                // Only CompressedOnly carries account-specific state data (delegated_amount,
+                // withheld_transfer_fee); other TLV variants are mint-level marker extensions.
                 let tlv: Option<Vec<ExtensionStruct>> = tlv_data.map(|exts| {
                     exts.iter()
                         .filter_map(|ext| match ext {
programs/compressed-token/program/src/transfer2/check_extensions.rs (2)

24-28: Consider extracting version 3 as a named constant.

The magic number 3 would be clearer as a named constant like VERSION_SHA_FLAT to improve readability and reduce the risk of inconsistencies if this version check appears elsewhere.

🔎 Suggested improvement
+const VERSION_SHA_FLAT: u8 = 3;
+
 // Validate TLV is only used with version 3 (ShaFlat)
-if tlv_data.is_some_and(|v| !v.is_empty() && version != 3) {
+if tlv_data.is_some_and(|v| !v.is_empty() && version != VERSION_SHA_FLAT) {

98-134: Uncomment Compress mode validation for restricted extensions.

The documentation at line 100 clearly states "Compress: NOT allowed (mints with restricted extensions must not be compressed)", but the validation code at lines 125-128 is commented out. This silently allows restricted mints to compress when they shouldn't be allowed.

The CompressAndClose mode correctly validates that outputs have CompressedOnly extension (lines 104-123), but the Compress arm does nothing. This inconsistency creates a security gap where restricted mints could be compressed without the required extension.

🔎 Recommended fix
 ZCompressionMode::Compress => {
-    // msg!("Mints with restricted extensions cannot be compressed");
-    // return Err(ErrorCode::MintHasRestrictedExtensions.into());
+    msg!("Mints with restricted extensions cannot be compressed");
+    return Err(ErrorCode::MintHasRestrictedExtensions.into());
 }
programs/compressed-token/program/src/shared/token_output.rs (1)

44-54: TLV config extraction intentionally filters to CompressedOnly.

This filtering is correct—only CompressedOnly carries account-specific state data (delegated_amount, withheld_transfer_fee). Other TLV variants like Pausable, PermanentDelegate, and TransferHook are mint-level marker extensions that don't need to be stored in individual account TLV.

A brief inline comment explaining this would improve clarity for future maintainers.

programs/compressed-token/program/src/extensions/check_mint_extensions.rs (2)

121-131: Silent error swallowing in get_extension_types() should be addressed.

As flagged in a past review, unwrap_or_default() at line 123 silently treats extension type enumeration failures as "no extensions". This could allow a malformed mint to bypass restricted extension validation.

Consider propagating the error:

🔎 Suggested fix:
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;

205-229: Same unwrap_or_default() concern in has_mint_extensions.

Line 207 has the same pattern as line 123 — silently treating get_extension_types() failures as empty. For account creation, this could result in undersized accounts if extension types cannot be determined.

🔎 Suggested fix:
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;

Comment on lines +178 to 181
version,
tlv_slice,
false, // Not frozen in tests
)
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

Test passes hardcoded is_frozen: false but generates random frozen flags in TLV.

At line 124, is_frozen is randomly set to 20% true in the TLV data, but at line 180, is_frozen: false is always passed to set_output_compressed_account. This means:

  1. The TLV data contains a potentially non-zero is_frozen field
  2. But the function is told the account is never frozen
  3. This creates a mismatch between the TLV state and the actual frozen state

The test will still pass because the expected data is built with the same hardcoded false, but this doesn't test the frozen state handling path properly.

🔎 Suggested fix to properly test frozen state
+        // Track frozen state from TLV data
+        let mut is_frozen_flags = Vec::new();
+
         for i in 0..num_outputs {
             if tlv_flags[i] {
+                let frozen = rng.gen_bool(0.2);
+                is_frozen_flags.push(frozen);
                 let ext = ExtensionInstructionData::CompressedOnly(
                     CompressedOnlyExtensionInstructionData {
                         delegated_amount: tlv_delegated_amounts[i],
                         withheld_transfer_fee: tlv_withheld_fees[i],
-                        is_frozen: rng.gen_bool(0.2), // 20% chance of frozen
+                        is_frozen: frozen,
                     },
                 );
                 // ...
             } else {
+                is_frozen_flags.push(false);
                 // ...
             }
         }

         // Later in the loop:
+        let is_frozen = is_frozen_flags[index];
         set_output_compressed_account(
             // ...
-            false, // Not frozen in tests
+            is_frozen,
         )

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/tests/token_output.rs around lines 124 and
178–181, the test randomly sets the TLV is_frozen flag (~20% true) at line 124
but then always passes a hardcoded false to set_output_compressed_account at
lines 178–181, creating a mismatch; fix by capturing the generated is_frozen
value into a local variable when building the TLV and pass that variable (not
false) into set_output_compressed_account, and update any expected/verification
data to use the same variable so the test exercises both frozen and unfrozen
paths correctly.

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: 2

Caution

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

⚠️ Outside diff range comments (1)
forester/tests/e2e_test.rs (1)

264-277: Indexer disabled but test requires it to run.

With enable_indexer: false, the test validator skips indexer startup via --skip-indexer, yet the test immediately makes indexer calls that will fail:

  • wait_for_indexer() at lines 1006, 1121, 1163, 1333
  • get_compressed_token_accounts_by_owner() at line 1010
  • get_validity_proof() at lines 1039, 1167, 1337
  • get_compressed_accounts_by_owner() at line 1125

The RPC is configured via LightClientConfig::local() with indexer_url: "http://127.0.0.1:8784", but the indexer process is never started. All indexer method calls use .unwrap(), so test execution will panic on connection errors.

Per the coding guidelines, local validator mode should automatically spawn the indexer. Change enable_indexer: false to enable_indexer: true and consider adding explicit indexer startup like spawn_prover() if needed.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 032eac5 and d5751a8.

📒 Files selected for processing (2)
  • forester/tests/e2e_test.rs (1 hunks)
  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
forester/**/*e2e*.rs

📄 CodeRabbit inference engine (forester/README.md)

forester/**/*e2e*.rs: Test environment variables should use TEST_MODE to specify 'local' or 'devnet' mode (default: devnet)
Test feature flags TEST_V1_STATE, TEST_V2_STATE, TEST_V1_ADDRESS, and TEST_V2_ADDRESS should default to true and control which test scenarios run
Devnet mode tests require PHOTON_RPC_URL, PHOTON_WSS_RPC_URL, PHOTON_INDEXER_URL, PHOTON_PROVER_URL, and PHOTON_API_KEY environment variables
Local validator mode tests should automatically spawn a local validator, start a local prover service, and use predefined local URLs (localhost:8899 for RPC, localhost:8784 for indexer)
Tests should automatically skip minting tokens, executing transactions, and root verification for disabled test types

Files:

  • forester/tests/e2e_test.rs
sdk-libs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in sdk-libs must not depend on light-test-utils; integration tests must be located in sdk-tests/

Files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
🧠 Learnings (34)
📓 Common learnings
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement rent constants with values: BASE_RENT = 1,220 lamports, RENT_PER_BYTE = 10 lamports, SLOTS_PER_EPOCH = 432,000.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: When working with compressible extension (rent management), must read and comply with documentation in program-libs/compressible/docs/ including RENT.md, CONFIG_ACCOUNT.md, and SOLANA_RENT.md
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Compressible extension rent management must follow the specifications in program-libs/compressible/docs/RENT.md including: rent authority can only compress when is_compressible() returns true, lamport distribution on close (rent → rent_sponsor, unutilized → destination), and compression incentive for foresters
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : RentConfig struct must be exactly 8 bytes and contain: base_rent (u16), compression_cost (u16), lamports_per_byte_per_epoch (u8), and 3 bytes of padding.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: The codebase provides rent calculation algorithms and account structure definitions used across light-compressed-token, light-registry, compressed-token-sdk, and related programs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/rent.rs : Implement rent calculation functions including `rent_curve_per_epoch`, `calculate_rent_and_balance`, `claimable_lamports`, and `calculate_close_lamports` in rent.rs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol rent is calculated per epoch as: 1220 minimum + (data_bytes * 10) lamports per epoch
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `calculate_close_lamports` function to return (u64, u64) tuple splitting lamports between rent recipient and user, with rent recipient receiving completed epochs and user receiving partial epoch remainder.
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/**/*e2e*.rs : Local validator mode tests should automatically spawn a local validator, start a local prover service, and use predefined local URLs (localhost:8899 for RPC, localhost:8784 for indexer)

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/**/*e2e*.rs : Test feature flags `TEST_V1_STATE`, `TEST_V2_STATE`, `TEST_V1_ADDRESS`, and `TEST_V2_ADDRESS` should default to `true` and control which test scenarios run

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/**/*e2e*.rs : Tests should automatically skip minting tokens, executing transactions, and root verification for disabled test types

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/**/*e2e*.rs : Test environment variables should use `TEST_MODE` to specify 'local' or 'devnet' mode (default: devnet)

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/**/*e2e*.rs : Devnet mode tests require `PHOTON_RPC_URL`, `PHOTON_WSS_RPC_URL`, `PHOTON_INDEXER_URL`, `PHOTON_PROVER_URL`, and `PHOTON_API_KEY` environment variables

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:53.971Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: forester/README.md:0-0
Timestamp: 2025-11-24T17:53:53.971Z
Learning: Applies to forester/src/main.rs : Required configuration must include: `--rpc-url`, `--ws-rpc-url`, `--indexer-url`, `--prover-url`, `--payer`, and `--derivation` parameters

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-12-06T00:50:31.314Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-06T00:50:31.314Z
Learning: Forester end-to-end tests must run with TEST_MODE=local environment variable

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-11-24T17:53:40.537Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: cli/README.md:0-0
Timestamp: 2025-11-24T17:53:40.537Z
Learning: The CLI should start the Light test validator as a background process with Solana test-validator, Light system programs, prover server, and Photon indexer

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/account_info/test_account_info.rs : Provide mock TestAccountInfo implementation (feature: `test-only`) for unit testing account validation logic

Applied to files:

  • forester/tests/e2e_test.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/decompress_context.rs : Decompression trait implementation (`DecompressContext`) with account accessors, PDA/token separation logic, and token processing delegation should be in `decompress_context.rs`

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/variant_enum.rs : Account variant enum (`CompressedAccountVariant`) generation and `CompressedAccountData` wrapper struct should be implemented in `variant_enum.rs`

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Implement account compressibility states in the following order: Funded (rent for current + 1 epoch) → Compressible (lacks rent for current + 1 epoch) → Claimable (funded but past epochs unclaimed)

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/instructions.rs : Compress/decompress instruction handlers and context struct generation should be implemented in `instructions.rs`, with compress using PDA-only and decompress supporting full PDA + ctoken

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account state in Solana programs must validate active state using `validate_active()` method before allowing new compressible token account creation

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement state validation methods (`validate_active`, `validate_not_inactive`) on CompressibleConfig account structure

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/mod.rs : Export new wrapper modules in `account_compression_cpi/mod.rs` using `pub mod new_operation;` and `pub use new_operation::*;`, then import in `lib.rs`

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/traits.rs : Core trait implementations (`HasCompressionInfo`, `CompressAs`, `Compressible`) should be defined in `traits.rs`

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/close_token_account.rs : Close token account instruction must return rent exemption to rent recipient if compressible and remaining lamports to destination account

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/docs/ACCOUNTS.md : All account documentation must include: description, discriminator, state layout, serialization example, hashing (for compressed accounts only), derivation (for PDAs only), and associated instructions (create, update, close)

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol accounts must satisfy both Solana rent exemption and Light Protocol rent requirements, plus an 11,000 lamport compression incentive during creation

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account size must be exactly 256 bytes including the 8-byte discriminator to maintain compatibility with Light Protocol Solana programs

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-06T00:50:17.433Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: DOCS.md:0-0
Timestamp: 2025-12-06T00:50:17.433Z
Learning: Applies to **/docs/*accounts* : Account documentation must include: description (what account represents and role), state layout (path to struct and field descriptions), associated instructions (list with discriminators), discriminator (8-byte value if applicable), size (in bytes), ownership (expected owner), serialization (zero-copy for programs and Borsh for clients with code snippets). For PDAs include derivation (seeds) and bump (stored or derived). For compressed accounts include version, hashing (method and discriminator), and data layout.

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/CREATE_TOKEN_ACCOUNT.md : Create Token Account Instructions documentation must cover creation of regular and associated ctoken accounts

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:54:38.537Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/README.md:0-0
Timestamp: 2025-11-24T17:54:38.537Z
Learning: Ensure compressed token program documentation is maintained at the official documentation site (https://zkcompression.com)

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:54:38.537Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/README.md:0-0
Timestamp: 2025-11-24T17:54:38.537Z
Learning: Implement compressed token program interfaces for third-party token creation and usage on Solana using ZK Compression

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/create_token_account.rs : Create token account instructions (CreateTokenAccount, CreateAssociatedCTokenAccount, CreateAssociatedTokenAccountIdempotent) require ACTIVE config validation only

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/ctoken_transfer.rs : CTokenTransfer instruction (discriminator: 3) must implement SPL-compatible transfers between decompressed accounts

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:54:33.614Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/anchor/README.md:0-0
Timestamp: 2025-11-24T17:54:33.614Z
Learning: Implement the Compressed Token Program interface for creating and using compressed tokens on Solana with ZK Compression

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T17:57:39.230Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/QUEUE_ACCOUNT.md:0-0
Timestamp: 2025-11-24T17:57:39.230Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/queue.rs : Validate account ownership by Light account compression program using `check_owner` from `light-account-checks` when deserializing `BatchedQueueAccount` with `output_from_account_info`

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Implement all 8 compressed token instructions: create_cmint, mint_to_ctoken, create_token_account_invoke, create_token_account_invoke_signed, create_ata_invoke, create_ata_invoke_signed, transfer_interface_invoke, and transfer_interface_invoke_signed

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: All compressed token account implementations must reference and comply with the account layout specifications in programs/compressed-token/program/docs/ACCOUNTS.md

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/*.rs : Create wrapper instruction module at `src/account_compression_cpi/new_operation.rs` with `NewOperationContext` struct defining required accounts

Applied to files:

  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs
🧬 Code graph analysis (1)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (3)
programs/compressed-token/anchor/src/instructions/create_token_pool.rs (1)
  • ExtensionType (58-58)
program-libs/ctoken-interface/src/state/compressed_token/token_data.rs (1)
  • state (53-55)
forester/tests/e2e_test.rs (2)
  • rpc (584-585)
  • rpc (604-605)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (21)
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: lint
  • GitHub Check: cli-v1
  • GitHub Check: programs (compressed-token-batched-tree, ["cargo-test-sbf -p compressed-token-test -- test_transf...
  • GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
  • GitHub Check: programs (light-system-program-address, ["cargo-test-sbf -p system-test -- test_with_address", "c...
  • GitHub Check: programs (light-system-program-compression, ["cargo-test-sbf -p system-test -- test_with_compress...
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: programs (compressed-token-and-e2e, ["cargo test -p light-compressed-token", "cargo-test-sbf -p c...
  • GitHub Check: Forester e2e test
  • GitHub Check: Test program-libs-fast
  • GitHub Check: Test program-libs-slow
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: system-programs (token test, ["cargo-test-sbf -p sdk-token-test"])
  • GitHub Check: system-programs (native, ["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-t...
  • GitHub Check: system-programs (anchor & pinocchio, ["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk...
  • GitHub Check: cli-v2
  • GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-ctoke...
  • GitHub Check: stateless-js-v1
  • GitHub Check: stateless-js-v2
🔇 Additional comments (2)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (2)

10-16: Solid selection of restricted extensions.

These four extension types make sense for requiring compression_only mode—each represents a mint-level control mechanism that free decompression would bypass:

  • Pausable: Decompression would circumvent pause state
  • PermanentDelegate: Bypass delegate authority
  • TransferFeeConfig: Skip transfer fees
  • TransferHook: Evade custom transfer logic

Consider adding a brief inline comment noting why these specific four were chosen, and whether future extensions (e.g., ConfidentialTransferMint) might need similar treatment as the Token 2022 extension set evolves.


86-94: Good improvement: dynamic compression_only based on mint extensions.

The dynamic determination of compression_only from mint extension data is a significant improvement over hardcoding. This ensures accounts are created with appropriate restrictions based on the actual mint configuration.

The flow now correctly:

  1. Inspects the mint for restricted extensions
  2. Sets compression_only accordingly in CompressibleParams
  3. Passes through to with_compressible() for proper account creation

This addresses the concern from the previous review about documenting the compression_only: true design—the behavior is now self-documenting through code.

Comment on lines 18 to 31
/// Check if a mint has any restricted extensions that require compression_only mode.
fn mint_has_restricted_extensions(mint_data: &[u8]) -> bool {
let Ok(mint_state) = StateWithExtensions::<Mint>::unpack(mint_data) else {
return false;
};
let Ok(extension_types) = mint_state.get_extension_types() else {
return false;
};
extension_types
.iter()
.any(|ext| RESTRICTED_EXTENSIONS.contains(ext))
}
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

Silent failure returns false, which may not be the safest default.

When StateWithExtensions::<Mint>::unpack or get_extension_types() fails, this function returns false (meaning "no restricted extensions"). If the mint account data is malformed or not actually a Token-2022 mint, this silently treats it as unrestricted.

For a Token-2022 mint with corrupted data, defaulting to compression_only = false could create an account with incorrect permissions. Consider whether this should:

  1. Return true (fail-safe, requiring compression_only when uncertain), or
  2. Return a Result so the caller can decide how to handle malformed data

If you intentionally want this behavior (e.g., to gracefully handle legacy SPL mints), a comment clarifying that rationale would help future maintainers.

🔎 Option: Fail-safe approach
 fn mint_has_restricted_extensions(mint_data: &[u8]) -> bool {
     let Ok(mint_state) = StateWithExtensions::<Mint>::unpack(mint_data) else {
-        return false;
+        // Cannot parse mint data; assume unrestricted for legacy SPL mints
+        // Token-2022 mints should always unpack successfully
+        return false; // or return true for fail-safe
     };
     let Ok(extension_types) = mint_state.get_extension_types() else {
-        return false;
+        // Extension types unavailable; assume no restrictions
+        return false;
     };

Comment on lines +80 to +86
// Check if mint has restricted extensions that require compression_only mode
let compression_only = match rpc.get_account(mint).await {
Ok(Some(mint_account)) => mint_has_restricted_extensions(&mint_account.data),
_ => false,
};
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

RPC failure silently defaults to compression_only = false—potential correctness risk.

If rpc.get_account(mint) fails due to network issues or returns None (mint doesn't exist), the code proceeds with compression_only = false. This has two concerning scenarios:

  1. Transient network failure: A real Token-2022 mint with TransferFeeConfig would get a non-compression-only account created due to RPC hiccup
  2. Non-existent mint: The function continues and will fail later when building the instruction, but not with a clear error about the mint

Consider propagating the error or at minimum logging the failure:

🔎 Proposed fix: Propagate RPC errors
-    // Check if mint has restricted extensions that require compression_only mode
-    let compression_only = match rpc.get_account(mint).await {
-        Ok(Some(mint_account)) => mint_has_restricted_extensions(&mint_account.data),
-        _ => false,
-    };
+    // Check if mint has restricted extensions that require compression_only mode
+    let mint_account = rpc
+        .get_account(mint)
+        .await?
+        .ok_or_else(|| RpcError::CustomError(format!("Mint account not found: {}", mint)))?;
+    let compression_only = mint_has_restricted_extensions(&mint_account.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
// Check if mint has restricted extensions that require compression_only mode
let compression_only = match rpc.get_account(mint).await {
Ok(Some(mint_account)) => mint_has_restricted_extensions(&mint_account.data),
_ => false,
};
// Check if mint has restricted extensions that require compression_only mode
let mint_account = rpc
.get_account(mint)
.await?
.ok_or_else(|| RpcError::CustomError(format!("Mint account not found: {}", mint)))?;
let compression_only = mint_has_restricted_extensions(&mint_account.data);
🤖 Prompt for AI Agents
In sdk-libs/token-client/src/actions/create_compressible_token_account.rs around
lines 80 to 84, the current match silently treats RPC errors or missing mint as
compression_only = false; change this so RPC failures are propagated and a
missing mint returns a clear error instead of defaulting to false. Specifically,
await rpc.get_account(mint) and if it returns an Err, return Err(...) from this
function with context (e.g., "failed to fetch mint account") so callers surface
network issues; if it returns Ok(None), return an explicit error like "mint
account not found"; only set compression_only when you have
Ok(Some(mint_account)) and compute
mint_has_restricted_extensions(&mint_account.data).

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)
sdk-libs/ctoken-sdk/src/ctoken/mod.rs (1)

87-109: Re-exports are consistent with the module's public API design.

The glob re-exports (pub use approve::*, etc.) align with the existing pattern used for other operations like burn, close, and create. This maintains a uniform public API surface.

Consider updating the module-level documentation (lines 1-66) to include the new operations (Approve, Revoke, Freeze, Thaw) under appropriate sections for discoverability.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5751a8 and 1b61088.

⛔ Files ignored due to path filters (24)
  • program-tests/compressed-token-test/tests/ctoken/approve_revoke.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/freeze_thaw.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/approve.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/burn.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/create_ata2.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/ctoken_mint_to.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/decompress_cmint.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/freeze.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/revoke.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/thaw.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/scenario_cmint.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/shared.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_approve_revoke.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_burn.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_close.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_create_ata.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_create_ata_v2.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_create_token_account.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_ctoken_mint_to.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_decompress_cmint.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_freeze_thaw.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_mint_to_ctoken.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_transfer.rs is excluded by none and included by none
📒 Files selected for processing (9)
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs (3 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs (4 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
sdk-libs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in sdk-libs must not depend on light-test-utils; integration tests must be located in sdk-tests/

Files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
programs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in programs must not depend on light-test-utils; integration tests must be located in program-tests/

Files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
🧠 Learnings (66)
📓 Common learnings
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: When working with compressible extension (rent management), must read and comply with documentation in program-libs/compressible/docs/ including RENT.md, CONFIG_ACCOUNT.md, and SOLANA_RENT.md
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Compressible extension rent management must follow the specifications in program-libs/compressible/docs/RENT.md including: rent authority can only compress when is_compressible() returns true, lamport distribution on close (rent → rent_sponsor, unutilized → destination), and compression incentive for foresters
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement rent constants with values: BASE_RENT = 1,220 lamports, RENT_PER_BYTE = 10 lamports, SLOTS_PER_EPOCH = 432,000.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: The codebase provides rent calculation algorithms and account structure definitions used across light-compressed-token, light-registry, compressed-token-sdk, and related programs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/rent.rs : Implement rent calculation functions including `rent_curve_per_epoch`, `calculate_rent_and_balance`, `claimable_lamports`, and `calculate_close_lamports` in rent.rs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : RentConfig struct must be exactly 8 bytes and contain: base_rent (u16), compression_cost (u16), lamports_per_byte_per_epoch (u8), and 3 bytes of padding.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol rent is calculated per epoch as: 1220 minimum + (data_bytes * 10) lamports per epoch
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `rent_curve_per_epoch` function with formula: base_rent + (num_bytes * lamports_per_byte_per_epoch).
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/instructions.rs : Compress/decompress instruction handlers and context struct generation should be implemented in `instructions.rs`, with compress using PDA-only and decompress supporting full PDA + ctoken

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/decompress_context.rs : Decompression trait implementation (`DecompressContext`) with account accessors, PDA/token separation logic, and token processing delegation should be in `decompress_context.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement default initialization for CToken V1 config in CompressibleConfig structure

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/MINT_ACTION.md : MintAction documentation must cover batch instruction for compressed mint management supporting 9 actions: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/*.rs : Create wrapper instruction module at `src/account_compression_cpi/new_operation.rs` with `NewOperationContext` struct defining required accounts

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/mint_action/*.rs : MintAction instruction (discriminator: 103) must support 9 action types: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/mod.rs : Export new wrapper modules in `account_compression_cpi/mod.rs` using `pub mod new_operation;` and `pub use new_operation::*;`, then import in `lib.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/seed_providers.rs : PDA and CToken seed provider implementations with client-side seed functions should be in `seed_providers.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/variant_enum.rs : Account variant enum (`CompressedAccountVariant`) generation and `CompressedAccountData` wrapper struct should be implemented in `variant_enum.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/error.rs : Error types must use numeric codes in the 19xxx range for CToken-specific errors

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Implement all 8 compressed token instructions: create_cmint, mint_to_ctoken, create_token_account_invoke, create_token_account_invoke_signed, create_ata_invoke, create_ata_invoke_signed, transfer_interface_invoke, and transfer_interface_invoke_signed

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/create_token_account.rs : Create token account instructions (CreateTokenAccount, CreateAssociatedCTokenAccount, CreateAssociatedTokenAccountIdempotent) require ACTIVE config validation only

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
📚 Learning: 2025-11-24T18:01:30.012Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-tests/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:30.012Z
Learning: Run compressed token core tests using `cargo test-sbf -p compressed-token-test --test ctoken`, `--test v1`, `--test mint`, and `--test transfer2`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/CREATE_TOKEN_ACCOUNT.md : Create Token Account Instructions documentation must cover creation of regular and associated ctoken accounts

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : CompressibleConfig account structure must support serialization via Anchor, Pinocchio, and Borsh formats for Light Registry program integration

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: All compressed token account implementations must reference and comply with the account layout specifications in programs/compressed-token/program/docs/ACCOUNTS.md

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/create_cmint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
📚 Learning: 2025-12-06T00:49:57.458Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-token-test/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:57.458Z
Learning: Applies to sdk-tests/sdk-token-test/**/*test.rs : Tests should use light-ctoken-sdk functions from sdk-libs/compressed-token-sdk for testing ctoken instructions

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use the builder pattern from `light-ctoken-sdk::ctoken` module for CPI operations instead of manual instruction building

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/pack_unpack.rs : Pubkey compression logic and `PackedXxx` struct generation with Pack/Unpack trait implementations should be in `pack_unpack.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/ctoken_transfer.rs : CTokenTransfer instruction (discriminator: 3) must implement SPL-compatible transfers between decompressed accounts

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/close_token_account.rs : Close token account instruction must return rent exemption to rent recipient if compressible and remaining lamports to destination account

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use `invoke_signed()` method from builder pattern for PDA-signed CPI calls, deriving PDA with `Pubkey::find_program_address()` before invocation

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use `invoke()` method from builder pattern for regular CPI calls where the program acts as authority

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Clone AccountInfo structs when building CPI builder pattern account structs to avoid borrow checker issues

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/src/{lib,main}.rs : Structure Solana BPF projects with src/lib.rs containing the program entrypoint and instruction handlers

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
📚 Learning: 2025-11-24T17:59:54.233Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/PACKED_ACCOUNTS.md:0-0
Timestamp: 2025-11-24T17:59:54.233Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/**/*.rs : Provide descriptive names in ProgramPackedAccounts error messages (e.g., 'token_mint' instead of 'account')

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: State and instruction data structures must be defined in the separate 'light-ctoken-interface' crate (program-libs/ctoken-types/) to allow SDKs to import types without pulling in program dependencies

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/approve.rs
  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/*.rs : Implement `process_new_operation()` function in wrapper module to handle PDA signer setup, account mapping, and CPI execution

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
  • sdk-libs/ctoken-sdk/src/ctoken/freeze.rs
  • sdk-libs/ctoken-sdk/src/ctoken/thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Provide account ownership validation functions (`check_owner`, `check_program`) in checks module

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/revoke.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/mod.rs : Module declaration should be kept in `mod.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Implement account compressibility states in the following order: Funded (rent for current + 1 epoch) → Compressible (lacks rent for current + 1 epoch) → Claimable (funded but past epochs unclaimed)

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/traits.rs : Core trait implementations (`HasCompressionInfo`, `CompressAs`, `Compressible`) should be defined in `traits.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement state validation methods (`validate_active`, `validate_not_inactive`) on CompressibleConfig account structure

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
  • sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs
📚 Learning: 2025-11-24T17:54:38.537Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/README.md:0-0
Timestamp: 2025-11-24T17:54:38.537Z
Learning: Implement compressed token program interfaces for third-party token creation and usage on Solana using ZK Compression

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:56:00.229Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:56:00.229Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/Cargo.toml : Depend on light-compressed-account crate for compressed account types and utilities

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/{solana,pinocchio}.rs : Implement AccountInfoTrait with SDK-specific Pubkey types: `solana_pubkey::Pubkey` for solana-program feature and `[u8; 32]` raw bytes for pinocchio feature

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/solana.rs : Use `std::cell::Ref` and `std::cell::RefMut` for DataRef and DataRefMut associated types when implementing AccountInfoTrait for solana-program SDK

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:56:50.011Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_STATE_TREE.md:0-0
Timestamp: 2025-11-24T17:56:50.011Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_state_tree.rs : Use `check_account_balance_is_rent_exempt` function from `light-account-checks` to verify rent exemption for both queue_account and merkle_tree_account during initialization

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Use generic `AccountInfoTrait` parameter for all account validation functions to enable compatibility with both Solana and Pinocchio runtimes

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Use procedural macros for generating rent-free account types and their hooks in Solana programs

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/*.rs : Use AccountInfoTrait abstraction for unified account handling across both solana-program and pinocchio SDKs

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:00:36.663Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `get_last_funded_epoch` function to determine the last epoch number covered by rent payments.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/{src/claim/*.rs,src/withdraw_funding_pool.rs} : Rent management instructions (Claim, WithdrawFundingPool) require validation that config is not INACTIVE (accepts ACTIVE or DEPRECATED states)

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:36.663Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `calculate_rent_and_balance` function to determine compressibility by checking if account balance covers required rent for epochs since last claim, returning (bool, u64) tuple.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Account closure must distribute lamports according to: Solana rent exemption → returned to user, completed epoch rent → rent recipient, partial epoch rent → user, compression incentive → forester node

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:36.663Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : RentConfig struct must be exactly 8 bytes and contain: base_rent (u16), compression_cost (u16), lamports_per_byte_per_epoch (u8), and 3 bytes of padding.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:36.663Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement rent constants with values: BASE_RENT = 1,220 lamports, RENT_PER_BYTE = 10 lamports, SLOTS_PER_EPOCH = 432,000.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol accounts must satisfy both Solana rent exemption and Light Protocol rent requirements, plus an 11,000 lamport compression incentive during creation

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:21.501Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/ERRORS.md:0-0
Timestamp: 2025-11-24T18:00:21.501Z
Learning: Applies to program-libs/compressible/docs/program-libs/compressible/src/**/*.rs : InvalidState (Error Code 19002): For account creation, ensure CompressibleConfig state is `Active` (1). For other operations (claim, withdraw, compress & close), ensure config state is not `Inactive` (0). Validate config state using `config.validate_active()` for creation and `config.validate_not_inactive()` for other operations.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: When working with compressible extension (rent management), must read and comply with documentation in program-libs/compressible/docs/ including RENT.md, CONFIG_ACCOUNT.md, and SOLANA_RENT.md

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account state in Solana programs must validate active state using `validate_active()` method before allowing new compressible token account creation

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:56:50.011Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_STATE_TREE.md:0-0
Timestamp: 2025-11-24T17:56:50.011Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_state_tree.rs : Return error `AccountError::AccountNotRentExempt` (error code 12011) when account balance is insufficient for rent exemption at calculated size

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/rent.rs : Implement rent calculation functions including `rent_curve_per_epoch`, `calculate_rent_and_balance`, `claimable_lamports`, and `calculate_close_lamports` in rent.rs

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_balance_is_rent_exempt` function to verify account size matches expected_size and account balance meets rent exemption requirement, returning rent exemption amount or errors `InvalidAccountSize` (20004), `InvalidAccountBalance` (20007), or `FailedBorrowRentSysvar` (20008)

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T18:00:21.501Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/ERRORS.md:0-0
Timestamp: 2025-11-24T18:00:21.501Z
Learning: Applies to program-libs/compressible/docs/program-libs/compressible/src/error.rs : FailedBorrowRentSysvar (Error Code 19001): Ensure the rent sysvar is properly initialized in test environments and verify the sysvar is accessible for on-chain programs. Use `solana_program::rent::Rent::get()` with proper error handling for borrowing rent sysvar.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:56:20.711Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_ADDRESS_TREE.md:0-0
Timestamp: 2025-11-24T17:56:20.711Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_address_tree.rs : Verify merkle_tree_account balance is rent-exempt in `src/initialize_address_tree.rs`. Use `check_account_balance_is_rent_exempt` from `light-account-checks` library. Return error `AccountError::AccountNotRentExempt` (error code: 12011) if account balance is insufficient for rent exemption at calculated size.

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Implement rent exemption checks (`check_account_balance_is_rent_exempt`) in checks module

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info_mut` function to validate account is writable, owned by program_id, and discriminator matches type T

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info_non_mut` function to validate account is NOT writable, owned by program_id, and discriminator matches type T

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:57:24.369Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INSERT_OUTPUT_QUEUE.md:0-0
Timestamp: 2025-11-24T17:57:24.369Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/queue.rs : Insert Into Output Queue: Inserts a compressed account hash into the output queue's currently processing batch by calling `BatchedQueueAccount::insert_into_current_batch` with hash_chain_value and current_slot parameters, updating value vec, hash chain, and managing batch state transitions in src/queue.rs

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : When deserializing CompressibleConfig accounts, skip the 8-byte discriminator `[180, 4, 231, 26, 220, 144, 55, 168]` before deserialization in zero-copy implementations using bytemuck::pod_from_bytes

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement PDA derivation methods (`derive_pda`, `derive_v1_config_pda`) on CompressibleConfig for account discovery

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
📚 Learning: 2025-11-24T17:57:14.561Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INSERT_INPUT_QUEUE.md:0-0
Timestamp: 2025-11-24T17:57:14.561Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/merkle_tree.rs : In `BatchedMerkleTreeAccount::insert_nullifier_into_queue`, insert compressed_account_hash into bloom filter (NOT the nullifier) for non-inclusion checks in subsequent transactions

Applied to files:

  • programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs
🧬 Code graph analysis (4)
sdk-libs/ctoken-sdk/src/ctoken/approve.rs (3)
sdk-libs/ctoken-sdk/src/ctoken/freeze.rs (5)
  • invoke (57-61)
  • invoke_signed (63-67)
  • instruction (53-55)
  • instruction (81-91)
  • from (71-77)
sdk-libs/ctoken-sdk/src/ctoken/revoke.rs (5)
  • invoke (53-57)
  • invoke_signed (59-63)
  • instruction (49-51)
  • instruction (76-86)
  • from (67-72)
sdk-libs/ctoken-sdk/src/ctoken/thaw.rs (5)
  • invoke (57-61)
  • invoke_signed (63-67)
  • instruction (53-55)
  • instruction (81-91)
  • from (71-77)
sdk-libs/ctoken-sdk/src/ctoken/freeze.rs (3)
sdk-libs/ctoken-sdk/src/ctoken/approve.rs (5)
  • invoke (65-74)
  • invoke_signed (76-85)
  • instruction (61-63)
  • instruction (100-114)
  • from (89-96)
sdk-libs/ctoken-sdk/src/ctoken/revoke.rs (5)
  • invoke (53-57)
  • invoke_signed (59-63)
  • instruction (49-51)
  • instruction (76-86)
  • from (67-72)
sdk-libs/ctoken-sdk/src/ctoken/thaw.rs (5)
  • invoke (57-61)
  • invoke_signed (63-67)
  • instruction (53-55)
  • instruction (81-91)
  • from (71-77)
sdk-libs/ctoken-sdk/src/ctoken/thaw.rs (3)
sdk-libs/ctoken-sdk/src/ctoken/approve.rs (5)
  • invoke (65-74)
  • invoke_signed (76-85)
  • instruction (61-63)
  • instruction (100-114)
  • from (89-96)
sdk-libs/ctoken-sdk/src/ctoken/freeze.rs (5)
  • invoke (57-61)
  • invoke_signed (63-67)
  • instruction (53-55)
  • instruction (81-91)
  • from (71-77)
sdk-libs/ctoken-sdk/src/ctoken/revoke.rs (5)
  • invoke (53-57)
  • invoke_signed (59-63)
  • instruction (49-51)
  • instruction (76-86)
  • from (67-72)
sdk-libs/ctoken-sdk/src/ctoken/mod.rs (4)
programs/compressed-token/anchor/src/lib.rs (7)
  • approve (181-186)
  • freeze (199-205)
  • freeze (204-204)
  • freeze (214-214)
  • mint_to (95-109)
  • revoke (190-195)
  • thaw (209-215)
sdk-libs/ctoken-sdk/src/compressed_token/v1/account.rs (1)
  • approve (101-134)
sdk-libs/ctoken-sdk/src/compressed_token/v1/approve/instruction.rs (1)
  • approve (86-88)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
  • transfer_ctoken (20-38)

Comment on lines +38 to +59
#[derive(Debug, Clone)]
pub struct DecompressCMint {
/// Mint seed pubkey (used to derive CMint PDA)
pub mint_seed_pubkey: Pubkey,
/// Fee payer
pub payer: Pubkey,
/// Mint authority (must sign)
pub authority: Pubkey,
/// State tree for the compressed mint
pub state_tree: Pubkey,
/// Input queue for reading compressed mint
pub input_queue: Pubkey,
/// Output queue for updated compressed mint
pub output_queue: Pubkey,
/// Compressed mint with context (from indexer)
pub compressed_mint_with_context: CompressedMintWithContext,
/// Validity proof for the compressed mint
pub proof: ValidityProof,
/// Rent payment in epochs (must be >= 2)
pub rent_payment: u8,
/// Lamports for future write operations
pub write_top_up: u32,
}
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 validating rent_payment >= 2 at construction or instruction time.

The doc comment states rent_payment "must be >= 2", but there's no enforcement in this SDK code. While the on-chain program likely validates this, failing early in the SDK provides better developer experience.

Based on learnings about rent management in program-libs/compressible/docs/RENT.md, the rent calculation is critical for proper account compressibility states.

🔎 Optional validation helper
 impl DecompressCMint {
     pub fn instruction(self) -> Result<Instruction, ProgramError> {
+        if self.rent_payment < 2 {
+            return Err(ProgramError::InvalidArgument);
+        }
+
         // Derive CMint PDA
         let (cmint_pda, cmint_bump) = find_cmint_address(&self.mint_seed_pubkey);

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

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs around lines 38 to 60, the
struct documents that rent_payment must be >= 2 but there is no enforcement; add
a validation to enforce rent_payment >= 2 at SDK construction/initializer or
before building the instruction (e.g., in constructors, builder::new, or a
validate() method) so callers get a clear early error instead of relying solely
on on-chain checks; return a Result/error when invalid, update any factory
functions that create DecompressCMint to call this validation, and add/adjust
unit tests and the doc comment to reflect the new client-side validation.

Comment on lines +221 to +237
impl<'info> TryFrom<&DecompressCMintCpi<'info>> for DecompressCMint {
type Error = ProgramError;

fn try_from(cpi: &DecompressCMintCpi<'info>) -> Result<Self, Self::Error> {
Ok(Self {
mint_seed_pubkey: *cpi.mint_seed.key,
payer: *cpi.payer.key,
authority: *cpi.authority.key,
state_tree: *cpi.state_tree.key,
input_queue: *cpi.input_queue.key,
output_queue: *cpi.output_queue.key,
compressed_mint_with_context: cpi.compressed_mint_with_context.clone(),
proof: cpi.proof,
rent_payment: cpi.rent_payment,
write_top_up: cpi.write_top_up,
})
}
}
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

TryFrom is infallible—consider using From instead.

The TryFrom implementation always returns Ok(...) and never produces an error. Using From would be more idiomatic and removes the need for ? at call sites.

🔎 Proposed simplification
-impl<'info> TryFrom<&DecompressCMintCpi<'info>> for DecompressCMint {
-    type Error = ProgramError;
-
-    fn try_from(cpi: &DecompressCMintCpi<'info>) -> Result<Self, Self::Error> {
-        Ok(Self {
+impl<'info> From<&DecompressCMintCpi<'info>> for DecompressCMint {
+    fn from(cpi: &DecompressCMintCpi<'info>) -> Self {
+        Self {
             mint_seed_pubkey: *cpi.mint_seed.key,
             payer: *cpi.payer.key,
             authority: *cpi.authority.key,
             state_tree: *cpi.state_tree.key,
             input_queue: *cpi.input_queue.key,
             output_queue: *cpi.output_queue.key,
             compressed_mint_with_context: cpi.compressed_mint_with_context.clone(),
             proof: cpi.proof,
             rent_payment: cpi.rent_payment,
             write_top_up: cpi.write_top_up,
-        })
+        }
     }
 }

Then update line 168:

     pub fn instruction(&self) -> Result<Instruction, ProgramError> {
-        DecompressCMint::try_from(self)?.instruction()
+        DecompressCMint::from(self).instruction()
     }

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

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs around lines 221–238, the
TryFrom<&DecompressCMintCpi<'info>> impl always returns Ok and never errors;
replace it with a From<&DecompressCMintCpi<'info>> implementation that returns
DecompressCMint directly (no Result or associated Error type), i.e. implement
From<&DecompressCMintCpi<'info>> for DecompressCMint and construct/return Self
with the same field mappings; then update call sites (e.g., places using
TryFrom::try_from or .try_from(...) with a trailing ? around line ~168) to use
From::from or direct conversion (DecompressCMint::from(&cpi)) and remove the
now-unnecessary ? error propagation.

MintActionMetaConfigCpiWrite,
};

// TODO: move to compressed_token.
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 TODO and consider adding an issue reference.

The comment "move to compressed_token" lacks specificity about the target module and intended timeline. To improve maintainability and tracking, consider either:

  • Expanding the comment with the full target path (e.g., // TODO: move to crate::compressed_token::mint_action)
  • Adding a GitHub issue reference (e.g., // TODO (#1234): move to compressed_token)

This will help future maintainers understand the refactoring intent and track the work.

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/ctoken/mint_to.rs around line 17, the TODO comment
"move to compressed_token." is too vague; update it to specify the exact target
path and/or a tracking issue by replacing it with a clearer comment such as "//
TODO (#<issue>): move to crate::compressed_token::mint_action" (or the actual
module/function name) so maintainers know where it should go and can track the
work; ensure the issue number and full module path are accurate before
committing.

Comment on lines +79 to +85
accounts: vec![
AccountMeta::new(self.token_account, false),
AccountMeta::new(self.owner, true),
AccountMeta::new_readonly(Pubkey::default(), false),
],
data: vec![5u8], // CTokenRevoke discriminator
})
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

# Find the revoke.rs file in the sdk
find . -name "revoke.rs" -type f | head -20

Repository: Lightprotocol/light-protocol

Length of output: 114


🏁 Script executed:

#!/bin/bash

# Read the revoke.rs file to understand the implementation
cat -n ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

# Also search for the program-side handler that processes revoke instructions (discriminator 5)
echo "=== Searching for CTokenRevoke or revoke handler in program ==="
find ./programs -name "*.rs" -type f | xargs grep -l "revoke\|discriminator.*5" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 4026


🏁 Script executed:

#!/bin/bash

# Examine the program-side revoke handler
cat -n ./programs/compressed-token/program/src/ctoken_approve_revoke.rs | head -150

# Also check if there's documentation about the revoke instruction
find ./programs/compressed-token -name "*.md" -type f | xargs grep -l "revoke\|Revoke" 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 5883


🏁 Script executed:

#!/bin/bash

# Check approve instruction in SDK for comparison
cat -n ./sdk-libs/ctoken-sdk/src/ctoken/approve.rs | grep -A 30 "pub fn instruction"

# Also check what pinocchio's revoke expects
find ./programs -name "*.rs" -type f | xargs grep -l "process_revoke" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 2198


🏁 Script executed:

#!/bin/bash

# Check pinocchio's revoke implementation signature
find . -path ./target -prune -o -name "*.rs" -type f -print | xargs grep -l "fn process_revoke" | head -5

# Get more details on what approve vs revoke actually need
echo "=== Checking if system_program is used in process_compressible_top_up ==="
grep -A 50 "fn process_compressible_top_up" ./programs/compressed-token/program/src/ctoken_approve_revoke.rs | grep -i "system"

# Check account indices more carefully
echo "=== Checking CLAUDE.md for revoke instruction documentation ==="
find ./programs/compressed-token -name "*.md" -type f -exec grep -l "revoke\|Revoke" {} \;

Repository: Lightprotocol/light-protocol

Length of output: 262


🏁 Script executed:

#!/bin/bash

# Let's check what pinocchio_token_program expects for revoke
find . -path ./target -prune -o -name "*.rs" -type f -print | xargs grep -B 5 -A 10 "pub.*fn process_revoke" 2>/dev/null | head -50

# Also check documentation about the account layout
echo "=== Looking for revoke instruction account docs ==="
find ./programs/compressed-token -name "*.md" -type f -exec cat {} \; 2>/dev/null | grep -A 20 -i "revoke"

Repository: Lightprotocol/light-protocol

Length of output: 1707


🏁 Script executed:

#!/bin/bash

# Check the delegate account difference between approve and revoke
echo "=== Comparing approve vs revoke account structure ==="
echo "APPROVE accounts:"
cat -n ./sdk-libs/ctoken-sdk/src/ctoken/approve.rs | grep -A 15 "pub fn instruction"

echo ""
echo "REVOKE accounts:"
cat -n ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs | grep -A 8 "pub fn instruction"

# Check if pinocchio's revoke is part of spl_token standards
echo ""
echo "=== Checking pinocchio token program ==="
find . -path ./target -prune -o -name "*.rs" -type f -print | xargs grep -l "pinocchio_token_program" | head -3

Repository: Lightprotocol/light-protocol

Length of output: 2699


🏁 Script executed:

#!/bin/bash

# Let's verify the exact mismatch by checking line-by-line what invoke() passes
echo "=== REVOKE: instruction vs invoke mismatch ==="
echo "Instruction accounts (from instruction()):"
sed -n '79,82p' ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

echo ""
echo "Account infos passed to invoke() (from invoke()):"
sed -n '53,56p' ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

# Check if approve has same pattern
echo ""
echo "=== APPROVE: instruction vs invoke ==="
echo "Instruction accounts (from instruction()):"
sed -n '106,111p' ./sdk-libs/ctoken-sdk/src/ctoken/approve.rs

echo ""
echo "Account infos passed to invoke() (from invoke()):"
sed -n '67,72p' ./sdk-libs/ctoken-sdk/src/ctoken/approve.rs

# Check how the program handler actually uses these accounts
echo ""
echo "=== Program handler account usage for REVOKE ==="
sed -n '62,87p' ./programs/compressed-token/program/src/ctoken_approve_revoke.rs

Repository: Lightprotocol/light-protocol

Length of output: 2246


🏁 Script executed:

#!/bin/bash

# Let's verify if the pinocchio process_revoke actually uses account index 2
# Check what accounts the SPL token revoke instruction needs
echo "=== Checking SPL Token revoke standard ==="
# Search for any reference to what pinocchio expects
find . -path ./target -prune -o -name "*.rs" -type f -print | xargs grep -B 2 -A 15 "process_revoke.*accounts" 2>/dev/null | grep -A 15 "pinocchio"

# Also check the SDK tests to see how revoke is actually used
echo ""
echo "=== Checking SDK tests for revoke usage ==="
find ./sdk-tests -name "*.rs" -type f -exec grep -l "revoke\|Revoke" {} \;

Repository: Lightprotocol/light-protocol

Length of output: 159


🏁 Script executed:

#!/bin/bash

# Let's check the transfer2.rs file to understand how system_program is used in compressible extension
echo "=== Checking how system_program is used in transfer ==="
grep -n "system_program\|transfer_lamports_via_cpi" ./programs/compressed-token/program/src/ctoken_approve_revoke.rs

# Let's look at the actual account consumption in the handler
echo ""
echo "=== Full process_compressible_top_up implementation ==="
sed -n '90,140p' ./programs/compressed-token/program/src/ctoken_approve_revoke.rs

# Check what accounts process_revoke from pinocchio actually needs
echo ""
echo "=== Check if there are any tests ==="
find ./programs/compressed-token -name "*test*" -type f | head -5

# Check what Pubkey::default() is being used for in similar SDK wrappers
echo ""
echo "=== Check other SDK instruction wrappers ==="
find ./sdk-libs/ctoken-sdk/src -name "*.rs" -type f | xargs grep "Pubkey::default()" | head -10

Repository: Lightprotocol/light-protocol

Length of output: 2975


🏁 Script executed:

#!/bin/bash

# The key question: does pinocchio's process_revoke actually need 3 accounts?
# Let's check the code flow and what transfer_lamports_via_cpi does
echo "=== Checking transfer_lamports_via_cpi implementation ==="
find ./programs/compressed-token -name "*.rs" -type f | xargs grep -B 3 -A 15 "fn transfer_lamports_via_cpi"

# Also verify: in the actual instruction, which accounts are actually used by process_revoke?
echo ""
echo "=== Verifying account counts ==="
sed -n '55,87p' ./programs/compressed-token/program/src/ctoken_approve_revoke.rs | grep -E "accounts.get|process_revoke"

Repository: Lightprotocol/light-protocol

Length of output: 1974


🏁 Script executed:

#!/bin/bash

# Critical check: Does the Pubkey::default() placeholder actually work with invoke()?
# Let's examine how invoke() validates accounts
echo "=== Looking at actual usage patterns ==="
grep -B 5 -A 5 "invoke(&instruction" ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

# Compare with what accounts the program actually uses
echo ""
echo "=== Program uses these account indices ==="
echo "REVOKE_ACCOUNT_SOURCE = 0"
echo "REVOKE_ACCOUNT_OWNER = 1"
grep "const REVOKE" ./programs/compressed-token/program/src/ctoken_approve_revoke.rs

# Check if system_program is actually needed
echo ""
echo "=== Does process_revoke from pinocchio access accounts beyond [0,1]? ==="
# The handler calls: process_revoke(accounts)
# Which expects to access token_account and owner only
echo "Program handler passes full accounts array to process_revoke(accounts)"
echo "But only extracts and uses accounts[0] and accounts[1]"

# Check instruction creation - is this correct?
echo ""
echo "=== Checking if Pubkey::default() matches actual pubkey passed ==="
sed -n '76,86p' ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

Repository: Lightprotocol/light-protocol

Length of output: 1538


🏁 Script executed:

#!/bin/bash

# Final verification: Check if system_program is actually needed/used
echo "=== Verify if system_program is actually needed ==="
# Check what pinocchio's revoke signature expects
grep -r "pinocchio_token_program::processor::revoke" ./programs/compressed-token

# Check if revoke actually modifies any system accounts
echo ""
echo "=== Check if account index 2 is ever accessed ==="
sed -n '62,88p' ./programs/compressed-token/program/src/ctoken_approve_revoke.rs | grep -E "accounts\[2\]|get\(2"

# Look at how transfer_lamports_via_cpi works - does it need system_program passed?
echo ""
echo "=== Full transfer_lamports_via_cpi ==="
cat ./programs/compressed-token/program/src/shared/transfer_lamports.rs | head -30

Repository: Lightprotocol/light-protocol

Length of output: 1160


🏁 Script executed:

#!/bin/bash

# Final check: verify this is in sdk-libs as specified in guidelines
echo "=== Verify file location ==="
ls -la ./sdk-libs/ctoken-sdk/src/ctoken/revoke.rs

# Check if there are any unit tests for revoke
echo ""
echo "=== Check for unit tests ==="
find ./sdk-libs -name "*test*" -type f | xargs grep -l "revoke\|Revoke" 2>/dev/null

Repository: Lightprotocol/light-protocol

Length of output: 224


Remove unused system_program account from RevokeCTokenCpi invoke methods.

The instruction metadata declares Pubkey::default() as the third account, but both invoke() and invoke_signed() pass system_program which the program handler never uses. The handler only accesses accounts[0] (token_account) and accounts[1] (owner). Unlike ApproveCToken which requires the delegate account, revoke only needs two accounts—remove the system_program from the account_infos array at lines 55 and 61.

🤖 Prompt for AI Agents
sdk-libs/ctoken-sdk/src/ctoken/revoke.rs around lines 79–85: the CPI instruction
includes a third account (Pubkey::default()) but both invoke() and
invoke_signed() currently pass the runtime system_program AccountInfo which the
on-chain handler never reads; remove the system_program entry from the
account_infos arrays at the call sites (lines ~55 and ~61) so the
invoke/invoke_signed calls pass only the two AccountInfos (token_account and
owner), and adjust any corresponding slices/signers for invoke_signed
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: 3

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/lib.rs (1)

143-145: CTokenTransfer logging is commented out—intentional?

Line 144 has // msg!("CTokenTransfer"); commented out while all other instruction types log. This may be intentional for performance on a hot path, but inconsistent. Consider documenting the rationale or uncommenting for consistency.

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

80-114: Add account delegate check to match documented behavior.

The function comment states it "Allows owner, account delegate, or permanent delegate" but the implementation only validates owner and permanent delegate. Account delegates (set via the approve instruction) should also be able to authorize compression operations. Add a delegate field check matching the pattern in verify_owner_or_delegate_signer to ensure delegates can authorize transfers up to their delegated amount.

♻️ Duplicate comments (2)
programs/compressed-token/program/src/shared/owner_validation.rs (1)

57-77: Verbose failure logging adds compute overhead.

This concern was raised in a previous review. The multiple msg! calls on authorization failure are helpful for debugging but consume compute units on every failure path. Consider consolidating into a single message or gating behind a feature flag for production deployments.

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

18-44: Clean design for extension tracking.

The AccountExtensionInfo struct provides a sensible way to track extension flags detected during a single deserialization pass. I appreciate that has_compressible is correctly excluded from t22_extensions_eq since compressible is a Light Protocol extension, not a T22 extension marker.

Regarding lines 37-43: the generic ProgramError::InvalidInstructionData error was already flagged in a previous review. A dedicated ExtensionMismatch error would improve diagnostics.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b61088 and bf8c54b.

⛔ Files ignored due to path filters (4)
  • sdk-tests/sdk-ctoken-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/src/transfer_checked.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/scenario_spl.rs is excluded by none and included by none
  • sdk-tests/sdk-ctoken-test/tests/test_transfer_checked.rs is excluded by none and included by none
📒 Files selected for processing (9)
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs (1 hunks)
  • programs/compressed-token/program/src/lib.rs (6 hunks)
  • programs/compressed-token/program/src/shared/owner_validation.rs (2 hunks)
  • programs/compressed-token/program/src/transfer/checked.rs (1 hunks)
  • programs/compressed-token/program/src/transfer/default.rs (1 hunks)
  • programs/compressed-token/program/src/transfer/mod.rs (1 hunks)
  • programs/compressed-token/program/src/transfer/shared.rs (1 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs (4 hunks)
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
programs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in programs must not depend on light-test-utils; integration tests must be located in program-tests/

Files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
sdk-libs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in sdk-libs must not depend on light-test-utils; integration tests must be located in sdk-tests/

Files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
🧠 Learnings (83)
📓 Common learnings
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/MINT_ACTION.md : MintAction documentation must cover batch instruction for compressed mint management supporting 9 actions: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/ctoken_transfer.rs : CTokenTransfer instruction (discriminator: 3) must implement SPL-compatible transfers between decompressed accounts
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: When working with compressible extension (rent management), must read and comply with documentation in program-libs/compressible/docs/ including RENT.md, CONFIG_ACCOUNT.md, and SOLANA_RENT.md
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Compressible extension rent management must follow the specifications in program-libs/compressible/docs/RENT.md including: rent authority can only compress when is_compressible() returns true, lamport distribution on close (rent → rent_sponsor, unutilized → destination), and compression incentive for foresters
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement rent constants with values: BASE_RENT = 1,220 lamports, RENT_PER_BYTE = 10 lamports, SLOTS_PER_EPOCH = 432,000.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: The codebase provides rent calculation algorithms and account structure definitions used across light-compressed-token, light-registry, compressed-token-sdk, and related programs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/rent.rs : Implement rent calculation functions including `rent_curve_per_epoch`, `calculate_rent_and_balance`, `claimable_lamports`, and `calculate_close_lamports` in rent.rs
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : RentConfig struct must be exactly 8 bytes and contain: base_rent (u16), compression_cost (u16), lamports_per_byte_per_epoch (u8), and 3 bytes of padding.
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol rent is calculated per epoch as: 1220 minimum + (data_bytes * 10) lamports per epoch
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `rent_curve_per_epoch` function with formula: base_rent + (num_bytes * lamports_per_byte_per_epoch).
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/mod.rs : Export new wrapper modules in `account_compression_cpi/mod.rs` using `pub mod new_operation;` and `pub use new_operation::*;`, then import in `lib.rs`

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/mod.rs : Module declaration should be kept in `mod.rs`

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/ctoken_transfer.rs : CTokenTransfer instruction (discriminator: 3) must implement SPL-compatible transfers between decompressed accounts

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/pack_unpack.rs : Pubkey compression logic and `PackedXxx` struct generation with Pack/Unpack trait implementations should be in `pack_unpack.rs`

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Provide account ownership validation functions (`check_owner`, `check_program`) in checks module

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Include PDA validation functions (`check_pda_seeds`, `check_pda_seeds_with_bump`) in checks module

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/*.rs : Create wrapper instruction module at `src/account_compression_cpi/new_operation.rs` with `NewOperationContext` struct defining required accounts

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T17:59:23.357Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:59:23.357Z
Learning: Applies to program-libs/account-checks/docs/src/**/checks.rs : Implement comprehensive validation functions in the checks module covering ownership validation, permission checks, discriminator handling, and PDA verification

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:59:54.233Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/PACKED_ACCOUNTS.md:0-0
Timestamp: 2025-11-24T17:59:54.233Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/**/*.rs : Provide descriptive names in ProgramPackedAccounts error messages (e.g., 'token_mint' instead of 'account')

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Provide discriminator validation and setting functions (`check_discriminator`, `set_discriminator`) in checks module

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Provide combined validators (`check_account_info_mut`, `check_account_info_non_mut`) for complete account validation

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
📚 Learning: 2025-11-24T17:59:54.233Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/PACKED_ACCOUNTS.md:0-0
Timestamp: 2025-11-24T17:59:54.233Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/**/*.rs : Implement `get_checked()` method for validated account retrieval with discriminator checking in ProgramPackedAccounts extensions

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T17:59:54.233Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/PACKED_ACCOUNTS.md:0-0
Timestamp: 2025-11-24T17:59:54.233Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/**/*.rs : Use `ProgramPackedAccounts` for index-based access to dynamically sized account arrays in Rust programs

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
📚 Learning: 2025-11-24T17:59:23.357Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:59:23.357Z
Learning: Applies to program-libs/account-checks/docs/src/**/packed_accounts.rs : Implement index-based dynamic account access with bounds checking for handling variable account sets (mint, owner, delegate accounts)

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Implement permission check functions (`check_mut`, `check_non_mut`, `check_signer`) for account state validation

Applied to files:

  • programs/compressed-token/program/src/transfer/mod.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/DECOMPRESSED_TRANSFER.md : Decompressed Transfer documentation must cover SPL-compatible transfers between decompressed accounts

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/decompress_context.rs : Decompression trait implementation (`DecompressContext`) with account accessors, PDA/token separation logic, and token processing delegation should be in `decompress_context.rs`

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/variant_enum.rs : Account variant enum (`CompressedAccountVariant`) generation and `CompressedAccountData` wrapper struct should be implemented in `variant_enum.rs`

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: All compressed token account implementations must reference and comply with the account layout specifications in programs/compressed-token/program/docs/ACCOUNTS.md

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-06T00:50:17.433Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: DOCS.md:0-0
Timestamp: 2025-12-06T00:50:17.433Z
Learning: Applies to **/docs/*instructions* : Error documentation must use format: `ErrorType::Variant` (error code: N) - Description with actual numeric codes. For standard Solana ProgramError variants use: InvalidInstructionData (3), InvalidAccountData (4), InsufficientFunds (6), MissingRequiredSignature (8), NotEnoughAccountKeys (11), InvalidSeeds (14). For custom errors show u32 value from logs. For external crate errors show directly without wrapping.

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/programs/compressed-token/anchor/src/lib.rs : Custom error codes must be defined in programs/compressed-token/anchor/src/lib.rs as part of the anchor_compressed_token::ErrorCode enum and returned as ProgramError::Custom(error_code as u32)

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/error.rs : Error types must use numeric codes in the 19xxx range for CToken-specific errors

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/mint_action/*.rs : MintAction instruction (discriminator: 103) must support 9 action types: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/instructions.rs : Compress/decompress instruction handlers and context struct generation should be implemented in `instructions.rs`, with compress using PDA-only and decompress supporting full PDA + ctoken

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/error.rs : Implement automatic error conversions from AccountError to ProgramError for both solana-program and pinocchio SDKs

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/error.rs : Support ProgramError conversions for Anchor, Pinocchio, and Solana frameworks in error implementation

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
📚 Learning: 2025-11-24T17:59:36.701Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/DISCRIMINATOR.md:0-0
Timestamp: 2025-11-24T17:59:36.701Z
Learning: Applies to program-libs/account-checks/docs/**/account-checks/**/*.rs : Document discriminator values in code comments to make account types discoverable and maintainable

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
📚 Learning: 2025-11-24T17:59:36.701Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/DISCRIMINATOR.md:0-0
Timestamp: 2025-11-24T17:59:36.701Z
Learning: Applies to program-libs/account-checks/docs/**/account-checks/**/*.rs : Use unique discriminators across all account types to prevent account type collision attacks

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
📚 Learning: 2025-11-24T17:59:36.701Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/DISCRIMINATOR.md:0-0
Timestamp: 2025-11-24T17:59:36.701Z
Learning: Applies to program-libs/account-checks/docs/**/account-checks/**/*.rs : Use human-readable ASCII string discriminators or hash-derived values, documented with byte values in comments

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
📚 Learning: 2025-11-24T17:56:00.229Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:56:00.229Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/*.rs : Error handling must use BatchedMerkleTreeError enum from errors.rs with u32 error codes in the 14301-14312 range

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Use generic `AccountInfoTrait` parameter for all account validation functions to enable compatibility with both Solana and Pinocchio runtimes

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/close_token_account.rs : Close token account instruction must return rent exemption to rent recipient if compressible and remaining lamports to destination account

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/create_token_account.rs : Create token account instructions (CreateTokenAccount, CreateAssociatedCTokenAccount, CreateAssociatedTokenAccountIdempotent) require ACTIVE config validation only

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/pinocchio.rs : Pinocchio AccountInfo implementations should use native on-chain implementations for PDA functions when available, with fallback to solana_pubkey off-chain

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/*.rs : Use AccountInfoTrait abstraction for unified account handling across both solana-program and pinocchio SDKs

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:59:36.701Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/DISCRIMINATOR.md:0-0
Timestamp: 2025-11-24T17:59:36.701Z
Learning: Applies to program-libs/account-checks/docs/**/account-checks/**/*.rs : Use check_account_info<T> function for full account validation including discriminator and ownership checks

Applied to files:

  • programs/compressed-token/program/src/transfer/shared.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/*.rs : Implement `process_new_operation()` function in wrapper module to handle PDA signer setup, account mapping, and CPI execution

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/account_info/{pinocchio,solana}.rs : Implement feature-gated AccountInfo adapters for pinocchio (feature: `pinocchio`) and solana (feature: `solana`)

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Clone AccountInfo structs when building CPI builder pattern account structs to avoid borrow checker issues

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/{**/error.rs,**/account_info/pinocchio.rs} : Map Pinocchio ProgramError with standard codes 1-11 and handle BorrowError conversions for safe data access

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Implement all 8 compressed token instructions: create_cmint, mint_to_ctoken, create_token_account_invoke, create_token_account_invoke_signed, create_ata_invoke, create_ata_invoke_signed, transfer_interface_invoke, and transfer_interface_invoke_signed

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/transfer/checked.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:59:46.693Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ERRORS.md:0-0
Timestamp: 2025-11-24T17:59:46.693Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/error.rs : Map Pinocchio standard ProgramError variants (InvalidArgument, InvalidInstructionData, InvalidAccountData, AccountDataTooSmall, InsufficientFunds, IncorrectProgramId, MissingRequiredSignature, AccountAlreadyInitialized, UninitializedAccount, NotEnoughAccountKeys, AccountBorrowFailed) to AccountError::PinocchioProgramError with codes 1-11

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/CREATE_TOKEN_ACCOUNT.md : Create Token Account Instructions documentation must cover creation of regular and associated ctoken accounts

Applied to files:

  • programs/compressed-token/program/src/ctoken_freeze_thaw.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-06T00:49:57.458Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-token-test/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:57.458Z
Learning: Applies to sdk-tests/sdk-token-test/**/*test.rs : Tests should use light-ctoken-sdk functions from sdk-libs/compressed-token-sdk for testing ctoken instructions

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/transfer/default.rs
  • programs/compressed-token/program/src/lib.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use the builder pattern from `light-ctoken-sdk::ctoken` module for CPI operations instead of manual instruction building

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/seed_providers.rs : PDA and CToken seed provider implementations with client-side seed functions should be in `seed_providers.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/Cargo.toml : Use path references in Cargo.toml dependencies pointing to `/Users/ananas/dev/light-protocol2/sdk-libs/` for light-ctoken-sdk, light-ctoken-types, light-sdk, light-sdk-types, and light-program-test

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: State and instruction data structures must be defined in the separate 'light-ctoken-interface' crate (program-libs/ctoken-types/) to allow SDKs to import types without pulling in program dependencies

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Implement account compressibility states in the following order: Funded (rent for current + 1 epoch) → Compressible (lacks rent for current + 1 epoch) → Claimable (funded but past epochs unclaimed)

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:55:17.323Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-libs/macros/src/compressible/README.md:0-0
Timestamp: 2025-11-24T17:55:17.323Z
Learning: Applies to sdk-libs/macros/src/compressible/**/traits.rs : Core trait implementations (`HasCompressionInfo`, `CompressAs`, `Compressible`) should be defined in `traits.rs`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement state validation methods (`validate_active`, `validate_not_inactive`) on CompressibleConfig account structure

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : CompressibleConfig account structure must support serialization via Anchor, Pinocchio, and Borsh formats for Light Registry program integration

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:01:30.012Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-tests/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:30.012Z
Learning: Run compressed token core tests using `cargo test-sbf -p compressed-token-test --test ctoken`, `--test v1`, `--test mint`, and `--test transfer2`

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/MINT_ACTION.md : MintAction documentation must cover batch instruction for compressed mint management supporting 9 actions: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T17:54:38.537Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/README.md:0-0
Timestamp: 2025-11-24T17:54:38.537Z
Learning: Implement compressed token program interfaces for third-party token creation and usage on Solana using ZK Compression

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement default initialization for CToken V1 config in CompressibleConfig structure

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:56:00.229Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:56:00.229Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/Cargo.toml : Depend on light-compressed-account crate for compressed account types and utilities

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/{solana,pinocchio}.rs : Implement AccountInfoTrait with SDK-specific Pubkey types: `solana_pubkey::Pubkey` for solana-program feature and `[u8; 32]` raw bytes for pinocchio feature

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/solana.rs : Use `std::cell::Ref` and `std::cell::RefMut` for DataRef and DataRefMut associated types when implementing AccountInfoTrait for solana-program SDK

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/mod.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/TRANSFER2.md : Transfer2 documentation must cover batch transfer instruction supporting compress/decompress/transfer operations

Applied to files:

  • programs/compressed-token/program/src/transfer/default.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_owner` function to verify account is owned by specified program, returning `AccountOwnedByWrongProgram` (20001) error on mismatch

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:57:39.230Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/QUEUE_ACCOUNT.md:0-0
Timestamp: 2025-11-24T17:57:39.230Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/queue.rs : Validate account ownership by Light account compression program using `check_owner` from `light-account-checks` when deserializing `BatchedQueueAccount` with `output_from_account_info`

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:59:46.693Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ERRORS.md:0-0
Timestamp: 2025-11-24T17:59:46.693Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks/**/*.rs : Validation functions (check_owner, check_signer, check_discriminator, check_pda_seeds) must return specific AccountError variants as documented: check_owner returns AccountOwnedByWrongProgram (20001), check_signer returns InvalidSigner (20009), check_discriminator returns InvalidDiscriminator (20000) or InvalidAccountSize (20004), check_pda_seeds returns InvalidSeeds (20010)

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:57:53.312Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/TREE_ACCOUNT.md:0-0
Timestamp: 2025-11-24T17:57:53.312Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/*.rs : Account validation for state trees must check: (1) account ownership by Light account compression program, (2) account discriminator is `BatchMta`, and (3) tree type is `STATE_MERKLE_TREE_TYPE_V2` (4)

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/{src/claim/*.rs,src/withdraw_funding_pool.rs} : Rent management instructions (Claim, WithdrawFundingPool) require validation that config is not INACTIVE (accepts ACTIVE or DEPRECATED states)

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info` function to validate account ownership by program_id and verify discriminator matches type T

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account state in Solana programs must validate active state using `validate_active()` method before allowing new compressible token account creation

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T18:00:36.663Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/RENT.md:0-0
Timestamp: 2025-11-24T18:00:36.663Z
Learning: Applies to program-libs/compressible/docs/**/*rent*.rs : Implement `calculate_rent_and_balance` function to determine compressibility by checking if account balance covers required rent for epochs since last claim, returning (bool, u64) tuple.

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Account closure must distribute lamports according to: Solana rent exemption → returned to user, completed epoch rent → rent recipient, partial epoch rent → user, compression incentive → forester node

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T18:00:48.449Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/SOLANA_RENT.md:0-0
Timestamp: 2025-11-24T18:00:48.449Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Light Protocol accounts must satisfy both Solana rent exemption and Light Protocol rent requirements, plus an 11,000 lamport compression incentive during creation

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig rent_sponsor PDA and compression_authority PDA must be derived separately using `get_rent_sponsor_seeds()` and `get_compression_authority_seeds()` helper methods with version-specific seeds

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Compressible extension rent management must follow the specifications in program-libs/compressible/docs/RENT.md including: rent authority can only compress when is_compressible() returns true, lamport distribution on close (rent → rent_sponsor, unutilized → destination), and compression incentive for foresters

Applied to files:

  • programs/compressed-token/program/src/shared/owner_validation.rs
📚 Learning: 2025-11-24T17:59:54.233Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/PACKED_ACCOUNTS.md:0-0
Timestamp: 2025-11-24T17:59:54.233Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/**/*.rs : Always validate retrieved accounts after retrieval from ProgramPackedAccounts using appropriate check methods

Applied to files:

  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_program` function to verify account key matches program_id AND is marked executable, returning `InvalidProgramId` (20011) or `ProgramNotExecutable` (20012) errors

Applied to files:

  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T17:59:46.693Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ERRORS.md:0-0
Timestamp: 2025-11-24T17:59:46.693Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/error.rs : All AccountError variants must convert automatically to `ProgramError::Custom(u32)` for both solana-program and pinocchio SDKs using From trait implementations

Applied to files:

  • programs/compressed-token/program/src/transfer/checked.rs
📚 Learning: 2025-11-24T18:01:54.689Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/**/*.md : Instruction documentation must include the following sections: path, description (including accounts and state layout), instruction_data, Accounts (in order with checks), instruction logic and checks, and Errors (with descriptions of causes)

Applied to files:

  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/docs/instructions/*.md : All instruction documentation must include: path to instruction code, description of what the instruction does including accounts used and state layouts, instruction_data paths, accounts list with checks, instruction logic and checks, and possible errors with descriptions

Applied to files:

  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-06T00:50:17.433Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: DOCS.md:0-0
Timestamp: 2025-12-06T00:50:17.433Z
Learning: Applies to **/docs/*instructions* : Every instruction must include in documentation: discriminator (instruction value), enum (variant name), path (to processor code), description (high-level overview with integrated key concepts, NOT separate sections), instruction_data (path to struct with field descriptions), Accounts (ordered list with name, type, signer/writable requirements, validation checks, purpose), instruction logic and checks (step-by-step: input validation, deserialization, business logic, state updates, CPIs), and Errors (comprehensive list with ErrorType::Variant format and numeric codes).

Applied to files:

  • programs/compressed-token/program/src/lib.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use `invoke_signed()` method from builder pattern for PDA-signed CPI calls, deriving PDA with `Pubkey::find_program_address()` before invocation

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use `invoke()` method from builder pattern for regular CPI calls where the program acts as authority

Applied to files:

  • sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs
🧬 Code graph analysis (8)
programs/compressed-token/program/src/transfer/mod.rs (1)
sdk-libs/ctoken-sdk/src/ctoken/mod.rs (1)
  • default (163-174)
programs/compressed-token/program/src/transfer/shared.rs (3)
program-libs/array-map/src/lib.rs (1)
  • pubkey_eq (171-181)
programs/compressed-token/program/src/extensions/check_mint_extensions.rs (1)
  • check_mint_extensions (104-180)
program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs (1)
  • zero_copy_at_mut_checked (506-522)
programs/compressed-token/program/src/ctoken_freeze_thaw.rs (1)
programs/compressed-token/program/src/shared/owner_validation.rs (1)
  • check_token_program_owner (15-25)
sdk-libs/ctoken-sdk/src/ctoken/mod.rs (4)
programs/compressed-token/anchor/src/lib.rs (7)
  • approve (181-186)
  • freeze (199-205)
  • freeze (204-204)
  • freeze (214-214)
  • mint_to (95-109)
  • revoke (190-195)
  • thaw (209-215)
sdk-libs/ctoken-sdk/src/compressed_token/v1/account.rs (1)
  • approve (101-134)
sdk-libs/ctoken-sdk/src/compressed_token/v1/approve/instruction.rs (1)
  • approve (86-88)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
  • transfer_ctoken (20-38)
programs/compressed-token/program/src/transfer/default.rs (1)
programs/compressed-token/program/src/transfer/shared.rs (1)
  • process_transfer_extensions (67-90)
programs/compressed-token/program/src/shared/owner_validation.rs (3)
program-libs/array-map/src/lib.rs (1)
  • pubkey_eq (171-181)
program-libs/account-checks/src/checks.rs (1)
  • check_signer (121-126)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
programs/compressed-token/program/src/transfer/checked.rs (3)
programs/compressed-token/program/src/transfer/shared.rs (1)
  • process_transfer_extensions (67-90)
programs/compressed-token/program/src/shared/owner_validation.rs (1)
  • check_token_program_owner (15-25)
sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs (1)
  • from (89-99)
sdk-libs/ctoken-sdk/src/ctoken/transfer_ctoken_checked.rs (1)
programs/compressed-token/program/src/lib.rs (1)
  • from (102-122)

Comment on lines 177 to 272
/// Process account extensions with mutable access.
/// Performs extension detection and compressible top-up calculation.
#[inline(always)]
#[profile]
fn process_account_extensions(
account: &AccountInfo,
current_slot: &mut u64,
) -> Result<AccountExtensionInfo, ProgramError> {
// Fast path: base account with no extensions
if account.data_len() == light_ctoken_interface::BASE_TOKEN_ACCOUNT_SIZE as usize {
return Ok(AccountExtensionInfo::default());
}

let mut account_data = account
.try_borrow_mut_data()
.map_err(convert_program_error)?;
let (token, remaining) = CToken::zero_copy_at_mut_checked(&mut account_data)?;
if !remaining.is_empty() {
return Err(ProgramError::InvalidAccountData);
}

let extensions = token.extensions.ok_or(CTokenError::InvalidAccountData)?;

let mut info = AccountExtensionInfo::default();

for extension in extensions {
match extension {
ZExtensionStructMut::Compressible(compressible_extension) => {
info.has_compressible = true;
// Get current slot for compressible top-up calculation
use pinocchio::sysvars::{clock::Clock, rent::Rent, Sysvar};
if *current_slot == 0 {
*current_slot = Clock::get()
.map_err(|_| CTokenError::SysvarAccessError)?
.slot;
}

let rent_exemption = Rent::get()
.map_err(|_| CTokenError::SysvarAccessError)?
.minimum_balance(account.data_len());

info.top_up_amount = compressible_extension
.info
.calculate_top_up_lamports(
account.data_len() as u64,
*current_slot,
account.lamports(),
rent_exemption,
)
.map_err(|_| CTokenError::InvalidAccountData)?;
}
ZExtensionStructMut::PausableAccount(_) => {
info.has_pausable = true;
}
ZExtensionStructMut::PermanentDelegateAccount(_) => {
info.has_permanent_delegate = true;
}
ZExtensionStructMut::TransferFeeAccount(_transfer_fee_ext) => {
info.has_transfer_fee = true;
// Note: Non-zero transfer fees are rejected by check_mint_extensions,
// so no fee withholding is needed here.
}
ZExtensionStructMut::TransferHookAccount(_) => {
info.has_transfer_hook = true;
// No runtime logic needed - we only support nil program_id
}
// Placeholder and TokenMetadata variants are not valid for CToken accounts
_ => {
return Err(CTokenError::InvalidAccountData.into());
}
}
}

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

Solid extension processing with good defensive checks.

The implementation follows a sound pattern:

  1. Fast path for base accounts (no extensions) avoids unnecessary deserialization
  2. zero_copy_at_mut_checked validates account state before processing
  3. Trailing data rejection (remaining.is_empty()) prevents malformed accounts
  4. Lazy Clock::get() via the sentinel pattern avoids redundant syscalls

The catch-all at line 244 with the comment on line 243 addresses the previous review concern about documenting intentionally rejected variants.

One potential optimization: Rent::get() is called inside the compressible extension handler (line 214). If processing multiple accounts with compressible extensions, this syscall is repeated. Consider caching rent similar to how current_slot is cached.

🔎 Optional: Cache Rent sysvar like Clock
 fn process_account_extensions(
     account: &AccountInfo,
     current_slot: &mut u64,
+    cached_rent: &mut Option<pinocchio::sysvars::rent::Rent>,
 ) -> Result<AccountExtensionInfo, ProgramError> {
     // ... inside Compressible handler:
-                let rent_exemption = Rent::get()
-                    .map_err(|_| CTokenError::SysvarAccessError)?
-                    .minimum_balance(account.data_len());
+                let rent = cached_rent.get_or_insert_with(|| {
+                    Rent::get().expect("rent sysvar")
+                });
+                let rent_exemption = rent.minimum_balance(account.data_len());

This would require threading the cached rent through validate_sender and validate_recipient.

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/src/transfer/shared.rs around lines
177–251, the code calls Rent::get() inside the loop for each Compressible
extension which can repeat the sysvar syscall; cache the Rent like current_slot
to avoid repeated sysvar accesses by retrieving Rent::get() once when first
needed (or passing in a cached Rent to this function), store its minimum_balance
result, and thread the cached rent/minimum_balance through downstream calls
(e.g., validate_sender/validate_recipient) so calculate_top_up_lamports uses the
cached value instead of calling Rent::get() repeatedly.

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 (6)
forester/src/compressible/subscriber.rs (1)

57-62: Remove the DataSize filter or update it to handle variable-sized accounts with extensions.

The RpcFilterType::DataSize(BASE_TOKEN_ACCOUNT_SIZE) filter at line 62 only matches accounts of exactly 258 bytes. Since the codebase now supports compressible token accounts with extensions (pausable, permanent_delegate, transfer_fee, transfer_hook, compressed_only), and those accounts are larger (263–275 bytes per test expectations), the forester will fail to subscribe to and track any extension-enabled accounts. This breaks rent collection and state management for extended accounts.

Either remove the size filter entirely and validate account structure in the handler, or use a different approach (e.g., memcmp on the account type discriminator at a fixed position).

programs/compressed-token/program/tests/mint.rs (1)

438-457: Incomplete test coverage: additional_metadata round-trip not verified.

Line 450 sets additional_metadata: vec![] with comment "Simplified for test", which means the borsh/zero-copy round-trip compatibility of additional_metadata is not verified in this test.

While test_rnd_create_compressed_mint_account does test with random additional_metadata, it doesn't explicitly verify the reconstruction from zero-copy like this test does. Consider either:

  1. Reconstructing actual additional_metadata from the zero-copy structure, or
  2. Adding a comment explaining why this simplification is acceptable (e.g., "additional_metadata uses standard Vec serialization, tested separately").
💡 Proposed fix to test additional_metadata round-trip
                             ZExtensionStruct::TokenMetadata(z_metadata) => {
                                 ExtensionStruct::TokenMetadata(TokenMetadata {
                                     update_authority: z_metadata.update_authority,
                                     mint: z_metadata.mint,
                                     name: z_metadata.name.to_vec(),
                                     symbol: z_metadata.symbol.to_vec(),
                                     uri: z_metadata.uri.to_vec(),
-                                    additional_metadata: vec![], // Simplified for test
+                                    additional_metadata: z_metadata
+                                        .additional_metadata
+                                        .as_ref()
+                                        .map(|am| {
+                                            am.iter()
+                                                .map(|item| AdditionalMetadata {
+                                                    key: item.key.to_vec(),
+                                                    value: item.value.to_vec(),
+                                                })
+                                                .collect()
+                                        })
+                                        .unwrap_or_default(),
                                 })
                             }
programs/compressed-token/program/src/shared/compressible_top_up.rs (2)

50-73: CMint always initializes sysvars, making CToken's lazy init dead code.

The CMint block (lines 51-74) unconditionally fetches Clock and Rent. The CToken block's check at lines 81-86 (if current_slot == 0) will never trigger since CMint runs first. Similarly, the early exit at lines 102-104 is now dead code.

Consider simplifying by removing the conditional checks in the CToken block:

🔎 Simplify by fetching sysvars once upfront
     let mut transfers = [...];

-    let mut current_slot = 0;
-    let mut rent: Option<Rent> = None;
+    let current_slot = Clock::get()
+        .map_err(|_| CTokenError::SysvarAccessError)?
+        .slot;
+    let rent = Rent::get().map_err(|_| CTokenError::SysvarAccessError)?;
     // Initialize budget: +1 allows exact match (total == max_top_up)
     let mut lamports_budget = (max_top_up as u64).saturating_add(1);

     // Calculate CMint top-up using zero-copy
     {
         let cmint_data = cmint.try_borrow_data().map_err(convert_program_error)?;
         let (mint, _) = CompressedMint::zero_copy_at(&cmint_data)
             .map_err(|_| CTokenError::CMintDeserializationFailed)?;
         // Access compression info directly from meta (all cmints now have compression embedded)
-        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(cmint.data_len());
+        let rent_exemption = rent.minimum_balance(cmint.data_len());

101-104: Dead code: Early exit unreachable with new embedded compression model.

Since both CMint and CToken now have embedded compression info that's always accessed, current_slot is always set to a non-zero value before reaching this check. This early exit is no longer reachable.

🔎 Remove dead code
-    // Exit early if no compressible accounts
-    if current_slot == 0 {
-        return Ok(());
-    }
-
     if transfers[0].amount == 0 && transfers[1].amount == 0 {
         return Ok(());
     }
programs/compressed-token/program/src/mint_action/actions/compress_and_close_cmint.rs (1)

79-111: Step numbering in comments is inconsistent.

The comments indicate steps 7 (line 79), then 6 (line 99), then 7 again (line 105). This appears to be a copy-paste artifact. Consider renumbering for clarity:

-    // 7. Check is_compressible (rent has expired)
+    // 6. Check is_compressible (rent has expired)
     ...
-    // 6. Transfer all lamports to rent_sponsor
+    // 7. Transfer all lamports to rent_sponsor
     ...
-    // 7. Close account (assign to system program, resize to 0)
+    // 8. Close account (assign to system program, resize to 0)
program-libs/ctoken-interface/tests/compressed_mint.rs (1)

104-114: Consider adding test iterations with extensions enabled.

Currently the loop only tests with_extensions: false. To fully exercise the TokenMetadata extension path and validate extensions roundtrip correctly, consider adding iterations with extensions enabled.

🔎 Suggested improvement
 for i in 0..100 {
-    let original_mint = generate_random_compressed_mint(&mut rng, false);
+    let with_ext = i % 2 == 0; // Alternate between with/without extensions
+    let original_mint = generate_random_compressed_mint(&mut rng, with_ext);
     let borsh_bytes = original_mint.try_to_vec().unwrap();

Note: You'd also need to update the CompressedMintConfig to include extensions when with_ext is true.

♻️ Duplicate comments (23)
programs/compressed-token/program/src/transfer/shared.rs (3)

39-46: Extension mismatch returns a generic error.

As noted in a previous review, ProgramError::InvalidInstructionData is generic. A dedicated CTokenError::ExtensionMismatch variant would provide clearer diagnostics for callers investigating transfer failures.


104-126: Budget enforcement is correct; minor optimization opportunity noted previously.

The saturating_add prevents overflow, and the "0 = no limit" semantics are implemented correctly. As mentioned in a prior review, the transfers array always includes both accounts even when one has a zero amount—filtering could avoid unnecessary work if multi_transfer_lamports doesn't already short-circuit zeros.


225-238: Rent sysvar is fetched on every account processed.

As noted in a prior review, Rent::get() is called inside this function for each account. When processing both sender and recipient accounts, this results in two syscalls. Consider caching the rent value similarly to how current_slot is cached—pass a &mut Option<Rent> and populate it lazily.

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

57-77: Verbose logging on authorization failure impacts compute budget.

Multiple msg! calls on failure add compute overhead on hot paths. This was flagged in a previous review. Consider consolidating into a single log message or gating behind a feature flag for debugging.

program-libs/ctoken-interface/tests/ctoken/size.rs (1)

6-57: Good test coverage for extension size combinations.

The test exercises:

  • Base size (no extensions)
  • Single extensions (pausable, permanent_delegate, transfer_fee, transfer_hook)
  • Pairs (pausable + permanent_delegate)
  • All four extensions

The comments clearly document the expected size math. While named constants could improve maintainability, the current approach is readable.

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

241-253: Consider extracting the frozen state constant.

Lines 244 and 257 use the magic number 2 for AccountState::Frozen. While the inline comment explains this, a named constant would improve clarity and reduce duplication.


270-276: Redundant delegate check can be simplified.

The has_delegate() check on line 271 already validates the delegate option prefix. The subsequent check compressed_token_account.delegate != 0 on line 274 is redundant since has_delegate() returns true only when the delegate is set.

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

134-136: unwrap_or_default() silently swallows extension enumeration errors.

If get_extension_types() fails, this returns an empty vector, causing has_restricted_extensions to be false. A malformed mint could bypass restricted extension checks.

🔎 Suggested fix
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;
forester/src/compressible/compressor.rs (1)

140-156: Index collision issue with delegate_index sentinel value.

Using unwrap_or(0) as a sentinel for "no delegate" collides with valid packed account index 0. When the owner is the first account inserted, insert_or_get() returns 0. Downstream code cannot distinguish:

  • No delegate (sentinel 0)
  • Delegate equals owner (legitimate index 0)
🔎 Suggested fix

Change delegate_index in CompressAndCloseIndices to Option<u8>:

-            let delegate_index = account_state
-                .account
-                .delegate
-                .map(|delegate| {
-                    let delegate_pubkey = Pubkey::new_from_array(delegate.to_bytes());
-                    packed_accounts.insert_or_get(delegate_pubkey)
-                })
-                .unwrap_or(0);
+            let delegate_index = account_state.account.delegate.map(|delegate| {
+                let delegate_pubkey = Pubkey::new_from_array(delegate.to_bytes());
+                packed_accounts.insert_or_get(delegate_pubkey)
+            });

Then update CompressAndCloseIndices struct and all consumers.

forester/src/compressible/state.rs (2)

14-38: Dynamic rent calculation is an improvement.

The refactor to accept account_size and compute rent_exemption dynamically via Rent::default().minimum_balance(account_size) is better than hardcoded values. The embedded compression access via account.compression.get_last_funded_epoch() aligns with the PR's migration.

Note: As flagged in a previous review, Rent::default() returns hardcoded mainnet values. For production accuracy, consider fetching the actual rent sysvar via RPC in calling contexts.


112-122: Silent skip on calculation failure preserved.

The error handling logs a warning and returns Ok(()) when calculate_compressible_slot fails. This was flagged in a previous review as potentially hiding issues. While it prevents one bad account from blocking updates, consider tracking failed accounts for later investigation or using a sentinel value.

programs/registry/src/compressible/compressed_token/compress_and_close.rs (4)

100-104: Use consistent error variant for CToken parsing failure.

The error RegistryError::InvalidSigner doesn't describe a CToken parsing failure. For consistency with line 97-98, use RegistryError::InvalidTokenAccountData.

🔎 Suggested fix
         let (ctoken, _) = CToken::zero_copy_at(&account_data).map_err(|e| {
             anchor_lang::prelude::msg!("Failed to parse CToken: {:?}", e);
-            RegistryError::InvalidSigner
+            RegistryError::InvalidTokenAccountData
         })?;

109-111: Document the frozen state constant.

The magic number 2 for AccountState::Frozen should be documented. Per relevant code snippets, is_frozen() methods in zero_copy.rs also use state == 2.

+        // AccountState: Uninitialized=0, Initialized=1, Frozen=2
         let is_frozen = ctoken.state == 2;

137-148: Critical: delegated_amount lost when no marker extensions present.

When an account has a delegate with delegated_amount > 0 but no marker extensions (not frozen, no transfer fees, etc.), the CompressedOnly extension isn't created, and delegated_amount is permanently lost. The output sets has_delegate: true at line 157, but the compressed account has no way to recover the original spending limit.

The condition should include delegated_amount > 0:

🔎 Suggested fix
-        if has_marker_extensions {
+        if has_marker_extensions || delegated_amount > 0 {
             out_tlv.push(vec![ExtensionInstructionData::CompressedOnly(
                 CompressedOnlyExtensionInstructionData {
                     delegated_amount,
                     withheld_transfer_fee,
                     is_frozen,
                 },
             )]);
         } else {
             out_tlv.push(vec![]);
         }

37-37: Clarify delegate_index sentinel semantics.

The comment says "0 if no delegate", but 0 is a valid index in packed_accounts. At line 157, has_delegate is derived from delegated_amount > 0 which provides a safety net, but the index semantics remain ambiguous.

Consider using u8::MAX (255) as the sentinel, or document that index 0 is reserved/cannot be used for delegates.

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

28-28: Documentation still doesn't match validation logic.

The comment states "rent_payment must be 0 or >= 2" but the validation at lines 62-64 only rejects rent_payment == 1. Consider revising to: "rent_payment must not be 1" to accurately reflect the code behavior.


61-65: Consider adding upper bound validation for rent_payment.

The validation only rejects rent_payment == 1 but allows unbounded values. Since max_funded_epochs exists in the config, consider adding:

if action.rent_payment > config.rent_config.max_funded_epochs as u32 {
    return Err(ErrorCode::RentPaymentExceedsMaximum.into());
}

This prevents excessively large epoch values from causing large lamport transfers.

programs/compressed-token/program/src/ctoken_approve_revoke.rs (2)

113-117: The saturating_add(1) on lamports_budget is unclear.

When max_top_up != 0, you set lamports_budget = (max_top_up as u64).saturating_add(1). The +1 appears to be a sentinel value, but this interacts confusingly with the explicit check at line 132. Consider documenting why this is needed or simplifying to just use the explicit check.


130-137: Redundant max_top_up enforcement.

The explicit transfer_amount > max_top_up check makes the lamports_budget mechanism somewhat redundant for enforcement purposes. The budget is passed to process_compression_top_up but doesn't seem to be the authoritative check here.

Consider either:

  1. Removing the explicit check and relying on budget exhaustion, or
  2. Clarifying with a comment why both checks exist.
program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (1)

122-177: Zero-copy deserialization handles all new discriminants correctly.

Each discriminant (27-31) correctly maps to its corresponding extension type and returns the appropriate ZExtensionStructMut variant. The pattern is consistent across all cases.

Note: The hardcoded discriminant literals (19, 27, 28, 29, 30, 31) would benefit from being extracted to named constants as noted in a past review. This would improve maintainability.

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

50-54: Redundant uninitialized account check (previously flagged).

This check duplicates validation already performed by CToken::zero_copy_at_mut_checked at line 48. The _checked variant returns an error when state == 0 (via InvalidAccountState). The explicit re-check here adds defensive depth but is functionally redundant.


64-70: Consider extracting magic number 2 for AccountState::Frozen.

The frozen state check uses the literal 2 for the frozen state. A named constant would improve readability:

const ACCOUNT_STATE_FROZEN: u8 = 2;

This was noted in a previous review and remains a minor readability improvement.


157-192: Missing delegate validation when CToken has existing delegate (previously flagged).

The delegate validation logic still has the gap identified in the prior review:

Scenario:

  • CToken has delegate A (ctoken.delegate() returns Some(A))
  • Input compressed account has delegated_amount > 0
  • But input_delegate parameter is None

Current flow:

  1. Line 161: enters if let Some(ctoken_delegate) block
  2. Line 163: if let Some(input_del) is false (input_delegate is None)
  3. Falls through to line 173 comment
  4. Line 184-191: adds delegated_amount without validating it came from the same delegate

Risk: Delegated amounts from potentially different delegates are merged without validation.

🔎 Proposed fix
     // Validate delegate compatibility
     if let Some(ctoken_delegate) = ctoken.delegate() {
         // CToken has a delegate - check if it matches the input delegate
         if let Some(input_del) = input_delegate_pubkey.as_ref() {
             if ctoken_delegate.to_bytes() != input_del.to_bytes() {
                 msg!(
                     "Decompress delegate mismatch: CToken delegate {:?} != input delegate {:?}",
                     ctoken_delegate.to_bytes(),
                     input_del.to_bytes()
                 );
                 return Err(ErrorCode::DecompressDelegateMismatch.into());
             }
+        } else if delegated_amount > 0 {
+            // CToken has delegate but input has delegated_amount without delegate info
+            msg!("Decompress: CToken has delegate but input has delegated_amount without delegate pubkey");
+            return Err(CTokenError::InvalidAccountData.into());
         }
         // Delegates match (or input has no delegated_amount) - continue

Comment on lines 401 to 420
// Reconstruct CompressionInfo from zero-copy fields
let compression = {
let zc = &zc_mint.meta.compression;
CompressionInfo {
config_account_version: u16::from(zc.config_account_version),
compress_to_pubkey: zc.compress_to_pubkey,
account_version: zc.account_version,
lamports_per_write: u32::from(zc.lamports_per_write),
compression_authority: zc.compression_authority,
rent_sponsor: zc.rent_sponsor,
last_claimed_slot: u64::from(zc.last_claimed_slot),
rent_config: light_compressible::rent::RentConfig {
base_rent: u16::from(zc.rent_config.base_rent),
compression_cost: u16::from(zc.rent_config.compression_cost),
lamports_per_byte_per_epoch: zc.rent_config.lamports_per_byte_per_epoch,
max_funded_epochs: zc.rent_config.max_funded_epochs,
max_top_up: u16::from(zc.rent_config.max_top_up),
},
}
};
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 adding a helper method or From trait for CompressionInfo reconstruction.

This manual reconstruction of CompressionInfo from zero-copy fields is verbose and creates maintenance burden. If the structure changes, this code must be updated in lockstep.

💡 Suggested refactoring approach

Consider implementing a From<&ZCompressionInfo> trait or a .to_owned() method on the zero-copy type to eliminate this boilerplate. Example:

// In the zero-copy CompressionInfo implementation
impl<'a> From<&ZCompressionInfo<'a>> for CompressionInfo {
    fn from(zc: &ZCompressionInfo<'a>) -> Self {
        CompressionInfo {
            config_account_version: u16::from(zc.config_account_version),
            compress_to_pubkey: zc.compress_to_pubkey,
            // ... remaining fields
        }
    }
}

Then this test code becomes:

-        let compression = {
-            let zc = &zc_mint.meta.compression;
-            CompressionInfo {
-                config_account_version: u16::from(zc.config_account_version),
-                // ... 15+ lines of manual field mapping
-            }
-        };
+        let compression = CompressionInfo::from(&zc_mint.meta.compression);

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/tests/mint.rs around lines 401 to 420, the
test manually reconstructs a CompressionInfo from zero-copy fields; implement a
conversion to remove this boilerplate by adding a From<&ZCompressionInfo> (or a
to_owned) implementation on the zero-copy type that maps each field (including
numeric casts like u16::from/u32::from/u64::from and reconstructing the
RentConfig sub-struct) into a CompressionInfo, then update the test to use
CompressionInfo::from(&zc_mint.meta.compression) or .into() instead of the
manual field-by-field reconstruction.

@ananas-block ananas-block force-pushed the jorrit/feat-more-ctoken-extensions2 branch from 23e48a4 to d293dc3 Compare December 21, 2025 23:57
);
return Err(ErrorCode::RentPaymentExceedsMax.into());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed misguided check, max_funded_epochs should only regulate top ups not the initial rent payment.

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: 20

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/program/tests/compress_and_close.rs (1)

73-84: Move this test to program-tests/ directory.

Unit tests in programs must be located in program-tests/ for integration testing, not in programs/*/tests/. The decimals value of 9 is correct for this test scenario (standard for Solana tokens), but the file location violates the project's testing structure guidelines.

program-libs/ctoken-interface/src/instructions/mint_action/instruction_data.rs (1)

117-152: Normalize empty extension lists to None for symmetry and cleaner encoding.

In TryFrom<CompressedMint> for CompressedMintInstructionData you always return extensions: Some(extension_list), even when there were no extensions on the mint and extension_list is empty. The reverse conversion treats an empty list as None, so the two paths are no longer symmetric and you emit a different Borsh shape (Some([]) vs None) for “no extensions”.

Consider only using Some when there is at least one extension:

Suggested change
-        Ok(Self {
-            supply: mint.base.supply,
-            decimals: mint.base.decimals,
-            metadata: mint.metadata,
-            mint_authority: mint.base.mint_authority,
-            freeze_authority: mint.base.freeze_authority,
-            extensions: Some(extension_list),
-        })
+        let extensions = if extension_list.is_empty() {
+            None
+        } else {
+            Some(extension_list)
+        };
+
+        Ok(Self {
+            supply: mint.base.supply,
+            decimals: mint.base.decimals,
+            metadata: mint.metadata,
+            mint_authority: mint.base.mint_authority,
+            freeze_authority: mint.base.freeze_authority,
+            extensions,
+        })

This keeps the InstructionData encoding consistent with how you deserialize it back into CompressedMint.

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

45-99: Consider extracting duplicate sysvar initialization logic.

The code correctly migrates from extension-scanning to embedded compression metadata access. However, the sysvar initialization logic (Lines 56-61 and 81-86) is duplicated between CMint and CToken sections.

Since both blocks perform identical operations:

if current_slot == 0 {
    current_slot = Clock::get()...;
    rent = Some(Rent::get()...);
}

Consider extracting this into a helper function or initializing once before both sections:

🔎 Suggested refactor
+    // Initialize sysvars once for both calculations
+    let current_slot = Clock::get()
+        .map_err(|_| CTokenError::SysvarAccessError)?
+        .slot;
+    let rent = Rent::get().map_err(|_| CTokenError::SysvarAccessError)?;
+
     // Calculate CMint top-up using zero-copy
     {
         let cmint_data = cmint.try_borrow_data().map_err(convert_program_error)?;
         let (mint, _) = CompressedMint::zero_copy_at(&cmint_data)
             .map_err(|_| CTokenError::CMintDeserializationFailed)?;
-        if current_slot == 0 {
-            current_slot = Clock::get()...;
-            rent = Some(Rent::get()...);
-        }
-        let rent_exemption = rent.as_ref().unwrap().minimum_balance(cmint.data_len());
+        let rent_exemption = rent.minimum_balance(cmint.data_len());

This eliminates the lazy initialization complexity and duplicate code.

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

79-97: Minor: Comment step numbering is inconsistent.

The doc comments at the top list steps 1-9, but the inline comments jump: 4→5→7→6→7→8→9. Consider renumbering for clarity.

🔎 Suggested numbering fix
-    // 7. Check is_compressible (rent has expired)
+    // 6. Check is_compressible (rent has expired)
     #[cfg(target_os = "solana")]
     let current_slot = Clock::get()
         .map_err(|_| ProgramError::UnsupportedSysvar)?
         .slot;

And subsequent steps should follow sequentially.

program-libs/ctoken-interface/tests/compressed_mint.rs (1)

104-114: Test currently only exercises non-extension path.

Line 105 hardcodes with_extensions: false. Consider adding a separate test iteration or test case that exercises with_extensions: true to ensure the TokenMetadata extension path is validated.

🔎 Consider adding extension coverage
// After the existing loop, add:
for i in 0..20 {
    let original_mint = generate_random_compressed_mint(&mut rng, true);
    // ... similar assertions for extension case
}
programs/compressed-token/program/src/shared/owner_validation.rs (1)

83-114: Unused _compression_amount parameter—remove or document intent.

The _compression_amount parameter is accepted but never used. If it's reserved for future use, consider adding a brief doc comment explaining the intent. Otherwise, remove it to avoid confusion.

🔎 If removing:
 pub fn check_ctoken_owner(
     compressed_token: &mut ZCTokenMut,
     authority_account: &AccountInfo,
     mint_checks: Option<&MintExtensionChecks>,
-    _compression_amount: u64,
 ) -> Result<(), ProgramError> {

Update all call sites accordingly.

♻️ Duplicate comments (44)
programs/compressed-token/program/src/mint_action/mint_input.rs (1)

40-41: Comment remains misleading.

This issue was previously flagged: the comment suggests returning CompressedMint to avoid double deserialization, but the function returns Ok(()). Either mark it as a TODO if you plan to refactor the signature later, or remove the comment to keep the code and documentation consistent.

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

81-100: Two transfer paths are correct, but rationale could be clearer.

The function correctly handles two scenarios:

  1. Extension decimals present: Validates decimals match, uses 3-account slice (source, destination, authority), calls process_transfer with None for expected_decimals
  2. No extension decimals: Validates mint ownership, uses all 4 accounts including mint, passes Some(decimals)

The different account layouts are necessary because the pinocchio transfer logic expects different account sets depending on whether decimals validation is needed. However, it would help to add a comment explaining why the mint is excluded in the extension path.

📝 Suggested clarification
     if let Some(extension_decimals) = extension_decimals {
         if extension_decimals != decimals {
             msg!("extension_decimals != decimals");
             return Err(ProgramError::InvalidInstructionData);
         }
-        // Create accounts slice without mint: [source, destination, authority]
-        // pinocchio expects 3 accounts when expected_decimals is None
+        // Extension decimals path: mint is excluded because decimals already validated
+        // against extension. The underlying SPL transfer validates the mint internally
+        // and doesn't require it in the accounts slice when expected_decimals is None.
         let transfer_accounts = [*source, *destination, *authority];
programs/compressed-token/program/src/transfer/shared.rs (3)

189-227: Extension processing with good defensive checks.

Key strengths:

  • zero_copy_at_mut_checked validates account state before processing
  • Trailing data rejection (line 203-205) prevents malformed accounts
  • Lazy Clock caching via sentinel pattern (line 219)
  • Mint field validation when mint account is provided (lines 208-212)

The Rent::get() call inside this function will repeat for each account processed. Consider caching rent similar to the clock pattern if this becomes a hot path.


39-46: Generic error for extension mismatch.

ProgramError::InvalidInstructionData is used when T22 extensions don't match between sender and recipient. A more specific error like CTokenError::ExtensionMismatch would help callers diagnose failures.


97-126: Budget enforcement is correct; zero-amount optimization opportunity.

The saturating_add prevents overflow, and "0 = no limit" semantics are properly implemented. The transfers array always includes both accounts even when one has zero amount.

program-libs/ctoken-interface/src/instructions/create_ctoken_account.rs (1)

100-119: Non-Solana target panics instead of returning error.

The unreachable!() on non-Solana targets will cause a panic in unit tests or off-chain tooling. Since this function returns Result, it would be more consistent to return an error variant.

🔎 Suggested fix
     #[cfg(not(target_os = "solana"))]
-    unreachable!("deriving a pda is only available on target `solana`");
+    {
+        // Off-chain: use solana_pubkey's software implementation
+        let mut seeds_with_bump: Vec<&[u8]> = seeds.to_vec();
+        seeds_with_bump.push(&[bump]);
+        solana_pubkey::Pubkey::create_program_address(&seeds_with_bump, &solana_pubkey::Pubkey::new_from_array(*program_id))
+            .map(|p| p.to_bytes())
+            .map_err(|_| CTokenError::InvalidAccountData)
+    }

This enables off-chain testing without panics.

programs/compressed-token/program/tests/token_output.rs (1)

51-68: Propagate and assert the frozen flag instead of hardcoding is_frozen: false.

You randomize is_frozen in CompressedOnlyExtensionInstructionData (Line 124) but always pass false into set_output_compressed_account (Line 180) and always set state: AccountState::Initialized in the expected AnchorTokenData (Line 212). That means:

  • The frozen-state path in set_output_compressed_account (state selection + frozen hashing) is never exercised.
  • The TLV is_frozen bit can conceptually diverge from the ctoken account state without any test catching it.

Consider tracking a per-output is_frozen flag and using it consistently:

  • Record is_frozen when building the TLV instruction data.
  • Pass that flag into set_output_compressed_account.
  • Drive the expected token_data.state from the same flag (e.g., AccountState::Frozen when true).

For example:

Suggested test adjustments
-        let mut merkle_tree_indices = Vec::new();
-        let mut tlv_flags = Vec::new();
-        let mut tlv_delegated_amounts = Vec::new();
-        let mut tlv_withheld_fees = Vec::new();
+        let mut merkle_tree_indices = Vec::new();
+        let mut tlv_flags = Vec::new();
+        let mut tlv_delegated_amounts = Vec::new();
+        let mut tlv_withheld_fees = Vec::new();
+        let mut is_frozen_flags = Vec::new();

@@
-        for i in 0..num_outputs {
+        for i in 0..num_outputs {
             if tlv_flags[i] {
-                let ext = ExtensionInstructionData::CompressedOnly(
-                    CompressedOnlyExtensionInstructionData {
-                        delegated_amount: tlv_delegated_amounts[i],
-                        withheld_transfer_fee: tlv_withheld_fees[i],
-                        is_frozen: rng.gen_bool(0.2), // 20% chance of frozen
-                    },
-                );
+                let is_frozen = rng.gen_bool(0.2); // 20% chance of frozen
+                let ext = ExtensionInstructionData::CompressedOnly(
+                    CompressedOnlyExtensionInstructionData {
+                        delegated_amount: tlv_delegated_amounts[i],
+                        withheld_transfer_fee: tlv_withheld_fees[i],
+                        is_frozen,
+                    },
+                );
+                is_frozen_flags.push(is_frozen);
@@
-            } else {
-                tlv_instruction_data_vecs.push(vec![]);
+            } else {
+                tlv_instruction_data_vecs.push(vec![]);
+                is_frozen_flags.push(false);
@@
-            set_output_compressed_account(
+            set_output_compressed_account(
                 output_account,
                 &mut hash_cache,
                 owner_pubkeys[index],
                 output_delegate,
                 amounts[index],
                 lamports.as_ref().and_then(|l| l[index]),
                 mint_pubkey,
                 merkle_tree_indices[index],
                 version,
                 tlv_slice,
-                false, // Not frozen in tests
+                is_frozen_flags[index],
             )
@@
-            let token_data = AnchorTokenData {
+            let is_frozen = is_frozen_flags[i];
+            let token_data = AnchorTokenData {
                 mint: mint_pubkey,
                 owner: owner_pubkeys[i],
                 amount: amounts[i],
                 delegate: token_delegate,
-                state: AccountState::Initialized as u8,
+                state: if is_frozen {
+                    AccountState::Frozen as u8
+                } else {
+                    AccountState::Initialized as u8
+                },
                 tlv: tlv.clone(),
             };

This keeps TLV is_frozen, token state, and hashing all in sync and actually tests the frozen path.

Also applies to: 114-135, 137-145, 159-181, 191-215

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

311-312: Update UnsupportedTlvExtensionType error message to reflect all supported extensions.

The message still claims “only CompressedOnly is currently implemented”, but the interface now supports multiple TLV extensions (TokenMetadata, CompressedOnly, PausableAccount, PermanentDelegateAccount, TransferFeeAccount, TransferHookAccount, etc.). Consider either listing the supported set or using a generic “Unsupported TLV extension type” message so it stays accurate as support evolves.

In the current light-ctoken-interface crate, which TLV extension variants are defined in `ExtensionStruct` and `ZExtensionInstructionData`, and which of them are actually supported by the compressed-token program?
programs/compressed-token/program/tests/multi_sum_check.rs (1)

92-92: Decimals field updated but remains unvalidated.

The decimals: 9 values are now set in the Compression structs (previously 0), but sum_check_multi_mint still performs no validation on this field—it only checks balance arithmetic. If the decimals field is meant to align with mint metadata, consider adding a separate validation step. Otherwise, document why this field is included but not checked.

Also, per previous review feedback: this test file should be moved from programs/compressed-token/program/tests/ to program-tests/ to comply with coding guidelines for integration tests.

Also applies to: 357-357

sdk-libs/client/src/indexer/types.rs (1)

887-893: Extract duplicated TLV deserialization and improve error handling.

The TLV deserialization logic is duplicated identically in both TokenAccount (lines 887-893) and TokenAccountV2 (lines 928-934). Additionally, both base64 decode and Borsh deserialization errors are mapped to the generic InvalidResponseData, making debugging difficult.

Recommend:

  1. Extract into a shared helper function: parse_indexer_tlv(tlv: &str) -> Result<Vec<ExtensionStruct>, IndexerError>
  2. Add specific error variants (InvalidBase64Data, BorshDeserializationError) to preserve failure context
  3. Add integration tests in sdk-tests/ covering valid/malformed TLVs
  4. Document why indexer returns Borsh-serialized Vec<ExtensionStruct> and SDK/indexer version coordination

Also applies to: 928-934

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

18-31: ⚠️ Silent error handling could lead to undersized allocations.

The function uses unwrap_or(0) on Line 25 when calculating extension sizes. If ExtensionStruct::byte_len(ext) returns an error, treating it as zero bytes could cause account allocation to be undersized, leading to buffer overflows or data truncation during serialization.

Consider propagating errors explicitly or at minimum using .expect() with a descriptive message if errors are truly impossible for valid configs.

🔎 Recommended approach
-pub fn calculate_ctoken_account_size(extensions: Option<&[ExtensionStructConfig]>) -> usize {
+pub fn calculate_ctoken_account_size(extensions: Option<&[ExtensionStructConfig]>) -> Result<usize, light_zero_copy::errors::ZeroCopyError> {
     let mut size = BASE_TOKEN_ACCOUNT_SIZE as usize;
 
     if let Some(exts) = extensions {
         if !exts.is_empty() {
             size += 4; // Vec length prefix
             for ext in exts {
-                size += ExtensionStruct::byte_len(ext).unwrap_or(0);
+                size += ExtensionStruct::byte_len(ext)?;
             }
         }
     }
 
-    size
+    Ok(size)
 }

Then update callers to handle the Result.

program-libs/ctoken-interface/tests/ctoken/size.rs (1)

1-57: Test coverage is comprehensive; consider named constants for clarity.

The test thoroughly exercises various extension combinations (base, pausable, permanent_delegate, transfer_fee, transfer_hook, and all combinations). However, hard-coded numeric literals (263, 264, 271, 275) could be replaced with named constants for better maintainability and clearer test intent.

For example:

const PAUSABLE_ONLY_SIZE: usize = 263;
const PAUSABLE_PERMANENT_DELEGATE_SIZE: usize = 264;

That said, the current implementation works correctly and documents the exact expected sizes.

forester/src/compressible/compressor.rs (1)

140-148: Index collision issue: using 0 as sentinel for "no delegate" is ambiguous.

The unwrap_or(0) pattern creates ambiguity—index 0 is returned both when no delegate exists (sentinel) and when the delegate is the first packed account (the owner). Downstream handlers cannot distinguish between these cases.

Consider using Option<u8> in CompressAndCloseIndices to represent the delegate index, where None clearly indicates no delegate.

programs/compressed-token/program/tests/mint.rs (1)

401-420: CompressionInfo reconstruction is correct but verbose.

The manual field-by-field reconstruction works but creates maintenance overhead. A previous review already suggested implementing From<&ZCompressionInfo> for CompressionInfo to reduce this boilerplate—that remains a good refactoring target.

program-libs/ctoken-interface/tests/cross_deserialization.rs (1)

83-98: Solid regression tests for account type discrimination.

These tests effectively verify that the discriminator at byte 165 prevents type confusion attacks. The test structure covers both zero-copy and Borsh deserialization paths.

Note: A previous review suggested deriving ACCOUNT_TYPE_OFFSET programmatically to catch layout changes earlier—that suggestion still applies here.

forester/src/compressible/state.rs (1)

21-22: Rent::default() uses hardcoded mainnet values.

As flagged in a prior review, Rent::default() returns hardcoded mainnet parameters. In test environments or if Solana updates rent parameters, this could produce incorrect compressible slot calculations. Consider accepting Rent as a parameter or fetching it via RPC.

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

54-59: TLV boolean flag and cast concerns remain from prior review.

A previous review flagged two issues here:

  1. Line 56: The tlv boolean is hardcoded to true, but should be !tlv_config.is_empty() to correctly signal when TLV data exists.
  2. Line 58-59: The as u32 cast is unchecked; prefer .try_into().map_err(...) for explicit overflow handling.
🔎 Suggested fix
                 let token_config = TokenDataConfig {
                     delegate: (has_delegate, ()),
-                    tlv: (true, tlv_config),
+                    tlv: (!tlv_config.is_empty(), tlv_config),
                 };
-                TokenData::byte_len(&token_config).map_err(|_| ProgramError::InvalidAccountData)?
-                    as u32
+                TokenData::byte_len(&token_config)
+                    .map_err(|_| ProgramError::InvalidAccountData)?
+                    .try_into()
+                    .map_err(|_| ProgramError::InvalidAccountData)?
programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs (2)

28-28: Documentation still doesn't match validation logic.

The comment says "rent_payment must be 0 or >= 2" but the validation only rejects rent_payment == 1. This was flagged in a previous review. The code accepts values like 1000 epochs without checking against max_funded_epochs.


61-65: Missing upper bound validation for rent_payment.

As noted in a previous review, there's no check against config.rent_config.max_funded_epochs. Large values could cause excessive lamport transfers or arithmetic issues in downstream rent calculations.

programs/registry/src/compressible/compressed_token/compress_and_close.rs (4)

100-104: Use appropriate error variant for CToken parsing failure.

RegistryError::InvalidSigner doesn't describe a CToken parsing failure. This was noted in a previous review—consider using RegistryError::InvalidTokenAccountData for consistency with line 97-98.


109-111: Document the AccountState frozen constant.

Using ctoken.state == 2 as a magic number was flagged before. Add a comment or constant:

// AccountState: Uninitialized=0, Initialized=1, Frozen=2
let is_frozen = ctoken.state == 2;

137-148: Preserve delegated_amount when delegate exists without marker extensions.

This was flagged as critical in a previous review: when an account has delegated_amount > 0 but no marker extensions, the CompressedOnly extension isn't created and delegated_amount is lost.

Fix: create CompressedOnly whenever has_marker_extensions || delegated_amount > 0.

🔎 Proposed fix
-        if has_marker_extensions {
+        if has_marker_extensions || delegated_amount > 0 {
             out_tlv.push(vec![ExtensionInstructionData::CompressedOnly(

36-38: Sentinel value for delegate_index is ambiguous.

Index 0 is a valid packed_accounts index, yet the comment says "0 if no delegate". The actual safety net is has_delegate: delegated_amount > 0 at line 157, but this indirect approach is fragile.

Consider using u8::MAX (255) as the sentinel value, or document that index 0 cannot be used for delegates.

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

148-158: Inconsistent error handling between optional field reads and CompressionInfo deserialization.

The deserialization uses let _ = buf.read_exact(...) to silently ignore errors for account_type, decimals, and compression_only (backward compatibility with shorter legacy accounts). However, CompressionInfo::deserialize_reader(buf)? on line 154 propagates errors.

This creates a subtle inconsistency:

  • If reading legacy data where bytes exist for account_type/decimals/compression_only but not for CompressionInfo, the function will fail
  • If reading even older data where none of these fields exist, it will also fail on CompressionInfo

Consider making CompressionInfo deserialization consistent with the tolerant pattern:

🔎 Suggested fix for consistent fallback behavior
-        // Read compression (CompressionInfo)
-        let compression = CompressionInfo::deserialize_reader(buf)?;
+        // Read compression (CompressionInfo) - use default if not present for backward compatibility
+        let compression = CompressionInfo::deserialize_reader(buf).unwrap_or_default();

This assumes CompressionInfo implements Default. If not, you may need to define a sensible default value.

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

57-77: Verbose failure logging already flagged.

program-libs/ctoken-interface/src/instructions/transfer2/instruction_data.rs (1)

34-37: TLV type change already flagged as breaking.

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

125-128: Uncomment Compress mode validation—already flagged.

programs/compressed-token/program/src/ctoken_approve_revoke.rs (2)

111-117: saturating_add(1) and current_slot usage already flagged.


130-137: Redundant max_top_up enforcement already flagged.

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

95-108: Add explanatory comment for TLV extension filtering.

The conversion correctly filters to only CompressedOnly extension since other variants (Pausable, PermanentDelegate, TransferHook) are marker extensions referencing mint configuration rather than account-specific state data. Consider adding a brief comment to clarify this intentional filtering for future maintainers:

// Only CompressedOnly carries account state data; marker extensions are metadata-only
programs/compressed-token/program/src/close_token_account/processor.rs (2)

115-120: State validation uses raw u8 comparison instead of enum match.

The pattern state if state == AccountState::Initialized as u8 works but is fragile. Consider adding a comment documenting the discriminant mapping:

// AccountState discriminants: Uninitialized=0, Initialized=1, Frozen=2

198-209: Compression authority incentive extraction could underflow in edge cases.

When authority == compression_authority, the checked_sub correctly prevents underflow. Consider adding a more descriptive error message for debugging:

 lamports_to_rent_sponsor = lamports_to_rent_sponsor
     .checked_sub(compression_cost)
-    .ok_or(ProgramError::InsufficientFunds)?;
+    .ok_or_else(|| {
+        msg!("Insufficient lamports for compression incentive");
+        ProgramError::InsufficientFunds
+    })?;
programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs (2)

103-112: Delegate lookup silently swallows errors via .ok().

When has_delegate() returns true but packed_accounts.get_u8() fails, the error is silently converted to None. This could mask legitimate account resolution failures. Consider propagating or logging the error:

🔎 Suggested improvement
 let input_delegate = matching_input_index.and_then(|idx| {
     let input = inputs.in_token_data.get(idx)?;
     if input.has_delegate() {
-        packed_accounts
+        match packed_accounts
             .get_u8(input.delegate, "input delegate")
-            .ok()
+        {
+            Ok(delegate) => Some(delegate),
+            Err(e) => {
+                msg!("Failed to resolve input delegate at index {}: {:?}", input.delegate, e);
+                None
+            }
+        }
     } else {
         None
     }
 });

88-94: Clarify that mint comparison uses packed account indices.

The matching logic input.mint == compression.mint compares packed account indices, not actual mint pubkeys. Adding a brief comment would help future readers understand this:

// Match by mint index (both are packed account indices, not pubkeys)
programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (3)

50-54: Redundant uninitialized account check.

This check duplicates validation already performed by CToken::zero_copy_at_mut_checked at line 48. The defensive check adds code without preventing any new failure mode.


67-70: Magic number 2 for frozen state should be a constant.

The frozen state check ctoken.meta.state == 2 uses a magic number. Define a constant for clarity:

const ACCOUNT_STATE_FROZEN: u8 = 2;

Then use ctoken.meta.state == ACCOUNT_STATE_FROZEN here and elsewhere.


161-181: Delegate validation gap remains when CToken has delegate but input has delegated_amount without delegate pubkey.

When CToken has a delegate and the input has delegated_amount > 0 but input_delegate is None, the code at line 172 falls through without validation. The check at line 177-180 only catches this when CToken has no delegate.

🔎 Proposed fix
     // Validate delegate compatibility
     if let Some(ctoken_delegate) = ctoken.delegate() {
         // CToken has a delegate - check if it matches the input delegate
         if let Some(input_del) = input_delegate_pubkey.as_ref() {
             if ctoken_delegate.to_bytes() != input_del.to_bytes() {
                 msg!(
                     "Decompress delegate mismatch: CToken delegate {:?} != input delegate {:?}",
                     ctoken_delegate.to_bytes(),
                     input_del.to_bytes()
                 );
                 return Err(ErrorCode::DecompressDelegateMismatch.into());
             }
+        } else if delegated_amount > 0 {
+            // CToken has delegate but input has delegated_amount without delegate info
+            msg!("Decompress: CToken has delegate but input has delegated_amount without delegate pubkey");
+            return Err(CTokenError::InvalidAccountData.into());
         }
         // Delegates match - add to delegated_amount
program-libs/ctoken-interface/tests/ctoken/spl_compat.rs (1)

87-92: Use constant instead of magic number for consistency.

Line 91 uses the literal 2 while line 406 uses ACCOUNT_TYPE_TOKEN_ACCOUNT. For maintainability, use the constant consistently throughout the file.

🔎 Suggested fix
     // Set account_type byte at position 165 to ACCOUNT_TYPE_TOKEN_ACCOUNT (2)
-    account_data[165] = 2;
+    account_data[165] = ACCOUNT_TYPE_TOKEN_ACCOUNT;
program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (1)

122-177: Discriminant values still hardcoded as magic numbers.

The discriminant literals (19, 27, 28, 29, 30, 31) are used in both zero_copy_at_mut and new_zero_copy match arms. This duplicates values across multiple locations, making future changes error-prone.

Consider extracting to named constants:

const TOKEN_METADATA_DISCRIMINANT: u8 = 19;
const PAUSABLE_ACCOUNT_DISCRIMINANT: u8 = 27;
const PERMANENT_DELEGATE_ACCOUNT_DISCRIMINANT: u8 = 28;
const TRANSFER_FEE_ACCOUNT_DISCRIMINANT: u8 = 29;
const TRANSFER_HOOK_ACCOUNT_DISCRIMINANT: u8 = 30;
const COMPRESSED_ONLY_DISCRIMINANT: u8 = 31;
programs/compressed-token/anchor/src/instructions/create_token_pool.rs (1)

62-85: Consider validating token_program is a known SPL program.

The CPI to initialize_account3 uses token_program.key directly. While Anchor's Interface<'info, TokenInterface> constraint should enforce this in the context struct, this helper function could theoretically be called with an arbitrary account.

🔎 Optional: Add program ID validation
 pub fn initialize_token_account<'info>(
     token_account: &AccountInfo<'info>,
     mint: &AccountInfo<'info>,
     authority: &AccountInfo<'info>,
     token_program: &AccountInfo<'info>,
 ) -> Result<()> {
+    // Validate token program is SPL Token or Token-2022
+    if *token_program.key != spl_token::id() && *token_program.key != spl_token_2022::id() {
+        return Err(crate::ErrorCode::InvalidTokenProgram.into());
+    }
     let ix = spl_token_2022::instruction::initialize_account3(
program-libs/ctoken-interface/src/state/mint/zero_copy.rs (1)

359-370: Validation order inconsistency between checked methods.

zero_copy_at_checked (lines 330-337) checks:

  1. is_cmint_account() first
  2. is_initialized() second

zero_copy_at_mut_checked (lines 362-367) checks:

  1. is_initialized() first
  2. is_cmint_account() second

While both checks must pass, consistent ordering improves maintainability.

🔎 Suggested fix
     let (mint, remaining) = CompressedMint::zero_copy_at_mut(bytes)
         .map_err(|_| CTokenError::CMintDeserializationFailed)?;

-    if !mint.is_initialized() {
-        return Err(CTokenError::CMintNotInitialized);
-    }
     if !mint.is_cmint_account() {
         return Err(CTokenError::InvalidAccountType);
     }
+    if !mint.is_initialized() {
+        return Err(CTokenError::CMintNotInitialized);
+    }

     Ok((mint, remaining))
programs/compressed-token/program/src/extensions/check_mint_extensions.rs (3)

134-136: unwrap_or_default() silently swallows extension type enumeration errors.

If get_extension_types() fails, this returns an empty vector and has_restricted_extensions becomes false—potentially allowing restricted mints to bypass validation.

🔎 Suggested improvement
-    let extension_types = mint_state.get_extension_types().unwrap_or_default();
+    let extension_types = mint_state.get_extension_types().map_err(|e| {
+        msg!("Failed to get extension types: {:?}", e);
+        ProgramError::InvalidAccountData
+    })?;

218-219: Same unwrap_or_default() issue in has_mint_extensions.

This duplicates the error-swallowing pattern. Consider propagating the error to prevent silently treating parse failures as "no extensions."


73-93: Heap allocation in calculate_account_size could be avoided.

Vec::new() allocates on every call. Since you're only counting extensions, consider accumulating size directly without building a vector.

🔎 Suggested refactor using stack allocation
     pub fn calculate_account_size(&self) -> u64 {
-        let mut extensions = Vec::new();
-        if self.has_pausable {
-            extensions.push(ExtensionStructConfig::PausableAccount(()));
-        }
-        // ... more pushes ...
-        let exts = if extensions.is_empty() {
-            None
-        } else {
-            Some(extensions.as_slice())
-        };
-        light_ctoken_interface::state::calculate_ctoken_account_size(exts) as u64
+        // Use fixed-size stack array
+        let mut extensions: [ExtensionStructConfig; 4] = [
+            ExtensionStructConfig::PausableAccount(()),
+            ExtensionStructConfig::PermanentDelegateAccount(()),
+            ExtensionStructConfig::TransferFeeAccount(()),
+            ExtensionStructConfig::TransferHookAccount(()),
+        ];
+        let mut count = 0usize;
+        if self.has_pausable { count += 1; }
+        if self.has_permanent_delegate { count += 1; }
+        if self.has_transfer_fee { count += 1; }
+        if self.has_transfer_hook { count += 1; }
+        // Note: This approach needs refinement to only include active extensions
+        // Consider direct size calculation instead
+        light_ctoken_interface::state::calculate_ctoken_account_size(
+            if count == 0 { None } else { Some(&extensions[..count]) }
+        ) as u64
     }

Comment on lines +116 to 120
// Check if account is a valid CToken account (account_type == 2)
if !ctoken.is_ctoken_account() {
debug!("Skipping account {} without compressible config", pubkey);
return Ok(false);
}
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

Log message may be misleading — the check is for account_type, not "compressible config".

The is_ctoken_account() method checks whether account_type == 2 (i.e., a CToken account type). The debug message "without compressible config" could confuse future maintainers since the actual validation is about account type, not configuration presence.

Consider updating the message to better reflect what's being checked:

🔎 Suggested fix
     // Check if account is a valid CToken account (account_type == 2)
     if !ctoken.is_ctoken_account() {
-        debug!("Skipping account {} without compressible config", pubkey);
+        debug!("Skipping account {} - not a CToken account (account_type != 2)", pubkey);
         return Ok(false);
     }
🤖 Prompt for AI Agents
In forester/src/compressible/bootstrap.rs around lines 116 to 120, the debug
message incorrectly says "without compressible config" even though
is_ctoken_account() is checking account_type == 2; change the log to accurately
reflect the account-type check (e.g., "Skipping account {}: not a CToken
account" or "Skipping account {}: account_type != CToken (2)") so the message
matches the actual condition being evaluated.

Comment on lines +71 to 74
/// decimals for spl token Compression/Decompression (used in transfer_checked)
/// rent_sponsor_is_signer flag for CompressAndClose (non-zero = true)
pub decimals: u8,
}
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

Document the dual-purpose semantics more prominently.

The decimals field serves two different purposes depending on CompressionMode:

  • For Compress/Decompress: actual SPL token decimals
  • For CompressAndClose: boolean flag (rent_sponsor_is_signer)

This overloading is documented in comments but could cause confusion. Consider either:

  1. Adding a helper method like rent_sponsor_is_signer(&self) -> bool for the CompressAndClose case
  2. Using a union-style comment pattern at the struct level
🤖 Prompt for AI Agents
In program-libs/ctoken-interface/src/instructions/transfer2/compression.rs
around lines 71 to 74, the struct's `decimals: u8` field is dual-purpose (actual
SPL decimals for Compress/Decompress and a boolean `rent_sponsor_is_signer` flag
for CompressAndClose); add a clear, small fix: implement a helper method on the
struct like `fn rent_sponsor_is_signer(&self) -> bool` that returns
`self.decimals != 0` for the CompressAndClose case and update the doc comment to
call out the helper, or alternatively expand the struct-level comment to use a
union-style description that explicitly documents the two semantic
interpretations and references the helper to avoid confusion.

Comment on lines +63 to +80
#[profile]
#[inline(always)]
pub fn build_mint_extension_cache<'a>(
inputs: &ZCompressedTokenInstructionDataTransfer2,
packed_accounts: &'a ProgramPackedAccounts<'a, AccountInfo>,
deny_restricted_extensions: bool, // true if has_output_compressed_accounts
) -> Result<MintExtensionCache, ProgramError> {
let mut cache: MintExtensionCache = ArrayMap::new();

// Collect mints from input token data
for input in inputs.in_token_data.iter() {
let mint_index = input.mint;
if cache.get_by_key(&mint_index).is_none() {
let mint_account = packed_accounts.get_u8(mint_index, "mint cache: input")?;
let checks = check_mint_extensions(mint_account, deny_restricted_extensions)?;
cache.insert(mint_index, checks, ErrorCode::MintCacheCapacityExceeded)?;
}
}
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 early-exit optimization for empty inputs.

If in_token_data and compressions are both empty, the function still allocates an empty cache. This is minor, but an early return could save a few cycles.

🔎 Optional early exit:
 pub fn build_mint_extension_cache<'a>(
     inputs: &ZCompressedTokenInstructionDataTransfer2,
     packed_accounts: &'a ProgramPackedAccounts<'a, AccountInfo>,
     deny_restricted_extensions: bool,
 ) -> Result<MintExtensionCache, ProgramError> {
+    // Fast path: no mints to check
+    if inputs.in_token_data.is_empty() && inputs.compressions.as_ref().map_or(true, |c| c.is_empty()) {
+        return Ok(ArrayMap::new());
+    }
+
     let mut cache: MintExtensionCache = ArrayMap::new();
🤖 Prompt for AI Agents
In programs/compressed-token/program/src/transfer2/check_extensions.rs around
lines 63 to 80, add an early-exit: before allocating the MintExtensionCache,
detect if inputs.in_token_data.is_empty() AND inputs.compressions.is_empty() and
immediately return Ok(empty cache) to avoid unnecessary allocation and work;
implement this by checking those two collections and returning the appropriate
empty MintExtensionCache wrapped in Ok instead of continuing to build and
iterate.

Comment on lines +82 to +96
// Collect mints from compressions
if let Some(compressions) = inputs.compressions.as_ref() {
for compression in compressions.iter() {
let mint_index = compression.mint;

if cache.get_by_key(&mint_index).is_none() {
let mint_account = packed_accounts.get_u8(mint_index, "mint cache: compression")?;
let checks = if compression.mode == ZCompressionMode::CompressAndClose {
check_mint_extensions(
mint_account,
false, // Allow restricted extensions, also if instruction has has_output_compressed_accounts
)?
} else {
check_mint_extensions(mint_account, deny_restricted_extensions)?
};
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

Compression mode branching affects deny_restricted_extensions flag.

For CompressAndClose mode, you pass false to allow restricted extensions (with subsequent CompressedOnly requirement), while other modes use the caller's flag. This asymmetry is intentional but worth a quick doc comment to make the rationale obvious to future maintainers.

🔎 Suggested clarification:
             if cache.get_by_key(&mint_index).is_none() {
                 let mint_account = packed_accounts.get_u8(mint_index, "mint cache: compression")?;
                 let checks = if compression.mode == ZCompressionMode::CompressAndClose {
+                    // CompressAndClose allows restricted extensions but requires CompressedOnly output
                     check_mint_extensions(
                         mint_account,
                         false, // Allow restricted extensions, also if instruction has has_output_compressed_accounts
                     )?
🤖 Prompt for AI Agents
programs/compressed-token/program/src/transfer2/check_extensions.rs around lines
82-96: the branch passes false for CompressAndClose to allow restricted
extensions (while other modes use deny_restricted_extensions), which is
intentional but not obvious; add a concise doc comment above this block
explaining that CompressAndClose purposely allows restricted extensions here
because those will be later enforced as CompressedOnly (or by downstream
checks), while other modes should respect the caller's
deny_restricted_extensions flag, and mention any invariant or post-check that
enforces the CompressedOnly requirement so future maintainers understand the
rationale.

Comment on lines 43 to 45
let mint_config_with_ext = CompressedMintConfig {
base: (),
metadata: (),
extensions: (true, extensions_config.clone()),
extensions: Some(extensions_config.clone()),
};
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

Unnecessary .clone() on extensions_config.

The extensions_config vector is moved into mint_config_with_ext and isn't used afterward in this test. The clone is redundant.

🔎 Suggested fix
     let mint_config_with_ext = CompressedMintConfig {
-        extensions: Some(extensions_config.clone()),
+        extensions: Some(extensions_config),
     };
📝 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_config_with_ext = CompressedMintConfig {
base: (),
metadata: (),
extensions: (true, extensions_config.clone()),
extensions: Some(extensions_config.clone()),
};
let mint_config_with_ext = CompressedMintConfig {
extensions: Some(extensions_config),
};
🤖 Prompt for AI Agents
In programs/compressed-token/program/tests/allocation_test.rs around lines 43 to
45, the extensions_config vector is unnecessarily cloned when constructing
CompressedMintConfig; remove the .clone() and pass extensions_config directly
into extensions: Some(extensions_config) so the value is moved (since it isn't
used later in the test).

Comment on lines 49 to 58
// Get compression info from meta
let compression = &ctoken_account.meta.compression;
let authority_index = packed_accounts.insert_or_get_config(
Pubkey::from(compression.compression_authority),
true,
true,
);
let rent_sponsor_index = packed_accounts.insert_or_get(Pubkey::from(compression.rent_sponsor));
// When compression authority closes, everything goes to rent sponsor
let destination_index = rent_sponsor_index;
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

Critical: Direct field access to meta.compression risks panic if None.

Lines 50-58 access ctoken_account.meta.compression directly without checking if compression metadata exists. The relevant snippet from account2.rs shows compression() returns Option<&Compression>, indicating this field can be absent. If compression is None, this code will panic at runtime.

Additionally, setting destination_index = rent_sponsor_index (line 58) routes all funds to the rent sponsor. Based on learnings about lamport distribution on close, unutilized lamports should go to the user's destination account, not the rent_sponsor. This conflates user funds with rent recovery.

Finally, marking the compression authority as writable (line 54, is_writable: true) appears unnecessary—the authority typically only signs but doesn't require mutable access.

🔎 Proposed fixes
-    // Get compression info from meta
-    let compression = &ctoken_account.meta.compression;
+    // Get compression info from meta - validate it exists
+    let compression = ctoken_account.meta.compression.as_ref()
+        .ok_or(CTokenSdkError::MissingCompressionMetadata)?;
+    
     let authority_index = packed_accounts.insert_or_get_config(
         Pubkey::from(compression.compression_authority),
         true,
-        true,
+        false,  // Authority only needs to sign, not be writable
     );
     let rent_sponsor_index = packed_accounts.insert_or_get(Pubkey::from(compression.rent_sponsor));
-    // When compression authority closes, everything goes to rent sponsor
-    let destination_index = rent_sponsor_index;
+    // User funds should go to owner, not rent_sponsor
+    let destination_index = owner_index;  // or derive from closure context

Note: You'll need to add MissingCompressionMetadata to the CTokenSdkError enum.

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

Comment on lines 273 to 279
// Get compression info from meta
let compression = &compressed_token.meta.compression;
let authority = Pubkey::from(compression.compression_authority);
let rent_sponsor = Pubkey::from(compression.rent_sponsor);

let destination_pubkey = if with_compression_authority {
actual_rent_sponsor
} else {
owner_pubkey
};
// When compression authority closes, everything goes to rent sponsor
let destination_pubkey = rent_sponsor;
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

Critical: Same direct access and fund routing issues as lines 49-58.

This code segment has identical problems to the earlier segment:

  1. Direct field access (line 274): Accesses compressed_token.meta.compression without validating it exists, risking a panic.

  2. Incorrect fund routing (line 279): Setting destination_pubkey = rent_sponsor routes all closure proceeds to the rent sponsor instead of separating user funds from rent recovery. Per the learnings on account closure, "unutilized lamports → destination account" (for the user), while "completed epoch rent → rent recipient."

🔎 Proposed fix
-        // Get compression info from meta
-        let compression = &compressed_token.meta.compression;
+        // Get compression info from meta - validate it exists
+        let compression = compressed_token.meta.compression.as_ref()
+            .ok_or(CTokenSdkError::MissingCompressionMetadata)?;
+            
         let authority = Pubkey::from(compression.compression_authority);
         let rent_sponsor = Pubkey::from(compression.rent_sponsor);
 
-        // When compression authority closes, everything goes to rent sponsor
-        let destination_pubkey = rent_sponsor;
+        // User funds should be returned to the owner, not conflated with rent
+        let destination_pubkey = owner_pubkey;

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

🤖 Prompt for AI Agents
In sdk-libs/ctoken-sdk/src/compressed_token/v2/compress_and_close.rs around
lines 273-279, the code unsafely accesses compressed_token.meta.compression and
routes all closure proceeds to the rent_sponsor; instead, first validate that
compressed_token.meta and its compression field exist (use a match or as_ref()
and return a clear error if missing) and extract compression_authority and
rent_sponsor from that validated compression; then set the closure destination
for unutilized lamports to the token owner (e.g., compressed_token.owner) and
reserve rent recovery/distribution to rent_sponsor (do not set
destination_pubkey = rent_sponsor), ensuring you assign destination_pubkey to
the token owner and use a separate rent_recipient variable for rent_sponsor.

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

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d293dc3 and ebd9e9e.

📒 Files selected for processing (1)
  • program-libs/ctoken-interface/tests/ctoken/failing.rs
🧰 Additional context used
📓 Path-based instructions (1)
program-libs/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests in program-libs must not depend on light-test-utils; tests requiring light-test-utils must be located in program-tests/

Files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
🧠 Learnings (37)
📓 Common learnings
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Use compressible token account extensions that allow accounts to be compressed back into compressed state with rent payment mechanisms
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: All compressed token account implementations must reference and comply with the account layout specifications in programs/compressed-token/program/docs/ACCOUNTS.md
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Implement all 8 compressed token instructions: create_cmint, mint_to_ctoken, create_token_account_invoke, create_token_account_invoke_signed, create_ata_invoke, create_ata_invoke_signed, transfer_interface_invoke, and transfer_interface_invoke_signed
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/README.md:0-0
Timestamp: 2025-11-24T17:54:38.537Z
Learning: Implement compressed token program interfaces for third-party token creation and usage on Solana using ZK Compression
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/transfer2/*.rs : Transfer2 instruction (discriminator: 101) supports batch operations including Compress, Decompress, and CompressAndClose with multi-mint support and sum checks
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/docs/instructions/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:54.689Z
Learning: Applies to programs/compressed-token/program/docs/instructions/instructions/MINT_ACTION.md : MintAction documentation must cover batch instruction for compressed mint management supporting 9 actions: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/anchor/README.md:0-0
Timestamp: 2025-11-24T17:54:33.614Z
Learning: Implement the Compressed Token Program interface for creating and using compressed tokens on Solana with ZK Compression
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-tests/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:30.012Z
Learning: Run compressed token core tests using `cargo test-sbf -p compressed-token-test --test ctoken`, `--test v1`, `--test mint`, and `--test transfer2`
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/mint_action/*.rs : MintAction instruction (discriminator: 103) must support 9 action types: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: The codebase provides rent calculation algorithms and account structure definitions used across light-compressed-token, light-registry, compressed-token-sdk, and related programs
📚 Learning: 2025-12-06T00:49:57.458Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-token-test/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:57.458Z
Learning: Applies to sdk-tests/sdk-token-test/**/*test.rs : Tests should use light-ctoken-sdk functions from sdk-libs/compressed-token-sdk for testing ctoken instructions

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Clone AccountInfo structs when building CPI builder pattern account structs to avoid borrow checker issues

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/{lib,main}.rs : Use the builder pattern from `light-ctoken-sdk::ctoken` module for CPI operations instead of manual instruction building

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T18:01:30.012Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-tests/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:30.012Z
Learning: Run compressed token core tests using `cargo test-sbf -p compressed-token-test --test ctoken`, `--test v1`, `--test mint`, and `--test transfer2`

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account size must be exactly 256 bytes including the 8-byte discriminator to maintain compatibility with Light Protocol Solana programs

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T18:01:30.012Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-tests/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:01:30.012Z
Learning: Run Light system program compression tests using `cargo test-sbf -p system-test -- test_with_compression` and `cargo test-sbf -p system-test --test test_re_init_cpi_account` to test compressed account operations

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:28.803Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: sdk-tests/sdk-ctoken-test/README.md:0-0
Timestamp: 2025-12-07T03:17:28.803Z
Learning: Applies to sdk-tests/sdk-ctoken-test/**/Cargo.toml : Use path references in Cargo.toml dependencies pointing to `/Users/ananas/dev/light-protocol2/sdk-libs/` for light-ctoken-sdk, light-ctoken-types, light-sdk, light-sdk-types, and light-program-test

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T18:02:15.670Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/registry/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:02:15.670Z
Learning: Applies to programs/registry/src/account_compression_cpi/mod.rs : Export new wrapper modules in `account_compression_cpi/mod.rs` using `pub mod new_operation;` and `pub use new_operation::*;`, then import in `lib.rs`

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : CompressibleConfig account structure must support serialization via Anchor, Pinocchio, and Borsh formats for Light Registry program integration

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:00.229Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/CLAUDE.md:0-0
Timestamp: 2025-11-24T17:56:00.229Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/Cargo.toml : Depend on light-compressed-account crate for compressed account types and utilities

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement default initialization for CToken V1 config in CompressibleConfig structure

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: All compressed token account implementations must reference and comply with the account layout specifications in programs/compressed-token/program/docs/ACCOUNTS.md

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:50.011Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_STATE_TREE.md:0-0
Timestamp: 2025-11-24T17:56:50.011Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_state_tree.rs : Return error `AccountError::InvalidAccountSize` (error code 12006) when account data length doesn't match calculated size requirements

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/create_token_account.rs : Create token account instructions (CreateTokenAccount, CreateAssociatedCTokenAccount, CreateAssociatedTokenAccountIdempotent) require ACTIVE config validation only

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : CompressibleConfig account state in Solana programs must validate active state using `validate_active()` method before allowing new compressible token account creation

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-06T00:49:21.983Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/CLAUDE.md:0-0
Timestamp: 2025-12-06T00:49:21.983Z
Learning: Applies to program-libs/compressible/src/config.rs : Implement state validation methods (`validate_active`, `validate_not_inactive`) on CompressibleConfig account structure

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:09:57.240Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/compressible/docs/CONFIG_ACCOUNT.md:0-0
Timestamp: 2025-12-07T18:09:57.240Z
Learning: Applies to program-libs/compressible/docs/**/*.rs : Anchor deserialization of CompressibleConfig must use `AccountDeserialize::try_deserialize()` which automatically validates the discriminator to prevent invalid account type mismatches

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_data_is_zeroed` function to verify first N bytes are zero (uninitialized), returning `AccountNotZeroed` (20013) error if data is initialized

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:50.011Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_STATE_TREE.md:0-0
Timestamp: 2025-11-24T17:56:50.011Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_state_tree.rs : Return error `AccountError::AccountNotRentExempt` (error code 12011) when account balance is insufficient for rent exemption at calculated size

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:20.711Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_ADDRESS_TREE.md:0-0
Timestamp: 2025-11-24T17:56:20.711Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_address_tree.rs : Verify merkle_tree_account data length matches calculated size in `src/initialize_address_tree.rs`. Return error `AccountError::InvalidAccountSize` (error code: 12006) if account data length doesn't match calculated size requirements.

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `set_discriminator` function to set 8-byte discriminator on uninitialized account, returning `AlreadyInitialized` (20006) if discriminator is non-zero

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:50.011Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_STATE_TREE.md:0-0
Timestamp: 2025-11-24T17:56:50.011Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_state_tree.rs : Use `check_account_balance_is_rent_exempt` function from `light-account-checks` to verify rent exemption for both queue_account and merkle_tree_account during initialization

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/packed_accounts.rs : Implement index-based account access in PackedAccounts with bounds-checked retrieval

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:57:53.312Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/TREE_ACCOUNT.md:0-0
Timestamp: 2025-11-24T17:57:53.312Z
Learning: Applies to program-libs/batched-merkle-tree/docs/**/*.rs : State trees must initialize with zero bytes root for the specified height and have a separate BatchedQueueAccount for output operations

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:59:03.485Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_INFO_TRAIT.md:0-0
Timestamp: 2025-11-24T17:59:03.485Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/account_info/test_account_info.rs : Create a test-only AccountInfo mock implementation without external dependencies for unit testing AccountInfoTrait implementations

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/checks.rs : Implement permission check functions (`check_mut`, `check_non_mut`, `check_signer`) for account state validation

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T03:17:42.217Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/CLAUDE.md:0-0
Timestamp: 2025-12-07T03:17:42.217Z
Learning: Applies to program-libs/account-checks/**/account_info/test_account_info.rs : Provide mock TestAccountInfo implementation (feature: `test-only`) for unit testing account validation logic

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_mut` function to verify account is writable, returning `AccountNotMutable` (20002) error if read-only

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_discriminator` function to verify first 8 bytes match expected discriminator for type T, returning `InvalidAccountSize` (20004) if < 8 bytes or `InvalidDiscriminator` (20000) if mismatch

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_balance_is_rent_exempt` function to verify account size matches expected_size and account balance meets rent exemption requirement, returning rent exemption amount or errors `InvalidAccountSize` (20004), `InvalidAccountBalance` (20007), or `FailedBorrowRentSysvar` (20008)

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:56:20.711Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/batched-merkle-tree/docs/INITIALIZE_ADDRESS_TREE.md:0-0
Timestamp: 2025-11-24T17:56:20.711Z
Learning: Applies to program-libs/batched-merkle-tree/docs/src/initialize_address_tree.rs : Verify merkle_tree_account balance is rent-exempt in `src/initialize_address_tree.rs`. Use `check_account_balance_is_rent_exempt` from `light-account-checks` library. Return error `AccountError::AccountNotRentExempt` (error code: 12011) if account balance is insufficient for rent exemption at calculated size.

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:54:20.982Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/zero-copy-derive/README.md:0-0
Timestamp: 2025-11-24T17:54:20.982Z
Learning: Applies to program-libs/zero-copy-derive/**/*.rs : Custom types in zero-copy structs must implement the ZeroCopyAt trait

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-12-07T18:10:14.606Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: programs/compressed-token/program/CLAUDE.md:0-0
Timestamp: 2025-12-07T18:10:14.606Z
Learning: Applies to programs/compressed-token/program/src/ctoken_transfer.rs : CTokenTransfer instruction (discriminator: 3) must implement SPL-compatible transfers between decompressed accounts

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info_non_mut` function to validate account is NOT writable, owned by program_id, and discriminator matches type T

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info_mut` function to validate account is writable, owned by program_id, and discriminator matches type T

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
📚 Learning: 2025-11-24T17:58:50.237Z
Learnt from: CR
Repo: Lightprotocol/light-protocol PR: 0
File: program-libs/account-checks/docs/ACCOUNT_CHECKS.md:0-0
Timestamp: 2025-11-24T17:58:50.237Z
Learning: Applies to program-libs/account-checks/docs/program-libs/account-checks/src/checks.rs : Implement `check_account_info` function to validate account ownership by program_id and verify discriminator matches type T

Applied to files:

  • program-libs/ctoken-interface/tests/ctoken/failing.rs
🧬 Code graph analysis (1)
program-libs/ctoken-interface/tests/ctoken/failing.rs (3)
program-libs/ctoken-interface/tests/ctoken/zero_copy_new.rs (1)
  • default_config (35-43)
program-libs/ctoken-interface/tests/ctoken/spl_compat.rs (1)
  • default_config (27-35)
program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs (1)
  • zero_copy_at_checked (409-422)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Test program-libs-slow
  • GitHub Check: Test program-libs-fast
  • GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-ctoke...
  • GitHub Check: system-programs (anchor & pinocchio, ["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk...
  • GitHub Check: system-programs (native, ["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-t...
  • GitHub Check: system-programs (token test, ["cargo-test-sbf -p sdk-token-test"])
  • GitHub Check: programs (compressed-token-and-e2e, ["cargo test -p light-compressed-token", "cargo-test-sbf -p c...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
  • GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...
  • GitHub Check: programs (light-system-program-address, ["cargo-test-sbf -p system-test -- test_with_address", "c...
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: cli-v1
  • GitHub Check: stateless-js-v1
  • GitHub Check: lint
  • GitHub Check: cli-v2
  • GitHub Check: stateless-js-v2
🔇 Additional comments (6)
program-libs/ctoken-interface/tests/ctoken/failing.rs (6)

1-6: LGTM! Clean import additions.

The new imports (Pubkey, BASE_TOKEN_ACCOUNT_SIZE) support the test refactoring and comply with the guideline that program-libs tests must not depend on light-test-utils.


19-28: LGTM! Clear test setup.

Using default_config() and BASE_TOKEN_ACCOUNT_SIZE makes this test more maintainable and the intent clearer.


31-40: LGTM! Correct validation test.

The test correctly validates that zero_copy_at_checked rejects uninitialized accounts (state byte = 0). Using BASE_TOKEN_ACCOUNT_SIZE improves maintainability.


43-52: LGTM! Test validates mutable variant correctly.

This test appropriately mirrors test_zero_copy_at_checked_uninitialized_account for the mutable API.

Note: The AI summary indicates this test was removed, but it's present in the final code. This is just an inconsistency in the summary—the test itself is correct.


55-64: LGTM! Correct error expectation.

The change to expect ZeroCopyError is correct—when the buffer is too small, zero_copy_at fails before the checked validation can execute. The comment clearly explains this ordering.


67-76: LGTM! Mutable variant test correct.

Appropriately mirrors the immutable test with the correct error expectation for undersized buffers.

Comment on lines +8 to +16
fn default_config() -> CompressedTokenConfig {
CompressedTokenConfig {
mint: Pubkey::default(),
owner: Pubkey::default(),
state: 1,
compression_only: false,
extensions: None,
}
}
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 duplicated helper to a shared test module.

This default_config() helper is duplicated in at least three test files (zero_copy_new.rs, spl_compat.rs, and this file). Extract it to a common test utilities module (e.g., tests/common/mod.rs) to reduce duplication and simplify maintenance.

Example structure

Create tests/common/mod.rs:

use light_compressed_account::Pubkey;
use light_ctoken_interface::state::CompressedTokenConfig;

pub fn default_config() -> CompressedTokenConfig {
    CompressedTokenConfig {
        mint: Pubkey::default(),
        owner: Pubkey::default(),
        state: 1,
        compression_only: false,
        extensions: None,
    }
}

Then import in test files:

mod common;
use common::default_config;
🤖 Prompt for AI Agents
In program-libs/ctoken-interface/tests/ctoken/failing.rs around lines 8 to 16,
the default_config() helper is duplicated across multiple test files; extract it
into a shared test utilities module (e.g., tests/common/mod.rs) and update the
tests to import and use that shared helper. Create tests/common/mod.rs that
exposes pub fn default_config() -> CompressedTokenConfig with the same body and
required imports (Pubkey and CompressedTokenConfig), then in failing.rs (and the
other test files) add mod common; and use common::default_config; (or use
crate::tests::common if using integration-style tests) and remove the local
duplicate function. Ensure module visibility and paths are correct so tests
compile.

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: 7

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/shared/compressible_top_up.rs (1)

100-103: Early exit condition may be unreachable.

Since the CMint block (lines 51-74) always sets current_slot at line 57-59, the condition current_slot == 0 at line 101 will never be true when execution reaches this point. This early exit appears to be dead code.

If this check was intended for a scenario where neither account has compression info, that's no longer possible with embedded compression in all accounts. Consider removing this block or updating the comment to clarify its purpose.

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

80-113: Update docstring to accurately reflect authorization checks.

The implementation only validates owner and permanent delegate authority, not account delegate. While this is the correct design for mint-level compression operations, the docstring (line 81) and error message comment (line 111) are misleading—they mention account delegate support that isn't implemented. Update line 81 to read: "Allows owner or permanent delegate (from mint) to authorize compression operations."

♻️ Duplicate comments (28)
programs/compressed-token/program/src/ctoken_approve_revoke.rs (3)

107-111: Clarify the purpose of saturating_add(1) in budget calculation.

The +1 added to max_top_up at line 110 is unclear. Does this account for some off-by-one behavior in process_compression_top_up, or is it unnecessary? If the budget is meant to be exactly max_top_up lamports, document why the extra lamport is needed. If it's not needed, remove it.

🔎 Suggested clarification

Either remove the +1:

     let mut lamports_budget = if max_top_up == 0 {
         u64::MAX
     } else {
-        (max_top_up as u64).saturating_add(1)
+        max_top_up as u64
     };

Or add a comment explaining its purpose:

     let mut lamports_budget = if max_top_up == 0 {
         u64::MAX
     } else {
+        // +1 allows exact max_top_up after saturating_sub in process_compression_top_up
         (max_top_up as u64).saturating_add(1)
     };

113-119: Document or eliminate the unused current_slot output.

The &mut 0 passed as current_slot to process_compression_top_up is never read afterward. If this is required by the function signature but the value isn't needed here, make the intent explicit by renaming to _current_slot or adding a brief comment.

🔎 Suggested cleanup
+    // current_slot output is unused here but required by function signature
     process_compression_top_up(
         &ctoken.base.compression,
         account,
         &mut 0,
         &mut transfer_amount,
         &mut lamports_budget,
     )?;

Or use a named binding:

+    let mut _current_slot = 0;
     process_compression_top_up(
         &ctoken.base.compression,
         account,
-        &mut 0,
+        &mut _current_slot,
         &mut transfer_amount,
         &mut lamports_budget,
     )?;

124-133: Fix the budget enforcement logic.

The condition at line 125 has a subtle bug: lamports_budget != 0 && transfer_amount > lamports_budget

When max_top_up == 0 (unlimited), lamports_budget is set to u64::MAX. The check lamports_budget != 0 is true, so we compare transfer_amount > u64::MAX, which can never be true. This works but is convoluted.

More critically, the condition lamports_budget != 0 means if the budget were exhausted (becomes 0), we would skip the check instead of failing. This seems backwards.

The cleaner approach is to directly check against max_top_up without the budget indirection:

🔎 Proposed fix
     // Drop borrow before CPI
     drop(account_data);

     if transfer_amount > 0 {
-        if lamports_budget != 0 && transfer_amount > lamports_budget {
+        if max_top_up != 0 && transfer_amount > max_top_up as u64 {
             return Err(CTokenError::MaxTopUpExceeded.into());
         }
         transfer_lamports_via_cpi(transfer_amount, payer, account)
             .map_err(convert_program_error)?;
     }

This directly enforces the limit: when max_top_up is non-zero, fail if transfer exceeds it. When max_top_up is 0 (unlimited), the check is skipped cleanly.

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

217-219: Optimize by caching the Rent sysvar to avoid redundant syscalls.

Rent::get() is called every time process_account_extensions executes. Since this function is invoked twice per transfer (sender + recipient), you're making two syscalls for the same immutable sysvar data. Cache the rent value similar to how current_slot is cached with a sentinel pattern.

🔎 Suggested optimization

Thread a cached Option<Rent> through the call chain:

 fn process_account_extensions(
     account: &AccountInfo,
     current_slot: &mut u64,
+    cached_rent: &mut Option<Rent>,
     mint: Option<&AccountInfo>,
 ) -> Result<AccountExtensionInfo, ProgramError> {
     // ... existing code ...
     
-    let rent_exemption = Rent::get()
-        .map_err(|_| CTokenError::SysvarAccessError)?
-        .minimum_balance(account.data_len());
+    let rent = cached_rent.get_or_insert_with(|| {
+        Rent::get().expect("rent sysvar")
+    });
+    let rent_exemption = rent.minimum_balance(account.data_len());

Update callers in validate_sender and validate_recipient to pass the cached rent reference.

Note: This aligns with the previous review feedback on caching sysvars for performance.

program-libs/ctoken-interface/tests/ctoken/size.rs (1)

6-62: Comprehensive test coverage for account size calculations.

The test methodically covers:

  • Base case (no extensions)
  • Single extensions (pausable, permanent_delegate, transfer_fee, transfer_hook)
  • Combined extensions

The inline comments explaining the arithmetic (e.g., "258 + 4 metadata + 1 discriminant = 263") effectively document the expected values. While named constants could improve maintainability, the current approach clearly shows how each size is derived.

Consider defining constants for frequently referenced sizes if these values are needed elsewhere in the codebase.

programs/registry/src/compressible/compressed_token/compress_and_close.rs (2)

100-104: Use a more appropriate error variant for CToken parsing failure.

RegistryError::InvalidSigner doesn't accurately describe a CToken parsing failure. For consistency and clarity (see line 97-98), use RegistryError::InvalidTokenAccountData.

🔎 Suggested fix
         let (ctoken, _) = CToken::zero_copy_at(&account_data).map_err(|e| {
             anchor_lang::prelude::msg!("Failed to parse CToken: {:?}", e);
-            RegistryError::InvalidSigner
+            RegistryError::InvalidTokenAccountData
         })?;

137-148: Preserve delegated_amount in CompressedOnly extension even without marker extensions.

When a CToken has a delegate with delegated_amount > 0 but none of the marker extension flags are set, the code pushes an empty TLV vector. This causes delegated_amount to be lost—the compressed output will have has_delegate: true (line 157) but no way to recover the original spending limit since it's only stored in CompressedOnly.

🔎 Proposed fix
-        // Build TLV extensions for this output if marker extensions are present
-        if has_marker_extensions {
+        // Build TLV extensions for this output if marker extensions are present
+        // OR if delegated_amount > 0 (to preserve the spending limit)
+        if has_marker_extensions || delegated_amount > 0 {
             out_tlv.push(vec![ExtensionInstructionData::CompressedOnly(
                 CompressedOnlyExtensionInstructionData {
                     delegated_amount,
                     withheld_transfer_fee,
                     is_frozen,
                 },
             )]);
         } else {
             out_tlv.push(vec![]);
         }
programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (2)

74-80: Consider extracting the frozen state constant.

The code uses magic number 2 for AccountState::Frozen. This appears multiple times in the codebase. Defining a constant would improve readability.

// At module level
const ACCOUNT_STATE_FROZEN: u8 = 2;

167-202: Missing delegate validation when CToken has existing delegate but input lacks delegate pubkey.

There's a gap in the validation logic. When:

  • CToken already has a delegate
  • Input compressed account has delegated_amount > 0
  • But input_delegate is None

The code falls through line 182 and proceeds to add delegated_amount at line 194-200. This merges delegated amounts that may belong to different delegates.

🔎 Proposed fix
         // Validate delegate compatibility
         if let Some(ctoken_delegate) = ctoken.delegate() {
             // CToken has a delegate - check if it matches the input delegate
             if let Some(input_del) = input_delegate_pubkey.as_ref() {
                 if ctoken_delegate.to_bytes() != input_del.to_bytes() {
                     msg!(
                         "Decompress delegate mismatch: CToken delegate {:?} != input delegate {:?}",
                         ctoken_delegate.to_bytes(),
                         input_del.to_bytes()
                     );
                     return Err(ErrorCode::DecompressDelegateMismatch.into());
                 }
+            } else if delegated_amount > 0 {
+                // CToken has delegate but input has delegated_amount without delegate info
+                msg!("Decompress: CToken has delegate but input has delegated_amount without delegate pubkey");
+                return Err(CTokenError::InvalidAccountData.into());
             }
             // Delegates match - add to delegated_amount
program-libs/ctoken-interface/tests/mint_borsh_zero_copy.rs (1)

169-172: Compression field round-trip validation uses default values.

As noted in a prior review, the reconstruction always uses CompressionInfo::default(). While this tests the default case thoroughly, it doesn't verify that non-default CompressionInfo values survive the Borsh ↔ zero-copy round-trip. Consider enhancing generate_random_mint() to randomize compression fields for complete coverage.

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

30-30: Documentation doesn't match validation logic.

The comment states "rent_payment must be 0 or >= 2" but the code at line 64 only rejects rent_payment == 1. Any value ≥ 2 is accepted without upper bound, as is 0. Consider updating to: "rent_payment must not be 1" or clarify the actual constraint.

programs/compressed-token/program/tests/mint.rs (1)

401-420: Manual CompressionInfo reconstruction is verbose but functional.

This reconstruction pattern works correctly, but as noted in a previous review, consider implementing a From<&ZCompressionInfo> trait or .to_owned() method on the zero-copy type to reduce boilerplate. The current approach requires updating multiple locations if the structure changes.

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

57-77: Verbose logging on authorization failure path.

This was previously flagged: emitting multiple msg! calls on every authorization failure adds compute overhead. Since this is the error path, the cost is acceptable for debugging, but consider consolidating into a single structured message for production paths if this becomes a hot path.

programs/compressed-token/anchor/src/lib.rs (2)

311-312: Error message is inaccurate—multiple TLV extension types are now supported.

The message claims "only CompressedOnly is currently implemented," but based on the PR context and codebase, multiple extension types are supported: CompressedOnly, TokenMetadata, PausableAccount, PermanentDelegateAccount, TransferFeeAccount, and TransferHookAccount. Update the message to reflect reality or use a generic phrasing like "Unsupported TLV extension type."

🔎 Suggested fix
-    #[msg("Unsupported TLV extension type - only CompressedOnly is currently implemented")]
+    #[msg("Unsupported TLV extension type")]
     UnsupportedTlvExtensionType,

526-529: Document unimplemented features with tracking references.

InLamportsUnimplemented and OutLamportsUnimplemented indicate incomplete functionality. Consider adding a brief comment or TODO with an issue tracker reference so developers understand the roadmap.

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

239-251: Consider extracting magic number for AccountState::Frozen.

The value 2 is used to represent AccountState::Frozen on lines 242 and 255. While the comment on line 240-241 explains this, extracting to a constant would improve clarity:

const ACCOUNT_STATE_FROZEN: u8 = 2;

This was previously flagged as a nitpick.


268-274: Redundant delegate checks in non-CompressedOnly branch.

Lines 269-274 check both has_delegate() (line 269) and delegate != 0 (line 272). The has_delegate() method already checks the underlying delegate_option_prefix field. If the field values are consistent, the second check is redundant.

🔎 Suggested fix—remove redundant check
         // Delegate should be None
         if compressed_token_account.has_delegate() {
             return Err(ErrorCode::CompressAndCloseDelegateNotAllowed.into());
         }
-        if compressed_token_account.delegate != 0 {
-            return Err(ErrorCode::CompressAndCloseDelegateNotAllowed.into());
-        }
programs/compressed-token/anchor/src/instructions/create_token_pool.rs (2)

63-86: Consider validating token_program is a known SPL program.

The initialize_token_account function invokes CPI using token_program.key directly. While the Anchor Interface<'info, TokenInterface> constraint should enforce this at the instruction level, this helper function could theoretically be called with an arbitrary account.

🔎 Optional: Add program ID validation
 pub fn initialize_token_account<'info>(
     token_account: &AccountInfo<'info>,
     mint: &AccountInfo<'info>,
     authority: &AccountInfo<'info>,
     token_program: &AccountInfo<'info>,
 ) -> Result<()> {
+    // Validate token program is SPL Token or Token-2022
+    if *token_program.key != spl_token::id() && *token_program.key != spl_token_2022::id() {
+        return Err(crate::ErrorCode::InvalidTokenProgram.into());
+    }
     let ix = spl_token_2022::instruction::initialize_account3(

106-142: Missing validation for confidential extension runtime states.

The documentation in this codebase (and previous reviews) indicates that certain confidential extensions have runtime constraints:

  • ConfidentialTransferMint: must be initialized but not enabled
  • ConfidentialMintBurn: must be initialized but not enabled
  • ConfidentialTransferFeeConfig: fees must be zero

Currently, assert_mint_extensions validates TransferFeeConfig (lines 120-130) and TransferHook (lines 133-139), but the three confidential extensions mentioned above are not validated. If these extensions are enabled, they alter token behavior and should be rejected during pool creation.

#!/bin/bash
# Check if confidential extension validation exists elsewhere in the codebase
rg -n "ConfidentialTransferMint|ConfidentialMintBurn|ConfidentialTransferFeeConfig" programs/compressed-token/ --type rust -B 2 -A 5 | head -100
programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs (1)

87-123: TODO indicates known inefficiency in input matching - address before release.

The matching logic iterates through all inputs to find one matching by mint index, which the TODO at lines 89-92 correctly identifies as inefficient and potentially problematic. The comment suggests passing the input index directly in instruction data.

Additionally, the delegate lookup at lines 107-118 uses .ok()? which silently swallows errors. If the delegate index is invalid but has_delegate() returns true, this will return None instead of surfacing the error.

Consider:

  1. Pass the input compressed account index explicitly in instruction data to avoid O(n) search
  2. Handle the delegate lookup error explicitly:
🔎 Suggested fix for delegate lookup
             let input_delegate = matching_input_index.and_then(|idx| {
                 let input = inputs.in_token_data.get(idx)?;
                 if input.has_delegate() {
-                    Some(
-                        packed_accounts
-                            .get_u8(input.delegate, "input delegate")
-                            .ok()?,
-                    )
+                    packed_accounts
+                        .get_u8(input.delegate, "input delegate")
+                        .ok() // Consider logging if this fails unexpectedly
                 } else {
                     None
                 }
             });
programs/compressed-token/program/src/close_token_account/processor.rs (1)

176-177: Non-Solana build has hardcoded slot of 0.

The #[cfg(not(target_os = "solana"))] block sets current_slot = 0, which will cause incorrect rent calculations in tests or off-chain simulations. Consider documenting this limitation or providing a way to inject a slot for testing.

#[cfg(not(target_os = "solana"))]
let current_slot = 0; // Note: Off-chain simulations will have incorrect rent calculations
program-libs/ctoken-interface/src/state/extensions/extension_struct.rs (1)

205-208: CompressedOnly uses LEN constant instead of byte_len method.

The CompressedOnly variant uses CompressedOnlyExtension::LEN directly instead of calling byte_len() like other variants. This was flagged in a previous review. If CompressedOnlyExtension has a fixed size (which LEN suggests), this is acceptable but should be documented.

🔎 Consider adding a comment for consistency
 ExtensionStructConfig::CompressedOnly(_) => {
-    // 1 byte for discriminant + 16 bytes for CompressedOnlyExtension (2 * u64)
+    // 1 byte for discriminant + 16 bytes for CompressedOnlyExtension (2 * u64)
+    // Uses LEN constant as CompressedOnlyExtension has a fixed size
     1 + CompressedOnlyExtension::LEN
 }
program-libs/ctoken-interface/tests/ctoken/spl_compat.rs (1)

87-92: Magic number 2 at line 91 should use ACCOUNT_TYPE_TOKEN_ACCOUNT constant.

The past review comment noted this should use the constant for consistency with line 406. While the commit history suggests this was addressed, the current code still shows a literal 2 at line 91.

🔎 Use constant instead of magic number
     // Set account_type byte at position 165 to ACCOUNT_TYPE_TOKEN_ACCOUNT (2)
-    account_data[165] = 2;
+    account_data[165] = ACCOUNT_TYPE_TOKEN_ACCOUNT;
program-libs/ctoken-interface/src/state/mint/zero_copy.rs (1)

319-340: Validation order inconsistency between checked methods persists.

The past review noted that zero_copy_at_checked checks is_cmint_account() first then is_initialized(), while zero_copy_at_mut_checked does the opposite. While functionally equivalent (both must pass), consistent ordering improves maintainability.

🔎 Suggested consistency fix
     pub fn zero_copy_at_mut_checked(
         bytes: &mut [u8],
     ) -> Result<(ZCompressedMintMut<'_>, &mut [u8]), CTokenError> {
         // ...
         let (mint, remaining) = CompressedMint::zero_copy_at_mut(bytes)
             .map_err(|_| CTokenError::CMintDeserializationFailed)?;

-        if !mint.is_initialized() {
-            return Err(CTokenError::CMintNotInitialized);
-        }
         if !mint.is_cmint_account() {
             return Err(CTokenError::InvalidAccountType);
         }
+        if !mint.is_initialized() {
+            return Err(CTokenError::CMintNotInitialized);
+        }

         Ok((mint, remaining))
     }

Also applies to: 347-370

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

96-119: TLV conversion only handles CompressedOnly extension type.

The conversion correctly filters to ZExtensionInstructionData::CompressedOnly and returns UnsupportedTlvExtensionType for other variants. Based on the past review comment, this is intentional because other variants (Pausable, PermanentDelegate, TransferHook) are marker extensions referencing mint configuration rather than account-specific state data.

Consider adding a brief comment explaining this design decision for future maintainers:

🔎 Add clarifying comment
                 // Convert instruction TLV data to state TLV
+                // Note: Only CompressedOnly carries account-specific state data;
+                // marker extensions (Pausable, PermanentDelegate, TransferHook)
+                // reference mint configuration and are not stored in token data.
                 let tlv: Option<Vec<ExtensionStruct>> = match tlv_data {
program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs (3)

128-128: Assign boolean value directly instead of casting to u8.

The compression_only field is declared as bool in the struct (line 57), but you're assigning config.compression_only as u8. The zero-copy derive macros handle the byte-level representation automatically. Assign the boolean directly:

base.compression_only = config.compression_only;

This maintains type correctness and is more idiomatic.

🔎 Proposed fix
-        base.compression_only = config.compression_only as u8;
+        base.compression_only = config.compression_only;

Based on past review feedback flagging the same pattern.


133-133: Assign true directly to boolean field instead of numeric literal.

The has_extensions field is declared as bool (line 59), so assign the boolean value directly:

*base.has_extensions = true;

The zero-copy machinery handles the u8 representation internally.

🔎 Proposed fix
-                *base.has_extensions = 1u8;
+                *base.has_extensions = true;

Based on past review feedback flagging the same pattern.


147-154: Extensions written but not reflected in returned struct.

The function writes extensions directly to storage but returns extensions: None. This design choice could confuse callers who expect the returned ZCTokenMut to reflect the actual account state. Consider either:

  1. Populating the extensions field in the returned struct (preferred for consistency), or
  2. Adding explicit documentation explaining why extensions aren't tracked in the returned wrapper.

Based on past review feedback noting this inconsistency.

Comment on lines +45 to +55
/// Check if an extension type is a restricted extension.
#[inline(always)]
pub const fn is_restricted_extension(ext: &ExtensionType) -> bool {
matches!(
ext,
ExtensionType::Pausable
| ExtensionType::PermanentDelegate
| ExtensionType::TransferFeeConfig
| ExtensionType::TransferHook
)
}
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 using RESTRICTED_EXTENSION_TYPES in is_restricted_extension().

The is_restricted_extension() function duplicates the logic of RESTRICTED_EXTENSION_TYPES using a matches! expression. While this works, changes to one require updating the other.

🔎 Alternative using the constant array
 pub const fn is_restricted_extension(ext: &ExtensionType) -> bool {
-    matches!(
-        ext,
-        ExtensionType::Pausable
-            | ExtensionType::PermanentDelegate
-            | ExtensionType::TransferFeeConfig
-            | ExtensionType::TransferHook
-    )
+    // Note: Can't use contains() in const fn, so matches! is appropriate here
+    // Just ensure this stays in sync with RESTRICTED_EXTENSION_TYPES
+    matches!(
+        ext,
+        ExtensionType::Pausable
+            | ExtensionType::PermanentDelegate
+            | ExtensionType::TransferFeeConfig
+            | ExtensionType::TransferHook
+    )
 }

The current implementation is correct for a const fn since array iteration isn't available. Consider adding a comment noting the synchronization requirement with RESTRICTED_EXTENSION_TYPES.

📝 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
/// Check if an extension type is a restricted extension.
#[inline(always)]
pub const fn is_restricted_extension(ext: &ExtensionType) -> bool {
matches!(
ext,
ExtensionType::Pausable
| ExtensionType::PermanentDelegate
| ExtensionType::TransferFeeConfig
| ExtensionType::TransferHook
)
}
/// Check if an extension type is a restricted extension.
#[inline(always)]
pub const fn is_restricted_extension(ext: &ExtensionType) -> bool {
// Note: Can't use contains() in const fn, so matches! is appropriate here
// Just ensure this stays in sync with RESTRICTED_EXTENSION_TYPES
matches!(
ext,
ExtensionType::Pausable
| ExtensionType::PermanentDelegate
| ExtensionType::TransferFeeConfig
| ExtensionType::TransferHook
)
}
🤖 Prompt for AI Agents
In program-libs/ctoken-interface/src/token_2022_extensions.rs around lines 45 to
55, the is_restricted_extension() function duplicates the logic of the
RESTRICTED_EXTENSION_TYPES constant which risks them getting out of sync; update
the file to either (preferred) replace the function with a call to the shared
constant (if you can make a non-const/const-compatible contains check) or (if
const constraints prevent that) add a clear comment above
is_restricted_extension() stating it must be kept in sync with
RESTRICTED_EXTENSION_TYPES (include the constant name and its location), and add
a unit test/assert that validates both representations match to prevent drift.

Comment on lines +98 to +131
pub fn calculate_account_size(&self) -> Result<u64, ZeroCopyError> {
// Use stack-allocated array to avoid heap allocation
// Maximum 4 extensions: pausable, permanent_delegate, transfer_fee, transfer_hook
let mut extensions: [ExtensionStructConfig; 4] = [
ExtensionStructConfig::Placeholder0,
ExtensionStructConfig::Placeholder0,
ExtensionStructConfig::Placeholder0,
ExtensionStructConfig::Placeholder0,
];
let mut count = 0;

if self.has_pausable {
extensions[count] = ExtensionStructConfig::PausableAccount(());
count += 1;
}
if self.has_permanent_delegate {
extensions[count] = ExtensionStructConfig::PermanentDelegateAccount(());
count += 1;
}
if self.has_transfer_fee {
extensions[count] = ExtensionStructConfig::TransferFeeAccount(());
count += 1;
}
if self.has_transfer_hook {
extensions[count] = ExtensionStructConfig::TransferHookAccount(());
count += 1;
}

let exts = if count == 0 {
None
} else {
Some(&extensions[..count])
};
crate::state::calculate_ctoken_account_size(exts).map(|size| size as u64)
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:

#!/bin/bash
# Find ExtensionStructConfig definition and Placeholder0 usage
echo "=== Searching for ExtensionStructConfig enum definition ==="
rg -n "enum ExtensionStructConfig" --type=rust -A 20

echo ""
echo "=== Searching for Placeholder0 references ==="
rg -n "Placeholder0" --type=rust -B 2 -A 2

echo ""
echo "=== Locating calculate_ctoken_account_size function ==="
rg -n "fn calculate_ctoken_account_size" --type=rust -A 30

Repository: Lightprotocol/light-protocol

Length of output: 6833


🏁 Script executed:

#!/bin/bash
# Find ExtensionStruct::byte_len implementation
echo "=== Searching for byte_len implementation ==="
rg -n "fn byte_len" --type=rust -B 2 -A 40 | head -80

echo ""
echo "=== Looking for match/match statement handling Placeholder0 ==="
rg -n "Placeholder0.*=>" --type=rust

Repository: Lightprotocol/light-protocol

Length of output: 7573


🏁 Script executed:

#!/bin/bash
# Search for ExtensionStruct definition and its byte_len implementation
echo "=== Finding ExtensionStruct enum definition ==="
rg -n "pub enum ExtensionStruct" --type=rust -A 50 | head -60

echo ""
echo "=== Searching for impl.*ExtensionStruct with byte_len ==="
rg -n "impl.*ExtensionStruct" --type=rust -A 100 | head -150

Repository: Lightprotocol/light-protocol

Length of output: 23880


Placeholder0 variants will cause an error if passed to byte_len().

The catch-all _ case in ExtensionStruct::byte_len() returns ZeroCopyError::InvalidConversion for any unmatched variants, including Placeholder0. While the current code works because only the first count elements are passed to calculate_ctoken_account_size(), this relies on correct count tracking. If count is miscalculated, Placeholder0 variants beyond the slice would cause a runtime error instead of contributing 0 bytes. Consider explicitly handling placeholder variants in the match statement to fail fast during development, or document that the count must always stay in bounds.

Comment on lines +166 to +175
// Check if DefaultAccountState is set to Frozen
// AccountState::Frozen as u8 = 2, ext.state is PodAccountState (u8)
let default_account_state_frozen = if has_default_account_state {
mint_state
.get_extension::<DefaultAccountState>()
.map(|ext| ext.state == AccountState::Frozen as u8)
.unwrap_or(false)
} else {
false
};
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

DefaultAccountState frozen detection is correct but could use unwrap_or_else for consistency.

The pattern unwrap_or(false) at line 172 is fine, but consider using unwrap_or_default() or adding a comment explaining why the fallback to false is safe here (extension exists but failed to parse).

🔎 Minor clarification suggestion
     let default_account_state_frozen = if has_default_account_state {
         mint_state
             .get_extension::<DefaultAccountState>()
             .map(|ext| ext.state == AccountState::Frozen as u8)
-            .unwrap_or(false)
+            .unwrap_or(false) // Safe: if extension parse fails, treat as not frozen
     } else {
         false
     };
📝 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
// Check if DefaultAccountState is set to Frozen
// AccountState::Frozen as u8 = 2, ext.state is PodAccountState (u8)
let default_account_state_frozen = if has_default_account_state {
mint_state
.get_extension::<DefaultAccountState>()
.map(|ext| ext.state == AccountState::Frozen as u8)
.unwrap_or(false)
} else {
false
};
// Check if DefaultAccountState is set to Frozen
// AccountState::Frozen as u8 = 2, ext.state is PodAccountState (u8)
let default_account_state_frozen = if has_default_account_state {
mint_state
.get_extension::<DefaultAccountState>()
.map(|ext| ext.state == AccountState::Frozen as u8)
.unwrap_or(false) // Safe: if extension parse fails, treat as not frozen
} else {
false
};
🤖 Prompt for AI Agents
In programs/compressed-token/program/src/extensions/check_mint_extensions.rs
around lines 166 to 175, the code uses unwrap_or(false) when converting the
DefaultAccountState extension to a boolean which is safe but inconsistent with
other patterns; replace unwrap_or(false) with unwrap_or_default() (or
unwrap_or_else(|| false)) so the fallback is idiomatic, or add a short comment
clarifying that returning false is safe when parsing fails; ensure the chosen
approach matches surrounding code style.

Comment on lines +86 to 105
let is_compressible = match compression_info.is_compressible(
cmint.data_len() as u64,
current_slot,
cmint.lamports(),
) {
Ok(is_compressible) => is_compressible,
Err(_) => {
if action.idempotent != 1 {
return Ok(());
} else {
msg!("CMint is not compressible (rent not expired)");
return Err(ErrorCode::CMintNotCompressible.into());
}
}
}
};

// 6. Transfer all lamports to rent_sponsor
let cmint_lamports = cmint.lamports();
if cmint_lamports > 0 {
transfer_lamports(cmint_lamports, cmint, rent_sponsor).map_err(convert_program_error)?;
if is_compressible.is_none() {
msg!("CMint is not compressible (rent not expired)");
return Err(ErrorCode::CMintNotCompressible.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

Critical: Inverted idempotent logic will cause incorrect behavior.

The conditional at lines 93-98 has inverted logic. When is_compressible returns an error:

  • If action.idempotent != 1 (meaning NOT idempotent), the code returns Ok(()) — silently succeeding.
  • If action.idempotent == 1 (meaning idempotent), it returns an error.

This is backwards. Idempotent mode should succeed silently on "already done" or "cannot proceed" conditions, not the non-idempotent mode.

🔎 Proposed fix
     let is_compressible = match compression_info.is_compressible(
         cmint.data_len() as u64,
         current_slot,
         cmint.lamports(),
     ) {
         Ok(is_compressible) => is_compressible,
         Err(_) => {
-            if action.idempotent != 1 {
+            if action.idempotent == 1 {
                 return Ok(());
             } else {
                 msg!("CMint is not compressible (rent not expired)");
                 return Err(ErrorCode::CMintNotCompressible.into());
             }
         }
     };
🤖 Prompt for AI Agents
In
programs/compressed-token/program/src/mint_action/actions/compress_and_close_cmint.rs
around lines 86 to 105, the idempotent branch is inverted when
compression_info.is_compressible() returns Err: currently it returns Ok(()) for
non-idempotent actions and Err for idempotent ones; flip the logic so that if
is_compressible() returns Err and action.idempotent == 1 you return Ok(())
(silent success), otherwise log the "CMint is not compressible (rent not
expired)" message and return Err(ErrorCode::CMintNotCompressible.into()); keep
the later check for is_compressible.is_none() unchanged.

Comment on lines +76 to 98
// Calculate CToken top-up
{
let account_data = ctoken.try_borrow_data().map_err(convert_program_error)?;
let (token, _) = CToken::zero_copy_at_checked(&account_data)?;
if let Some(ref extensions) = token.extensions {
for extension in extensions.iter() {
if let ZExtensionStruct::Compressible(compressible_ext) = extension {
if current_slot == 0 {
use pinocchio::sysvars::{clock::Clock, Sysvar};
current_slot = Clock::get()
.map_err(|_| CTokenError::SysvarAccessError)?
.slot;
}
transfers[1].amount = compressible_ext
.info
.calculate_top_up_lamports(
ctoken.data_len() as u64,
current_slot,
ctoken.lamports(),
COMPRESSIBLE_TOKEN_RENT_EXEMPTION,
)
.map_err(|_| CTokenError::InvalidAccountData)?;
lamports_budget = lamports_budget.saturating_sub(transfers[1].amount);
break;
}
}
} else {
// Only Compressible extensions are implemented for ctoken accounts.
return Err(CTokenError::InvalidAccountData.into());
// Access compression info directly from meta (all ctokens now have compression embedded)
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 = token
.compression
.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);
}
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

Duplicate lazy initialization code could be consolidated.

Lines 81-86 duplicate the lazy initialization pattern from lines 56-61. Since the CMint block always executes first and sets current_slot and rent, the condition at line 81 (if current_slot == 0) will never be true.

Consider moving the sysvar initialization before both blocks to simplify:

     let mut current_slot = 0;
     let mut rent: Option<Rent> = None;
+    
+    // Initialize sysvars once
+    current_slot = Clock::get()
+        .map_err(|_| CTokenError::SysvarAccessError)?
+        .slot;
+    rent = Some(Rent::get().map_err(|_| CTokenError::SysvarAccessError)?);
+
     // Initialize budget: +1 allows exact match (total == max_top_up)
     let mut lamports_budget = (max_top_up as u64).saturating_add(1);

     // Calculate CMint top-up using zero-copy
     {
         let cmint_data = cmint.try_borrow_data().map_err(convert_program_error)?;
         let (mint, _) = CompressedMint::zero_copy_at(&cmint_data)
             .map_err(|_| CTokenError::CMintDeserializationFailed)?;
-        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(cmint.data_len());

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/src/shared/compressible_top_up.rs around
lines 76 to 98, the lazy sysvar initialization (Clock::get and Rent::get) is
duplicated and the second check (if current_slot == 0) is redundant because the
CMint block runs first; move the sysvar initialization (populate current_slot
and rent with the same error handling) to a single spot before both the CMint
and CToken blocks, remove the duplicate if/current_slot check and its body in
the CToken block, and replace the subsequent rent.as_ref().unwrap() usage with
the already-initialized rent reference so both blocks use the shared values and
no duplicate sysvar calls remain.

Comment on lines +30 to +43
impl AccountExtensionInfo {
#[inline(always)]
fn check_t22_extensions(&self, other: &Self) -> Result<(), ProgramError> {
if self.flags.has_pausable != other.flags.has_pausable
|| self.flags.has_permanent_delegate != other.flags.has_permanent_delegate
|| self.flags.has_transfer_fee != other.flags.has_transfer_fee
|| self.flags.has_transfer_hook != other.flags.has_transfer_hook
{
Err(ProgramError::InvalidInstructionData)
} else {
Ok(())
}
}
}
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 implementing PartialEq on MintExtensionFlags to simplify comparison.

The manual field-by-field comparison of extension flags could be replaced with a simple equality check if MintExtensionFlags implements PartialEq. This would be more maintainable when additional extension flags are added.

🔎 Suggested simplification

If MintExtensionFlags implements PartialEq:

 fn check_t22_extensions(&self, other: &Self) -> Result<(), ProgramError> {
-    if self.flags.has_pausable != other.flags.has_pausable
-        || self.flags.has_permanent_delegate != other.flags.has_permanent_delegate
-        || self.flags.has_transfer_fee != other.flags.has_transfer_fee
-        || self.flags.has_transfer_hook != other.flags.has_transfer_hook
-    {
+    if self.flags != other.flags {
         Err(ProgramError::InvalidInstructionData)
     } else {
         Ok(())
     }
 }

Otherwise, verify whether this manual comparison is necessary.

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/src/transfer/shared.rs around lines 30 to
43, the code manually compares each field of the extension flags which is
fragile and verbose; implement or derive PartialEq (and Eq if appropriate) for
the MintExtensionFlags/MintExtensionFlags-like type and replace the
field-by-field checks with a single equality comparison (e.g., if self.flags !=
other.flags then return Err(...)); if there is a deliberate reason to compare
fields individually, add a comment explaining why and keep the explicit checks.

Comment on lines +17 to +18
// Anchor custom error codes start at offset 6000
const ANCHOR_ERROR_OFFSET: u32 = 6000;
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 importing ERROR_CODE_OFFSET from the anchor crate.

The constant ANCHOR_ERROR_OFFSET duplicates the ERROR_CODE_OFFSET defined in programs/compressed-token/anchor/src/lib.rs (lines 539-541). If the offset changes in the source, this test will silently use an outdated value.

🔎 Proposed fix
+use anchor_compressed_token::ERROR_CODE_OFFSET;
+
-// Anchor custom error codes start at offset 6000
-const ANCHOR_ERROR_OFFSET: u32 = 6000;

Then update line 146:

-                ANCHOR_ERROR_OFFSET + ErrorCode::CompressAndCloseDuplicateOutput as u32,
+                ERROR_CODE_OFFSET + ErrorCode::CompressAndCloseDuplicateOutput as u32,

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

🤖 Prompt for AI Agents
In programs/compressed-token/program/tests/compress_and_close.rs around lines
17-18, the local ANCHOR_ERROR_OFFSET constant duplicates the value defined in
the anchor crate and can drift; remove that local const and import the canonical
ERROR_CODE_OFFSET from the anchor crate (e.g. use anchor::ERROR_CODE_OFFSET;)
then update the test usage at line 146 to reference ERROR_CODE_OFFSET instead of
the hard-coded ANCHOR_ERROR_OFFSET so the test always uses the source-of-truth
value.

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