Skip to content

fix: accumulate delegated amount at decompression#2242

Merged
ananas-block merged 5 commits intomainfrom
jorrit/fix-accumulate-delegate-amount-at-decompression
Feb 10, 2026
Merged

fix: accumulate delegated amount at decompression#2242
ananas-block merged 5 commits intomainfrom
jorrit/fix-accumulate-delegate-amount-at-decompression

Conversation

@ananas-block
Copy link
Contributor

@ananas-block ananas-block commented Feb 5, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Unified decompression amount validation to prevent mismatches and related failures.
    • Fixed delegate handling so delegated balances now correctly accumulate when delegates match or are newly assigned; removes erroneous error paths when no delegate is present.
    • Strengthened destination ownership checks during decompression to reduce invalid restores.
  • Documentation

    • Updated extension docs with clarified decompression rules, state-preservation matrix, and new error codes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Warning

Rate limit exceeded

@ananas-block has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Reworked decompression delegate and amount validation paths: always validate compression_amount equals input_amount; apply_delegate now sets a delegate if absent, accumulates delegated_amount when a delegate is present and matches (or was just set), and no longer errors when delegated_amount > 0 without an input delegate.

Changes

Cohort / File(s) Summary
Decompress logic
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs
Made compression amount vs input amount check unconditional; refactored apply_delegate to assign delegate if missing, accumulate delegated_amount with overflow checks when a delegate is present, and require an input delegate to proceed. Removed old error path for delegated_amount without delegate.
Documentation
programs/compressed-token/program/docs/EXTENSIONS.md
Updated Decompress and CompressedOnly validation flow, clarified destination ownership checks, documented three-case delegate handling (no delegate → set & accumulate; match → accumulate; mismatch → skip), updated state-preservation matrix and error codes, and marked two open questions as implemented.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

