-
Notifications
You must be signed in to change notification settings - Fork 87
fix: cmint instruction data #2164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
… compressed_token
- Add CalculateTopUp trait for generic top-up calculations - Implement trait for CompressionInfo, ZCompressionInfo, ZCompressionInfoMut - Unify process_compression_top_up to use shared implementation - Remove duplicate function from compress_or_decompress_ctokens.rs - Fix extension ordering in assert_create_token_account (insert at index 0)
- Replace redundant .get().ok_or() with direct indexing after length validation - Add SAFETY comments documenting the length invariants - Make payer optional - only required when top-up transfer is needed - Remove redundant process_extensions function in transfer/default.rs - Use CTokenError::MissingPayer consistently when payer is required
- Remove CreateSplMint enum variant and CreateSplMintAction struct (never activated) - Remove create_spl_mint.rs file from ctoken-interface - Add MAX_COMPRESSIONS constant (32) with meaningful error message - Remove dead no_output_compressed_accounts field from Transfer2Config - Update documentation (MINT_ACTION.md, CLAUDE.md, lib.rs) - Update JS layout to remove CreateSplMint - Refactor check_extensions tests for better coverage
- Add config state validation (active required) during account parsing - Change compressible_config field to store parsed CompressibleConfig - Remove redundant config parsing in decompress_mint action - Refactor mint_output into serialize_decompressed_mint and serialize_compressed_mint helpers - Use cmint_decompressed flag directly after process_actions
Change validation from >= to > to fix off-by-one error that prevented using exactly MAX_COMPRESSIONS compression operations.
📝 WalkthroughWalkthroughThis pull request restructures the compressed-token program's module hierarchy from a flat layout to hierarchical organization under Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Program
participant CompressAndClose
participant TokenAccount
participant CMint
participant Blockchain
User->>Program: Send CompressAndClose instruction
Program->>CompressAndClose: process_compress_and_close()
CompressAndClose->>TokenAccount: Validate account (owner, mint, version)
TokenAccount-->>CompressAndClose: Account data
CompressAndClose->>TokenAccount: Check CompressedOnly extension
TokenAccount-->>CompressAndClose: Extension state
CompressAndClose->>TokenAccount: Validate delegate/fee/frozen state
TokenAccount-->>CompressAndClose: Validation result
CompressAndClose->>CMint: Retrieve CMint account
CMint-->>CompressAndClose: Compressed mint data
CompressAndClose->>Blockchain: Close token account via CPI
Blockchain-->>CompressAndClose: Success/Error
CompressAndClose->>Program: Return compressed account updates
Program->>User: Instruction complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes This PR exhibits high heterogeneity across multiple dimensions: module reorganization affecting dozens of import paths, introduction of a new trait abstraction with multiple implementations, new major functionality (compress-and-close) requiring deep validation logic, account initialization refactoring with security-critical changes (ownership checks, validation ordering), and error handling standardization. While individual patterns repeat (import updates, similar refactoring shapes), each area demands separate reasoning due to different semantics and implications. The compress-and-close implementation and account validation logic are particularly dense; the CMint/CToken data structure changes require careful tracing through test code; and the config/account parsing consolidation affects multiple call sites with subtle semantic shifts. Possibly related PRs
Suggested labels
Suggested reviewers
Principal Engineer's PerspectiveThis is a substantial architectural refactoring with several critical design decisions embedded throughout. Let me highlight the key areas requiring careful attention: 1. Module Reorganization Philosophy – The move from flat to hierarchical structure ( 2. CalculateTopUp Trait Design – The trait abstraction for rent calculation is elegant but introduces a generic dependency. Ensure that callers can correctly instantiate the generic 3. CreateSplMint Removal – This removes a mint creation path entirely. Verify in the documentation (CLAUDE.md) and changelog that migration guidance is clear. The signer requirement changes are subtle; audit all paths checking 4. Account Initialization Refactoring – The centralization of mint validation in 5. Compress-and-Close Implementation – This is the most security-sensitive new feature. The validation logic (
6. CMint Data Structure Shrinkage – Reducing 7. Error Code Consistency – The new error mapping utilities are valuable, but ensure all error paths use them consistently. The offset handling (+6000, +6900) should be documented and validated across all error conversions. 8. Test Coverage – The new test modules (especially
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
programs/compressed-token/program/tests/queue_indices.rs (1)
28-278: Consider adding write mode test coverage in the future.The comprehensive test suite thoroughly covers execute mode (write_to_cpi_context=false) and no-CPI scenarios. However, all CPI context test cases use
first_set_context=falseandset_context=false. Consider adding test cases for write mode (where either flag is true) to ensure complete coverage of the QueueIndices behavior in both execution paths.This is a pre-existing gap outside the scope of this PR's refactoring, but worth noting for future enhancement.
programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs (1)
88-95: TODO: SPL Token-2022 compatibility incomplete.The commented code indicates that adding new metadata keys is not yet supported. This limits SPL Token-2022 compatibility as users can only update existing keys.
Do you want me to help implement this feature or open a new issue to track it?
programs/compressed-token/program/docs/ctoken/CLOSE.md (1)
147-156: Edge case documentation is clear, but known limitation should be highlighted.Line 150 notes that SetAuthority is unimplemented and close_authority is always None. This is important context, but it might be worth making more prominent in the main description (perhaps early in the "Accounts" section) so users understand this constraint before implementing against the API.
The note about using CompressAndClose mode in Transfer2 (line 151) is good guidance for handling non-zero balances—this correctly directs users to the right instruction.
Consider restructuring the edge cases section or adding a Limitations section upfront that clarifies:
- SetAuthority is not implemented; close_authority is always None
- Users needing to close accounts with non-zero balances must use Transfer2's CompressAndClose mode instead
- Only 3 accounts required for non-compressible accounts (vs. 4 for compressible)
This would help users understand constraints earlier when designing their workflows.
programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md (2)
66-97: Fix CMint deserialization function name: usezero_copy_at_checked, notzero_copy_at.The documentation at line 74 states
CompressedMint::zero_copy_at, but the actual implementation inprograms/compressed-token/program/src/shared/compressible_top_up.rs(line 53) usesCompressedMint::zero_copy_at_checked. All other function names, parameters, and logic flow match correctly.
98-116: Fix ArithmeticOverflow error code in documentation.The error code for
ArithmeticOverflowis documented as (24), but the actual CTokenError implementation maps it to (18003). Update the documentation to reflect the correct code, as developers debugging this error will search for 18003, not 24, in the error logs.All other CTokenError codes (18002, 18036, 18053, 18020, 18043, 18047, 18056) have been verified and match the actual enum definitions.
programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs (1)
140-155: Consider documenting the magic bytes for the account-compression program check.The heuristic at line 151 uses raw bytes
[9, 44, 54, 236, 34, 245, 23, 131]to identify tree/queue accounts by matching the first 8 bytes of the account owner. While this works, a named constant or brief comment explaining these are the first 8 bytes of the account-compression program ID would improve maintainability.📝 Suggested documentation improvement
+/// First 8 bytes of the Light account-compression program ID +/// Used as a heuristic to identify tree/queue accounts +const ACCOUNT_COMPRESSION_PROGRAM_PREFIX: [u8; 8] = [9, 44, 54, 236, 34, 245, 23, 131]; + /// Extract tree accounts by checking account owner matches account-compression program #[profile] #[inline(always)] pub fn extract_tree_accounts<'info>( packed_accounts: &'info ProgramPackedAccounts<'info, AccountInfo>, ) -> Vec<&'info Pubkey> { let mut tree_accounts = Vec::with_capacity(8); for account_info in packed_accounts.accounts { // As heuristic which accounts are tree or queue accounts we // check that the first 8 bytes of the account compression program // equal the first 8 bytes of the account owner. - if account_info.owner()[0..8] == [9, 44, 54, 236, 34, 245, 23, 131] { + if account_info.owner()[0..8] == ACCOUNT_COMPRESSION_PROGRAM_PREFIX { tree_accounts.push(account_info.key()); } } tree_accounts }programs/compressed-token/program/docs/compressed_token/TRANSFER2.md (1)
344-373: Fix maximum transfer limit in TooManyCompressionTransfers documentation.The error code documentation is accurate, but one description contains an outdated limit. The
TooManyCompressionTransferserror (6095) states "Maximum 40 transfers allowed" in the documentation, but the actual implementation enforces a limit of 32 transfers. Update the documentation to match:- `ErrorCode::TooManyCompressionTransfers` (error code: 6095) - Too many compression transfers. Maximum 32 transfers allowed per instructionAll other error codes and their numeric values are correct and match the
ErrorCodeenum inprograms/compressed-token/anchor/src/lib.rs.programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs (1)
60-72: Fix the config validation for decompress operations—using wrong strictness level.The config is validated upstream in
accounts.rsvianext_config_account(), which callsvalidate_active(). However, this is overly restrictive for decompress operations.Per the documented error requirements: DecompressMint and CompressAndCloseCMint should validate that config is not INACTIVE (allowing both ACTIVE and DEPRECATED states), not ACTIVE-only. Create token account requires ACTIVE-only.
The current code reuses
next_config_account()for both operation types throughneeds_compressible_accounts(), which lumps decompress/compress operations together with account creation validation. This blocks deprecated configs from decompressing mints when they should be allowed.Required fix: Either create a separate
next_config_account_not_inactive()function for decompress/compress operations, or branch the validation in the account parsing based on operation type.
🤖 Fix all issues with AI agents
In @programs/compressed-token/program/docs/CLAUDE.md:
- Line 11: Add a blank line after the list item "**`INSTRUCTIONS.md`** - Full
instruction reference and discriminator table" so the heading that follows has a
proper separating newline (fixes MD022); simply insert an empty line immediately
after that line in the document.
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md:
- Around line 225-227: Add a blank line immediately before the "### Spl mint
migration" heading to satisfy MD022 (blanks-around-headings); update the
MINT_ACTION.md content so there is an empty line above that heading line (the
heading text "Spl mint migration") to fix the markdown lint error.
In @programs/compressed-token/program/docs/ctoken/CREATE.md:
- Around line 89-91: Reword the awkward phrase "compression_only is only set" in
the CREATE.md validation bullet to remove the repeated adverb; for example
change it to "compression_only may only be set for mints with restricted
extensions" or "compression_only is set exclusively for mints with restricted
extensions" so the validation reads clearly and concisely.
In @programs/compressed-token/program/docs/EXTENSIONS.md:
- Line 139: The EXTENSIONS.md entry for validate_permanent_delegate points to
the call site instead of the actual function definition; update the documented
reference for validate_permanent_delegate so it points to the function
definition (not the surrounding transfer logic/call) in the source and leave the
other two entries unchanged.
In @programs/compressed-token/program/docs/INSTRUCTIONS.md:
- Around line 84-106: The markdown headings for "Compressed Token Operations
(`compressed_token/`)", "CToken Operations (`ctoken/`)", "Compressible
Operations (`compressible/`)" and "Token Pool Operations (root)" need
surrounding blank lines and the lists under each heading should be sequentially
numbered starting at 1 (or converted to unordered lists with explicit
instruction IDs if those numbers are meant to be global); update the
INSTRUCTIONS.md section so each heading has a blank line above and below it and
renumber the items under each heading (e.g., Transfer2/MintAction/Freeze/Thaw
under compressed_token/ start at 1, Create/Close/Transfer/... under ctoken/
start at 1, Claim/Withdraw under compressible/ start at 1, Create Token Pool/Add
Token Pool under Token Pool Operations start at 1) or change them to bullet
lists with parenthetical IDs to preserve global IDs.
In @programs/compressed-token/program/src/compressed_token/mod.rs:
- Around line 1-2: Add public re-exports for commonly used items from the child
modules to improve ergonomics: after the existing `pub mod mint_action;` and
`pub mod transfer2;` declarations, add `pub use` statements to re-export the
public types/functions from `mint_action` and `transfer2` (e.g., `pub use
mint_action::*;` and `pub use transfer2::*;`) so consumers can import those
instruction/data/processor items directly from the parent module.
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Line 118: Fix the grammar in the inline comment that currently reads "// -
Decompress: no check it restores existing state" by changing it to a correct
phrasing such as "// - Decompress: now check it restores existing state" (or "//
- Decompress: check that it restores existing state") in check_extensions.rs so
the intent is clear; locate the comment near the Decompress bullet in the
transfer2/check_extensions.rs diff and update the text accordingly.
In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 92-97: The non-compressible token account case relies on implicit
ownership validation but initialize_ctoken_account currently does not perform
explicit checks; call check_token_program_owner(token_account_info) near the
start of initialize_ctoken_account (before any writes) to validate the account
is owned by a supported token program (SPL Token, Token-2022, or CToken) and
return an appropriate error if it fails; ensure you import/use the utility from
src/shared/owner_validation.rs and place the check early so subsequent logic
assumes a validated owner.
In @programs/compressed-token/program/src/ctoken/mod.rs:
- Around line 1-21: The close module is exposing its function via a nested path
(close::processor::process_close_token_account) which is inconsistent with other
modules; fix this by changing the close module to re-export the processor symbol
at the close module root (e.g., add in close/mod.rs: pub use
processor::process_close_token_account) so this file can simply use pub use
close::process_close_token_account to match the flat exports of the other
modules (or alternatively move the function to close::process_close module root
and update references accordingly).
In @programs/compressed-token/program/src/shared/config_account.rs:
- Around line 13-30: Define a module-level constant for the hard-coded program
id (e.g., CONFIG_PROGRAM_ID or PROGRAM_ID) and replace the inline pubkey!
invocation inside parse_config_account's call to check_owner with that constant;
specifically, add the constant near the top of the file and update
parse_config_account to call check_owner(&CONFIG_PROGRAM_ID.to_bytes(),
config_account) so the program id is centralized (affects the pubkey! usage in
parse_config_account and keeps CompressibleConfig checks unchanged).
In @programs/compressed-token/program/src/shared/owner_validation.rs:
- Line 106: Update the docstring above the owner validation logic to remove the
incorrect mention of "account delegate" and accurately state the supported
authorization paths: owner or permanent delegate from the mint; replace the
existing docstring lines with a concise description such as "Verify and update
token account authority using zero-copy compressed token format. Allows owner or
permanent delegate (from mint) to authorize compression operations." and ensure
this text sits immediately above the owner validation function in
owner_validation.rs so it matches the implemented checks.
In @programs/compressed-token/program/tests/mint_validation.rs:
- Around line 38-62: Add a short inline comment explaining why byte index 165 is
used in MintData::WithAccountType within the to_bytes() implementation (e.g.,
that Token-2022 layout places AccountType at offset 165: 82 bytes base + 83
bytes padding), so future readers understand the magic number; update the
comment near the assignment to account_type (data[165] = *account_type) and keep
it concise referencing MintData::WithAccountType and to_bytes().
| - **`../CLAUDE.md`** (parent) - Main entry point with summary and instruction index | ||
| - **`ACCOUNTS.md`** - Complete account layouts and data structures | ||
| - **`EXTENSIONS.md`** - Token-2022 extension validation across ctoken instructions | ||
| - **`INSTRUCTIONS.md`** - Full instruction reference and discriminator table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Fix markdown formatting for proper heading spacing.
Add a blank line after the INSTRUCTIONS.md entry to comply with markdown formatting standards (MD022).
📝 Proposed fix
- **`INSTRUCTIONS.md`** - Full instruction reference and discriminator table
+
- **`RESTRICTED_T22_EXTENSIONS.md`** - SPL Token-2022 behavior for 5 restricted extensions🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
11-11: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/CLAUDE.md at line 11, Add a blank
line after the list item "**`INSTRUCTIONS.md`** - Full instruction reference and
discriminator table" so the heading that follows has a proper separating newline
(fixes MD022); simply insert an empty line immediately after that line in the
document.
| ### Spl mint migration | ||
| - cmint to spl mint migration is unimplemented and not planned. | ||
| - A way to support it in the future would require a new instruction that creates an spl mint in the mint pda solana account and mints the supply to the spl interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Add blank line before heading per markdown lint rules.
The static analysis flagged MD022 (blanks-around-headings) for line 225.
📝 Markdown formatting fix
- `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts
+
### Spl mint migration
- cmint to spl mint migration is unimplemented and not planned.Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
225-225: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md
around lines 225 - 227, Add a blank line immediately before the "### Spl mint
migration" heading to satisfy MD022 (blanks-around-headings); update the
MINT_ACTION.md content so there is an empty line above that heading line (the
heading text "Spl mint migration") to fix the markdown lint error.
| 4.3.1. Validate compression_only is only set for mints with restricted extensions: | ||
| - If compression_only != 0 and mint has no restricted extensions | ||
| - Error: `ErrorCode::CompressionOnlyNotAllowed` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor style: adverb repetition.
Line 89 has "compression_only is only set" which reads awkwardly. Consider rephrasing to "compression_only is set exclusively for mints with restricted extensions" or "compression_only may only be set for mints with restricted extensions".
📝 Suggested wording
- 4.3.1. Validate compression_only is only set for mints with restricted extensions:
+ 4.3.1. Validate compression_only is set exclusively for mints with restricted extensions:🧰 Tools
🪛 LanguageTool
[style] ~89-~89: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ... 4.3.1. Validate compression_only is only set for mints with restricted extension...
(ADVERB_REPETITION_PREMIUM)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/ctoken/CREATE.md around lines 89 -
91, Reword the awkward phrase "compression_only is only set" in the CREATE.md
validation bullet to remove the repeated adverb; for example change it to
"compression_only may only be set for mints with restricted extensions" or
"compression_only is set exclusively for mints with restricted extensions" so
the validation reads clearly and concisely.
| - `programs/compressed-token/program/src/extensions/check_mint_extensions.rs:77-84` - Extracts delegate pubkey in `parse_mint_extensions()` | ||
| - `programs/compressed-token/program/src/shared/owner_validation.rs:30-78` - `verify_owner_or_delegate_signer()` validates delegate/permanent delegate signer | ||
| - `programs/compressed-token/program/src/transfer/shared.rs:164-179` - `validate_permanent_delegate()` | ||
| - `programs/compressed-token/program/src/ctoken/transfer/shared.rs:164-179` - `validate_permanent_delegate()` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify extension validation function locations
echo "=== Checking validate_permanent_delegate ==="
rg -n "pub fn validate_permanent_delegate" programs/compressed-token/program/src/ --type=rust
echo -e "\n=== Checking build_mint_extension_cache ==="
rg -n "pub fn build_mint_extension_cache" programs/compressed-token/program/src/ --type=rust
echo -e "\n=== Checking transfer2 processor.rs ==="
fd "processor.rs" programs/compressed-token/program/src/compressed_token/transfer2/ -x ls -lh {}
echo -e "\n=== Checking ctoken/transfer/shared.rs existence ==="
ls -lh programs/compressed-token/program/src/ctoken/transfer/shared.rs 2>&1 | head -5
echo -e "\n=== Checking compressed_token/transfer2/ structure ==="
ls -la programs/compressed-token/program/src/compressed_token/transfer2/ 2>&1 | head -20Repository: Lightprotocol/light-protocol
Length of output: 1486
🏁 Script executed:
#!/bin/bash
# Verify actual documentation content and broader function search
echo "=== Checking EXTENSIONS.md line 139 context ==="
sed -n '135,145p' programs/compressed-token/program/docs/EXTENSIONS.md
echo -e "\n=== Broader search for validate_permanent_delegate (any variation) ==="
rg "validate_permanent_delegate" programs/compressed-token/program/src/ --type=rust
echo -e "\n=== Search in ctoken/transfer/shared.rs specifically ==="
cat -n programs/compressed-token/program/src/ctoken/transfer/shared.rs | head -30
echo "..."
cat -n programs/compressed-token/program/src/ctoken/transfer/shared.rs | tail -30
echo -e "\n=== Searching for 'permanent' anywhere in src ==="
rg "permanent" programs/compressed-token/program/src/ --type=rust -lRepository: Lightprotocol/light-protocol
Length of output: 4398
🏁 Script executed:
#!/bin/bash
# Get exact line numbers for the function definition
echo "=== Finding exact line numbers for validate_permanent_delegate ==="
rg -n "fn validate_permanent_delegate" programs/compressed-token/program/src/ctoken/transfer/shared.rs --type=rust -A 15
echo -e "\n=== Counting total lines in ctoken/transfer/shared.rs ==="
wc -l programs/compressed-token/program/src/ctoken/transfer/shared.rs
echo -e "\n=== Checking lines 164-179 specifically ==="
sed -n '160,185p' programs/compressed-token/program/src/ctoken/transfer/shared.rs | cat -nRepository: Lightprotocol/light-protocol
Length of output: 1845
Correct the line numbers for validate_permanent_delegate().
The function is located at lines 195-210, not 164-179 as documented. Lines 164-179 contain the validate_permanent_delegate() function call and surrounding transfer logic, but the actual function definition begins at line 195.
The other two paths are correct:
build_mint_extension_cache()atcompressed_token/transfer2/check_extensions.rs:77✓processoratcompressed_token/transfer2/processor.rs✓
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/EXTENSIONS.md at line 139, The
EXTENSIONS.md entry for validate_permanent_delegate points to the call site
instead of the actual function definition; update the documented reference for
validate_permanent_delegate so it points to the function definition (not the
surrounding transfer logic/call) in the source and leave the other two entries
unchanged.
| ## Compressed Token Operations (`compressed_token/`) | ||
| 1. **Transfer2** - Batch transfer instruction supporting compress/decompress/transfer operations | ||
| 2. **MintAction** - Batch instruction for compressed mint management (9 actions) | ||
| 3. **Freeze** - Freeze compressed token accounts (Anchor) | ||
| 4. **Thaw** - Thaw frozen compressed token accounts (Anchor) | ||
|
|
||
| ## Anchor Program Instructions (Compressed Token Accounts) | ||
| ## CToken Operations (`ctoken/`) | ||
| 5. **Create** - Create regular and associated ctoken accounts | ||
| 6. **Close** - Close decompressed token accounts with rent distribution | ||
| 7. **Transfer** - SPL-compatible transfers between decompressed accounts | ||
| 8. **Approve/Revoke** - Approve and revoke delegate on decompressed CToken accounts | ||
| 9. **MintTo** - Mint tokens to decompressed CToken account | ||
| 10. **Burn** - Burn tokens from decompressed CToken account | ||
| 11. **Freeze/Thaw** - Freeze and thaw decompressed CToken accounts | ||
| 12. **Checked Operations** - TransferChecked, ApproveChecked, MintToChecked, BurnChecked | ||
|
|
||
| These instructions operate on compressed token accounts (stored in Merkle trees) and require ZK proofs: | ||
| ## Compressible Operations (`compressible/`) | ||
| 13. **Claim** - Rent reclamation from expired compressible accounts | ||
| 14. **Withdraw Funding Pool** - Withdraw funds from rent recipient pool | ||
|
|
||
| 15. **Compressed Token Freeze** (`compressed_token/FREEZE.md`) - Freeze compressed token accounts | ||
| 16. **Compressed Token Thaw** (`compressed_token/THAW.md`) - Thaw frozen compressed token accounts | ||
| ## Token Pool Operations (root) | ||
| 15. **Create Token Pool** - Create initial token pool PDA for SPL/T22 mint compression | ||
| 16. **Add Token Pool** - Add additional token pools for a mint (up to 5 per mint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Fix markdown formatting issues.
Static analysis flagged multiple formatting violations:
- Missing blank lines around headings (lines 84, 90, 100, 104)
- Non-sequential list numbering (lines 91-106) - lists start at 5, 13, 15, 16 instead of 1
If the numbers represent global instruction IDs, consider using unordered lists with explicit IDs in the text, or reset numbering to 1 within each section.
♻️ Suggested fix
## Compressed Token Operations (`compressed_token/`)
+
1. **Transfer2** - Batch transfer instruction supporting compress/decompress/transfer operations
2. **MintAction** - Batch instruction for compressed mint management (9 actions)
3. **Freeze** - Freeze compressed token accounts (Anchor)
4. **Thaw** - Thaw frozen compressed token accounts (Anchor)
## CToken Operations (`ctoken/`)
+
-5. **Create** - Create regular and associated ctoken accounts
-6. **Close** - Close decompressed token accounts with rent distribution
-7. **Transfer** - SPL-compatible transfers between decompressed accounts
-8. **Approve/Revoke** - Approve and revoke delegate on decompressed CToken accounts
-9. **MintTo** - Mint tokens to decompressed CToken account
-10. **Burn** - Burn tokens from decompressed CToken account
-11. **Freeze/Thaw** - Freeze and thaw decompressed CToken accounts
-12. **Checked Operations** - TransferChecked, ApproveChecked, MintToChecked, BurnChecked
+1. **Create** - Create regular and associated ctoken accounts
+2. **Close** - Close decompressed token accounts with rent distribution
+3. **Transfer** - SPL-compatible transfers between decompressed accounts
+4. **Approve/Revoke** - Approve and revoke delegate on decompressed CToken accounts
+5. **MintTo** - Mint tokens to decompressed CToken account
+6. **Burn** - Burn tokens from decompressed CToken account
+7. **Freeze/Thaw** - Freeze and thaw decompressed CToken accounts
+8. **Checked Operations** - TransferChecked, ApproveChecked, MintToChecked, BurnChecked
## Compressible Operations (`compressible/`)
+
-13. **Claim** - Rent reclamation from expired compressible accounts
-14. **Withdraw Funding Pool** - Withdraw funds from rent recipient pool
+1. **Claim** - Rent reclamation from expired compressible accounts
+2. **Withdraw Funding Pool** - Withdraw funds from rent recipient pool
## Token Pool Operations (root)
+
-15. **Create Token Pool** - Create initial token pool PDA for SPL/T22 mint compression
-16. **Add Token Pool** - Add additional token pools for a mint (up to 5 per mint)
+1. **Create Token Pool** - Create initial token pool PDA for SPL/T22 mint compression
+2. **Add Token Pool** - Add additional token pools for a mint (up to 5 per mint)🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
84-84: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
90-90: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
91-91: Ordered list item prefix
Expected: 1; Actual: 5; Style: 1/2/3
(MD029, ol-prefix)
92-92: Ordered list item prefix
Expected: 2; Actual: 6; Style: 1/2/3
(MD029, ol-prefix)
93-93: Ordered list item prefix
Expected: 3; Actual: 7; Style: 1/2/3
(MD029, ol-prefix)
94-94: Ordered list item prefix
Expected: 4; Actual: 8; Style: 1/2/3
(MD029, ol-prefix)
95-95: Ordered list item prefix
Expected: 5; Actual: 9; Style: 1/2/3
(MD029, ol-prefix)
96-96: Ordered list item prefix
Expected: 6; Actual: 10; Style: 1/2/3
(MD029, ol-prefix)
97-97: Ordered list item prefix
Expected: 7; Actual: 11; Style: 1/2/3
(MD029, ol-prefix)
98-98: Ordered list item prefix
Expected: 8; Actual: 12; Style: 1/2/3
(MD029, ol-prefix)
100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
101-101: Ordered list item prefix
Expected: 1; Actual: 13; Style: 1/2/3
(MD029, ol-prefix)
102-102: Ordered list item prefix
Expected: 2; Actual: 14; Style: 1/2/3
(MD029, ol-prefix)
104-104: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
105-105: Ordered list item prefix
Expected: 1; Actual: 15; Style: 1/2/3
(MD029, ol-prefix)
106-106: Ordered list item prefix
Expected: 2; Actual: 16; Style: 1/2/3
(MD029, ol-prefix)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/INSTRUCTIONS.md around lines 84 -
106, The markdown headings for "Compressed Token Operations
(`compressed_token/`)", "CToken Operations (`ctoken/`)", "Compressible
Operations (`compressible/`)" and "Token Pool Operations (root)" need
surrounding blank lines and the lists under each heading should be sequentially
numbered starting at 1 (or converted to unordered lists with explicit
instruction IDs if those numbers are meant to be global); update the
INSTRUCTIONS.md section so each heading has a blank line above and below it and
renumber the items under each heading (e.g., Transfer2/MintAction/Freeze/Thaw
under compressed_token/ start at 1, Create/Close/Transfer/... under ctoken/
start at 1, Claim/Withdraw under compressible/ start at 1, Create Token Pool/Add
Token Pool under Token Pool Operations start at 1) or change them to bullet
lists with parenthetical IDs to preserve global IDs.
| // Compress/Decompress don't need additional validation here: | ||
| // - Compress: blocked by check_mint_extensions when outputs exist | ||
| // - Decompress: bypassed (restoring existing state) | ||
| // - Decompress: no check it restores existing state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Fix comment grammar.
The comment has a minor grammatical issue.
📝 Suggested fix
- // - Decompress: no check it restores existing state
+ // - Decompress: no check as it restores existing state📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // - Decompress: no check it restores existing state | |
| // - Decompress: no check as it restores existing state |
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
at line 118, Fix the grammar in the inline comment that currently reads "// -
Decompress: no check it restores existing state" by changing it to a correct
phrasing such as "// - Decompress: now check it restores existing state" (or "//
- Decompress: check that it restores existing state") in check_extensions.rs so
the intent is clear; locate the comment near the Decompress bullet in the
transfer2/check_extensions.rs diff and update the text accordingly.
| pub mod approve_revoke; | ||
| pub mod burn; | ||
| pub mod close; | ||
| pub mod create; | ||
| pub mod create_ata; | ||
| pub mod freeze_thaw; | ||
| pub mod mint_to; | ||
| pub mod transfer; | ||
|
|
||
| pub use approve_revoke::{ | ||
| process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke, | ||
| }; | ||
| pub use burn::{process_ctoken_burn, process_ctoken_burn_checked}; | ||
| pub use close::processor::process_close_token_account; | ||
| pub use create::process_create_token_account; | ||
| pub use create_ata::{ | ||
| process_create_associated_token_account, process_create_associated_token_account_idempotent, | ||
| }; | ||
| pub use freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account}; | ||
| pub use mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked}; | ||
| pub use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Clean module organization centralizing ctoken operations.
This new module file properly declares and re-exports all ctoken submodules, following the coding guidelines for module organization. The structure aligns with the PR's goal of restructuring the module hierarchy into organized tiers (compressed_token, ctoken, compressible).
One minor note: the close module uses a nested processor path (line 14: close::processor::process_close_token_account) while other modules export functions directly. This inconsistency is minor but worth noting for future cleanup.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/ctoken/mod.rs around lines 1 - 21, The
close module is exposing its function via a nested path
(close::processor::process_close_token_account) which is inconsistent with other
modules; fix this by changing the close module to re-export the processor symbol
at the close module root (e.g., add in close/mod.rs: pub use
processor::process_close_token_account) so this file can simply use pub use
close::process_close_token_account to match the flat exports of the other
modules (or alternatively move the function to close::process_close module root
and update references accordingly).
| pub fn parse_config_account( | ||
| config_account: &AccountInfo, | ||
| ) -> Result<&CompressibleConfig, ProgramError> { | ||
| // Validate config account owner | ||
| check_owner( | ||
| &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(), | ||
| config_account, | ||
| )?; | ||
| // Parse config data | ||
| let data = unsafe { config_account.borrow_data_unchecked() }; | ||
| check_discriminator::<CompressibleConfig>(data)?; | ||
| let config = bytemuck::pod_from_bytes::<CompressibleConfig>(&data[8..]).map_err(|e| { | ||
| msg!("Failed to deserialize CompressibleConfig: {:?}", e); | ||
| ProgramError::InvalidAccountData | ||
| })?; | ||
|
|
||
| Ok(config) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Extract hard-coded program ID to a constant.
The program ID Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX is hard-coded on line 18. This should be defined as a module-level constant for maintainability and to prevent typos.
♻️ Suggested refactor
At the top of the file (after imports):
+pub const COMPRESSIBLE_PROGRAM_ID: Pubkey = pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX");
+
#[profile]
#[inline(always)]
pub fn parse_config_account(Then update the usage:
check_owner(
- &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
+ &COMPRESSIBLE_PROGRAM_ID.to_bytes(),
config_account,
)?;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/config_account.rs around lines
13 - 30, Define a module-level constant for the hard-coded program id (e.g.,
CONFIG_PROGRAM_ID or PROGRAM_ID) and replace the inline pubkey! invocation
inside parse_config_account's call to check_owner with that constant;
specifically, add the constant near the top of the file and update
parse_config_account to call check_owner(&CONFIG_PROGRAM_ID.to_bytes(),
config_account) so the program id is centralized (affects the pubkey! usage in
parse_config_account and keeps CompressibleConfig checks unchanged).
| if let Some(permanent_delegate) = &checks.permanent_delegate { | ||
| if pubkey_eq(authority_key, permanent_delegate) { | ||
| return Ok(()); // Permanent delegate can compress any account of this mint | ||
| return Ok(()); // Permanent delegate can (de)compress any account of this mint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, read the owner_validation.rs file around the mentioned lines
echo "=== Reading owner_validation.rs ==="
head -130 programs/compressed-token/program/src/shared/owner_validation.rs | tail -50
echo -e "\n=== Full check_ctoken_owner function ==="
grep -n "fn check_ctoken_owner" -A 30 programs/compressed-token/program/src/shared/owner_validation.rs
echo -e "\n=== MintExtensionChecks struct definition ==="
rg "struct MintExtensionChecks" -A 10 --type=rs programs/compressed-token/program/srcRepository: Lightprotocol/light-protocol
Length of output: 3388
🏁 Script executed:
#!/bin/bash
# Search for check_ctoken_owner usage in compression contexts
echo "=== check_ctoken_owner usage in transfer2 ==="
rg "check_ctoken_owner" -B 3 -A 5 --type=rs programs/compressed-token/program/src/transfer2
echo -e "\n=== Delegate handling in compression ==="
rg "permanent_delegate|account.*delegate" -i -B 2 -A 2 --type=rs programs/compressed-token/program/src/transfer2 | head -60Repository: Lightprotocol/light-protocol
Length of output: 230
🏁 Script executed:
#!/bin/bash
# Check CLAUDE.md for compression/decompression delegate requirements
echo "=== CLAUDE.md compression requirements ==="
cat programs/compressed-token/program/CLAUDE.md | grep -i -A 5 -B 5 "compress\|delegate" | head -100Repository: Lightprotocol/light-protocol
Length of output: 6022
Update docstring to accurately reflect supported authorization paths.
The docstring at lines 83-84 claims support for "account delegate" but the implementation only authorizes owner and permanent delegate from mint. Update the docstring to remove the reference to account delegate authorization.
/// Verify and update token account authority using zero-copy compressed token format.
/// Allows owner or permanent delegate (from mint) to authorize compression operations.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/owner_validation.rs at line
106, Update the docstring above the owner validation logic to remove the
incorrect mention of "account delegate" and accurately state the supported
authorization paths: owner or permanent delegate from the mint; replace the
existing docstring lines with a concise description such as "Verify and update
token account authority using zero-copy compressed token format. Allows owner or
permanent delegate (from mint) to authorize compression operations." and ensure
this text sits immediately above the owner validation function in
owner_validation.rs so it matches the implemented checks.
| /// Data configurations for testing | ||
| #[derive(Debug, Clone)] | ||
| enum MintData { | ||
| Empty, | ||
| TooSmall(usize), // < 82 bytes | ||
| ExactSplSize, // 82 bytes (valid for all) | ||
| BetweenSizes(usize), // 83-165 bytes | ||
| WithAccountType(u8), // 166+ bytes with specific AccountType | ||
| } | ||
|
|
||
| impl MintData { | ||
| fn to_bytes(&self) -> Vec<u8> { | ||
| match self { | ||
| MintData::Empty => vec![], | ||
| MintData::TooSmall(size) => vec![0u8; *size], | ||
| MintData::ExactSplSize => vec![0u8; 82], | ||
| MintData::BetweenSizes(size) => vec![0u8; *size], | ||
| MintData::WithAccountType(account_type) => { | ||
| let mut data = vec![0u8; 170]; | ||
| data[165] = *account_type; | ||
| data | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider documenting the magic number 165 for AccountType offset.
The WithAccountType variant places the account type at byte offset 165. While this is correct per Token-2022's layout (82 bytes base + 83 bytes padding to reach the AccountType field at 165), a brief comment explaining this would help future readers understand the test data construction.
📝 Suggested documentation
impl MintData {
fn to_bytes(&self) -> Vec<u8> {
match self {
MintData::Empty => vec![],
MintData::TooSmall(size) => vec![0u8; *size],
MintData::ExactSplSize => vec![0u8; 82],
MintData::BetweenSizes(size) => vec![0u8; *size],
MintData::WithAccountType(account_type) => {
+ // Token-2022 extended mint layout: 82 base + padding, AccountType at offset 165
let mut data = vec![0u8; 170];
data[165] = *account_type;
data
}
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Data configurations for testing | |
| #[derive(Debug, Clone)] | |
| enum MintData { | |
| Empty, | |
| TooSmall(usize), // < 82 bytes | |
| ExactSplSize, // 82 bytes (valid for all) | |
| BetweenSizes(usize), // 83-165 bytes | |
| WithAccountType(u8), // 166+ bytes with specific AccountType | |
| } | |
| impl MintData { | |
| fn to_bytes(&self) -> Vec<u8> { | |
| match self { | |
| MintData::Empty => vec![], | |
| MintData::TooSmall(size) => vec![0u8; *size], | |
| MintData::ExactSplSize => vec![0u8; 82], | |
| MintData::BetweenSizes(size) => vec![0u8; *size], | |
| MintData::WithAccountType(account_type) => { | |
| let mut data = vec![0u8; 170]; | |
| data[165] = *account_type; | |
| data | |
| } | |
| } | |
| } | |
| } | |
| /// Data configurations for testing | |
| #[derive(Debug, Clone)] | |
| enum MintData { | |
| Empty, | |
| TooSmall(usize), // < 82 bytes | |
| ExactSplSize, // 82 bytes (valid for all) | |
| BetweenSizes(usize), // 83-165 bytes | |
| WithAccountType(u8), // 166+ bytes with specific AccountType | |
| } | |
| impl MintData { | |
| fn to_bytes(&self) -> Vec<u8> { | |
| match self { | |
| MintData::Empty => vec![], | |
| MintData::TooSmall(size) => vec![0u8; *size], | |
| MintData::ExactSplSize => vec![0u8; 82], | |
| MintData::BetweenSizes(size) => vec![0u8; *size], | |
| MintData::WithAccountType(account_type) => { | |
| // Token-2022 extended mint layout: 82 base + padding, AccountType at offset 165 | |
| let mut data = vec![0u8; 170]; | |
| data[165] = *account_type; | |
| data | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In @programs/compressed-token/program/tests/mint_validation.rs around lines 38 -
62, Add a short inline comment explaining why byte index 165 is used in
MintData::WithAccountType within the to_bytes() implementation (e.g., that
Token-2022 layout places AccountType at offset 165: 82 bytes base + 83 bytes
padding), so future readers understand the magic number; update the comment near
the assignment to account_type (data[165] = *account_type) and keep it concise
referencing MintData::WithAccountType and to_bytes().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
programs/compressed-token/program/src/ctoken/close/processor.rs (1)
46-46: Clarify terminology: "rent authority" should be "compression_authority".The comment states "The rent authority can close the account" but the actual validation at lines 94-98 checks
compression.info.compression_authority, not a "rent authority". This terminology inconsistency can confuse developers, especially given that there is a separaterent_sponsorfield in the compressible extension.📝 Suggested clarification
-/// Validates that a ctoken solana account is ready to be closed. -/// The rent authority can close the account. +/// Validates that a ctoken solana account is ready to be closed via compress-and-close. +/// Only the compression_authority can compress and close the account.programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs (3)
26-26: Remove debug logging or gate behind feature flag.This debug log statement appears to be leftover development code. Logging consumes compute units and should either be removed or conditionally compiled behind a debug feature flag.
🧹 Proposed cleanup
- anchor_lang::solana_program::log::msg!("inputs {:?}", inputs);Or gate it:
+ #[cfg(feature = "debug")] anchor_lang::solana_program::log::msg!("inputs {:?}", inputs);
29-33: Clarify the flag logic and field naming.The field name
with_lamports_change_account_merkle_tree_indexsuggests a boolean presence flag (following thewith_*naming pattern), but it's being compared to0as an integer. This makes the logic harder to understand at first glance.If this is indeed a boolean-like flag, consider:
- Using a more explicit name like
has_lamports_change_account_merkle_tree_index- Or documenting why it's compared to 0 instead of using a boolean
70-80: Replace magic numbers with named constants or enum.The values
0and1foris_compressare magic numbers that reduce code readability. Consider defining named constants or using an enum to make the intent clearer.♻️ Proposed refactor
At the module level:
const IS_DECOMPRESS: u8 = 1; const IS_COMPRESS: u8 = 0;Then update the code:
let (change_lamports, is_compress) = if total_input_lamports > total_output_lamports { ( total_input_lamports.saturating_sub(total_output_lamports), - 0, + IS_COMPRESS, ) } else { ( total_output_lamports.saturating_sub(total_input_lamports), - 1, + IS_DECOMPRESS, ) };Or if the field type supports it, use a boolean directly.
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (1)
73-80: New rent parameter for caching optimization.The
&mut Noneargument is for the optionalRentsysvar cache. Looking at the shared implementation, whenNoneis passed,Rent::get()is called if needed. Since this function may callprocess_compression_top_uptwice (Compress path then potentially another branch), passing&mut Noneeach time means the rent sysvar could be fetched twice.Consider caching the rent at the function level if both compress and decompress branches with compressible extensions are possible in the same call:
♻️ Potential optimization
pub fn compress_or_decompress_ctokens( inputs: CTokenCompressionInputs, transfer_amount: &mut u64, lamports_budget: &mut u64, ) -> Result<(), ProgramError> { // ... existing setup code ... let mut current_slot = 0; + let mut rent_cache: Option<Rent> = None; // Calculate new balance using effective amount match mode { ZCompressionMode::Compress => { // ... existing code ... if let Some(compression) = ctoken.get_compressible_extension() { process_compression_top_up( &compression.info, token_account_info, &mut current_slot, transfer_amount, lamports_budget, - &mut None, + &mut rent_cache, )?; }However, since only one branch executes per call (Compress XOR Decompress XOR CompressAndClose), this optimization provides no benefit in practice. The current implementation is correct.
programs/compressed-token/program/src/ctoken/mint_to.rs (1)
28-34: Clarify the validation logic for optional payer.The validation checks
accounts.len() < 3and returnsNotEnoughAccountKeys, stating "expected at least 3 accounts". However, line 59 usesaccounts.get(2)which returnsOption, and the called functioncalculate_and_execute_compressible_top_upsacceptsOption<&AccountInfo>for the payer.This creates confusion:
- If the payer (index 2) is truly optional, the validation should be
accounts.len() < 2and the error message should say "expected at least 2 accounts"- If the payer is required, then use
&accounts[2]directly instead of.get(2)Looking at the relevant code snippet,
calculate_and_execute_compressible_top_upsreturnsCTokenError::MissingPayerwhen payer is None, suggesting the payer becomes required only when top-up is needed. However, the current validation requires 3 accounts upfront, making the Option wrapper unnecessary.The same issue exists in
process_ctoken_mint_to_checkedat lines 80-86 and 109-112.Also applies to: 56-59
programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs (1)
140-153: Use full 32-byte program ID comparison instead of first 8 bytes for account ownership validation.The hardcoded bytes
[9, 44, 54, 236, 34, 245, 23, 131]represent a heuristic that compares only the first 8 bytes of the account compression program ID. Solana's official guidance and best practices recommend comparing the full 32-byte program ID, not a prefix. While the collision probability for this heuristic is astronomically small (~1 in 2^64), using a partial match contradicts standard Solana owner validation patterns and makes the code fragile.Either:
- Change the check to compare the full 32-byte program ID, or
- If the heuristic is intentional for performance, add a detailed comment explaining: which program this is, why the first 8 bytes are sufficient in this specific context, and document the collision trade-off.
Reference: Per Solana docs, proper ownership checks should use full Pubkey equality or
find_program_address()for PDAs.programs/compressed-token/program/src/lib.rs (1)
220-222: Consider adding discriminator conflict test.The TODO at line 221 notes the need for a test verifying no discriminator conflicts with Anchor instructions. This is important since the fallback path at line 222 forwards unmatched discriminators to Anchor.
Would you like me to help create an issue to track adding a test that verifies InstructionType discriminators don't conflict with Anchor instruction discriminators?
🤖 Fix all issues with AI agents
In @programs/compressed-token/program/CLAUDE.md:
- Around line 118-139: The fenced code block containing the ASCII directory tree
in CLAUDE.md is missing a language specifier, so update the opening fence for
that block from ``` to ```text (or ```plaintext) to ensure proper rendering of
the tree; locate the block that starts with "src/" and change its opening fence
only, leaving the block contents and closing fence unchanged.
- Around line 141-168: The Markdown headings "Compressed Token Operations
(`compressed_token/`)", "CToken Operations (`ctoken/`)", and "Rent Management"
need blank lines before and after them for consistent formatting; update the
README/CLAUDE.md by inserting an empty line above each of those heading lines
and an empty line after each heading so the subsequent paragraph (e.g.,
"Operations on compressed accounts stored in Merkle trees.") is separated from
the heading.
In @programs/compressed-token/program/docs/CLAUDE.md:
- Around line 38-42: There is no blank line before the "## Navigation Tips"
heading, which violates markdownlint rules; add a single blank line between the
preceding THAW_ACCOUNT.md entry and the "## Navigation Tips" heading so headings
are surrounded by blank lines, ensuring the file's section separator is a single
empty line above the "Navigation Tips" heading to satisfy markdownlint.
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md:
- Line 73: Fix the inconsistent unordered list indentation in MINT_ACTION.md by
aligning the "- (signer) - required if create_mint is Some or DecompressMint
action present" list item with the surrounding list items (adjust leading spaces
to match the other bullet lines in the file so it is a sibling list item rather
than mis-indented). Ensure the hyphen and text use the same indentation level as
the other bullets in the "MINT_ACTION" section for consistent markdown
rendering.
- Around line 225-227: Add a blank line immediately before the heading "### Spl
mint migration" in MINT_ACTION.md so the heading is separated from the preceding
paragraph and complies with Markdown rules; locate the section where the two
lines "- cmint to spl mint migration..." and "- A way to support it..." follow
the heading and insert a single empty line above "### Spl mint migration".
In @programs/compressed-token/program/docs/ctoken/CREATE.md:
- Around line 89-91: Reword the validation bullet so it avoids adverb repetition
while keeping the same logic and symbols; e.g., clarify that if compression_only
!= 0 and the mint lacks restricted extensions, return
ErrorCode::CompressionOnlyNotAllowed — update the phrase "compression_only is
only set for mints with restricted extensions" to a more concise form like
"compression_only may be set only on mints with restricted extensions" or
"disallow setting compression_only on mints without restricted extensions."
In @programs/compressed-token/program/docs/INSTRUCTIONS.md:
- Line 106: Add a single trailing newline to the end of the INSTRUCTIONS.md file
so it ends with one newline character; place it immediately after the last line
containing "16. **Add Token Pool** - Add additional token pools for a mint (up
to 5 per mint)" and ensure there are no extra blank lines after it.
- Around line 84-89: Documentation inconsistency: INSTRUCTIONS.md lists
MintAction as "9 actions" while CLAUDE.md counts "10 action types" because
CreateCompressedMint is invoked via the create_mint field rather than an Action
enum variant. Update docs to use consistent terminology: either state "9 Action
enum variants + create_mint (CreateCompressedMint)" or change both files to "10
action types" if you want to count create_mint as an action; reference the
Action enum, MintAction, create_mint field, and CreateCompressedMint when
updating the text so both INSTRUCTIONS.md and CLAUDE.md convey the same
breakdown.
In
@programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs:
- Around line 56-69: The function split_mint_and_token_accounts assumes at least
one element and will panic on empty slices; change its signature to return
Result<(_, _), E> (or a suitable error type) and at the top check if
output_compressed_accounts.is_empty() and return Err(descriptive_error) before
calling split_at_mut(1), then update callers to propagate the error;
alternatively, if the caller truly guarantees non-empty input, add a
debug_assert!(!output_compressed_accounts.is_empty(), "expected at least one
output_compressed_account") at the start to make the contract explicit.
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Line 118: Update the unclear inline comment in check_extensions.rs that
currently reads "- Decompress: no check it restores existing state" to a
grammatically correct version, e.g. "- Decompress: no check that it restores the
existing state" (or "- Decompress: does not check that it restores the existing
state") so the intent is clear; locate the comment near the Decompress
test/section in the file (the line showing "- Decompress: no check it restores
existing state") and replace it with the corrected wording.
In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 92-97: The non-compressible branch currently assumes token_account
is pre-existing and owned by the CToken program but doesn't enforce it; add an
explicit ownership check in the None branch of the create logic: verify
token_account.owner equals the CToken program id (use the same program id
constant/validator used elsewhere in create.rs) and return a clear error if not,
or if you prefer to keep it voluntary, expand the comment on the None branch to
explicitly describe the security model and why ownership is validated later
(referencing token_account, CToken program, and the SPL initialize_account3
contrast).
In @programs/compressed-token/program/src/shared/config_account.rs:
- Around line 17-20: The hardcoded program id passed into check_owner (the
pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes()) should be
extracted to a named constant (suggest LIGHT_REGISTRY_PROGRAM_ID) to improve
readability and maintainability; define LIGHT_REGISTRY_PROGRAM_ID as the
appropriate type (e.g., a Pubkey or raw byte array using the existing pubkey!
macro) near the top of the module and replace the inline pubkey! call in the
check_owner invocation with a reference to that constant (e.g.,
LIGHT_REGISTRY_PROGRAM_ID.to_bytes() or LIGHT_REGISTRY_PROGRAM_ID as needed).
In @programs/compressed-token/program/src/shared/mint_to_token_pool.rs:
- Line 10: Summary: Centralize error conversion by using convert_program_error
in mint_to_compressed processors and update the doc comment; remove any
now-unused imports. Replace prior ad-hoc error mapping in the mint_to_token_pool
logic with a call to convert_program_error (ensure convert_program_error is
imported and invoked where previous manual conversions occurred), update the doc
comment above the processor to state it uses convert_program_error for
mint_to_compressed processors, and if LIGHT_CPI_SIGNER is no longer used after
this change remove that import to avoid an unused-import warning; apply the same
replacement to the other occurrence of the manual conversion in this file so
both locations use convert_program_error.
In @programs/compressed-token/program/tests/mint_validation.rs:
- Around line 322-358: The test currently executes each case twice (first loop
calls is_valid_mint for reporting, then run_test_case runs them again);
consolidate by running each case only once: in the first loop call
run_test_case(tc) or call is_valid_mint(tc.owner.pubkey(), &tc.data.to_bytes())
and capture the Result alongside tc (e.g., a Vec of (tc, result)), use that
captured result for printing and for assertions so you can remove the second
loop; update references to is_valid_mint, run_test_case, test_cases, and
Expected to use the single collected outcome and fail the test if any captured
result indicates failure.
| ``` | ||
| src/ | ||
| ├── compressed_token/ # Operations on compressed accounts (in Merkle trees) | ||
| │ ├── mint_action/ # MintAction instruction (103) | ||
| │ └── transfer2/ # Transfer2 instruction (101) | ||
| ├── compressible/ # Rent management | ||
| │ ├── claim.rs # Claim instruction (104) | ||
| │ └── withdraw_funding_pool.rs # WithdrawFundingPool instruction (105) | ||
| ├── ctoken/ # Operations on CToken Solana accounts (decompressed) | ||
| │ ├── approve_revoke.rs # CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) | ||
| │ ├── burn.rs # CTokenBurn (8), CTokenBurnChecked (15) | ||
| │ ├── close/ # CloseTokenAccount instruction (9) | ||
| │ ├── create.rs # CreateTokenAccount instruction (18) | ||
| │ ├── create_ata.rs # CreateAssociatedCTokenAccount (100, 102) | ||
| │ ├── freeze_thaw.rs # CTokenFreezeAccount (10), CTokenThawAccount (11) | ||
| │ ├── mint_to.rs # CTokenMintTo (7), CTokenMintToChecked (14) | ||
| │ └── transfer/ # CTokenTransfer (3), CTokenTransferChecked (12) | ||
| ├── extensions/ # Extension handling | ||
| ├── shared/ # Common utilities | ||
| ├── convert_account_infos.rs | ||
| └── lib.rs # Entry point and instruction dispatch | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Add language specifier to fenced code block.
The directory structure block should have a language specifier for proper rendering. Use text or plaintext for ASCII tree structures.
Proposed fix
-```
+```text
src/
├── compressed_token/ # Operations on compressed accounts (in Merkle trees)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| src/ | |
| ├── compressed_token/ # Operations on compressed accounts (in Merkle trees) | |
| │ ├── mint_action/ # MintAction instruction (103) | |
| │ └── transfer2/ # Transfer2 instruction (101) | |
| ├── compressible/ # Rent management | |
| │ ├── claim.rs # Claim instruction (104) | |
| │ └── withdraw_funding_pool.rs # WithdrawFundingPool instruction (105) | |
| ├── ctoken/ # Operations on CToken Solana accounts (decompressed) | |
| │ ├── approve_revoke.rs # CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) | |
| │ ├── burn.rs # CTokenBurn (8), CTokenBurnChecked (15) | |
| │ ├── close/ # CloseTokenAccount instruction (9) | |
| │ ├── create.rs # CreateTokenAccount instruction (18) | |
| │ ├── create_ata.rs # CreateAssociatedCTokenAccount (100, 102) | |
| │ ├── freeze_thaw.rs # CTokenFreezeAccount (10), CTokenThawAccount (11) | |
| │ ├── mint_to.rs # CTokenMintTo (7), CTokenMintToChecked (14) | |
| │ └── transfer/ # CTokenTransfer (3), CTokenTransferChecked (12) | |
| ├── extensions/ # Extension handling | |
| ├── shared/ # Common utilities | |
| ├── convert_account_infos.rs | |
| └── lib.rs # Entry point and instruction dispatch | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
118-118: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/CLAUDE.md around lines 118 - 139, The
fenced code block containing the ASCII directory tree in CLAUDE.md is missing a
language specifier, so update the opening fence for that block from ``` to
```text (or ```plaintext) to ensure proper rendering of the tree; locate the
block that starts with "src/" and change its opening fence only, leaving the
block contents and closing fence unchanged.
| ## Compressed Token Operations (`compressed_token/`) | ||
| Operations on compressed accounts stored in Merkle trees. | ||
|
|
||
| - **`mint_action/`** - MintAction instruction for compressed mint management | ||
| - `processor.rs` - Main instruction processor | ||
| - `accounts.rs` - Account validation and parsing | ||
| - `actions/` - Individual action handlers (create_mint, mint_to, decompress_mint, etc.) | ||
| - **`transfer2/`** - Unified transfer instruction supporting multiple modes | ||
| - `compression/` - Compress & decompress functionality | ||
| - `ctoken/` - CToken-specific compression (compress_and_close.rs, decompress.rs, etc.) | ||
| - `spl.rs` - SPL token compression | ||
| - `processor.rs` - Main instruction processor | ||
| - `accounts.rs` - Account validation and parsing | ||
| - **`mint_action/`** - Mint tokens to compressed/decompressed accounts | ||
| - **`ctoken_approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) | ||
| - **`ctoken_mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14) | ||
| - **`ctoken_burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15) | ||
| - **`ctoken_freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11) | ||
|
|
||
| ## Rent Management | ||
| ## CToken Operations (`ctoken/`) | ||
| Operations on CToken Solana accounts (decompressed compressed tokens). | ||
|
|
||
| - **`create.rs`** - Create regular ctoken accounts with optional compressible extension | ||
| - **`create_ata.rs`** - Create deterministic ATA accounts | ||
| - **`close/`** - Close ctoken accounts, handle rent distribution | ||
| - **`transfer/`** - SPL-compatible transfers between decompressed accounts | ||
| - `default.rs` - CTokenTransfer (discriminator: 3) | ||
| - `checked.rs` - CTokenTransferChecked (discriminator: 12) | ||
| - `shared.rs` - Common transfer utilities | ||
| - **`approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) | ||
| - **`mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14) | ||
| - **`burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15) | ||
| - **`freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor: Add blank lines around headings for consistent markdown formatting.
Per markdown best practices, headings should be surrounded by blank lines. The headings at "Compressed Token Operations", "CToken Operations", and "Rent Management" are missing the preceding blank line.
Proposed fix (example for line 141)
Compressed Token Operations (compressed_token/)
-Operations on compressed accounts stored in Merkle trees.
+
+Operations on compressed accounts stored in Merkle trees.
</details>
> Committable suggestion skipped: line range outside the PR's diff.
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>
141-141: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
---
155-155: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
In @programs/compressed-token/program/CLAUDE.md around lines 141 - 168, The
Markdown headings "Compressed Token Operations (compressed_token/)", "CToken
Operations (ctoken/)", and "Rent Management" need blank lines before and after
them for consistent formatting; update the README/CLAUDE.md by inserting an
empty line above each of those heading lines and an empty line after each
heading so the subsequent paragraph (e.g., "Operations on compressed accounts
stored in Merkle trees.") is separated from the heading.
</details>
<!-- fingerprinting:phantom:medusa:ocelot -->
<!-- This is an auto-generated comment by CodeRabbit -->
|
|
||
| ## Navigation Tips | ||
| - Start with `../CLAUDE.md` for the instruction index and overview | ||
| - Use `ACCOUNTS.md` for account structure reference | ||
| - Use `INSTRUCTIONS.md` for discriminator reference and instruction index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Add blank line before "Navigation Tips" heading.
Per the markdownlint hint, headings should be surrounded by blank lines. Line 38 (THAW_ACCOUNT.md entry) directly precedes the ## Navigation Tips heading.
📝 Suggested fix
- `FREEZE_ACCOUNT.md` - Freeze CToken account
- `THAW_ACCOUNT.md` - Thaw frozen CToken account
+
## Navigation Tips
- Start with `../CLAUDE.md` for the instruction index and overview🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
39-39: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/CLAUDE.md around lines 38 - 42, There
is no blank line before the "## Navigation Tips" heading, which violates
markdownlint rules; add a single blank line between the preceding
THAW_ACCOUNT.md entry and the "## Navigation Tips" heading so headings are
surrounded by blank lines, ensuring the file's section separator is a single
empty line above the "Navigation Tips" heading to satisfy markdownlint.
| Optional accounts (based on configuration): | ||
| 2. mint_signer | ||
| - (signer) - required if create_mint is Some or CreateSplMint action present | ||
| - (signer) - required if create_mint is Some or DecompressMint action present |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor markdown formatting issue.
Static analysis flagged unordered list indentation at this line. Consider adjusting indentation for consistency with the rest of the document.
🔧 Suggested fix
2. mint_signer
- - (signer) - required if create_mint is Some or DecompressMint action present
+- (signer) - required if create_mint is Some or DecompressMint action present🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
73-73: Unordered list indentation
Expected: 0; Actual: 3
(MD007, ul-indent)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md at
line 73, Fix the inconsistent unordered list indentation in MINT_ACTION.md by
aligning the "- (signer) - required if create_mint is Some or DecompressMint
action present" list item with the surrounding list items (adjust leading spaces
to match the other bullet lines in the file so it is a sibling list item rather
than mis-indented). Ensure the hyphen and text use the same indentation level as
the other bullets in the "MINT_ACTION" section for consistent markdown
rendering.
| ### Spl mint migration | ||
| - cmint to spl mint migration is unimplemented and not planned. | ||
| - A way to support it in the future would require a new instruction that creates an spl mint in the mint pda solana account and mints the supply to the spl interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Add blank line before heading for markdown compliance.
Static analysis flagged missing blank line before this heading.
🔧 Suggested fix
- `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts
+
### Spl mint migrationCommittable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
225-225: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
In @programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md
around lines 225 - 227, Add a blank line immediately before the heading "### Spl
mint migration" in MINT_ACTION.md so the heading is separated from the preceding
paragraph and complies with Markdown rules; locate the section where the two
lines "- cmint to spl mint migration..." and "- A way to support it..." follow
the heading and insert a single empty line above "### Spl mint migration".
| // Compress/Decompress don't need additional validation here: | ||
| // - Compress: blocked by check_mint_extensions when outputs exist | ||
| // - Decompress: bypassed (restoring existing state) | ||
| // - Decompress: no check it restores existing state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor comment grammar issue.
The comment reads "no check it restores existing state" which is slightly unclear.
📝 Suggested fix
- // - Decompress: no check it restores existing state
+ // - Decompress: no check - it restores existing state📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // - Decompress: no check it restores existing state | |
| // - Decompress: no check - it restores existing state |
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
at line 118, Update the unclear inline comment in check_extensions.rs that
currently reads "- Decompress: no check it restores existing state" to a
grammatically correct version, e.g. "- Decompress: no check that it restores the
existing state" (or "- Decompress: does not check that it restores the existing
state") so the intent is clear; locate the comment near the Decompress
test/section in the file (the line showing "- Decompress: no check it restores
existing state") and replace it with the corrected wording.
| check_owner( | ||
| &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(), | ||
| config_account, | ||
| )?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider extracting the hardcoded pubkey to a named constant.
The Light registry program ID Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX appears inline. Extracting it to a named constant (e.g., LIGHT_REGISTRY_PROGRAM_ID) would improve readability and make it easier to update if needed.
💡 Suggested improvement
+const LIGHT_REGISTRY_PROGRAM_ID: [u8; 32] =
+ pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes();
+
#[profile]
#[inline(always)]
pub fn parse_config_account(
config_account: &AccountInfo,
) -> Result<&CompressibleConfig, ProgramError> {
// Validate config account owner
check_owner(
- &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(),
+ &LIGHT_REGISTRY_PROGRAM_ID,
config_account,
)?;🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/config_account.rs around lines
17 - 20, The hardcoded program id passed into check_owner (the
pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes()) should be
extracted to a named constant (suggest LIGHT_REGISTRY_PROGRAM_ID) to improve
readability and maintainability; define LIGHT_REGISTRY_PROGRAM_ID as the
appropriate type (e.g., a Pubkey or raw byte array using the existing pubkey!
macro) near the top of the module and replace the inline pubkey! call in the
check_owner invocation with a reference to that constant (e.g.,
LIGHT_REGISTRY_PROGRAM_ID.to_bytes() or LIGHT_REGISTRY_PROGRAM_ID as needed).
| /// Internal: Map SPL Token error code (0-18) to ErrorCode. | ||
| fn convert_spl_token_error_code(code: u32) -> anchor_lang::prelude::ProgramError { | ||
| let error_code = match code { | ||
| 0 => ErrorCode::NotRentExempt, | ||
| 1 => ErrorCode::InsufficientFunds, | ||
| 2 => ErrorCode::InvalidMint, | ||
| 3 => ErrorCode::MintMismatch, | ||
| 4 => ErrorCode::OwnerMismatch, | ||
| 5 => ErrorCode::FixedSupply, | ||
| 6 => ErrorCode::AlreadyInUse, | ||
| 7 => ErrorCode::InvalidNumberOfProvidedSigners, | ||
| 8 => ErrorCode::InvalidNumberOfRequiredSigners, | ||
| 9 => ErrorCode::UninitializedState, | ||
| 10 => ErrorCode::NativeNotSupported, | ||
| 11 => ErrorCode::NonNativeHasBalance, | ||
| 12 => ErrorCode::InvalidInstruction, | ||
| 13 => ErrorCode::InvalidState, | ||
| 14 => ErrorCode::Overflow, | ||
| 15 => ErrorCode::AuthorityTypeNotSupported, | ||
| 16 => ErrorCode::MintHasNoFreezeAuthority, | ||
| 17 => ErrorCode::AccountFrozen, | ||
| 18 => ErrorCode::MintDecimalsMismatch, | ||
| // Pass through unknown/higher codes with standard +6900 offset | ||
| _ => return anchor_lang::prelude::ProgramError::Custom(code + 6900), | ||
| }; | ||
| error_code.into() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the ErrorCode enum definition
echo "=== Looking for ErrorCode enum in programs/compressed-token/anchor/src/lib.rs ==="
cat programs/compressed-token/anchor/src/lib.rs | head -200
echo ""
echo "=== Searching for ErrorCode enum definition ==="
rg -A 100 "^pub enum ErrorCode" programs/compressed-token/anchor/src/lib.rsRepository: Lightprotocol/light-protocol
Length of output: 303
🌐 Web query:
Light Protocol anchor_compressed_token ErrorCode enum definition github
💡 Result:
Found the enum definition (from the Light Protocol compressed-token crate). Variants with their repr(u32) values:
PublicKeyAmountMissmatch = 0
ComputeInputSumFailed = 1
ComputeOutputSumFailed = 2
ComputeCompressSumFailed = 3
ComputeDecompressSumFailed = 4
SumCheckFailed = 5
DecompressRecipientUndefinedForDecompress = 6
CompressedPdaUndefinedForDecompress = 7
DeCompressAmountUndefinedForDecompress = 8
CompressedPdaUndefinedForCompress = 9
DeCompressAmountUndefinedForCompress = 10
DelegateSignerCheckFailed = 11
MintTooLarge = 12
SplTokenSupplyMismatch = 13
HeapMemoryCheckFailed = 14
InstructionNotCallable = 15
ArithmeticUnderflow = 16
HashToFieldError = 17
InvalidAuthorityMint = 18
InvalidFreezeAuthority = 19
InvalidDelegateIndex = 20
TokenPoolPdaUndefined = 21
IsTokenPoolPda = 22
InvalidTokenPoolPda = 23
NoInputTokenAccountsProvided = 24
NoInputsProvided = 25
MintHasNoFreezeAuthority = 26
MintWithInvalidExtension = 27
InsufficientTokenAccountBalance = 28
InvalidTokenPoolBump = 29
FailedToDecompress = 30
FailedToBurnSplTokensFromTokenPool = 31
NoMatchingBumpFound = 32
NoAmount = 33
AmountsAndAmountProvided = 34
Source: docs for the light_compressed_token crate (enum ErrorCode). [1]
Repository: Light Protocol GitHub. [2]
References:
[1] docs.rs — light_compressed_token enum ErrorCode.
[2] Lightprotocol / light-protocol GitHub.
Fix non-existent ErrorCode variant references causing compilation failure.
The convert_spl_token_error_code function references ErrorCode variants (NotRentExempt, InsufficientFunds, InvalidMint, MintMismatch, OwnerMismatch, FixedSupply, AlreadyInUse, InvalidNumberOfProvidedSigners, InvalidNumberOfRequiredSigners, UninitializedState, NativeNotSupported, NonNativeHasBalance, InvalidInstruction, InvalidState, Overflow, AuthorityTypeNotSupported, MintHasNoFreezeAuthority, AccountFrozen, MintDecimalsMismatch) that do not exist in the anchor_compressed_token::ErrorCode enum. The actual enum contains completely different variants specific to the compressed-token program (PublicKeyAmountMissmatch, ComputeInputSumFailed, ComputeOutputSumFailed, etc. with codes 0-34+). This mapping logic cannot compile. Either map to existing ErrorCode variants or clarify the intent of this SPL Token error conversion.
| }; | ||
|
|
||
| use crate::LIGHT_CPI_SIGNER; | ||
| use crate::{shared::convert_program_error, LIGHT_CPI_SIGNER}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Good refactoring to centralize error conversion.
Using the shared convert_program_error utility improves consistency across the codebase. The doc comment update at line 13 correctly reflects that this is now used specifically by mint_to_compressed processors.
Also applies to: 58-58
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/mint_to_token_pool.rs at line
10, Summary: Centralize error conversion by using convert_program_error in
mint_to_compressed processors and update the doc comment; remove any now-unused
imports. Replace prior ad-hoc error mapping in the mint_to_token_pool logic with
a call to convert_program_error (ensure convert_program_error is imported and
invoked where previous manual conversions occurred), update the doc comment
above the processor to state it uses convert_program_error for
mint_to_compressed processors, and if LIGHT_CPI_SIGNER is no longer used after
this change remove that import to avoid an unused-import warning; apply the same
replacement to the other occurrence of the manual conversion in this file so
both locations use convert_program_error.
| println!( | ||
| "\nRunning {} test cases for is_valid_mint:\n", | ||
| test_cases.len() | ||
| ); | ||
|
|
||
| let mut passed = 0; | ||
| let mut failed = 0; | ||
|
|
||
| for tc in &test_cases { | ||
| print!(" {:60} ... ", tc.description); | ||
| let data = tc.data.to_bytes(); | ||
| let result = is_valid_mint(tc.owner.pubkey(), &data); | ||
|
|
||
| let success = match tc.expected { | ||
| Expected::Valid => result.as_ref().map(|v| *v).unwrap_or(false), | ||
| Expected::Invalid => result.as_ref().map(|v| !*v).unwrap_or(false), | ||
| Expected::IncorrectProgramId => { | ||
| result.as_ref().err() == Some(&ProgramError::IncorrectProgramId) | ||
| } | ||
| }; | ||
|
|
||
| if success { | ||
| println!("ok"); | ||
| passed += 1; | ||
| } else { | ||
| println!("FAILED (got {:?})", result); | ||
| failed += 1; | ||
| } | ||
| } | ||
|
|
||
| println!("\nResults: {} passed, {} failed\n", passed, failed); | ||
|
|
||
| // Now run assertions to fail the test if any failed | ||
| for tc in &test_cases { | ||
| run_test_case(tc); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor optimization: Consider removing duplicate test execution.
The test iterates through test cases twice - once for status reporting (lines 330-350) and again for assertions (lines 355-357). While this provides nice output, it doubles execution time unnecessarily.
♻️ Suggested consolidation
- for tc in &test_cases {
- print!(" {:60} ... ", tc.description);
- let data = tc.data.to_bytes();
- let result = is_valid_mint(tc.owner.pubkey(), &data);
-
- let success = match tc.expected {
- Expected::Valid => result.as_ref().map(|v| *v).unwrap_or(false),
- Expected::Invalid => result.as_ref().map(|v| !*v).unwrap_or(false),
- Expected::IncorrectProgramId => {
- result.as_ref().err() == Some(&ProgramError::IncorrectProgramId)
- }
- };
-
- if success {
- println!("ok");
- passed += 1;
- } else {
- println!("FAILED (got {:?})", result);
- failed += 1;
- }
- }
-
- println!("\nResults: {} passed, {} failed\n", passed, failed);
-
- // Now run assertions to fail the test if any failed
- for tc in &test_cases {
- run_test_case(tc);
- }
+ for tc in &test_cases {
+ print!(" {:60} ... ", tc.description);
+ // Use std::panic::catch_unwind or check inline
+ let result = std::panic::catch_unwind(|| run_test_case(tc));
+ if result.is_ok() {
+ println!("ok");
+ passed += 1;
+ } else {
+ println!("FAILED");
+ failed += 1;
+ }
+ }
+
+ println!("\nResults: {} passed, {} failed\n", passed, failed);
+ assert_eq!(failed, 0, "Some test cases failed");🤖 Prompt for AI Agents
In @programs/compressed-token/program/tests/mint_validation.rs around lines 322
- 358, The test currently executes each case twice (first loop calls
is_valid_mint for reporting, then run_test_case runs them again); consolidate by
running each case only once: in the first loop call run_test_case(tc) or call
is_valid_mint(tc.owner.pubkey(), &tc.data.to_bytes()) and capture the Result
alongside tc (e.g., a Vec of (tc, result)), use that captured result for
printing and for assertions so you can remove the second loop; update references
to is_valid_mint, run_test_case, test_cases, and Expected to use the single
collected outcome and fail the test if any captured result indicates failure.
- Move compressedAddress from top-level to mint.metadata - Remove tokenPoolBump and tokenPoolIndex from instruction data - Update all instruction builders and tests accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs (1)
118-145: Replace panic with proper error or remove unreachable code.After a successful
cache.insert()on line 115 (which returns early on failure), the mint should always be found in the cache on line 118. Theelsebranch on lines 141-144 appears unreachable, yet contains a panic with a TODO comment indicating uncertainty.Two issues:
- Panic is inappropriate: If this code path is somehow reachable, it should return a proper
ProgramErrorinstead of panicking- Dead code: If the branch is truly unreachable, it should be removed or replaced with an assertion in debug builds
🔧 Suggested fix
If the branch is unreachable, remove it:
- if let Some(checks) = cache.get_by_key(&mint_index) { + let checks = cache + .get_by_key(&mint_index) + .expect("mint must exist in cache after successful insert"); // CompressAndClose with restricted extensions requires CompressedOnly output. // Compress/Decompress don't need additional validation here: // - Compress: blocked by check_mint_extensions when outputs exist - // - Decompress: no check it restores existing state + // - Decompress: no check, it restores existing state if checks.has_restricted_extensions && compression.mode.is_compress_and_close() { let output_idx = compression.get_compressed_token_account_index()?; let has_compressed_only = inputs .out_tlv .as_ref() .and_then(|tlvs| tlvs.get(output_idx as usize)) .map(|tlv| { tlv.iter() .any(|e| matches!(e, ZExtensionInstructionData::CompressedOnly(_))) }) .unwrap_or(false); if !has_compressed_only { msg!("Mint has restricted extensions - CompressedOnly output required"); return Err( ErrorCode::CompressAndCloseMissingCompressedOnlyExtension.into() ); } } - } else { - // TODO: double check. - panic!("Mint cache: compression: mint index not found"); - }Or if there's genuine uncertainty, return an error:
} else { - // TODO: double check. - panic!("Mint cache: compression: mint index not found"); + msg!("Mint cache: compression: mint index not found after insert"); + return Err(ErrorCode::MintCacheCapacityExceeded.into()); }Based on coding guidelines requiring proper error handling in Solana programs.
programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs (1)
43-47: TODO flagged: Idempotent behavior needs test coverage.The TODO on line 43 indicates missing test coverage for the idempotent exit path. This is a valid concern—silent success without observable side effects can mask bugs if not properly tested.
Would you like me to open an issue to track adding a test case that verifies the instruction exits silently when
is_idempotent()is true andcmint_decompressedis false?programs/compressed-token/program/src/ctoken/close/processor.rs (1)
279-287: Clarify the magic number in error conversion.The
+ 6000offset for the error code lacks documentation. While I understand this likely aligns with Anchor's custom error offset convention, a brief comment would help future maintainers understand why this specific value is used.📝 Suggested documentation
fn finalize_account_closure(accounts: &CloseTokenAccountAccounts<'_>) -> Result<(), ProgramError> { unsafe { accounts.token_account.assign(&[0u8; 32]); } match accounts.token_account.resize(0) { Ok(()) => Ok(()), - Err(e) => Err(ProgramError::Custom(u64::from(e) as u32 + 6000)), + // 6000 offset aligns with Anchor's custom error code base + Err(e) => Err(ProgramError::Custom(u64::from(e) as u32 + 6000)), } }
🤖 Fix all issues with AI agents
In @program-libs/compressible/src/rent/config.rs:
- Around line 9-12: The rent constants in compressible::rent::config are 10x too
low; update the constant values so BASE_RENT = 1_220 (u16), RENT_PER_BYTE = 10
(u8), and SLOTS_PER_EPOCH = 432_000 (u64) in the file where those symbols are
defined (BASE_RENT, RENT_PER_BYTE, SLOTS_PER_EPOCH), and remove or update the
"TODO: multiply by 10" comments and any misleading epoch-duration comment to
reflect the corrected values.
In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs:
- Line 86: Implement the TODO in amount_from_account_info: first validate the
AccountInfo data length and check the account_type byte (offset 165 when token
extensions are present) equals ACCOUNT_TYPE_TOKEN_ACCOUNT before calling
amount_from_slice; if the data is too short or the account_type mismatches,
return an appropriate error or None (matching the function's error convention)
instead of extracting the amount. Use the existing symbols
amount_from_account_info, amount_from_slice, and ACCOUNT_TYPE_TOKEN_ACCOUNT to
locate where to add the checks and ensure you handle both missing extensions
(insufficient length) and wrong account_type cases consistently with surrounding
error handling.
- Around line 83-85: The borrow failure from account_info.try_borrow_data() is
being mapped to ZeroCopyError::Size which is incorrect; change the error mapping
to a dedicated borrow-failure variant instead of Size — either add
ZeroCopyError::BorrowFailed to the ZeroCopyError enum (mirroring patterns like
AccountError::BorrowAccountDataFailed) and return that on map_err, or
propagate/convert the error to the existing CTokenError::CMintBorrowFailed (as
used elsewhere, e.g., compressed_mint.rs) so try_borrow_data() failures are
represented semantically rather than as Size.
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs:
- Around line 188-190: The deserializer currently accepts any account_type byte
(bytes[0]) and skips validating it against ACCOUNT_TYPE_TOKEN_ACCOUNT; change
the logic in zero_copy.rs so that when bytes is not empty and bytes[0] !=
ACCOUNT_TYPE_TOKEN_ACCOUNT you return a clear error (add or use an
InvalidAccountType variant on ZeroCopyError if missing) instead of proceeding,
e.g., validate account_type before using it and return
ZeroCopyError::InvalidAccountType with context when the check fails to prevent
type confusion and layout misinterpretation.
- Around line 229-231: The mutable zero-copy deserialization path reads
account_type from bytes[0] but does not validate it (same missing check as the
immutable path), so add the same account_type validation: in the mutable
deserializer (the function performing zero-copy mutable deserialization), check
that bytes is non-empty and that bytes[0] equals ACCOUNT_TYPE_TOKEN_ACCOUNT and
return the appropriate error (or result::Err) if it does not, before proceeding
to construct the mutable view; reference the bytes[0] read and the
ACCOUNT_TYPE_TOKEN_ACCOUNT constant and mirror the validation logic used in the
immutable deserialization path.
- Line 113: new_zero_copy is missing a check that prevents overwriting an
already-initialized account; before writing the base struct, read the state byte
at offset 108 from the account data and return a
ZeroCopyError::AlreadyInitialized if that byte != 0. Add an AlreadyInitialized
variant to the ZeroCopyError enum (if absent) and use it in new_zero_copy to
abort initialization when the state byte indicates prior initialization,
ensuring you perform the read using the same account data slice used later for
writing.
- Around line 161-166: Remove the misleading TODO that says "remaining bytes
should be checked to be zero" and replace it with a short clarifying comment
indicating that the code enforces exact buffer sizing by requiring remaining to
be empty; update the comment near the ACCOUNT_TYPE_TOKEN_ACCOUNT branch and the
`if !remaining.is_empty() { return
Err(light_zero_copy::errors::ZeroCopyError::Size); }` check to state that any
extra bytes are invalid and must not exist rather than being zero-checked.
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the core::panic import and the panic!() call in
check_extensions.rs and replace it with proper error propagation: return a
Result::Err containing an appropriate Solana program error (e.g.,
Err(ProgramError::InvalidArgument) or a domain-specific error variant such as
CompressedTokenError::InvalidExtension converted via Into<ProgramError>), and
optionally log context with msg!("...") before returning; ensure the function
signature returns a Result so callers can handle the error instead of the
program aborting.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs:
- Around line 119-124: The TODO asking to double-check the decompress check
should be resolved: confirm and keep the validation that when
compression.mode.is_decompress() and
compression_to_input[compression_index].is_some() the code returns
ErrorCode::CompressedOnlyRequiresCTokenDecompress (i.e., CompressedOnly inputs
must decompress to CToken accounts), remove the inline TODO comment from the if
condition in mod.rs, and add a unit test that constructs a CompressedOnly input
where compression.mode.is_decompress() is true and
compression_to_input[compression_index] is Some(...) to assert the function
returns the CompressedOnlyRequiresCTokenDecompress error; reference the
compression.mode.is_decompress() call, compression_to_input array access,
CompressedOnly concept, and ErrorCode::CompressedOnlyRequiresCTokenDecompress
when locating code to change and tests to add.
In
@programs/compressed-token/program/src/compressed_token/transfer2/processor.rs:
- Around line 143-147: The version check currently verifies all
inputs.out_token_data have version == 3 but returns the wrong error; update the
branch that returns CTokenError::CompressedTokenAccountTlvUnimplemented to
instead return ErrorCode::TlvRequiresVersion3 (mirror the behavior in
check_extensions.rs) so that failing the version validation for TLV outputs uses
the correct error variant; leave the allowed calculation
(inputs.out_token_data.iter().all(|c| c.version == 3)) intact and only change
the returned error value.
In @programs/compressed-token/program/src/ctoken/close/processor.rs:
- Around line 85-87: The guard checking accounts.token_account.key() ==
accounts.destination.key() inside the COMPRESS_AND_CLOSE branch is redundant
because the same check (token_account == destination) is already performed
before branching; remove the duplicate condition and its
Err(ProgramError::InvalidAccountData) return from the COMPRESS_AND_CLOSE
handling (i.e., delete the if checking accounts.token_account.key() ==
accounts.destination.key() and its error path) so the code relies on the earlier
validation.
- Line 195: The code uses light_account_checks::check_owner for validating
LIGHT_CPI_SIGNER.owner; replace this with the shared utility
check_token_program_owner() from shared/owner_validation.rs (update the import
and call in processor.rs and the matching usage in accounts.rs) so owner
validation follows the centralized pattern; if you need stricter
program-id-specific checks for LIGHT_CPI_SIGNER, implement a small wrapper
function in shared/owner_validation.rs (e.g., check_light_cpi_signer_owner())
that performs the extra program_id comparison and use that wrapper instead of
calling light_account_checks::check_owner directly.
In @programs/compressed-token/program/src/ctoken/create.rs:
- Around line 91-96: Before calling CToken::new_zero_copy in
initialize_ctoken_account, verify the target account is uninitialized: read the
zero-copy CToken account header (or use the existing is_initialized() helper)
and ensure state == Uninitialized (0); if it is already initialized return an
appropriate error instead of proceeding. Apply this check in both the
compressible-path branch and the non-compressible-path branch (the code that
currently calls CToken::new_zero_copy without validation), so you never call
new_zero_copy on an initialized account.
In @programs/compressed-token/program/src/shared/compressible_top_up.rs:
- Line 13: The import ordering is wrong for the symbol LIGHT_CPI_SIGNER; move
the line `use crate::LIGHT_CPI_SIGNER;` to the crate-level import group (after
external crate imports and before super:: or self:: imports) so it follows the
project's import order convention and removes the lint warning; ensure it is
grouped with other `crate::` imports if present and adjust surrounding use
statements accordingly.
- Line 71: The inline TODO comment "// TODO: add from account info" next to the
check_owner(&LIGHT_CPI_SIGNER.program_id, ctoken)? call is ambiguous; either
remove it if there's no planned change, or replace it with a clear note
describing the intended work (e.g., "TODO: extract owner validation into
from_account_info-based constructor to accept AccountInfo instead of program_id
and ctoken"). Update the comment to reference the exact target (check_owner,
LIGHT_CPI_SIGNER, and ctoken) and the expected API change or timeline, or simply
delete the comment if it's stale.
| pub const BASE_RENT: u16 = 128; // TODO: multiply by 10 | ||
| pub const RENT_PER_BYTE: u8 = 1; // TODO: multiply by 10 | ||
| // Epoch duration: 1.5 hours, 90 minutes * 60 seconds / 0.4 seconds per slot = 13,500 slots per epoch | ||
| pub const SLOTS_PER_EPOCH: u64 = 13500; // TODO: multiply by 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Rent constants are 10x too low per documented specification.
According to the rent configuration learnings, these constants should be:
BASE_RENT = 1_220(currently 128)RENT_PER_BYTE = 10(currently 1)SLOTS_PER_EPOCH = 432_000(currently 13,500)
The TODO comments indicate you're aware of the 10x scaling requirement, but these values directly affect rent calculations throughout the protocol. Accounts will be marked compressible far too early, and rent collection will be orders of magnitude off. This must be corrected before merge.
🐛 Proposed fix for rent constants
-pub const BASE_RENT: u16 = 128; // TODO: multiply by 10
-pub const RENT_PER_BYTE: u8 = 1; // TODO: multiply by 10
+pub const BASE_RENT: u16 = 1_220;
+pub const RENT_PER_BYTE: u8 = 10;
-pub const SLOTS_PER_EPOCH: u64 = 13500; // TODO: multiply by 10
+pub const SLOTS_PER_EPOCH: u64 = 432_000;Based on learnings from program-libs/compressible/docs/RENT.md, rent is calculated per epoch as: base_rent + (data_bytes × lamports_per_byte_per_epoch), and slot-per-epoch duration determines how frequently rent is assessed.
🤖 Prompt for AI Agents
In @program-libs/compressible/src/rent/config.rs around lines 9 - 12, The rent
constants in compressible::rent::config are 10x too low; update the constant
values so BASE_RENT = 1_220 (u16), RENT_PER_BYTE = 10 (u8), and SLOTS_PER_EPOCH
= 432_000 (u64) in the file where those symbols are defined (BASE_RENT,
RENT_PER_BYTE, SLOTS_PER_EPOCH), and remove or update the "TODO: multiply by 10"
comments and any misleading epoch-duration comment to reflect the corrected
values.
| let data = account_info | ||
| .try_borrow_data() | ||
| .map_err(|_| ZeroCopyError::Size)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the ZeroCopyError definition
find . -type f -name "*.rs" | xargs grep -l "enum ZeroCopyError" | head -5Repository: Lightprotocol/light-protocol
Length of output: 110
🏁 Script executed:
# Search for the specific file mentioned in the review
find . -type f -path "*/ctoken-interface/src/state/ctoken/ctoken_struct.rs"Repository: Lightprotocol/light-protocol
Length of output: 137
🏁 Script executed:
# Find error definitions in the zero-copy module
find . -type f -path "*zero-copy*" -name "*.rs" | grep -E "(error|lib)" | head -10Repository: Lightprotocol/light-protocol
Length of output: 602
🏁 Script executed:
# Examine the ZeroCopyError enum definition
cat -n ./program-libs/zero-copy/src/errors.rsRepository: Lightprotocol/light-protocol
Length of output: 5775
🏁 Script executed:
# Examine the specific code in ctoken_struct.rs around lines 83-85
cat -n ./program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs | head -120 | tail -50Repository: Lightprotocol/light-protocol
Length of output: 1847
🏁 Script executed:
# Check if there are other usages of try_borrow_data in the codebase and how errors are handled
rg "try_borrow_data\(\)" -A 2 -B 2 program-libs/ | head -100Repository: Lightprotocol/light-protocol
Length of output: 4238
🏁 Script executed:
# Check if CTokenError exists and what it looks like
find . -type f -name "*.rs" | xargs grep -l "enum CTokenError\|struct CTokenError" | head -5Repository: Lightprotocol/light-protocol
Length of output: 116
🏁 Script executed:
# Check the ctoken-interface module structure
find ./program-libs/ctoken-interface -type f -name "*.rs" | head -20Repository: Lightprotocol/light-protocol
Length of output: 1551
🏁 Script executed:
# Look for error handling patterns in ctoken-interface
find ./program-libs/ctoken-interface -type f -name "*.rs" | xargs grep -l "Error\|error" | head -10Repository: Lightprotocol/light-protocol
Length of output: 754
🏁 Script executed:
# Check the CTokenError definition
cat -n ./program-libs/ctoken-interface/src/error.rsRepository: Lightprotocol/light-protocol
Length of output: 12351
Map borrow failures to a dedicated error variant, not Size.
The ZeroCopyError::Size variant (error code 15010) semantically represents data that's malformed or too small—not access failures. Mapping BorrowError from try_borrow_data() to Size conflates two distinct failure modes: the data cannot be accessed (already borrowed mutably elsewhere) versus the data structure is invalid. This masks the true cause during debugging.
The codebase establishes a clear pattern: dedicated borrow failure variants exist elsewhere (CTokenError::CMintBorrowFailed, AccountError::BorrowAccountDataFailed). Add a BorrowFailed variant to ZeroCopyError (consistent with the error hierarchy in this crate), or return CTokenError which already has CMintBorrowFailed (error code 18046) for exactly this scenario. See how compressed_mint.rs handles the same operation correctly.
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs around lines
83 - 85, The borrow failure from account_info.try_borrow_data() is being mapped
to ZeroCopyError::Size which is incorrect; change the error mapping to a
dedicated borrow-failure variant instead of Size — either add
ZeroCopyError::BorrowFailed to the ZeroCopyError enum (mirroring patterns like
AccountError::BorrowAccountDataFailed) and return that on map_err, or
propagate/convert the error to the existing CTokenError::CMintBorrowFailed (as
used elsewhere, e.g., compressed_mint.rs) so try_borrow_data() failures are
represented semantically rather than as Size.
program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs
Outdated
Show resolved
Hide resolved
| bytes: &'a mut [u8], | ||
| config: Self::ZeroCopyConfig, | ||
| ) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> { | ||
| // TODO: check that this function fails if the account is already initialized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implement the initialization check to prevent data corruption.
The TODO indicates a critical missing validation. Without checking if the account is already initialized, new_zero_copy could overwrite an existing account's data, leading to state corruption or loss of funds.
🔒 Suggested implementation
Before writing the base struct, verify the account is uninitialized by checking that the state byte (at offset 108) is zero:
fn new_zero_copy(
bytes: &'a mut [u8],
config: Self::ZeroCopyConfig,
) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> {
- // TODO: check that this function fails if the account is already initialized
+ // Check account is uninitialized (state byte at offset 108 must be 0)
+ if bytes.len() > 108 && bytes[108] != 0 {
+ return Err(light_zero_copy::errors::ZeroCopyError::AlreadyInitialized);
+ }
// Use derived new_zero_copy for base struct (config type is () for fixed-size struct)Note: You may need to add AlreadyInitialized variant to ZeroCopyError if it doesn't exist.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs at line 113,
new_zero_copy is missing a check that prevents overwriting an
already-initialized account; before writing the base struct, read the state byte
at offset 108 from the account data and return a
ZeroCopyError::AlreadyInitialized if that byte != 0. Add an AlreadyInitialized
variant to the ZeroCopyError enum (if absent) and use it in new_zero_copy to
abort initialization when the state byte indicates prior initialization,
ensuring you perform the read using the same account data slice used later for
writing.
| if !bytes.is_empty() { | ||
| // && bytes[0] == ACCOUNT_TYPE_TOKEN_ACCOUNT should throw an error | ||
| let account_type = bytes[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Enforce account_type validation to prevent type confusion.
The commented logic at line 189 explicitly states that bytes[0] == ACCOUNT_TYPE_TOKEN_ACCOUNT should throw an error when false, but this validation is deliberately bypassed. This allows any account type byte to be accepted when deserializing CToken accounts.
This creates a type-safety hole where non-CToken accounts could be interpreted as CToken accounts, potentially leading to:
- Type confusion vulnerabilities
- Data corruption from misinterpreting account layouts
- Security exploits if an attacker can supply accounts with unexpected types
Based on learnings, CToken accounts must maintain strict layout compatibility with SPL token accounts.
🔒 Proposed fix to enforce account_type validation
// Check if there are extensions by looking at account_type byte at position 165
if !bytes.is_empty() {
- // && bytes[0] == ACCOUNT_TYPE_TOKEN_ACCOUNT should throw an error
let account_type = bytes[0];
+ // Validate account type matches expected CToken discriminator
+ if account_type != ACCOUNT_TYPE_TOKEN_ACCOUNT {
+ return Err(light_zero_copy::errors::ZeroCopyError::InvalidAccountType);
+ }
// Skip account_type byteNote: You may need to add InvalidAccountType variant to ZeroCopyError if it doesn't exist.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs around lines 188
- 190, The deserializer currently accepts any account_type byte (bytes[0]) and
skips validating it against ACCOUNT_TYPE_TOKEN_ACCOUNT; change the logic in
zero_copy.rs so that when bytes is not empty and bytes[0] !=
ACCOUNT_TYPE_TOKEN_ACCOUNT you return a clear error (add or use an
InvalidAccountType variant on ZeroCopyError if missing) instead of proceeding,
e.g., validate account_type before using it and return
ZeroCopyError::InvalidAccountType with context when the check fails to prevent
type confusion and layout misinterpretation.
programs/compressed-token/program/src/ctoken/close/processor.rs
Outdated
Show resolved
Hide resolved
programs/compressed-token/program/src/ctoken/close/processor.rs
Outdated
Show resolved
Hide resolved
programs/compressed-token/program/src/shared/compressible_top_up.rs
Outdated
Show resolved
Hide resolved
programs/compressed-token/program/src/shared/compressible_top_up.rs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
programs/compressed-token/anchor/src/process_transfer.rs (1)
645-652: Clarify the discriminator override comment to reflect version-specific behavior.The comment "We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version" is imprecise. The discriminator is only overridden for
ShaFlatandV2versions (lines 307–320). For V1 (the default path whenversionisNone), the discriminator set here is not overridden—it's used as-is.Suggested improvement:
- discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version + discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // V1 default; overridden for ShaFlat/V2 in add_data_hash_to_input_compressed_accounts_with_versionThis clarification removes ambiguity for future maintainers and aligns with best practices for documenting discriminator usage.
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs (1)
255-263: Normalize frozen state comparison to boolean values.The direct comparison
ctoken.state != compression_only_extension.is_frozencompares incompatible type encodings.ctoken.stateis anAccountStateenum (0, 1, or 2), where 2 represents frozen.is_frozenis aboolwhere 1 (true) represents frozen. When an account is frozen, the comparison2 != 1incorrectly triggers a mismatch error despite both values representing the same frozen state.Normalize both values to boolean before comparison:
Fix
let ctoken_frozen = ctoken.state == 2; let ext_frozen = compression_only_extension.is_frozen; if ctoken_frozen != ext_frozen { msg!( "is_frozen mismatch: ctoken {} != extension {}", ctoken_frozen, ext_frozen ); return Err(ErrorCode::CompressAndCloseFrozenMismatch.into()); }programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs (1)
54-61: Add bounds validation for compression_index before array access.The TODO on line 56 correctly identifies a critical issue. Since
compression_indexis a u8 provided by untrusted instruction data, any value ≥ 32 will panic when accessing the array on line 58. Add validation before the duplicate check:if let ZExtensionInstructionData::CompressedOnly(co) = ext { let idx = co.compression_index as usize; - // TODO check that it is not out of bounds + if idx >= MAX_COMPRESSIONS { + return Err(CTokenError::InvalidCompressionIndex.into()); + } // Check uniqueness - error if compression_index already used if compression_to_input[idx].is_some() { return Err(CTokenError::DuplicateCompressionIndex.into()); } compression_to_input[idx] = Some(i as u8);Add
InvalidCompressionIndextoCTokenErrorinprogram-libs/ctoken-interface/src/error.rswith the next error code (18062).programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs (1)
36-115: Critical: Variables used outside their defining scope cause compilation failure.The block from lines 36-95 defines
input_owner_bytes,derived_ata, andwallet_owner_bytes, but lines 97-115 reference these variables after the block closes at line 95. This is the root cause of the pipeline failures you're seeing.Looking at the logic, the checks at lines 97-104 are also redundant—lines 75-82 already verify
derived_ata == destination_account.key(), and lines 87-94 verifyinput_owner_bytes == derived_ata. By transitivity, lines 97-104 add no new validation.Similarly, line 108's check using
wallet_owner_bytesduplicates what should be verified within the block.🔧 Proposed fix: Remove dead code outside the block
if !pubkey_eq(&input_owner_bytes, &derived_ata) { msg!( "Decompress ATA: compressed owner {:?} != derived ATA {:?}", solana_pubkey::Pubkey::new_from_array(input_owner_bytes), solana_pubkey::Pubkey::new_from_array(derived_ata) ); return Err(CTokenError::DecompressDestinationMismatch.into()); } - } - if !pubkey_eq(&input_owner_bytes, destination_account.key()) { - msg!( - "Decompress ATA mismatch: derived {:?} != destination {:?}", - solana_pubkey::Pubkey::new_from_array(derived_ata), - solana_pubkey::Pubkey::new_from_array(*destination_account.key()) - ); - return Err(CTokenError::DecompressDestinationMismatch.into()); - } - - // Also verify destination CToken owner matches wallet_owner - // (destination should be wallet's ATA, owned by wallet) - if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) { - msg!( - "Decompress ATA: wallet owner {:?} != destination owner {:?}", - solana_pubkey::Pubkey::new_from_array(*wallet_owner_bytes), - solana_pubkey::Pubkey::new_from_array(ctoken.base.owner.to_bytes()) - ); - return Err(CTokenError::DecompressDestinationMismatch.into()); + // Also verify destination CToken owner matches wallet_owner + // (destination should be wallet's ATA, owned by wallet) + if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) { + msg!( + "Decompress ATA: wallet owner {:?} != destination owner {:?}", + solana_pubkey::Pubkey::new_from_array(*wallet_owner_bytes), + solana_pubkey::Pubkey::new_from_array(ctoken.base.owner.to_bytes()) + ); + return Err(CTokenError::DecompressDestinationMismatch.into()); + } }programs/compressed-token/program/src/ctoken/approve_revoke.rs (2)
29-43: Update misleading documentation.The comment on line 30 states "Handles compressible extension top-up before delegating to pinocchio," but the code actually calls
process_approvefirst (line 43), then handles the top-up afterward (lines 49-61). While this order may be intentional to ensure rent exemption is maintained after the operation, the comment should accurately reflect the implementation.📝 Suggested fix
-/// Process CToken approve instruction. -/// Handles compressible extension top-up before delegating to pinocchio. -/// +/// Process CToken approve instruction. +/// Delegates to pinocchio for approval, then handles compressible extension top-up. +///
64-79: Update misleading documentation.Same issue as
process_ctoken_approve: the comment states top-up happens "before delegating to pinocchio," but the code callsprocess_revokefirst (line 79), then handles top-up afterward.📝 Suggested fix
-/// Process CToken revoke instruction. -/// Handles compressible extension top-up before delegating to pinocchio. -/// +/// Process CToken revoke instruction. +/// Delegates to pinocchio for revocation, then handles compressible extension top-up. +///
🤖 Fix all issues with AI agents
In @programs/compressed-token/anchor/src/process_transfer.rs:
- Around line 659-662: Uncomment the TLV presence check so TLV is validated
fail-fast during input validation: restore the conditional using
input_token_data.tlv.is_some() (the commented block with unimplemented!("Tlv is
unimplemented.")) so the code returns the unimplemented/error immediately
instead of letting TLV slip through to the runtime path referenced by the later
unimplemented! at line 98; mirror the existing FROZEN_INPUTS-style gating if you
need to toggle behavior, or if leaving it commented is intentional open an issue
to track adding a const-generic flag similar to FROZEN_INPUTS.
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the import "use core::panic;" and replace any panic!
calls in this module (the panic! invocation referenced in check_extensions.rs)
with proper error returns; specifically, change panic!("<msg>") to returning a
Result::Err with a Solana program error (e.g.,
Err(ProgramError::Custom(<unique_code>)) or a defined program error type) and
optionally emit a diagnostic via msg!("<msg>") before returning; ensure every
occurrence of panic! in check_extensions.rs is converted to an Err return so the
program fails gracefully without consuming remaining compute units.
- Around line 144-147: Replace the panic!("Mint cache: compression: mint index
not found") with a proper error return: detect the surrounding function's error
type (e.g., Result<..., ProgramError> or a crate-specific error enum) and return
an appropriate error (for example ProgramError::InvalidAccountData or a
CompressedTokenError::MintNotFound converted into the function's error type).
Locate the preceding insert and the get_by_key call and either (a) remove the
redundant lookup and use the inserted value directly, or (b) use
get_by_key().ok_or_else(|| <appropriate error>) so the code defensively returns
an error instead of panicking.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 97-98: Remove the stale inline question comment "// what about is
ata?" that follows the condition check for compress_to_pubkey || is_ata in the
compress_and_close logic; the code already handles the is_ata branch so simply
delete that leftover comment near the if (compress_to_pubkey || is_ata) to clean
up the file compress_and_close.rs.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 188-190: Replace the unchecked addition of delegated amounts on
the ctoken (ctoken.base.delegated_amount.set(current + delegated_amount)) with a
checked addition using checked_add; if checked_add returns None, return
Err(ProgramError::ArithmeticOverflow). Mirror the error handling pattern used in
compress_or_decompress_ctokens.rs so you call checked_add on the current value
and delegated_amount, set the field only on Some(sum), and propagate
ProgramError::ArithmeticOverflow on overflow.
In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Around line 122-134: The max_top_up validation is inconsistent: instead of
passing &mut 0 for lamports_budget and manually checking max_top_up after
calling process_compression_top_up, follow the pattern used in
process_ctoken_approve_checked by computing a lamports_budget value from
max_top_up (e.g., let mut lamports_budget = if max_top_up > 0 { max_top_up as
u64 } else { 0 };) pass &mut lamports_budget into process_compression_top_up,
and remove the manual if-check that returns CTokenError::MaxTopUpExceeded so the
budget validation is handled inside process_compression_top_up.
| // TODO: add const generic to wrap this check | ||
| // if input_token_data.tlv.is_some() { | ||
| // unimplemented!("Tlv is unimplemented."); | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider uncommenting the TLV check for fail-fast validation.
The TODO suggests adding a const generic to wrap the TLV check (similar to the FROZEN_INPUTS pattern). However, with the check currently commented out, TLV usage will only be caught later at runtime (line 98 returns unimplemented!), rather than failing early during input validation.
Consider one of these approaches:
-
Uncomment the check for now to provide fail-fast validation:
- // TODO: add const generic to wrap this check - // if input_token_data.tlv.is_some() { - // unimplemented!("Tlv is unimplemented."); - // } + // TODO: add const generic to wrap this check + if input_token_data.tlv.is_some() { + unimplemented!("Tlv is unimplemented."); + }
-
Or open an issue to track the const generic refactor if the commented-out state is intentional for testing purposes.
📝 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.
| // TODO: add const generic to wrap this check | |
| // if input_token_data.tlv.is_some() { | |
| // unimplemented!("Tlv is unimplemented."); | |
| // } | |
| // TODO: add const generic to wrap this check | |
| if input_token_data.tlv.is_some() { | |
| unimplemented!("Tlv is unimplemented."); | |
| } |
🤖 Prompt for AI Agents
In @programs/compressed-token/anchor/src/process_transfer.rs around lines 659 -
662, Uncomment the TLV presence check so TLV is validated fail-fast during input
validation: restore the conditional using input_token_data.tlv.is_some() (the
commented block with unimplemented!("Tlv is unimplemented.")) so the code
returns the unimplemented/error immediately instead of letting TLV slip through
to the runtime path referenced by the later unimplemented! at line 98; mirror
the existing FROZEN_INPUTS-style gating if you need to toggle behavior, or if
leaving it commented is intentional open an issue to track adding a
const-generic flag similar to FROZEN_INPUTS.
| use core::panic; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using panic! in on-chain programs.
Importing core::panic solely to use panic! in on-chain code is a red flag. Panics in Solana programs consume all remaining compute units and provide poor error messages to users. This import hints at the problematic usage on line 146.
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
around lines 1 - 2, Remove the import "use core::panic;" and replace any panic!
calls in this module (the panic! invocation referenced in check_extensions.rs)
with proper error returns; specifically, change panic!("<msg>") to returning a
Result::Err with a Solana program error (e.g.,
Err(ProgramError::Custom(<unique_code>)) or a defined program error type) and
optionally emit a diagnostic via msg!("<msg>") before returning; ensure every
occurrence of panic! in check_extensions.rs is converted to an Err return so the
program fails gracefully without consuming remaining compute units.
| } else { | ||
| // TODO: double check. | ||
| panic!("Mint cache: compression: mint index not found"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace panic! with a proper error return.
On-chain programs should never use panic!. Even if this code path is believed to be unreachable, a panic will consume all compute units and give users an opaque error. The TODO comment suggests uncertainty about this logic.
Since you just inserted the mint into the cache on lines 114-118, the get_by_key on line 121 should always succeed. However, defensive programming dictates returning an error rather than panicking.
🐛 Proposed fix to replace panic with error
if let Some(checks) = cache.get_by_key(&mint_index) {
// CompressAndClose with restricted extensions requires CompressedOnly output.
// ...validation logic...
} else {
- // TODO: double check.
- panic!("Mint cache: compression: mint index not found");
+ // This should be unreachable since we just inserted the mint above,
+ // but return an error instead of panicking for safety.
+ return Err(ErrorCode::MintCacheCapacityExceeded.into());
}Consider whether this else branch is even necessary - if you're confident the insert succeeded, you could use unwrap_or_else with a proper error, or restructure to avoid the redundant lookup entirely.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
around lines 144 - 147, Replace the panic!("Mint cache: compression: mint index
not found") with a proper error return: detect the surrounding function's error
type (e.g., Result<..., ProgramError> or a crate-specific error enum) and return
an appropriate error (for example ProgramError::InvalidAccountData or a
CompressedTokenError::MintNotFound converted into the function's error type).
Locate the preceding insert and the get_by_key call and either (a) remove the
redundant lookup and use the inserted value directly, or (b) use
get_by_key().ok_or_else(|| <appropriate error>) so the code defensively returns
an error instead of panicking.
...ressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
Outdated
Show resolved
Hide resolved
| let current = ctoken.base.delegated_amount.get(); | ||
| ctoken.base.delegated_amount.set(current + delegated_amount); | ||
| ctoken.base.delegated_amount.set(current + delegated_amount); // TODO: use checked_add | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and check the code at lines 188-190
fd "decompress.rs" --path "*/transfer2/compression/ctoken/*" -x cat -n {} | head -200 | tail -30Repository: Lightprotocol/light-protocol
Length of output: 287
🏁 Script executed:
# Find the CTokenError enum definition
rg "pub enum CTokenError" -A 100 --type rust programs/compressed-token/program/Repository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
# Check for checked_add usage patterns in the codebase
rg "checked_add" --type rust programs/compressed-token/program/ | head -20Repository: Lightprotocol/light-protocol
Length of output: 1175
🏁 Script executed:
# Check for overflow error handling patterns
rg "Overflow|overflow" --type rust programs/compressed-token/program/src/ -B 2 -A 2 | head -60Repository: Lightprotocol/light-protocol
Length of output: 3638
🏁 Script executed:
# Read the decompress.rs file to see imports and current error handling
head -50 programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rsRepository: Lightprotocol/light-protocol
Length of output: 2221
🏁 Script executed:
# Check what error types are used in decompress.rs
rg "use.*Error|CTokenError|ProgramError" programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs | head -20Repository: Lightprotocol/light-protocol
Length of output: 929
🏁 Script executed:
# Verify the context around line 189 in decompress.rs
sed -n '180,200p' programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rsRepository: Lightprotocol/light-protocol
Length of output: 1116
🏁 Script executed:
# Check if CTokenError exists and what variants it has
rg "pub enum CTokenError" --type rust programs/ -A 50 | head -100Repository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
# Compare with compress_or_decompress_ctokens.rs to see the same pattern
sed -n '1,50p' programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rsRepository: Lightprotocol/light-protocol
Length of output: 1771
Address the TODO: Use checked_add to prevent delegated amount overflow.
The TODO correctly identifies a critical risk. If current + delegated_amount overflows, silent wrapping in release builds corrupts token state. Following the pattern used elsewhere in this module (see compress_or_decompress_ctokens.rs), use ProgramError::ArithmeticOverflow for overflow detection.
🔧 Proposed fix
// Add delegated_amount (only when we're setting the delegate)
if delegated_amount > 0 {
let current = ctoken.base.delegated_amount.get();
- ctoken.base.delegated_amount.set(current + delegated_amount); // TODO: use checked_add
+ let new_amount = current
+ .checked_add(delegated_amount)
+ .ok_or(ProgramError::ArithmeticOverflow)?;
+ ctoken.base.delegated_amount.set(new_amount);
}📝 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.
| let current = ctoken.base.delegated_amount.get(); | |
| ctoken.base.delegated_amount.set(current + delegated_amount); | |
| ctoken.base.delegated_amount.set(current + delegated_amount); // TODO: use checked_add | |
| } | |
| let current = ctoken.base.delegated_amount.get(); | |
| let new_amount = current | |
| .checked_add(delegated_amount) | |
| .ok_or(ProgramError::ArithmeticOverflow)?; | |
| ctoken.base.delegated_amount.set(new_amount); | |
| } |
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs
around lines 188 - 190, Replace the unchecked addition of delegated amounts on
the ctoken (ctoken.base.delegated_amount.set(current + delegated_amount)) with a
checked addition using checked_add; if checked_add returns None, return
Err(ProgramError::ArithmeticOverflow). Mirror the error handling pattern used in
compress_or_decompress_ctokens.rs so you call checked_add on the current value
and delegated_amount, set the field only on Some(sum), and propagate
ProgramError::ArithmeticOverflow on overflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs (1)
97-115: Critical: Remove duplicate validation logic.Lines 97-104 duplicate the validation already performed at lines 84-94. Since you've already verified:
derived_ata == destination_account(lines 74-82)input_owner == derived_ata(lines 84-94)The transitive property proves
input_owner == destination_account, making lines 97-104 redundant.Additionally, once the inner block is removed, these variables will be in scope, but the logic should still be deleted to avoid redundant checks.
🗑️ Proposed fix
Remove the duplicate validation:
- if !pubkey_eq(&input_owner_bytes, destination_account.key()) { - msg!( - "Decompress ATA mismatch: derived {:?} != destination {:?}", - solana_pubkey::Pubkey::new_from_array(derived_ata), - solana_pubkey::Pubkey::new_from_array(*destination_account.key()) - ); - return Err(CTokenError::DecompressDestinationMismatch.into()); - } - // Also verify destination CToken owner matches wallet_owner // (destination should be wallet's ATA, owned by wallet) if !pubkey_eq(wallet_owner_bytes, &ctoken.base.owner.to_bytes()) {programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs (1)
256-263: Critical type mismatch in frozen state validation—comparing incompatible values.The comparison
ctoken.state != compression_only_extension.is_frozenis incorrect.ctoken.stateuses the fullAccountStateenum (Uninitialized=0, Initialized=1, Frozen=2), whileis_frozenis a boolean (0/1). When an account is frozen,ctoken.state == 2butis_frozen == 1, causing the comparison to incorrectly fail (2 != 1).Use boolean comparison instead:
(ctoken.state == 2) != compression_only_extension.is_frozenReference: AccountState enum and current code
if ctoken.state != compression_only_extension.is_frozen { msg!( "is_frozen mismatch: ctoken {} != extension {}", ctoken.state, compression_only_extension.is_frozen ); return Err(ErrorCode::CompressAndCloseFrozenMismatch.into()); }programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs (1)
151-162: LGTM!The new zero-amount output handling correctly ensures that mints for zero-amount outputs undergo full validation with restricted extension checks, which is appropriate since these represent new state being created.
Minor note: Line 158 hardcodes
truefordeny_restricted_extensions. While correct (zero-amount outputs implyout_token_datais non-empty, sodeny_restricted_extensionswould betrue), consider using the variable from line 88 for consistency and clarity.programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs (1)
55-62: Add bounds check for compression_index to prevent panic.The
compression_indexfrom instruction data is used directly as an array index without validation (line 55-58). Since this value comes from untrusted user input, an attacker could provide a value >=MAX_COMPRESSIONSto cause a panic, resulting in a denial-of-service attack.🔒 Proposed fix
if let ZExtensionInstructionData::CompressedOnly(co) = ext { let idx = co.compression_index as usize; - // TODO check that it is not out of bounds + // Validate compression_index is within bounds + if idx >= MAX_COMPRESSIONS { + return Err(CTokenError::CompressionIndexOutOfBounds.into()); + } // Check uniqueness - error if compression_index already used if compression_to_input[idx].is_some() { return Err(CTokenError::DuplicateCompressionIndex.into()); }Note: This assumes
CTokenError::CompressionIndexOutOfBoundsexists. If not, you'll need to add it to the error enum.programs/compressed-token/program/src/ctoken/approve_revoke.rs (1)
223-240: Inconsistent budget enforcement pattern compared toprocess_compressible_top_up.This function uses
lamports_budgettracking (lines 223-227, 234, 238-240) to enforce max_top_up, whileprocess_compressible_top_up(lines 119-133) bypasses the budget parameter and checksmax_top_up < transfer_amountdirectly.Both approaches work, but the inconsistency makes the code harder to maintain. Consider unifying the budget enforcement approach across both functions—either always use
lamports_budgettracking or always use post-hoc comparison.
🤖 Fix all issues with AI agents
In @programs/compressed-token/anchor/src/process_transfer.rs:
- Line 647: The comment saying the discriminator will be overridden is
misleading because the override in
add_data_hash_to_input_compressed_accounts_with_version is conditional; update
the comment near TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR to state that this value
acts as the V1/default discriminator set by get_input_compressed_accounts and
will only be replaced when
add_data_hash_to_input_compressed_accounts_with_version is called with
TokenDataVersion::ShaFlat or TokenDataVersion::V2 (calls where version is Some);
when version is None the initial discriminator remains.
In
@programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:
- Around line 1-2: Remove the unnecessary explicit import `use core::panic;`
from the top of check_extensions.rs; the `panic!()` macro used later (line ~146)
is in the Rust prelude and works without this import, so delete that use
statement to clean up unused imports.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 97-99: The inline comment "what about is ata?" in the if condition
checking compress_to_pubkey || is_ata should be removed or replaced with a clear
explanatory comment; update the comment near the if (compress_to_pubkey ||
is_ata) in compress_and_close.rs to state that ATA (associated token accounts)
follow the same owner-validation path as pubkey-targeted compression so owner
must match the token account pubkey when compressing to either a pubkey or an
ATA, or simply delete the question-style remark.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs:
- Around line 187-190: Replace the unchecked addition on
ctoken.base.delegated_amount with checked arithmetic: call
current.checked_add(delegated_amount) and handle the None case (overflow) by
returning an appropriate error or aborting the operation; update the call on the
ctoken.base.delegated_amount.set(...) site to use the checked sum when Some(sum)
and propagate or convert the overflow None into the function's error path
instead of performing unchecked addition.
- Around line 36-95: Remove the unnecessary inner scope (the extra pair of curly
braces) that wraps the ATA-decompress logic so variables declared
there—wallet_owner_bytes, derived_ata, input_owner_bytes (and
bump_seed/ata_seeds)—remain in scope for the subsequent code; simply delete the
opening '{' before "let wallet_owner = wallet_owner.ok_or_else..." and the
matching closing '}' after the input_owner_bytes check so the variables are
available later.
In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Line 164: The TODO on process_ctoken_approve_checked must be resolved: either
delete the function and remove all references (call sites, exports, tests) if
it's unused, or keep it and replace the inline TODO with a tracked issue
reference (create an issue describing why/when to remove, add issue ID or URL)
and add a comment explaining why it remains and planned timeline; update any
public API docs or changelog entries accordingly and ensure tests/build pass
after the change.
In @programs/compressed-token/program/src/shared/token_input.rs:
- Line 86: Consolidate the ATA derivation and signer checks by moving the
associated token account (ATA) pubkey derivation logic into the token input
validation function in token_input.rs (the function responsible for input
validation, e.g., validate/validate_inputs), compute the ATA deterministically
from owner and mint there, verify the provided ATA/account matches the derived
pubkey and that required signers are present, remove duplicate ATA derivation
and signer checks from call sites, and update any tests to reflect the
centralized validation and failure messages.
| let compressed_account = InAccount { | ||
| lamports: input_token_data.lamports.unwrap_or_default(), | ||
| discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, | ||
| discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Clarify discriminator override behavior in comments.
The comment at line 647 states the discriminator will be overridden in add_data_hash_to_input_compressed_accounts_with_version, but examining the function (lines 307–320) shows the override is conditional: it only occurs for TokenDataVersion::ShaFlat (line 312) or TokenDataVersion::V2 (line 319). When version=None (as called at line 136), no override happens and the initial discriminator persists. The comment at line 317 correctly notes "V1 is the default set in get_input_compressed_accounts," but line 647's comment should clarify that the discriminator acts as a V1 default that may be overridden only for specific versions.
📝 Proposed comment clarification
- discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // We override the discriminator in add_data_hash_to_input_compressed_accounts_with_version
+ discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, // V1 default; may be overridden for V2/ShaFlat in add_data_hash_to_input_compressed_accounts_with_versionAlso applies to: 317-320
🤖 Prompt for AI Agents
In @programs/compressed-token/anchor/src/process_transfer.rs at line 647, The
comment saying the discriminator will be overridden is misleading because the
override in add_data_hash_to_input_compressed_accounts_with_version is
conditional; update the comment near TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR to
state that this value acts as the V1/default discriminator set by
get_input_compressed_accounts and will only be replaced when
add_data_hash_to_input_compressed_accounts_with_version is called with
TokenDataVersion::ShaFlat or TokenDataVersion::V2 (calls where version is Some);
when version is None the initial discriminator remains.
| // TODO: add const generic to wrap this check | ||
| // if input_token_data.tlv.is_some() { | ||
| // unimplemented!("Tlv is unimplemented."); | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Address the commented-out TLV check TODO.
Line 659 contains a TODO with commented-out code intended to validate TLV extensions at compile time via const generic. However, a similar unimplemented check at lines 96–100 remains active. Either the check should be consistently applied (or skipped) across both sites, or the TODO should be tracked and completed to avoid divergent validation logic.
Would you like me to help implement the const generic wrapper to conditionally enable TLV validation, or should this be tracked as a separate follow-up task?
programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs
Outdated
Show resolved
Hide resolved
| let checks = if compression.mode.is_compress_and_close() || no_compressed_outputs { | ||
| // Bypass extension state checks (paused, non-zero fees, non-nil transfer hook) | ||
| // when exiting compressed state: CompressAndClose, Decompress, or CToken→SPL | ||
| // when CompressAndClose, full Decompress, or CToken→SPL (compress and full decompress) | ||
| parse_mint_extensions(mint_account)? | ||
| } else { | ||
| check_mint_extensions(mint_account, deny_restricted_extensions)? | ||
| }; | ||
| cache.insert(mint_index, checks, ErrorCode::MintCacheCapacityExceeded)?; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace panic with proper error handling.
Lines 144-147 contain a panic that could trigger during normal operation if cache insertion previously failed due to capacity limits. This creates an inconsistency: cache insertions properly return MintCacheCapacityExceeded on failure (lines 101, 118), but the subsequent lookup assumes success and panics.
🔒 Proposed fix
- } else {
- // TODO: double check.
- panic!("Mint cache: compression: mint index not found");
- }
+ } else {
+ return Err(ErrorCode::MintCacheCapacityExceeded.into());
+ }Additionally, the comment on line 113 mentions "CToken→SPL (compress and full decompress)" which is confusing—this branch handles CompressAndClose or scenarios with no compressed outputs. Consider clarifying this comment.
Committable suggestion skipped: line range outside the PR's diff.
...ressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
Outdated
Show resolved
Hide resolved
...ressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
Outdated
Show resolved
Hide resolved
...ams/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs
Outdated
Show resolved
Hide resolved
| if delegated_amount > 0 { | ||
| let current = ctoken.base.delegated_amount.get(); | ||
| ctoken.base.delegated_amount.set(current + delegated_amount); | ||
| ctoken.base.delegated_amount.set(current + delegated_amount); // TODO: use checked_add | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Implement checked arithmetic to prevent overflow.
The TODO at line 189 correctly identifies the need for checked_add. Unchecked addition on token amounts can overflow, potentially leading to incorrect balances.
✅ Proposed fix using checked_add
// Add delegated_amount (only when we're setting the delegate)
if delegated_amount > 0 {
let current = ctoken.base.delegated_amount.get();
- ctoken.base.delegated_amount.set(current + delegated_amount); // TODO: use checked_add
+ let new_amount = current
+ .checked_add(delegated_amount)
+ .ok_or(ProgramError::ArithmeticOverflow)?;
+ ctoken.base.delegated_amount.set(new_amount);
}🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs
around lines 187 - 190, Replace the unchecked addition on
ctoken.base.delegated_amount with checked arithmetic: call
current.checked_add(delegated_amount) and handle the None case (overflow) by
returning an appropriate error or aborting the operation; update the call on the
ctoken.base.delegated_amount.set(...) site to use the checked sum when Some(sum)
and propagate or convert the overflow None into the function's error path
instead of performing unchecked addition.
| .find_map(|ext| { | ||
| if let ZExtensionInstructionData::CompressedOnly(data) = ext { | ||
| if data.is_ata != 0 { | ||
| // TODO: move ata derivation here, all signer checks must be in the input validation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
TODO noted: ATA derivation consolidation.
The TODO identifies a valid refactoring opportunity to centralize ATA derivation and signer validation in the input validation phase.
Would you like me to generate an issue to track this refactoring work? This would involve moving the ATA pubkey derivation logic into this validation function to ensure all signer checks happen in one place.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/shared/token_input.rs at line 86,
Consolidate the ATA derivation and signer checks by moving the associated token
account (ATA) pubkey derivation logic into the token input validation function
in token_input.rs (the function responsible for input validation, e.g.,
validate/validate_inputs), compute the ATA deterministically from owner and mint
there, verify the provided ATA/account matches the derived pubkey and that
required signers are present, remove duplicate ATA derivation and signer checks
from call sites, and update any tests to reflect the centralized validation and
failure messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
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/src/lib.rs (1)
131-227: Instruction dispatch routing is well-structured.The
process_instructionfunction correctly routes each discriminator to handlers in the new module structure. The fallback to anchor instructions viaconvert_account_infosmaintains backward compatibility.One observation: Line 136 accesses
instruction_data[0]without bounds checking. Ifinstruction_datais empty, this will panic.🛡️ Suggested defensive check
pub fn process_instruction( program_id: &pinocchio::pubkey::Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), ProgramError> { + if instruction_data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } let discriminator = InstructionType::from(instruction_data[0]);programs/compressed-token/program/src/ctoken/close/processor.rs (2)
76-98: Use owner validation utilities as required by coding guidelines.The manual authority validation using
pubkey_eqviolates the coding guideline: "Owner validation logic must be implemented using utilities fromsrc/shared/owner_validation.rs".Refactor to use the utilities from the designated module to ensure consistent validation patterns across the codebase.
Based on coding guidelines.
52-64: Missing CompressibleConfig state validation for compress-and-close operations.When closing accounts with a compressible extension via the compression_authority (line 153), the CompressibleConfig account state must be validated using
validate_not_inactive()before allowing the operation. This is explicitly required for "compress & close" operations per the error handling documentation.The compress-and-close path exists at line 153 where
accounts.authority.key() == &compression.info.compression_authorityis checked, but the config state validation is missing. Compare withclaim.rsandwithdraw_funding_pool.rs, which correctly callvalidate_not_inactive()on the CompressibleConfig account before processing compression operations.Add CompressibleConfig to the
CloseTokenAccountAccountsstructure and callvalidate_not_inactive()before line 153 to enforce that the config is not in the Inactive state.programs/compressed-token/program/src/shared/token_input.rs (1)
79-120: Strict validation required:is_ataflag must not silently fall back toowner_accounton verification failure.When
is_ata != 0, the code intends to validate that the owner is actually the wallet owner (stored atowner_index) and that the account being decompressed matches the Light-specific PDA derived from[wallet_owner, LIGHT_CPI_SIGNER.program_id, mint, bump]. The comments correctly note thatowner_accountis the ATA "that can't sign."However, the current logic returns
Noneon any failure (missingowner_index, PDA derivation failure, or mismatch) and thenunwrap_or(owner_account)silently proceeds with the invalid account. This defeats the security intent of theis_ataflag. Whenis_ata=true, every validation step is critical—if the wallet owner can't be retrieved, the PDA doesn't derive, or the account doesn't match, the operation should fail explicitly rather than fall back.The
verify_owner_or_delegate_signer()check that follows validates signer status but doesn't catch that strict ATA validation was skipped. Replace the silent fallback with strict error handling:
- Return an error (e.g.,
ProgramError::InvalidArgumentorProgramError::InvalidSeeds) when ATA validation fails- Only use
owner_accountas the default whenis_ata == 0(non-ATA path)Also note: The seed format is Light-specific (
[wallet_owner, LIGHT_CPI_SIGNER.program_id, mint, bump]) rather than the standard SPL Associated Token Account derivation. Update comments to clarify this is a Light PDA, not the standard ATA format.program-libs/ctoken-interface/src/instructions/mint_action/decompress_mint.rs (1)
10-20: Instruction ABI mismatch between Borsh serialization and zero-copy deserialization forDecompressMintAction.The struct combines
#[repr(C)](memory-aware layout) with bothAnchorSerialize(Borsh) andZeroCopyderives. With#[repr(C)], the compiler inserts 2 bytes of padding before theu32 write_top_upfield (layout: 1+1+2pad+4=8 bytes). However, Borsh serialization omits the padding and serializes only the fields (1+1+4=6 bytes).On-chain, the
mint_actionprocessor deserializes instruction data viaMintActionCompressedInstructionData::zero_copy_at(), which expects the 8-byte#[repr(C)]layout. Thewrite_top_upfield is actively accessed (decompress_mint.rs lines 76, 79, 105) to validate rent constraints. A mismatch between the 6-byte wire format and 8-byte memory layout will cause misalignment during zero-copy deserialization.Recommended fix: Make padding explicit so the wire format matches the memory layout:
Explicit padding approach
pub struct DecompressMintAction { /// PDA bump for CMint account verification pub cmint_bump: u8, /// Rent payment in epochs (prepaid). pub rent_payment: u8, + /// Explicit padding to align u32 field in both Borsh and zero-copy layouts. + pub _padding: [u8; 2], /// Lamports allocated for future write operations (top-up per write). /// Must not exceed config.rent_config.max_top_up. pub write_top_up: u32, }This ensures both Borsh serialization and zero-copy deserialization produce/expect the same 8-byte layout.
programs/compressed-token/program/src/shared/compressible_top_up.rs (1)
19-101: Add explicit destination mutability checks before scheduling lamport top-ups.
calculate_and_execute_compressible_top_upsvalidates CMint ownership (good), but for CToken relies onCToken::from_account_info_checked, which only validates ownership—not mutability. Since this function schedules lamport transfers into both accounts viamulti_transfer_lamports, add an explicitcheck_mut(ctoken)call to fail fast with a clear error (AccountNotMutable) rather than deferring to runtime failures intransfer_lamports. This provides symmetric validation with the CMint ownership check and aligns with defensive programming patterns.
🤖 Fix all issues with AI agents
In @program-libs/ctoken-interface/src/instructions/extensions/mod.rs:
- Around line 57-66: Add a test module in
program-libs/ctoken-interface/src/instructions/extensions/mod.rs that exercises
the find_compressed_only function: create TLV slices of
ZExtensionInstructionData containing (1) a single
ZCompressedOnlyExtensionInstructionData and assert Some(...) is returned, (2) no
ZCompressedOnlyExtensionInstructionData (e.g., other variant(s)) and assert None
is returned, and (3) a mixed slice with multiple variants where one is
ZCompressedOnlyExtensionInstructionData and assert the function finds that
entry; use the existing types ZExtensionInstructionData and
ZCompressedOnlyExtensionInstructionData to construct test values and
#[cfg(test)]/#[test] functions to run the assertions.
In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs:
- Around line 68-78: Move the helper function check_token_account so its
definition appears before its use in the surrounding function: locate the call
to check_token_account(data)? and cut the inner fn check_token_account(bytes:
&[u8]) -> Result<(), ZeroCopyError> { ... } block (which references
ACCOUNT_TYPE_TOKEN_ACCOUNT and ZeroCopyError) and paste it above the call,
keeping the exact signature and behavior; this keeps validation logic unchanged
but places the check_token_account definition before invocation for clearer,
conventional Rust flow.
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs:
- Around line 165-170: The TODO comment "remaining bytes should be checked to be
zero" is misleading because the code already returns
Err(light_zero_copy::errors::ZeroCopyError::Size) when remaining is not empty;
either remove the TODO or replace it with a clarified comment stating that
non-empty remaining triggers ZeroCopyError::Size, or if the intent was to ensure
remaining bytes are present but zero-valued, implement an explicit check over
remaining bytes (e.g., verify all bytes == 0 and return ZeroCopyError::Size or a
more specific error) rather than just checking emptiness; update the comment
accordingly near ACCOUNT_TYPE_TOKEN_ACCOUNT, remaining, and ZeroCopyError::Size.
In @program-libs/ctoken-interface/src/state/extensions/compressible.rs:
- Around line 101-106: The is_ata() helper was only implemented on
ZCompressibleExtensionMut; add the same immutable getter to
ZCompressibleExtension to keep the API symmetric with decimals() — implement pub
fn is_ata(&self) -> bool { self.is_ata != 0 } on the ZCompressibleExtension type
(matching the existing ZCompressibleExtensionMut implementation) so callers
using the immutable view can query ATA status without needing a mutable
reference.
In @program-libs/ctoken-interface/src/state/mint/zero_copy.rs:
- Around line 110-114: Add a short inline comment above the
IS_INITIALIZED_OFFSET constant explaining how the 45 byte offset was derived
from the struct fields (mint_authority_option_prefix: 4, mint_authority: 32,
supply: 8, decimals: 1) so future maintainers can see why
bytes[IS_INITIALIZED_OFFSET] checks is_initialized; keep the existing check
using IS_INITIALIZED_OFFSET and bytes.len() unchanged and only add the
explanatory comment near the IS_INITIALIZED_OFFSET definition and the if
bytes.len() ... memory check.
In
@programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs:
- Around line 150-168: Replace the direct call to
compressed_mint.compression.calculate_top_up_lamports with the shared helper
process_compression_top_up from src/shared/compressible_top_up.rs: when
!accounts_config.has_decompress_mint_action, call process_compression_top_up
passing the compressed_mint (or its compression field), the required
size/num_bytes, current_lamports, and current slot (use Clock::get() as now),
handle its Result mapping to ErrorCode::CMintTopUpCalculationFailed, and then
add the returned top_up to deficit instead of calling calculate_top_up_lamports
directly so rent top-up logic uses the centralized helper.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs:
- Around line 162-208: validate_compressed_only_ext currently only checks
delegate fields when ctoken.delegate() is Some, which misses the case where the
compressed output declares a delegate but the ctoken has none; add an explicit
check early in validate_compressed_only_ext that if
compressed_token_account.has_delegate() is true but ctoken.delegate().is_none()
then return ErrorCode::CompressAndCloseInvalidDelegate, keeping the existing
branch that validates matching keys when both delegates exist (use the same
ErrorCode and existing packed_accounts/get_u8 logic).
- Around line 265-311: validate_ctoken_account in the compress_and_close flow
doesn't enforce config state gating, allowing closes when the CompressibleConfig
is inactive; to fix, parse the CompressibleConfig for the token (the same way
claim.rs does) and call validate_not_inactive(...) before allowing
compression/close (i.e., after obtaining compression via
ctoken.get_compressible_extension() and before checking is_compressible),
ensuring compress_and_close follows the same config-state check as the claim
path.
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs:
- Around line 18-21: The compilation error comes from passing a mutable
reference to a temporary (&mut None) into process_compression_top_up which
expects rent: &mut Option<Rent>; instead, add Rent to the imports and declare a
local mutable variable like let mut rent: Option<Rent> = None; in the
surrounding function scope and pass &mut rent to both calls to
process_compression_top_up (replace the &mut None occurrences), thereby caching
the Rent sysvar as intended and reusing the same mutable Option<Rent> for both
invocations.
In @programs/compressed-token/program/src/ctoken/approve_revoke.rs:
- Around line 108-115: The call to process_compression_top_up is passing
placeholder out-parameters (&mut 0, &mut None) indicating an API mismatch;
replace this with a focused helper that computes and returns the needed top-up
values for a single account using the utilities in
src/shared/compressible_top_up.rs (e.g., compute_rent_exempt_and_transfer_amount
or add a process_compression_top_up_single_account wrapper), update the call
site to use the helper's returned transfer_amount (and any rent_exempt/decimals
if needed) instead of passing &mut 0/&mut None, and adjust or overload
process_compression_top_up signature if necessary so single-account callers no
longer need to provide unused mutable placeholders.
| /// Find the CompressedOnly extension from a TLV slice. | ||
| #[inline(always)] | ||
| pub fn find_compressed_only<'a>( | ||
| tlv: &'a [ZExtensionInstructionData<'a>], | ||
| ) -> Option<&'a ZCompressedOnlyExtensionInstructionData<'a>> { | ||
| tlv.iter().find_map(|ext| match ext { | ||
| ZExtensionInstructionData::CompressedOnly(data) => Some(data), | ||
| _ => None, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let me examine the full structure of the extensions mod.rs file to understand the context
cat -n program-libs/ctoken-interface/src/instructions/extensions/mod.rs | head -100Repository: Lightprotocol/light-protocol
Length of output: 2527
🏁 Script executed:
# Search for similar helper functions in the same file to check for consistency
rg -n 'pub fn find_' program-libs/ctoken-interface/src/instructions/extensions/mod.rsRepository: Lightprotocol/light-protocol
Length of output: 107
🏁 Script executed:
# Search for usage of find_compressed_only in the codebase
rg -n 'find_compressed_only' --type rust -C 2Repository: Lightprotocol/light-protocol
Length of output: 1967
🏁 Script executed:
# Check for tests related to extension finding in ctoken-interface
rg -n '#\[test\]|#\[cfg\(test\)\]' program-libs/ctoken-interface/src/instructions/extensions/ -A 10Repository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
# Check if CLAUDE.md exists in the compressed-token program directory with guidance
find . -name 'CLAUDE.md' -path '*/compressed-token/*' -exec cat {} \;Repository: Lightprotocol/light-protocol
Length of output: 17782
🏁 Script executed:
# Look at the structure of the extensions directory
fd -type f -path 'program-libs/ctoken-interface/src/instructions/extensions*' | head -20Repository: Lightprotocol/light-protocol
Length of output: 242
Add unit tests for the extension helper function.
The find_compressed_only helper is correctly implemented and actively used in the decompression logic. However, unit tests are missing and required for program-libs code. Add tests to program-libs/ctoken-interface/src/instructions/extensions/mod.rs verifying:
- Successfully finding CompressedOnly extension in a TLV slice
- Returning None when CompressedOnly is absent
- Correct behavior with mixed extension types
Regarding API consistency: similar helpers for TokenMetadata and Compressible extensions don't exist. Given that compressed mints support exactly one extension (per CLAUDE.md) and CToken operations may only require CompressedOnly lookup, this may be intentional design rather than an inconsistency to address.
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/instructions/extensions/mod.rs around
lines 57 - 66, Add a test module in
program-libs/ctoken-interface/src/instructions/extensions/mod.rs that exercises
the find_compressed_only function: create TLV slices of
ZExtensionInstructionData containing (1) a single
ZCompressedOnlyExtensionInstructionData and assert Some(...) is returned, (2) no
ZCompressedOnlyExtensionInstructionData (e.g., other variant(s)) and assert None
is returned, and (3) a mixed slice with multiple variants where one is
ZCompressedOnlyExtensionInstructionData and assert the function finds that
entry; use the existing types ZExtensionInstructionData and
ZCompressedOnlyExtensionInstructionData to construct test values and
#[cfg(test)]/#[test] functions to run the assertions.
| check_token_account(data)?; | ||
|
|
||
| #[inline(always)] | ||
| fn check_token_account(bytes: &[u8]) -> Result<(), ZeroCopyError> { | ||
| if bytes.len() == 165 || (bytes.len() > 165 && bytes[165] == ACCOUNT_TYPE_TOKEN_ACCOUNT) | ||
| { | ||
| Ok(()) | ||
| } else { | ||
| Err(ZeroCopyError::InvalidConversion) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Validation logic is correct, but function placement is unconventional.
The check_token_account helper correctly validates token account format:
- 165 bytes = valid base SPL token account (no extensions)
-
165 bytes = must have
ACCOUNT_TYPE_TOKEN_ACCOUNTat byte 165
However, the inner function is defined after it's called (line 68 calls it, lines 70-78 define it). While Rust allows this, it's unconventional and can confuse readers. Consider moving the definition before the call:
pub fn amount_from_slice(data: &[u8]) -> Result<u64, ZeroCopyError> {
const AMOUNT_OFFSET: usize = 64;
+ #[inline(always)]
+ fn check_token_account(bytes: &[u8]) -> Result<(), ZeroCopyError> {
+ if bytes.len() == 165 || (bytes.len() > 165 && bytes[165] == ACCOUNT_TYPE_TOKEN_ACCOUNT) {
+ Ok(())
+ } else {
+ Err(ZeroCopyError::InvalidConversion)
+ }
+ }
+
check_token_account(data)?;
-
- #[inline(always)]
- fn check_token_account(bytes: &[u8]) -> Result<(), ZeroCopyError> {
- ...
- }🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs around lines
68 - 78, Move the helper function check_token_account so its definition appears
before its use in the surrounding function: locate the call to
check_token_account(data)? and cut the inner fn check_token_account(bytes:
&[u8]) -> Result<(), ZeroCopyError> { ... } block (which references
ACCOUNT_TYPE_TOKEN_ACCOUNT and ZeroCopyError) and paste it above the call,
keeping the exact signature and behavior; this keeps validation logic unchanged
but places the check_token_account definition before invocation for clearer,
conventional Rust flow.
| // TODO: remaining bytes should be checked to be zero | ||
| (ACCOUNT_TYPE_TOKEN_ACCOUNT, None) | ||
| }; | ||
|
|
||
| if !remaining.is_empty() { | ||
| return Err(light_zero_copy::errors::ZeroCopyError::Size); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
TODO comment is misleading - logic already enforces empty remaining.
The TODO at line 165 says "remaining bytes should be checked to be zero" but lines 168-170 immediately return ZeroCopyError::Size if remaining is not empty. Either remove the TODO or clarify what additional check is intended.
} else {
- // TODO: remaining bytes should be checked to be zero
+ // No extensions case - verify exact size match
(ACCOUNT_TYPE_TOKEN_ACCOUNT, None)
};📝 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.
| // TODO: remaining bytes should be checked to be zero | |
| (ACCOUNT_TYPE_TOKEN_ACCOUNT, None) | |
| }; | |
| if !remaining.is_empty() { | |
| return Err(light_zero_copy::errors::ZeroCopyError::Size); | |
| } | |
| // No extensions case - verify exact size match | |
| (ACCOUNT_TYPE_TOKEN_ACCOUNT, None) | |
| }; | |
| if !remaining.is_empty() { | |
| return Err(light_zero_copy::errors::ZeroCopyError::Size); | |
| } |
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/ctoken/zero_copy.rs around lines 165
- 170, The TODO comment "remaining bytes should be checked to be zero" is
misleading because the code already returns
Err(light_zero_copy::errors::ZeroCopyError::Size) when remaining is not empty;
either remove the TODO or replace it with a clarified comment stating that
non-empty remaining triggers ZeroCopyError::Size, or if the intent was to ensure
remaining bytes are present but zero-valued, implement an explicit check over
remaining bytes (e.g., verify all bytes == 0 and return ZeroCopyError::Size or a
more specific error) rather than just checking emptiness; update the comment
accordingly near ACCOUNT_TYPE_TOKEN_ACCOUNT, remaining, and ZeroCopyError::Size.
|
|
||
| /// Returns whether this account is an ATA | ||
| #[inline(always)] | ||
| pub fn is_ata(&self) -> bool { | ||
| self.is_ata != 0 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding is_ata() to the immutable view as well.
The new is_ata() method is only added to ZCompressibleExtensionMut. For API consistency, you might want to also add it to ZCompressibleExtension (lines 63-73), since the decimals() getter exists on both views.
If this asymmetry is intentional (e.g., only mutable contexts need this check during compress_and_close), the current implementation is fine.
♻️ Optional: Add to immutable view for symmetry
impl ZCompressibleExtension<'_> {
/// Returns the decimals if present
#[inline(always)]
pub fn decimals(&self) -> Option<u8> {
if self.decimals_option == 1 {
Some(self.decimals)
} else {
None
}
}
+
+ /// Returns whether this account is an ATA
+ #[inline(always)]
+ pub fn is_ata(&self) -> bool {
+ self.is_ata != 0
+ }
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/extensions/compressible.rs around
lines 101 - 106, The is_ata() helper was only implemented on
ZCompressibleExtensionMut; add the same immutable getter to
ZCompressibleExtension to keep the API symmetric with decimals() — implement pub
fn is_ata(&self) -> bool { self.is_ata != 0 } on the ZCompressibleExtension type
(matching the existing ZCompressibleExtensionMut implementation) so callers
using the immutable view can query ATA status without needing a mutable
reference.
| // Check that the account is not already initialized (is_initialized byte at offset 45) | ||
| const IS_INITIALIZED_OFFSET: usize = 45; // 4 + 32 + 8 + 1 = 45 | ||
| if bytes.len() > IS_INITIALIZED_OFFSET && bytes[IS_INITIALIZED_OFFSET] != 0 { | ||
| return Err(light_zero_copy::errors::ZeroCopyError::MemoryNotZeroed); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Good initialization guard to prevent re-initialization.
The check at IS_INITIALIZED_OFFSET (byte 45) correctly identifies already-initialized accounts and returns MemoryNotZeroed. The offset calculation 4 + 32 + 8 + 1 = 45 matches the struct layout:
mint_authority_option_prefix: 4 bytesmint_authority: 32 bytessupply: 8 bytesdecimals: 1 byte- →
is_initializedat byte 45 ✓
Consider adding a brief comment documenting this derivation for future maintainers.
📝 Optional: Document offset derivation
// Check that the account is not already initialized (is_initialized byte at offset 45)
- const IS_INITIALIZED_OFFSET: usize = 45; // 4 + 32 + 8 + 1 = 45
+ // Offset = mint_authority_option_prefix (4) + mint_authority (32) + supply (8) + decimals (1)
+ const IS_INITIALIZED_OFFSET: usize = 45;🤖 Prompt for AI Agents
In @program-libs/ctoken-interface/src/state/mint/zero_copy.rs around lines 110 -
114, Add a short inline comment above the IS_INITIALIZED_OFFSET constant
explaining how the 45 byte offset was derived from the struct fields
(mint_authority_option_prefix: 4, mint_authority: 32, supply: 8, decimals: 1) so
future maintainers can see why bytes[IS_INITIALIZED_OFFSET] checks
is_initialized; keep the existing check using IS_INITIALIZED_OFFSET and
bytes.len() unchanged and only add the explanatory comment near the
IS_INITIALIZED_OFFSET definition and the if bytes.len() ... memory check.
| fn validate_compressed_only_ext( | ||
| packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, | ||
| compressed_token_account: &ZMultiTokenTransferOutputData<'_>, | ||
| ctoken: &ZCTokenMut, | ||
| ext: &light_ctoken_interface::instructions::extensions::compressed_only::ZCompressedOnlyExtensionInstructionData, | ||
| ) -> Result<(), ProgramError> { | ||
| // 7a. Delegated amount must match | ||
| let ext_delegated: u64 = ext.delegated_amount.into(); | ||
| if ext_delegated != ctoken.delegated_amount.get() { | ||
| return Err(ErrorCode::CompressAndCloseDelegatedAmountMismatch.into()); | ||
| } | ||
|
|
||
| // 7b. Delegate pubkey must match (if present) | ||
| if let Some(delegate) = ctoken.delegate() { | ||
| if !compressed_token_account.has_delegate() { | ||
| return Err(ErrorCode::CompressAndCloseInvalidDelegate.into()); | ||
| } | ||
| let output_delegate = packed_accounts | ||
| .get_u8(compressed_token_account.delegate, "delegate")? | ||
| .key(); | ||
| if !pubkey_eq(output_delegate, &delegate.to_bytes()) { | ||
| return Err(ErrorCode::CompressAndCloseInvalidDelegate.into()); | ||
| } | ||
| } | ||
|
|
||
| // 7c. Withheld fee must match | ||
| let ctoken_fee = ctoken | ||
| .extensions | ||
| .as_ref() | ||
| .and_then(|exts| { | ||
| exts.iter().find_map(|e| match e { | ||
| ZExtensionStructMut::TransferFeeAccount(f) => Some(f.withheld_amount.get()), | ||
| _ => None, | ||
| }) | ||
| }) | ||
| .unwrap_or(0); | ||
| if u64::from(ext.withheld_transfer_fee) != ctoken_fee { | ||
| return Err(ErrorCode::CompressAndCloseWithheldFeeMismatch.into()); | ||
| } | ||
|
|
||
| // 7d. Frozen state must match | ||
| if ctoken.is_frozen() != ext.is_frozen() { | ||
| return Err(ErrorCode::CompressAndCloseFrozenMismatch.into()); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation gap: allowlist “output delegate present while ctoken has no delegate”.
validate_compressed_only_ext only validates delegate when ctoken.delegate().is_some(). If ctoken.delegate() is None but compressed_token_account.has_delegate() is true, this should likely fail (otherwise you can emit compressed outputs with unexpected delegate state).
Proposed fix
// 7b. Delegate pubkey must match (if present)
if let Some(delegate) = ctoken.delegate() {
if !compressed_token_account.has_delegate() {
return Err(ErrorCode::CompressAndCloseInvalidDelegate.into());
}
@@
if !pubkey_eq(output_delegate, &delegate.to_bytes()) {
return Err(ErrorCode::CompressAndCloseInvalidDelegate.into());
}
+ } else if compressed_token_account.has_delegate() {
+ return Err(ErrorCode::CompressAndCloseInvalidDelegate.into());
}🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
around lines 162 - 208, validate_compressed_only_ext currently only checks
delegate fields when ctoken.delegate() is Some, which misses the case where the
compressed output declares a delegate but the ctoken has none; add an explicit
check early in validate_compressed_only_ext that if
compressed_token_account.has_delegate() is true but ctoken.delegate().is_none()
then return ErrorCode::CompressAndCloseInvalidDelegate, keeping the existing
branch that validates matching keys when both delegates exist (use the same
ErrorCode and existing packed_accounts/get_u8 logic).
| /// Validates that a ctoken solana account is ready to be compressed and closed. | ||
| /// Only the compression_authority can compress the account. | ||
| #[profile] | ||
| fn validate_ctoken_account( | ||
| token_account: &AccountInfo, | ||
| authority: &AccountInfo, | ||
| rent_sponsor: &AccountInfo, | ||
| ctoken: &ZCTokenMut<'_>, | ||
| ) -> Result<(), ProgramError> { | ||
| // Check for Compressible extension | ||
| let compressible = ctoken.get_compressible_extension(); | ||
|
|
||
| // CompressAndClose requires Compressible extension | ||
| let compression = compressible.ok_or_else(|| { | ||
| msg!("compress and close requires compressible extension"); | ||
| ProgramError::InvalidAccountData | ||
| })?; | ||
|
|
||
| // Validate rent_sponsor matches | ||
| if compression.info.rent_sponsor != *rent_sponsor.key() { | ||
| msg!("rent recipient mismatch"); | ||
| return Err(ProgramError::InvalidAccountData); | ||
| } | ||
|
|
||
| if compression.info.compression_authority != *authority.key() { | ||
| msg!("compress and close requires compression authority"); | ||
| return Err(ProgramError::InvalidAccountData); | ||
| } | ||
|
|
||
| let current_slot = pinocchio::sysvars::clock::Clock::get() | ||
| .map_err(convert_program_error)? | ||
| .slot; | ||
| compression | ||
| .info | ||
| .is_compressible( | ||
| token_account.data_len() as u64, | ||
| current_slot, | ||
| token_account.lamports(), | ||
| ) | ||
| .map_err(|_| ProgramError::InvalidAccountData)? | ||
| .ok_or_else(|| { | ||
| msg!("account not compressible"); | ||
| ProgramError::InvalidAccountData | ||
| })?; | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and inspect close_token_account processor implementation
find programs/compressed-token/program/src -name "*.rs" -type f | xargs grep -l "fn close_token_account"Repository: Lightprotocol/light-protocol
Length of output: 135
🏁 Script executed:
#!/bin/bash
# Look for the actual close_token_account function definition and validate_not_inactive usage
rg -n "fn close_token_account|validate_not_inactive|validate_active" -A 20 programs/compressed-token/program/src/ctoken/close/ 2>/dev/null || rg -n "fn close_token_account|validate_not_inactive|validate_active" -A 20 programs/compressed-token/program/src/Repository: Lightprotocol/light-protocol
Length of output: 2412
🏁 Script executed:
#!/bin/bash
# Check lamport distribution logic in close operations
rg -n "rent_sponsor|destination|transfer_lamports|calculate.*close" -B 2 -A 2 programs/compressed-token/program/src/ctoken/close/ 2>/dev/null || rg -n "rent_sponsor|destination.*lamport" -B 2 -A 2 programs/compressed-token/program/src/ctoken/Repository: Lightprotocol/light-protocol
Length of output: 9880
🏁 Script executed:
#!/bin/bash
# Check for config state validation in close flow
rg -n "validate_not_inactive|validate_active|config.*state|CompressibleConfig" -B 3 -A 3 programs/compressed-token/program/src/ctoken/close/Repository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check finalize_account_closure function
rg -n "fn finalize_account_closure" -A 15 programs/compressed-token/program/src/ctoken/close/Repository: Lightprotocol/light-protocol
Length of output: 1005
🏁 Script executed:
#!/bin/bash
# Check the instruction handler that calls close_token_account to see if validation happens there
rg -n "compress_and_close|close_token_account" -B 5 -A 5 programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/Repository: Lightprotocol/light-protocol
Length of output: 23457
🏁 Script executed:
#!/bin/bash
# Check if config state is validated at the transfer2 instruction level or before compress_and_close
rg -n "CompressibleConfig|validate_not_inactive|config.*state" programs/compressed-token/program/src/compressed_token/transfer2/ -B 2 -A 2Repository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check the transfer2 instruction handler for config state validation
find programs/compressed-token/program/src/compressed_token/transfer2 -name "*.rs" | head -5 | xargs ls -lhRepository: Lightprotocol/light-protocol
Length of output: 785
🏁 Script executed:
#!/bin/bash
# Check the main transfer2 processor or instruction handler
rg -n "fn process_transfer2|fn handle_transfer2|CompressibleConfig" -B 3 -A 10 programs/compressed-token/program/src/compressed_token/transfer2/processor.rs 2>/dev/null || rg -n "fn process.*transfer" -B 3 -A 10 programs/compressed-token/program/src/compressed_token/Repository: Lightprotocol/light-protocol
Length of output: 698
🏁 Script executed:
#!/bin/bash
# Check the transfer2 accounts validation for config state
rg -n "validate_active|validate_not_inactive|config" programs/compressed-token/program/src/compressed_token/transfer2/accounts.rsRepository: Lightprotocol/light-protocol
Length of output: 418
🏁 Script executed:
#!/bin/bash
# Check the full process_transfer2 function for config state validation
rg -n "process_transfer2" -A 50 programs/compressed-token/program/src/compressed_token/transfer2/processor.rs | head -80Repository: Lightprotocol/light-protocol
Length of output: 2079
🏁 Script executed:
#!/bin/bash
# Search for CompressibleConfig in the entire ctoken crate
find programs/compressed-token/program/src -name "*.rs" | xargs rg "CompressibleConfig" | head -20Repository: Lightprotocol/light-protocol
Length of output: 2737
🏁 Script executed:
#!/bin/bash
# Check if transfer2 has any config account handling
rg -n "config|Config" programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs | head -30Repository: Lightprotocol/light-protocol
Length of output: 418
🏁 Script executed:
#!/bin/bash
# Look at the compress_and_close flow to see if config validation happens there
cat programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs | head -120Repository: Lightprotocol/light-protocol
Length of output: 4545
🏁 Script executed:
#!/bin/bash
# Check if Transfer2Config or CompressibleConfig is involved in transfer2
rg -n "struct Transfer2Config|pub config" programs/compressed-token/program/src/compressed_token/transfer2/config.rsRepository: Lightprotocol/light-protocol
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Check what config.rs contains
cat programs/compressed-token/program/src/compressed_token/transfer2/config.rsRepository: Lightprotocol/light-protocol
Length of output: 1894
🏁 Script executed:
#!/bin/bash
# Check how claim operation validates config state (as reference)
rg -n "validate_not_inactive|config_account" programs/compressed-token/program/src/compressible/claim.rs -A 3 -B 3Repository: Lightprotocol/light-protocol
Length of output: 2440
🏁 Script executed:
#!/bin/bash
# Check if Transfer2Accounts does any config validation
cat programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs | head -200Repository: Lightprotocol/light-protocol
Length of output: 6536
🏁 Script executed:
#!/bin/bash
# Check if config validation is in validate_ctoken_account (the function shown in the review)
rg -n "validate_ctoken_account" programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs -A 30Repository: Lightprotocol/light-protocol
Length of output: 2224
🏁 Script executed:
#!/bin/bash
# Check the full validate_ctoken_account function
sed -n '265,311p' programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rsRepository: Lightprotocol/light-protocol
Length of output: 1624
Config state validation is missing from the compress_and_close path; lamport distribution rules are correctly implemented.
Lamport distribution is sound—rent flows to rent_sponsor (lines 167–174) and remainder to destination (lines 176–184), using the transfer_lamports helper as required. However, config state validation is absent. The validate_ctoken_account function checks the Compressible extension, authority, and rent_sponsor, but does not enforce config state gating via validate_not_inactive(). By contrast, the claim instruction explicitly validates config state (claim.rs line 37–39). Per documented learnings, close operations must validate that config is not inactive. Since the transfer2 flow does not parse or validate CompressibleConfig, accounts can be closed even if the config is inactive—inconsistent with the pattern for claim and withdraw operations.
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs
around lines 265 - 311, validate_ctoken_account in the compress_and_close flow
doesn't enforce config state gating, allowing closes when the CompressibleConfig
is inactive; to fix, parse the CompressibleConfig for the token (the same way
claim.rs does) and call validate_not_inactive(...) before allowing
compression/close (i.e., after obtaining compression via
ctoken.get_compressible_extension() and before checking is_compressible),
ensuring compress_and_close follows the same config-state check as the claim
path.
| use crate::shared::{ | ||
| compressible_top_up::process_compression_top_up, owner_validation::check_ctoken_owner, | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check the file to see the current state
cat -n programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs | head -130Repository: Lightprotocol/light-protocol
Length of output: 5458
🏁 Script executed:
# Look at the process_compression_top_up function signature
rg -A 10 "fn process_compression_top_up" programs/compressed-token/program/src/shared/compressible_top_up.rsRepository: Lightprotocol/light-protocol
Length of output: 427
🏁 Script executed:
# Check how rent variables are typically handled in similar files
rg -B 5 -A 5 "let mut rent: Option" programs/compressed-token/program/src/ | head -50Repository: Lightprotocol/light-protocol
Length of output: 1101
Fix Rust compilation error: &mut None is a mutable borrow of a temporary and will not compile.
The function signature for process_compression_top_up requires rent: &mut Option<Rent>. Passing &mut None (lines 79 and 107) is invalid because Rust forbids creating mutable references to temporary values—they would immediately drop and leave a dangling reference.
Declare a rent variable once in the function scope and reuse it across both calls. This also enables the intended rent sysvar caching pattern (as used elsewhere in the project):
Proposed fix
let current_balance: u64 = ctoken.base.amount.get();
let mut current_slot = 0;
+ let mut rent: Option<Rent> = None;
match mode {
ZCompressionMode::Compress => {
// ...
if let Some(compression) = ctoken.get_compressible_extension() {
process_compression_top_up(
&compression.info,
token_account_info,
&mut current_slot,
transfer_amount,
lamports_budget,
- &mut None,
+ &mut rent,
)?;
}
}
ZCompressionMode::Decompress => {
// ...
if let Some(compression) = ctoken.get_compressible_extension() {
process_compression_top_up(
&compression.info,
token_account_info,
&mut current_slot,
transfer_amount,
lamports_budget,
- &mut None,
+ &mut rent,
)?;
}
}Also add the Rent import at the top of the file.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs
around lines 18 - 21, The compilation error comes from passing a mutable
reference to a temporary (&mut None) into process_compression_top_up which
expects rent: &mut Option<Rent>; instead, add Rent to the imports and declare a
local mutable variable like let mut rent: Option<Rent> = None; in the
surrounding function scope and pass &mut rent to both calls to
process_compression_top_up (replace the &mut None occurrences), thereby caching
the Rent sysvar as intended and reusing the same mutable Option<Rent> for both
invocations.
| /// Apply delegate state. Resolves delegate only when needed (inside the check). | ||
| #[inline(always)] | ||
| fn apply_delegate( | ||
| ctoken: &mut ZCTokenMut, | ||
| ext_data: &ZCompressedOnlyExtensionInstructionData, | ||
| inputs: &DecompressCompressOnlyInputs, | ||
| packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, | ||
| ) -> Result<(), ProgramError> { | ||
| // Skip if destination already has delegate | ||
| if ctoken.delegate().is_some() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let delegated_amount: u64 = ext_data.delegated_amount.into(); | ||
|
|
||
| // Resolve delegate only when needed | ||
| let input_delegate = if inputs.input_token_data.has_delegate() { | ||
| Some(packed_accounts.get_u8(inputs.input_token_data.delegate, "delegate")?) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| if let Some(delegate_acc) = input_delegate { | ||
| ctoken | ||
| .base | ||
| .set_delegate(Some(Pubkey::from(*delegate_acc.key())))?; | ||
| if delegated_amount > 0 { | ||
| let current = ctoken.base.delegated_amount.get(); | ||
| ctoken.base.delegated_amount.set(current + delegated_amount); | ||
| } | ||
| } else if delegated_amount > 0 { | ||
| msg!("Decompress: delegated_amount > 0 but no delegate"); | ||
| return Err(CTokenError::DecompressDelegatedAmountWithoutDelegate.into()); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug risk: apply_delegate skips when a delegate already exists, dropping delegated_amount and allowing mismatches.
If the destination already has a delegate, you likely still need to:
- validate it matches the input delegate (when provided), and
- apply
delegated_amount(when> 0) to keep the accounting consistent.
Also, current + delegated_amount should be checked_add.
Proposed fix
fn apply_delegate(
ctoken: &mut ZCTokenMut,
ext_data: &ZCompressedOnlyExtensionInstructionData,
inputs: &DecompressCompressOnlyInputs,
packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>,
) -> Result<(), ProgramError> {
- // Skip if destination already has delegate
- if ctoken.delegate().is_some() {
- return Ok(());
- }
-
let delegated_amount: u64 = ext_data.delegated_amount.into();
// Resolve delegate only when needed
let input_delegate = if inputs.input_token_data.has_delegate() {
Some(packed_accounts.get_u8(inputs.input_token_data.delegate, "delegate")?)
} else {
None
};
- if let Some(delegate_acc) = input_delegate {
+ if let Some(delegate_acc) = input_delegate {
+ // If destination already has a delegate, it must match.
+ if let Some(existing) = ctoken.delegate() {
+ if !pubkey_eq(delegate_acc.key(), &existing.to_bytes()) {
+ msg!("Decompress: existing delegate mismatch");
+ return Err(CTokenError::DecompressDelegatedAmountWithoutDelegate.into());
+ }
+ } else {
+ ctoken
+ .base
+ .set_delegate(Some(Pubkey::from(*delegate_acc.key())))?;
+ }
- ctoken
- .base
- .set_delegate(Some(Pubkey::from(*delegate_acc.key())))?;
if delegated_amount > 0 {
let current = ctoken.base.delegated_amount.get();
- ctoken.base.delegated_amount.set(current + delegated_amount);
+ ctoken
+ .base
+ .delegated_amount
+ .set(current.checked_add(delegated_amount).ok_or(ProgramError::ArithmeticOverflow)?);
}
} else if delegated_amount > 0 {
msg!("Decompress: delegated_amount > 0 but no delegate");
return Err(CTokenError::DecompressDelegatedAmountWithoutDelegate.into());
}
Ok(())
}Committable suggestion skipped: line range outside the PR's diff.
| process_compression_top_up( | ||
| &compressible.info, | ||
| account, | ||
| &mut 0, | ||
| &mut transfer_amount, | ||
| &mut 0, | ||
| &mut None, | ||
| )?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Unused out parameters suggest potential API mismatch.
The call to process_compression_top_up passes several unused mutable references (&mut 0, &mut None). These parameters are computed but discarded:
process_compression_top_up(
&compressible.info,
account,
&mut 0, // current_slot - discarded
&mut transfer_amount,
&mut 0, // rent_exemption - discarded
&mut None, // decimals - discarded
)?;This works but suggests the function might benefit from a simpler wrapper or a refactored signature for single-account scenarios. As per the coding guideline, rent top-up calculations should use utilities from src/shared/compressible_top_up.rs - consider whether a dedicated single-account helper would be cleaner.
🤖 Prompt for AI Agents
In @programs/compressed-token/program/src/ctoken/approve_revoke.rs around lines
108 - 115, The call to process_compression_top_up is passing placeholder
out-parameters (&mut 0, &mut None) indicating an API mismatch; replace this with
a focused helper that computes and returns the needed top-up values for a single
account using the utilities in src/shared/compressible_top_up.rs (e.g.,
compute_rent_exempt_and_transfer_amount or add a
process_compression_top_up_single_account wrapper), update the call site to use
the helper's returned transfer_amount (and any rent_exempt/decimals if needed)
instead of passing &mut 0/&mut None, and adjust or overload
process_compression_top_up signature if necessary so single-account callers no
longer need to provide unused mutable placeholders.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.