A patch that tends the ledger's seam,
Sets delegates where none had been,
Adds amounts up, with careful eyes—
No orphaned sums, no silent sighs. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing logic to accumulate delegated amounts during decompression, which aligns with the core modifications in the decompress.rs file.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jorrit/fix-accumulate-delegate-amount-at-decompression

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs`:
- Around line 134-141: Extract the duplicated delegated_amount accumulation into
a small helper function (e.g., fn accumulate_delegated_amount(ctoken: &mut
ZTokenMut, amount: u64) -> Result<(), ProgramError>) that checks amount > 0,
reads current via ctoken.base.delegated_amount.get(), performs
current.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow) and sets the
field, returning Ok(()); then replace both existing blocks that update
delegated_amount with calls to accumulate_delegated_amount(ctoken,
delegated_amount)? to eliminate the repeated code.
- Around line 125-145: The existing branch that handles when ctoken.delegate()
returns Some(...) currently ignores delegated_amount when input_delegate is
None, causing delegated amounts to be dropped; update the branch in
decompress.rs to validate that if delegated_amount > 0 and
input_delegate.is_none() you return the same error used elsewhere
(DecompressDelegatedAmountWithoutDelegate), otherwise proceed with the existing
delegate-matching logic (using pubkey_eq on existing_delegate.array_ref() and
delegate_acc.key()) and the accumulation into ctoken.base.delegated_amount with
checked_add as currently implemented.

Comment on lines +125 to +145
// Check if destination already has a delegate
if let Some(existing_delegate) = ctoken.delegate() {
// If input has a delegate, check if it matches to accumulate amount
if let Some(delegate_acc) = input_delegate {
// If delegates don't match, skip accumulation
if !pubkey_eq(existing_delegate.array_ref(), delegate_acc.key()) {
return Ok(());
}
// Same delegate - accumulate the delegated_amount
if delegated_amount > 0 {
let current = ctoken.base.delegated_amount.get();
ctoken.base.delegated_amount.set(
current
.checked_add(delegated_amount)
.ok_or(ProgramError::ArithmeticOverflow)?,
);
}
}
// If no input delegate or delegates don't match, nothing to do
return Ok(());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing validation: delegated_amount > 0 with existing delegate but no input delegate

There's an inconsistency in how you handle delegated_amount > 0 without an input delegate:

  • When no existing delegate (lines 160-162): You correctly return DecompressDelegatedAmountWithoutDelegate error
  • When existing delegate is present (lines 142-144): You silently return Ok(), ignoring the delegated_amount

This means if a compressed account is decompressed with delegated_amount = 100 but no input_delegate, and the CToken already has a delegate, that 100 just vanishes into the void.

Consider adding the same validation check:

🐛 Proposed fix to add missing validation
         }
-        // If no input delegate or delegates don't match, nothing to do
+        // No input delegate provided
+        if delegated_amount > 0 {
+            msg!("Decompress: delegated_amount > 0 but no delegate");
+            return Err(TokenError::DecompressDelegatedAmountWithoutDelegate.into());
+        }
         return Ok(());
     }
🤖 Prompt for AI Agents
In
`@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs`
around lines 125 - 145, The existing branch that handles when ctoken.delegate()
returns Some(...) currently ignores delegated_amount when input_delegate is
None, causing delegated amounts to be dropped; update the branch in
decompress.rs to validate that if delegated_amount > 0 and
input_delegate.is_none() you return the same error used elsewhere
(DecompressDelegatedAmountWithoutDelegate), otherwise proceed with the existing
delegate-matching logic (using pubkey_eq on existing_delegate.array_ref() and
delegate_acc.key()) and the accumulation into ctoken.base.delegated_amount with
checked_add as currently implemented.

Comment on lines +134 to +141
if delegated_amount > 0 {
let current = ctoken.base.delegated_amount.get();
ctoken.base.delegated_amount.set(
current
.checked_add(delegated_amount)
.ok_or(ProgramError::ArithmeticOverflow)?,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Duplicated checked_add logic for delegated_amount accumulation

The accumulation logic appears twice with identical structure. Consider extracting to a helper:

♻️ Proposed helper extraction
#[inline(always)]
fn accumulate_delegated_amount(ctoken: &mut ZTokenMut, amount: u64) -> Result<(), ProgramError> {
    if amount > 0 {
        let current = ctoken.base.delegated_amount.get();
        ctoken.base.delegated_amount.set(
            current
                .checked_add(amount)
                .ok_or(ProgramError::ArithmeticOverflow)?,
        );
    }
    Ok(())
}

Then both call sites simplify to:

accumulate_delegated_amount(ctoken, delegated_amount)?;

Also applies to: 152-159

🤖 Prompt for AI Agents
In
`@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs`
around lines 134 - 141, Extract the duplicated delegated_amount accumulation
into a small helper function (e.g., fn accumulate_delegated_amount(ctoken: &mut
ZTokenMut, amount: u64) -> Result<(), ProgramError>) that checks amount > 0,
reads current via ctoken.base.delegated_amount.get(), performs
current.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow) and sets the
field, returning Ok(()); then replace both existing blocks that update
delegated_amount with calls to accumulate_delegated_amount(ctoken,
delegated_amount)? to eliminate the repeated code.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@programs/compressed-token/program/docs/EXTENSIONS.md`:
- Around line 295-304: Update the documentation for
validate_and_apply_compressed_only to note the precondition that the delegate
three-case handling only runs when an input delegate is present: explicitly
state that apply_delegate returns early (no-op) if
input_token_data.has_delegate() is false, so delegate state is not modified when
no input delegate is provided; reference validate_and_apply_compressed_only,
apply_delegate, and input_token_data.has_delegate to guide readers to the
relevant code paths.

Comment on lines +295 to +304
**State Restoration (`validate_and_apply_compressed_only` function, lines 15-64):**
1. Return early if no decompress inputs or no CompressedOnly extension (lines 23-29)
2. Validate amount matches for ATA or compress_to_pubkey decompress (lines 31-46)
3. Validate destination ownership via `validate_destination` (lines 48-56)
4. Restore delegate pubkey and delegated_amount via `apply_delegate` (lines 58-59)
5. Restore `withheld_transfer_fee` via `apply_withheld_fee` (lines 61-62)
6. Restore frozen state via `ctoken.base.set_frozen()` (lines 64-67)

**Validation (`validate_destination`, lines 77-106):**
2. Validate amount matches compression amount (lines 31-40)
3. Validate destination ownership via `validate_destination` (lines 42-50)
4. Apply delegate state via `apply_delegate` (line 53):
- If no existing delegate: set delegate and accumulate delegated_amount
- If existing delegate matches input: accumulate delegated_amount
- If existing delegate differs: skip (no accumulation, silent ignore)
5. Restore `withheld_transfer_fee` via `apply_withheld_fee` (line 56)
6. Restore frozen state via `ctoken.base.set_frozen()` (lines 58-61)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Documentation accurately reflects the code — one small gap to consider.

I cross-referenced each step against decompress.rs and everything checks out: the amount validation, destination ownership, delegate three-case handling, withheld fee, and frozen state restoration all match the implementation.

One gap: the docs describe the three delegate sub-cases (lines 300–302) but don't mention the precondition that all three cases only apply when input_token_data.has_delegate() is true. When no input delegate is provided, apply_delegate returns Ok(()) early (line 118–120 in code). Consider adding a brief note like:

If no input delegate is present, delegate state is not modified.

This makes the docs self-contained for someone who hasn't read the code.

🤖 Prompt for AI Agents
In `@programs/compressed-token/program/docs/EXTENSIONS.md` around lines 295 - 304,
Update the documentation for validate_and_apply_compressed_only to note the
precondition that the delegate three-case handling only runs when an input
delegate is present: explicitly state that apply_delegate returns early (no-op)
if input_token_data.has_delegate() is false, so delegate state is not modified
when no input delegate is provided; reference
validate_and_apply_compressed_only, apply_delegate, and
input_token_data.has_delegate to guide readers to the relevant code paths.

Comment on lines -31 to -36
// === VALIDATE amount matches for ATA or compress_to_pubkey decompress ===
let compress_to_pubkey = ctoken
.get_compressible_extension()
.map(|ext| ext.info.compress_to_pubkey())
.unwrap_or(false);
if ext_data.is_ata() || compress_to_pubkey {
Copy link
Contributor

Choose a reason for hiding this comment

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

rebase and check if this goes away

@ananas-block ananas-block force-pushed the jorrit/fix-accumulate-delegate-amount-at-decompression branch from 988a7ff to 9383738 Compare February 10, 2026 22:43
@ananas-block ananas-block merged commit b19f2d5 into main Feb 10, 2026
30 checks passed
@ananas-block ananas-block deleted the jorrit/fix-accumulate-delegate-amount-at-decompression branch February 10, 2026 23:21
SwenSchaeferjohann pushed a commit that referenced this pull request Feb 11, 2026
* fix: accumulate delegated amount at decompression

* fix lint

* refactor: simplify apply_delegate to single accumulation path

* fix: ignore delegated_amount without delegate

* restore decompress amount check
SwenSchaeferjohann added a commit that referenced this pull request Feb 11, 2026
refactor

remove fetch-accounts

renaming, simplify trait

fomat

excl photon-api submodule

fix: multi-pass cold account lookup in test indexer RPC

Align get_account_interface and get_multiple_account_interfaces with
Photon's lookup strategy: search compressed_accounts by onchain_pubkey,
then by PDA seed derivation, then token_compressed_accounts, then by
token_data.owner. Also fix as_mint() to accept ColdContext::Account
since Photon returns mints as generic compressed accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>

lint

fix lint

fix: reject rent sponsor self-referencing the token account (#2257)

* fix: reject rent sponsor self-referencing the token account

Audit issue #9 (INFO): The rent payer could be the same account as
the target token account being created. Add a check that rejects
this self-reference to prevent accounting issues.

* test: add failing test rent sponsor self reference

fix: process metadata add/remove actions in sequential order (#2256)

* fix: process metadata add/remove actions in sequential order

Audit issue #16 (LOW): should_add_key checked for any add and any
remove independently, ignoring action ordering. An add-remove-add
sequence would incorrectly remove the key. Process actions
sequentially so the final state reflects the actual order.

* chore: format

* test: add randomized test for metadata action processing

Validates that process_extensions_config_with_actions produces correct
AdditionalMetadataConfig for random sequences of UpdateMetadataField
and RemoveMetadataKey actions, covering the add-remove-add bug from
audit issue #16.

* test: add integration test for audit issue #13 (no double rent charge)

Verifies that two compress operations targeting the same compressible
CToken account in a single Transfer2 instruction do not double-charge
the rent top-up budget.

* chore: format extensions_metadata test

fix: validate authority on self-transfer early return (#2252)

* fix: handle self-transfer in ctoken transfer and transfer_checked

Validate that the authority is a signer and is the owner or delegate
before allowing self-transfer early return. Previously the self-transfer
path returned Ok(()) without any authority validation.

* fix: simplify map_or to is_some_and per clippy

* fix: use pubkey_eq for self-transfer check

* refactor: extract self-transfer validation into shared function

Extract duplicate self-transfer check from default.rs and checked.rs
into validate_self_transfer() in shared.rs with cold path for authority
validation.

* chore: format

* fix: deduplicate random metadata keys in test_random_mint_action

Random key generation could produce duplicate keys, causing
DuplicateMetadataKey error (18040) with certain seeds.

fix: enforce mint extension checks in cToken-to-cToken decompress (#2246)

* fix: enforce mint extension checks in cToken-to-cToken decompress hot path

Add enforce_extension_state() to MintExtensionChecks and call it in the
Decompress branch when decompress_inputs is None (hot-path, not
CompressedOnly restore). This prevents cToken-to-cToken transfers from
bypassing pause, transfer fee, and transfer hook restrictions.

* fix test

chore: reject compress for mints with restricted extensions in build_mint_extension_cache (#2240)

* chore: reject compress for mints with restricted extensions in mint check

* Update programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

* fix: format else-if condition for lint

---------

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

fix: token-pool index 0 check (#2239)

fix(programs): add MintCloseAuthority as restricted extension (M-03) (#2263)

* fix: add MintCloseAuthority as restricted extension (M-03)

A mint with MintCloseAuthority can be closed and re-opened with
different extensions. Treating it as restricted ensures compressed
tokens from such mints require CompressedOnly mode.

* test: add MintCloseAuthority compression_only requirement tests

Add test coverage for MintCloseAuthority requiring compression_only mode,
complementing the fix in f2da063.

refactor: light program pinocchio macro (#2247)

* refactor: light program pinocchio macro

* fix: address CodeRabbit review comments on macro codegen

- Fix account order bug in process_update_config (config=0, authority=1)
- Use backend-provided serialize/deserialize derives in LightAccountData
- Remove redundant is_pinocchio() branch for has_le_bytes unpack fields
- DRY doc attribute generation across 4 struct generation methods
- Unify unpack_data path using account_crate re-export for both backends

chore(libs): bump versions (#2272)

fix(programs): allow account-level delegate to compress CToken (M-02) (#2262)

* fix: allow account-level delegate to compress tokens from CToken (M-02)

check_ctoken_owner() only checked owner and permanent delegate.
An account-level delegate (approved via CTokenApprove) could not
compress tokens. Added delegate check after permanent delegate.

* test: compress by delegate

fix: accumulate delegated amount at decompression (#2242)

* fix: accumulate delegated amount at decompression

* fix lint

* refactor: simplify apply_delegate to single accumulation path

* fix: ignore delegated_amount without delegate

* restore decompress amount check

fix programtest, wallet owner tracking for ata

fmt and lint
SwenSchaeferjohann added a commit that referenced this pull request Feb 11, 2026
refactor

remove fetch-accounts

renaming, simplify trait

fomat

excl photon-api submodule

fix: multi-pass cold account lookup in test indexer RPC

Align get_account_interface and get_multiple_account_interfaces with
Photon's lookup strategy: search compressed_accounts by onchain_pubkey,
then by PDA seed derivation, then token_compressed_accounts, then by
token_data.owner. Also fix as_mint() to accept ColdContext::Account
since Photon returns mints as generic compressed accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>

lint

fix lint

fix: reject rent sponsor self-referencing the token account (#2257)

* fix: reject rent sponsor self-referencing the token account

Audit issue #9 (INFO): The rent payer could be the same account as
the target token account being created. Add a check that rejects
this self-reference to prevent accounting issues.

* test: add failing test rent sponsor self reference

fix: process metadata add/remove actions in sequential order (#2256)

* fix: process metadata add/remove actions in sequential order

Audit issue #16 (LOW): should_add_key checked for any add and any
remove independently, ignoring action ordering. An add-remove-add
sequence would incorrectly remove the key. Process actions
sequentially so the final state reflects the actual order.

* chore: format

* test: add randomized test for metadata action processing

Validates that process_extensions_config_with_actions produces correct
AdditionalMetadataConfig for random sequences of UpdateMetadataField
and RemoveMetadataKey actions, covering the add-remove-add bug from
audit issue #16.

* test: add integration test for audit issue #13 (no double rent charge)

Verifies that two compress operations targeting the same compressible
CToken account in a single Transfer2 instruction do not double-charge
the rent top-up budget.

* chore: format extensions_metadata test

fix: validate authority on self-transfer early return (#2252)

* fix: handle self-transfer in ctoken transfer and transfer_checked

Validate that the authority is a signer and is the owner or delegate
before allowing self-transfer early return. Previously the self-transfer
path returned Ok(()) without any authority validation.

* fix: simplify map_or to is_some_and per clippy

* fix: use pubkey_eq for self-transfer check

* refactor: extract self-transfer validation into shared function

Extract duplicate self-transfer check from default.rs and checked.rs
into validate_self_transfer() in shared.rs with cold path for authority
validation.

* chore: format

* fix: deduplicate random metadata keys in test_random_mint_action

Random key generation could produce duplicate keys, causing
DuplicateMetadataKey error (18040) with certain seeds.

fix: enforce mint extension checks in cToken-to-cToken decompress (#2246)

* fix: enforce mint extension checks in cToken-to-cToken decompress hot path

Add enforce_extension_state() to MintExtensionChecks and call it in the
Decompress branch when decompress_inputs is None (hot-path, not
CompressedOnly restore). This prevents cToken-to-cToken transfers from
bypassing pause, transfer fee, and transfer hook restrictions.

* fix test

chore: reject compress for mints with restricted extensions in build_mint_extension_cache (#2240)

* chore: reject compress for mints with restricted extensions in mint check

* Update programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

* fix: format else-if condition for lint

---------

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

fix: token-pool index 0 check (#2239)

fix(programs): add MintCloseAuthority as restricted extension (M-03) (#2263)

* fix: add MintCloseAuthority as restricted extension (M-03)

A mint with MintCloseAuthority can be closed and re-opened with
different extensions. Treating it as restricted ensures compressed
tokens from such mints require CompressedOnly mode.

* test: add MintCloseAuthority compression_only requirement tests

Add test coverage for MintCloseAuthority requiring compression_only mode,
complementing the fix in f2da063.

refactor: light program pinocchio macro (#2247)

* refactor: light program pinocchio macro

* fix: address CodeRabbit review comments on macro codegen

- Fix account order bug in process_update_config (config=0, authority=1)
- Use backend-provided serialize/deserialize derives in LightAccountData
- Remove redundant is_pinocchio() branch for has_le_bytes unpack fields
- DRY doc attribute generation across 4 struct generation methods
- Unify unpack_data path using account_crate re-export for both backends

chore(libs): bump versions (#2272)

fix(programs): allow account-level delegate to compress CToken (M-02) (#2262)

* fix: allow account-level delegate to compress tokens from CToken (M-02)

check_ctoken_owner() only checked owner and permanent delegate.
An account-level delegate (approved via CTokenApprove) could not
compress tokens. Added delegate check after permanent delegate.

* test: compress by delegate

fix: accumulate delegated amount at decompression (#2242)

* fix: accumulate delegated amount at decompression

* fix lint

* refactor: simplify apply_delegate to single accumulation path

* fix: ignore delegated_amount without delegate

* restore decompress amount check

fix programtest, wallet owner tracking for ata

fmt and lint
SwenSchaeferjohann added a commit that referenced this pull request Feb 11, 2026
refactor

remove fetch-accounts

renaming, simplify trait

fomat

excl photon-api submodule

fix: multi-pass cold account lookup in test indexer RPC

Align get_account_interface and get_multiple_account_interfaces with
Photon's lookup strategy: search compressed_accounts by onchain_pubkey,
then by PDA seed derivation, then token_compressed_accounts, then by
token_data.owner. Also fix as_mint() to accept ColdContext::Account
since Photon returns mints as generic compressed accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>

lint

fix lint

fix: reject rent sponsor self-referencing the token account (#2257)

* fix: reject rent sponsor self-referencing the token account

Audit issue #9 (INFO): The rent payer could be the same account as
the target token account being created. Add a check that rejects
this self-reference to prevent accounting issues.

* test: add failing test rent sponsor self reference

fix: process metadata add/remove actions in sequential order (#2256)

* fix: process metadata add/remove actions in sequential order

Audit issue #16 (LOW): should_add_key checked for any add and any
remove independently, ignoring action ordering. An add-remove-add
sequence would incorrectly remove the key. Process actions
sequentially so the final state reflects the actual order.

* chore: format

* test: add randomized test for metadata action processing

Validates that process_extensions_config_with_actions produces correct
AdditionalMetadataConfig for random sequences of UpdateMetadataField
and RemoveMetadataKey actions, covering the add-remove-add bug from
audit issue #16.

* test: add integration test for audit issue #13 (no double rent charge)

Verifies that two compress operations targeting the same compressible
CToken account in a single Transfer2 instruction do not double-charge
the rent top-up budget.

* chore: format extensions_metadata test

fix: validate authority on self-transfer early return (#2252)

* fix: handle self-transfer in ctoken transfer and transfer_checked

Validate that the authority is a signer and is the owner or delegate
before allowing self-transfer early return. Previously the self-transfer
path returned Ok(()) without any authority validation.

* fix: simplify map_or to is_some_and per clippy

* fix: use pubkey_eq for self-transfer check

* refactor: extract self-transfer validation into shared function

Extract duplicate self-transfer check from default.rs and checked.rs
into validate_self_transfer() in shared.rs with cold path for authority
validation.

* chore: format

* fix: deduplicate random metadata keys in test_random_mint_action

Random key generation could produce duplicate keys, causing
DuplicateMetadataKey error (18040) with certain seeds.

fix: enforce mint extension checks in cToken-to-cToken decompress (#2246)

* fix: enforce mint extension checks in cToken-to-cToken decompress hot path

Add enforce_extension_state() to MintExtensionChecks and call it in the
Decompress branch when decompress_inputs is None (hot-path, not
CompressedOnly restore). This prevents cToken-to-cToken transfers from
bypassing pause, transfer fee, and transfer hook restrictions.

* fix test

chore: reject compress for mints with restricted extensions in build_mint_extension_cache (#2240)

* chore: reject compress for mints with restricted extensions in mint check

* Update programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

* fix: format else-if condition for lint

---------

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

fix: token-pool index 0 check (#2239)

fix(programs): add MintCloseAuthority as restricted extension (M-03) (#2263)

* fix: add MintCloseAuthority as restricted extension (M-03)

A mint with MintCloseAuthority can be closed and re-opened with
different extensions. Treating it as restricted ensures compressed
tokens from such mints require CompressedOnly mode.

* test: add MintCloseAuthority compression_only requirement tests

Add test coverage for MintCloseAuthority requiring compression_only mode,
complementing the fix in f2da063.

refactor: light program pinocchio macro (#2247)

* refactor: light program pinocchio macro

* fix: address CodeRabbit review comments on macro codegen

- Fix account order bug in process_update_config (config=0, authority=1)
- Use backend-provided serialize/deserialize derives in LightAccountData
- Remove redundant is_pinocchio() branch for has_le_bytes unpack fields
- DRY doc attribute generation across 4 struct generation methods
- Unify unpack_data path using account_crate re-export for both backends

chore(libs): bump versions (#2272)

fix(programs): allow account-level delegate to compress CToken (M-02) (#2262)

* fix: allow account-level delegate to compress tokens from CToken (M-02)

check_ctoken_owner() only checked owner and permanent delegate.
An account-level delegate (approved via CTokenApprove) could not
compress tokens. Added delegate check after permanent delegate.

* test: compress by delegate

fix: accumulate delegated amount at decompression (#2242)

* fix: accumulate delegated amount at decompression

* fix lint

* refactor: simplify apply_delegate to single accumulation path

* fix: ignore delegated_amount without delegate

* restore decompress amount check

fix programtest, wallet owner tracking for ata

fmt and lint

upd amm test

simplify client usage, remove unnecessary endpoints

clean

cleanup

lint
SwenSchaeferjohann added a commit that referenced this pull request Feb 11, 2026
refactor

remove fetch-accounts

renaming, simplify trait

fomat

excl photon-api submodule

fix: multi-pass cold account lookup in test indexer RPC

Align get_account_interface and get_multiple_account_interfaces with
Photon's lookup strategy: search compressed_accounts by onchain_pubkey,
then by PDA seed derivation, then token_compressed_accounts, then by
token_data.owner. Also fix as_mint() to accept ColdContext::Account
since Photon returns mints as generic compressed accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>

lint

fix lint

fix: reject rent sponsor self-referencing the token account (#2257)

* fix: reject rent sponsor self-referencing the token account

Audit issue #9 (INFO): The rent payer could be the same account as
the target token account being created. Add a check that rejects
this self-reference to prevent accounting issues.

* test: add failing test rent sponsor self reference

fix: process metadata add/remove actions in sequential order (#2256)

* fix: process metadata add/remove actions in sequential order

Audit issue #16 (LOW): should_add_key checked for any add and any
remove independently, ignoring action ordering. An add-remove-add
sequence would incorrectly remove the key. Process actions
sequentially so the final state reflects the actual order.

* chore: format

* test: add randomized test for metadata action processing

Validates that process_extensions_config_with_actions produces correct
AdditionalMetadataConfig for random sequences of UpdateMetadataField
and RemoveMetadataKey actions, covering the add-remove-add bug from
audit issue #16.

* test: add integration test for audit issue #13 (no double rent charge)

Verifies that two compress operations targeting the same compressible
CToken account in a single Transfer2 instruction do not double-charge
the rent top-up budget.

* chore: format extensions_metadata test

fix: validate authority on self-transfer early return (#2252)

* fix: handle self-transfer in ctoken transfer and transfer_checked

Validate that the authority is a signer and is the owner or delegate
before allowing self-transfer early return. Previously the self-transfer
path returned Ok(()) without any authority validation.

* fix: simplify map_or to is_some_and per clippy

* fix: use pubkey_eq for self-transfer check

* refactor: extract self-transfer validation into shared function

Extract duplicate self-transfer check from default.rs and checked.rs
into validate_self_transfer() in shared.rs with cold path for authority
validation.

* chore: format

* fix: deduplicate random metadata keys in test_random_mint_action

Random key generation could produce duplicate keys, causing
DuplicateMetadataKey error (18040) with certain seeds.

fix: enforce mint extension checks in cToken-to-cToken decompress (#2246)

* fix: enforce mint extension checks in cToken-to-cToken decompress hot path

Add enforce_extension_state() to MintExtensionChecks and call it in the
Decompress branch when decompress_inputs is None (hot-path, not
CompressedOnly restore). This prevents cToken-to-cToken transfers from
bypassing pause, transfer fee, and transfer hook restrictions.

* fix test

chore: reject compress for mints with restricted extensions in build_mint_extension_cache (#2240)

* chore: reject compress for mints with restricted extensions in mint check

* Update programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

* fix: format else-if condition for lint

---------

Co-authored-by: 0xa5df-c <172008956+0xa5df-c@users.noreply.github.com>

fix: token-pool index 0 check (#2239)

fix(programs): add MintCloseAuthority as restricted extension (M-03) (#2263)

* fix: add MintCloseAuthority as restricted extension (M-03)

A mint with MintCloseAuthority can be closed and re-opened with
different extensions. Treating it as restricted ensures compressed
tokens from such mints require CompressedOnly mode.

* test: add MintCloseAuthority compression_only requirement tests

Add test coverage for MintCloseAuthority requiring compression_only mode,
complementing the fix in f2da063.

refactor: light program pinocchio macro (#2247)

* refactor: light program pinocchio macro

* fix: address CodeRabbit review comments on macro codegen

- Fix account order bug in process_update_config (config=0, authority=1)
- Use backend-provided serialize/deserialize derives in LightAccountData
- Remove redundant is_pinocchio() branch for has_le_bytes unpack fields
- DRY doc attribute generation across 4 struct generation methods
- Unify unpack_data path using account_crate re-export for both backends

chore(libs): bump versions (#2272)

fix(programs): allow account-level delegate to compress CToken (M-02) (#2262)

* fix: allow account-level delegate to compress tokens from CToken (M-02)

check_ctoken_owner() only checked owner and permanent delegate.
An account-level delegate (approved via CTokenApprove) could not
compress tokens. Added delegate check after permanent delegate.

* test: compress by delegate

fix: accumulate delegated amount at decompression (#2242)

* fix: accumulate delegated amount at decompression

* fix lint

* refactor: simplify apply_delegate to single accumulation path

* fix: ignore delegated_amount without delegate

* restore decompress amount check

fix programtest, wallet owner tracking for ata

fmt and lint

upd amm test

simplify client usage, remove unnecessary endpoints

clean

cleanup

lint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants