Skip to content

Comments

chore: add mint creation fee#2309

Open
ananas-block wants to merge 5 commits intomainfrom
jorrit/feat-add-mint-fee
Open

chore: add mint creation fee#2309
ananas-block wants to merge 5 commits intomainfrom
jorrit/feat-add-mint-fee

Conversation

@ananas-block
Copy link
Contributor

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

  1. Added mint creation fee constant: MINT_CREATION_FEE = 50,000 lamports to compressed-token program
  2. Added rent_sponsor field to instruction inputs: New required field in CreateMintInputs struct to track the fee recipient
  3. Updated account validation logic: Changed from needs_compressible_accounts() to needs_rent_sponsor() to require rent_sponsor for both pure mint creation and compressible operations
  4. Added CPI context validation: Prevents mint creation when writing to CPI context (fee cannot be charged without rent_sponsor account)
  5. Implemented fee transfer: Added logic in process_mint_action() to transfer 50,000 lamports from fee_payer to rent_sponsor
  6. Extended mint action account structures: Added rent_sponsor field to MintActionCpiAccounts and updated account iteration/building
  7. Added SDK helper method: New with_rent_sponsor() method on MintActionMetaConfig for configuring the fee recipient
  8. Added test assertion utility: New assert_mint_creation_fee() function to verify fee was correctly transferred
  9. Updated test configurations: Modified test mint creation calls to include rent_sponsor account in proper position

Summary by CodeRabbit

  • New Features

    • Mint creation now requires/provides a rent sponsor and charges a 50,000-lamport mint creation fee forwarded to that sponsor.
    • SDKs updated to accept and propagate compressible config and rent sponsor for mint flows.
  • Bug Fixes / Validation

    • New validations prevent unsupported combinations of actions and surface errors for missing or mismatched rent-sponsor/executor accounts.
    • CPI account ordering updated to include rent sponsor and system program where applicable.

Entire-Checkpoint: 3b001a578b94
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • sdk-tests/sdk-token-test/tests/ctoken_pda.rs is excluded by none and included by none

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds on-chain rent_sponsor plumbing and a 50,000-lamport MINT_CREATION_FEE collected via CPI for compressed-mint creation; threads compressible_config and rent_sponsor through SDK and CPI account metas; updates program parsing/validation to require/validate rent_sponsor and adjust CPI account layout and guards.

Changes

Cohort / File(s) Summary
Program: Mint Action Accounts
programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs
Added AccountsConfig::needs_rent_sponsor(), parse/write-mode write_mode_rent_sponsor: Option<&AccountInfo>, changed parsing logic and CPI-slice offsets, and added validation disallowing create_mint with CompressAndCloseCMint.
Program: Processor & Constants
programs/compressed-token/program/src/lib.rs, programs/compressed-token/program/src/compressed_token/mint_action/processor.rs
Introduced MINT_CREATION_FEE and RENT_SPONSOR_V1; when create_mint is true validate rent_sponsor PDA and transfer fee via CPI (with convert_program_error mapping); adjusted idempotent-exit handling and added logging.
SDK: Create-Mint Inputs
sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs
Added compressible_config: Pubkey and rent_sponsor: Pubkey to CreateMintInputs and CPI-write inputs; propagate both into meta/config construction for create-mint flows.
SDK: MintAction Meta & Account Metas
sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs, sdk-libs/token-sdk/src/instruction/create_mint.rs, sdk-libs/token-sdk/src/instruction/create_mints.rs
MintActionMetaConfig::new_create_mint now takes compressible_config and rent_sponsor; added with_rent_sponsor builder; call sites updated to thread new PDAs through single/multi-mint and CPI write paths.
SDK CPI Accounts: CPI Accounts & Metas
sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs, sdk-libs/sdk-types/src/interface/cpi/create_mints.rs
Added optional compressible_config and rent_sponsor to CPI account structs; parsing, to_account_infos, and to_account_metas updated; CPI account ordering revised to include rent_sponsor and system_program.
Tests
sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs, programs/compressed-token/program/tests/mint_action.rs
Tests extended to include compressible_config and rent_sponsor (LIGHT_TOKEN_RENT_SPONSOR) in required accounts and assertions; program tests updated for write-mode gating conditions.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Instruction Builder
    participant Program as Mint Action Processor
    participant FeePayer as Fee Payer (payer account)
    participant RentSponsor as Rent Sponsor PDA
    participant CPI as System/CPI

    Client->>Program: submit create_compressed_mint(with compressible_config, rent_sponsor, create_mint)
    Program->>Program: parse AccountsConfig (needs_rent_sponsor -> true)
    alt CPI write context set
        Program-->>Client: error (CpiContextSetNotUsable / validation)
    else not CPI write
        Program->>Program: validate rent_sponsor PDA == config or RENT_SPONSOR_V1
        Program->>CPI: invoke transfer_lamports_via_cpi(FeePayer -> RentSponsor, MINT_CREATION_FEE)
        CPI-->>RentSponsor: lamports transferred
        Program->>Program: create/init compressed mint accounts
        Program-->>Client: success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

🪙 A PDA waits to catch the fee,
Compressible config and rent align,
Fifty thousand lamports sail by CPI sea,
Guards ensure we only mint in time,
New accounts wired — the token sings in kind.

🚥 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 'chore: add mint creation fee' is clearly related to the main change: introducing MINT_CREATION_FEE (50,000 lamports) and implementing the corresponding fee infrastructure across the codebase.
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/feat-add-mint-fee

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

Caution

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

⚠️ Outside diff range comments (2)
sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs (1)

75-91: ⚠️ Potential issue | 🟠 Major

create_compressed_mint_cpi_write in this file is permanently broken by the program guard.

The companion function create_compressed_mint_cpi_write (lines 123-174) enforces that first_set_context || set_context is true before building the instruction. Because the program now rejects any create_mint instruction where write_to_cpi_context = true, every instruction produced by create_compressed_mint_cpi_write will be rejected at runtime with CpiContextSetNotUsable.

This function should be deprecated (with a #[deprecated] attribute and a clear explanation) or removed in this PR to avoid shipping dead, misleading API surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs`
around lines 75 - 91, The create_compressed_mint_cpi_write function is
permanently broken by the program guard that rejects instructions with
write_to_cpi_context=true; remove or deprecate this dead API: add a
#[deprecated] attribute to create_compressed_mint_cpi_write with a clear message
explaining it always triggers CpiContextSetNotUsable and suggest the proper
alternative (e.g., create_compressed_mint or the non-CPI helper), update the
public API/export list to avoid re-exporting it if necessary, and remove or mark
any related tests/docs referencing create_compressed_mint_cpi_write to prevent
confusion.
sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs (1)

202-246: 🧹 Nitpick | 🔵 Trivial

to_account_infos() and to_account_metas() ordering is consistent and correct.

Both methods place rent_sponsor after authority and before fee_payer, which aligns with the program's account iterator order for create_mint. The capacity hint (20 + ctoken_accounts.len()) should be bumped to 21 to account for the new optional rent_sponsor slot, avoiding a reallocation in the common create_mint path.

✏️ Suggested fix
-        let mut accounts = Vec::with_capacity(20 + self.ctoken_accounts.len());
+        let mut accounts = Vec::with_capacity(21 + self.ctoken_accounts.len());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs`
around lines 202 - 246, The capacity hint for the accounts Vec is off by one
because the optional rent_sponsor adds an extra slot; update the allocation in
to_account_infos (and the sibling to_account_metas) from Vec::with_capacity(20 +
self.ctoken_accounts.len()) to Vec::with_capacity(21 +
self.ctoken_accounts.len()) so the common create_mint path avoids a reallocation
while preserving the existing account order (mint_signer, authority, optional
rent_sponsor, then fee_payer, ...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 113-114: The parsed optional account rent_sponsor (obtained via
iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor())) is not being
validated and can be an arbitrary writable account; add an on-chain check that
the account.key() equals the expected protocol PDA constant
(LIGHT_TOKEN_RENT_SPONSOR) before proceeding with any fee transfer or
rent-sponsor logic, and return an appropriate error if it does not; apply the
same check in the other occurrence handling CMint create/close (the block that
mirrors this logic around the other use site).
- Around line 460-467: The function create_compressed_mint_cpi_write produces
instructions that will always hit the guard in accounts.rs (write_to_cpi_context
&& parsed_instruction_data.create_mint.is_some()) and thus always return
ErrorCode::CpiContextSetNotUsable; remove create_compressed_mint_cpi_write (and
the helper new_mint_write_to_cpi_context if unused) or mark
create_compressed_mint_cpi_write #[deprecated] and change its implementation to
return a clear compile/runtime error (or None) so SDK consumers cannot produce
broken instructions—ensure you update/remove any references and add a short doc
comment explaining the deprecation decision.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 53-56: The code currently uses
ErrorCode::MintActionMissingExecutingAccounts for both executing being None and
rent_sponsor being None; add a new distinct error variant (e.g.,
MintCreationFeeRentSponsorMissing) to the ErrorCode enum and replace the second
.ok_or(...) on rent_sponsor with
.ok_or(ErrorCode::MintCreationFeeRentSponsorMissing) so the rent fee-recipient
missing case is reported separately (keep the existing
.ok_or(ErrorCode::MintActionMissingExecutingAccounts) for executing).

In `@programs/compressed-token/program/src/lib.rs`:
- Line 39: Add a doc comment above the MINT_CREATION_FEE constant explaining why
it is set to 50_000 lamports (e.g., what costs or policy it covers and how it
relates to related constants like COMPRESSION_COST and COMPRESSION_INCENTIVE);
update the declaration for MINT_CREATION_FEE with a concise justification that
references those related constants (COMPRESSION_COST = 10_000,
COMPRESSION_INCENTIVE = 1_000) so maintainers/auditors can understand the fee
basis and any assumptions used to derive the 50_000 value.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs`:
- Line 92: The rent_sponsor account is only checked for writability via
next_option_mut, so update the parsing in try_from_account_infos_full (the block
that assigns let rent_sponsor = iter.next_option_mut("rent_sponsor",
config.create_mint)?) to validate the account's public key against the constant
LIGHT_TOKEN_RENT_SPONSOR when present; if the optional account exists and its
key does not equal LIGHT_TOKEN_RENT_SPONSOR return a descriptive error (e.g.,
InvalidAccountKey or a custom error) instead of accepting any writable account.
Ensure you use the same iterator/variable names (rent_sponsor, iter, config) and
keep the optional semantics (only validate when Some) so callers get an early,
descriptive failure.

In `@sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs`:
- Around line 289-297: Add a negative test that ensures AccountsConfig::new()
rejects create_mint when the CPI context set flag is used: construct a
ZMintActionCompressedInstructionData (use the SDK helper
new_mint_write_to_cpi_context) with create_mint populated and
cpi_context.first_set_context/set_context = true and then call
AccountsConfig::new(...); assert it returns Err(CpiContextSetNotUsable). Place
this alongside the existing mint tests (referencing LIGHT_TOKEN_RENT_SPONSOR in
the test setup) to cover the new guard and prevent regressions.

---

Outside diff comments:
In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs`:
- Around line 75-91: The create_compressed_mint_cpi_write function is
permanently broken by the program guard that rejects instructions with
write_to_cpi_context=true; remove or deprecate this dead API: add a
#[deprecated] attribute to create_compressed_mint_cpi_write with a clear message
explaining it always triggers CpiContextSetNotUsable and suggest the proper
alternative (e.g., create_compressed_mint or the non-CPI helper), update the
public API/export list to avoid re-exporting it if necessary, and remove or mark
any related tests/docs referencing create_compressed_mint_cpi_write to prevent
confusion.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs`:
- Around line 202-246: The capacity hint for the accounts Vec is off by one
because the optional rent_sponsor adds an extra slot; update the allocation in
to_account_infos (and the sibling to_account_metas) from Vec::with_capacity(20 +
self.ctoken_accounts.len()) to Vec::with_capacity(21 +
self.ctoken_accounts.len()) so the common create_mint path avoids a reallocation
while preserving the existing account order (mint_signer, authority, optional
rent_sponsor, then fee_payer, ...).

---

Duplicate comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 49-59: The transfer currently sends MINT_CREATION_FEE to whichever
writable account is passed as executing.rent_sponsor without validating its
identity; before calling transfer_lamports_via_cpi in the block guarded by
accounts_config.create_mint, verify the rent_sponsor is the expected account
(e.g., compare executing.rent_sponsor pubkey against the validated/expected rent
sponsor from parsed accounts or require it is a signer/authority similar to the
check in accounts.rs) and return ErrorCode::MintActionMissingExecutingAccounts
(or a new appropriate error) if the identity check fails; perform this
validation immediately after fetching executing and rent_sponsor and only then
call transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer,
rent_sponsor).

Comment on lines +113 to +114
// Parse rent_sponsor when creating mint (fee recipient) or when creating/closing CMint
let rent_sponsor = iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor())?;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

rent_sponsor not validated against the expected protocol PDA — fee bypass vulnerability.

iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor()) accepts any writable account. A caller can pass their own wallet as rent_sponsor, causing MINT_CREATION_FEE to transfer from fee_payer back to a caller-controlled address. Net effect: zero cost to the caller and zero revenue for the protocol.

Since LIGHT_TOKEN_RENT_SPONSOR is the only valid recipient, the program must verify the key on-chain before the fee transfer:

🛡️ Proposed fix — add key check after parsing
 // Parse rent_sponsor when creating mint (fee recipient) or when creating/closing CMint
 let rent_sponsor = iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor())?;
+if config.create_mint {
+    if let Some(sponsor) = rent_sponsor {
+        if sponsor.key() != &light_sdk_types::LIGHT_TOKEN_RENT_SPONSOR {
+            msg!("Invalid rent_sponsor: expected LIGHT_TOKEN_RENT_SPONSOR");
+            return Err(ErrorCode::InvalidRentSponsor.into());
+        }
+    }
+}

Also applies to: 379-384

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`
around lines 113 - 114, The parsed optional account rent_sponsor (obtained via
iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor())) is not being
validated and can be an arbitrary writable account; add an on-chain check that
the account.key() equals the expected protocol PDA constant
(LIGHT_TOKEN_RENT_SPONSOR) before proceeding with any fee transfer or
rent-sponsor logic, and return an appropriate error if it does not; apply the
same check in the other occurrence handling CMint create/close (the block that
mirrors this logic around the other use site).

Comment on lines 53 to 56
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
let rent_sponsor = executing
.rent_sponsor
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Two distinct failure modes share the same error code.

ErrorCode::MintActionMissingExecutingAccounts is used for both:

  1. executing being None (shouldn't happen given the CPI guard)
  2. rent_sponsor being None (missing required fee-recipient account)

Using a dedicated error code (e.g., MintCreationFeeRentSponsorMissing) for case 2 makes on-chain failures immediately actionable for SDK consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`
around lines 53 - 56, The code currently uses
ErrorCode::MintActionMissingExecutingAccounts for both executing being None and
rent_sponsor being None; add a new distinct error variant (e.g.,
MintCreationFeeRentSponsorMissing) to the ErrorCode enum and replace the second
.ok_or(...) on rent_sponsor with
.ok_or(ErrorCode::MintCreationFeeRentSponsorMissing) so the rent fee-recipient
missing case is reported separately (keep the existing
.ok_or(ErrorCode::MintActionMissingExecutingAccounts) for executing).

derive_light_cpi_signer!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");

pub const MAX_ACCOUNTS: usize = 30;
pub const MINT_CREATION_FEE: u64 = 50_000;
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider documenting the rationale for the 50_000 lamport value.

The constant is clean, but given that related cost constants exist elsewhere (e.g., COMPRESSION_COST = 10_000, COMPRESSION_INCENTIVE = 1_000), a brief doc comment explaining the basis for 50_000 would help future maintainers and auditors understand the fee policy.

✏️ Suggested addition
+/// Mint creation fee charged to the fee_payer and transferred to the rent_sponsor PDA.
+/// Set at 50,000 lamports (5× COMPRESSION_COST) to cover protocol operational overhead.
 pub const MINT_CREATION_FEE: u64 = 50_000;
📝 Committable suggestion

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

Suggested change
pub const MINT_CREATION_FEE: u64 = 50_000;
/// Mint creation fee charged to the fee_payer and transferred to the rent_sponsor PDA.
/// Set at 50,000 lamports (5× COMPRESSION_COST) to cover protocol operational overhead.
pub const MINT_CREATION_FEE: u64 = 50_000;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/compressed-token/program/src/lib.rs` at line 39, Add a doc comment
above the MINT_CREATION_FEE constant explaining why it is set to 50_000 lamports
(e.g., what costs or policy it covers and how it relates to related constants
like COMPRESSION_COST and COMPRESSION_INCENTIVE); update the declaration for
MINT_CREATION_FEE with a concise justification that references those related
constants (COMPRESSION_COST = 10_000, COMPRESSION_INCENTIVE = 1_000) so
maintainers/auditors can understand the fee basis and any assumptions used to
derive the 50_000 value.

return Err(AccountError::InvalidSigner.into());
}

let rent_sponsor = iter.next_option_mut("rent_sponsor", config.create_mint)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

rent_sponsor accepted without key validation against LIGHT_TOKEN_RENT_SPONSOR.

next_option_mut only checks writability. A CPI caller parsing inbound accounts via try_from_account_infos_full will accept any writable account as rent_sponsor, masking a mismatch before the transaction hits the program. Adding the key check here gives callers early, descriptive failures:

🛡️ Proposed fix
 let rent_sponsor = iter.next_option_mut("rent_sponsor", config.create_mint)?;
+if config.create_mint {
+    if let Some(sponsor) = rent_sponsor {
+        if sponsor.key() != light_sdk_types::LIGHT_TOKEN_RENT_SPONSOR {
+            msg!("rent_sponsor must be LIGHT_TOKEN_RENT_SPONSOR");
+            return Err(AccountError::InvalidProgramId.into());
+        }
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs`
at line 92, The rent_sponsor account is only checked for writability via
next_option_mut, so update the parsing in try_from_account_infos_full (the block
that assigns let rent_sponsor = iter.next_option_mut("rent_sponsor",
config.create_mint)?) to validate the account's public key against the constant
LIGHT_TOKEN_RENT_SPONSOR when present; if the optional account exists and its
key does not equal LIGHT_TOKEN_RENT_SPONSOR return a descriptive error (e.g.,
InvalidAccountKey or a custom error) instead of accepting any writable account.
Ensure you use the same iterator/variable names (rent_sponsor, iter, config) and
keep the optional semantics (only validate when Some) so callers get an early,
descriptive failure.

Comment on lines +289 to +297
// Rent sponsor (required for create_mint, known PDA)
create_test_account(
LIGHT_TOKEN_RENT_SPONSOR,
[0u8; 32],
false,
true,
false,
vec![],
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Happy-path test for rent_sponsor is correct; missing negative test for the new CPI-context guard.

The new LIGHT_TOKEN_RENT_SPONSOR account and the assert!(parsed.rent_sponsor.is_some()) assertion are correct. However, AccountsConfig::new() in the program now rejects create_mint when write_to_cpi_context = true — that invariant has no test coverage. A failing test case like the one below would prevent regressions:

✅ Suggested negative test
#[test]
fn test_create_mint_rejected_when_write_to_cpi_context() {
    // Build a ZMintActionCompressedInstructionData with create_mint set
    // AND cpi_context.first_set_context = true / set_context = true.
    // AccountsConfig::new() must return Err(CpiContextSetNotUsable).
    //
    // (Use the SDK's new_mint_write_to_cpi_context builder to construct the data.)
}

Also applies to: 354-354

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk-libs/token-sdk/tests/mint_action_cpi_accounts_tests.rs` around lines 289
- 297, Add a negative test that ensures AccountsConfig::new() rejects
create_mint when the CPI context set flag is used: construct a
ZMintActionCompressedInstructionData (use the SDK helper
new_mint_write_to_cpi_context) with create_mint populated and
cpi_context.first_set_context/set_context = true and then call
AccountsConfig::new(...); assert it returns Err(CpiContextSetNotUsable). Place
this alongside the existing mint tests (referencing LIGHT_TOKEN_RENT_SPONSOR in
the test setup) to cover the new guard and prevent regressions.

Entire-Checkpoint: b5b9ac87e33d
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
sdk-libs/token-sdk/src/instruction/create_mints.rs (1)

282-307: ⚠️ Potential issue | 🔴 Critical

Fix invoke_multiple_mints: The multi-mint CPI write path is broken by the new guard.

invoke_cpi_write constructs each write instruction via MintActionCompressedInstructionData::new_mint_write_to_cpi_context, which sets both create_mint: Some(CreateMint::default()) and cpi_context: Some(cpi_context). This means the write_to_cpi_context flag (derived from cpi_context.first_set_context() || cpi_context.set_context()) will always be true. The guard in AccountsConfig::new at lines 468–474 of accounts.rs explicitly rejects any instruction where both conditions hold, returning CpiContextSetNotUsable. Every call to invoke_cpi_write in the N>1 loop (lines 293–295) will therefore fail immediately at accounts validation, making invoke_multiple_mints — and thus CreateMintsCpi with N≥2 or nonzero cpi_context_offset — unreachable. The module docstring still advertises the N>1 path, so callers will silently hit this hard error.

invoke_single_mint and invoke_execute are unaffected because they use invoke_execute, which does not set write_to_cpi_context (flags are both false). The guard must either be removed for write operations or invoke_cpi_write must be redesigned to avoid the incompatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk-libs/token-sdk/src/instruction/create_mints.rs` around lines 282 - 307,
invoke_multiple_mints fails because invoke_cpi_write builds instructions that
set both create_mint (Some) and cpi_context (Some), which the new AccountsConfig
guard rejects; fix by changing invoke_cpi_write so the CPI write instructions do
not set create_mint at all (i.e., build the MintActionCompressedInstructionData
for a pure "write to CPI context" path with create_mint = None and only the
cpi_context set), or use/introduce an alternative constructor that produces a
write-only instruction; update invoke_cpi_write (and any helper constructor
calls it uses) to avoid the simultaneous create_mint + cpi_context combination
so the accounts validation no longer returns CpiContextSetNotUsable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 454-458: Replace the reused
ErrorCode::CompressAndCloseCMintMustBeOnlyAction with a new, distinct error
variant to represent the separate invariant that create_mint (top-level) cannot
be combined with CompressAndCloseCMint: add a new enum variant e.g.,
CannotCombineCreateMintWithCompressAndClose to the ErrorCode definition, give it
a clear error message, and update the check in accounts.rs that currently
references ErrorCode::CompressAndCloseCMintMustBeOnlyAction (the block using
has_compress_and_close_cmint_action &&
parsed_instruction_data.create_mint.is_some()) to return the new variant
instead; keep the original CompressAndCloseCMintMustBeOnlyAction for the
existing actions-only case.

In `@programs/compressed-token/program/tests/mint_action.rs`:
- Around line 348-362: The variable is_creating_mint is misnamed:
instruction_data.mint.is_none() actually represents the
compressed-mint-decompressed flag (cmint_decompressed) in production; rename
is_creating_mint to cmint_decompressed to match accounts.rs semantics and avoid
confusion, and if you intended to detect a true "creating new mint" case
instead, replace that expression with the correct condition
(create_mint.is_some() && mint.is_none() or create_mint.is_some() &&
mint.is_some() per your creation logic) and update the combined error check that
references is_creating_mint accordingly.

---

Outside diff comments:
In `@sdk-libs/token-sdk/src/instruction/create_mints.rs`:
- Around line 282-307: invoke_multiple_mints fails because invoke_cpi_write
builds instructions that set both create_mint (Some) and cpi_context (Some),
which the new AccountsConfig guard rejects; fix by changing invoke_cpi_write so
the CPI write instructions do not set create_mint at all (i.e., build the
MintActionCompressedInstructionData for a pure "write to CPI context" path with
create_mint = None and only the cpi_context set), or use/introduce an
alternative constructor that produces a write-only instruction; update
invoke_cpi_write (and any helper constructor calls it uses) to avoid the
simultaneous create_mint + cpi_context combination so the accounts validation no
longer returns CpiContextSetNotUsable.

---

Duplicate comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 114-115: The rent_sponsor account returned by
iter.next_option_mut("rent_sponsor", config.needs_rent_sponsor()) is not being
validated and can be replaced by an attacker to receive MINT_CREATION_FEE;
derive the expected protocol PDA for LIGHT_TOKEN_RENT_SPONSOR (using the same
seeds/constants used elsewhere in this crate and the program id, e.g., via
Pubkey::find_program_address) and compare it to the provided rent_sponsor.key
(and error if missing or mismatched), so that the processor only transfers
MINT_CREATION_FEE to the canonical LIGHT_TOKEN_RENT_SPONSOR PDA; update the
validation near where rent_sponsor is parsed (the code around
iter.next_option_mut, config.needs_rent_sponsor(), and the MINT_CREATION_FEE
transfer) to perform this check and return an appropriate error if the PDA does
not match.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 49-58: The code is using a single error variant
MintActionMissingExecutingAccounts for three different failure modes; update the
error enum to add distinct variants (e.g., MintActionMissingExecutingAccounts,
MintActionMissingRentSponsor, MintActionIdempotentExit) and replace the
.ok_or(...) calls accordingly: leave
validated_accounts.executing.ok_or(MintActionMissingExecutingAccounts) for the
missing executing accounts case, use
rent_sponsor.ok_or(MintActionMissingRentSponsor) for the missing rent_sponsor
case, and change the idempotent-exit usage (the other spot that currently
returns MintActionMissingExecutingAccounts) to return MintActionIdempotentExit;
update any matching error handling or docs to reflect the new variants so
callers can differentiate the three failure reasons.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs`:
- Around line 123-174: The create_compressed_mint_cpi_write function builds an
instruction using new_mint_write_to_cpi_context which always sets create_mint =
Some(...), causing write_to_cpi_context=true + create_mint populated (and thus
CpiContextSetNotUsable) — fix by using an instruction constructor that does not
set create_mint (or explicitly set create_mint = None) when building
MintActionCompressedInstructionData for CPI write, or switch to the correct
factory (e.g., a non-creating "write only" constructor) so create_mint is not
populated; update create_compressed_mint_cpi_write to call that alternative
constructor (or clear the create_mint field) and ensure cpi_context flags remain
consistent with the chosen constructor.

Comment on lines +454 to +458
// Validation: Cannot combine create_mint with CompressAndCloseCMint
if has_compress_and_close_cmint_action && parsed_instruction_data.create_mint.is_some() {
msg!("Cannot combine create_mint with CompressAndCloseCMint");
return Err(ErrorCode::CompressAndCloseCMintMustBeOnlyAction.into());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CompressAndCloseCMintMustBeOnlyAction reused for a semantically distinct condition.

Lines 448–452 already use this error for "CompressAndCloseCMint must be the only action in actions." Lines 454–458 reuse it for "cannot combine create_mint (a separate top-level field) with CompressAndCloseCMint," which is a different invariant. When SDK consumers receive this error, there is no way to distinguish between the two causes. A dedicated variant (e.g., CannotCombineCreateMintWithCompressAndClose) would make failures immediately actionable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`
around lines 454 - 458, Replace the reused
ErrorCode::CompressAndCloseCMintMustBeOnlyAction with a new, distinct error
variant to represent the separate invariant that create_mint (top-level) cannot
be combined with CompressAndCloseCMint: add a new enum variant e.g.,
CannotCombineCreateMintWithCompressAndClose to the ErrorCode definition, give it
a clear error message, and update the check in accounts.rs that currently
references ErrorCode::CompressAndCloseCMintMustBeOnlyAction (the block using
has_compress_and_close_cmint_action &&
parsed_instruction_data.create_mint.is_some()) to return the new variant
instead; keep the original CompressAndCloseCMintMustBeOnlyAction for the
existing actions-only case.

Entire-Checkpoint: 75af397ef5ab
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs`:
- Around line 132-137: The with_rent_sponsor setter allows setting rent_sponsor
without compressible_config which can reorder account metas and break
create_mint/compressible parsing; update the implementation to guard against
this by either (A) making with_rent_sponsor also require/attach a
compressible_config (or call into with_compressible_mint) or (B) add a
validation step in the build/validate method that checks if rent_sponsor is
Some(...) while compressible_config is None and then return/error/panic with a
clear message; also update the doc comment on with_rent_sponsor to state it must
be paired with compressible_config (or use with_compressible_mint) and reference
the compressible_config field and with_compressible_mint method for callers.

---

Duplicate comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 456-460: The current branch that checks
has_compress_and_close_cmint_action &&
parsed_instruction_data.create_mint.is_some() reuses
CompressAndCloseCMintMustBeOnlyAction which is misleading; add a new error
variant (e.g. CreateMintWithCompressAndCloseCMintNotAllowed) to the ErrorCode
enum and replace the Err(...) return in this if-block with the new variant, and
update the msg! invocation to a clear, specific message referencing create_mint
+ CompressAndCloseCMint; also update any error docs/tests that assert the old
variant.
- Around line 469-476: The current guard blocks any instruction with
parsed_instruction_data.create_mint when write_to_cpi_context is true, which
makes the helper create_compressed_mint_cpi_write produce unusable instructions;
remove or deprecate the create_compressed_mint_cpi_write helper (and any SDK
bindings that call it) so callers cannot generate instructions that will always
fail due to this guard (referencing write_to_cpi_context,
parsed_instruction_data.create_mint and the ErrorCode::CpiContextSetNotUsable
check), or alternatively change the helper to never set create_mint when
targeting CPI-write contexts so the guard no longer triggers.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 48-68: Change the generic error mapping for a missing rent_sponsor
to a distinct error variant: instead of mapping rent_sponsor absence to
ErrorCode::MintActionMissingExecutingAccounts, add a new ErrorCode variant
(e.g., ErrorCode::MintActionMissingRentSponsor) and return that when
executing.rent_sponsor is None; update the check in the mint creation block (the
code using accounts_config.create_mint, validated_accounts.executing, let
rent_sponsor = executing.rent_sponsor.ok_or(...)? and the subsequent
rent_sponsor.key() comparison) to use the new ErrorCode and adjust any tests or
match arms that expect the old error.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/cpi_accounts.rs`:
- Around line 94-96: After reading rent_sponsor via
iter.next_option_mut("rent_sponsor", config.create_mint) validate that
rent_sponsor.key() equals the expected PDA before proceeding: derive the
expected PDA using the same seeds/program-id logic used when the rent sponsor
PDA is originally created (use config.create_mint and the program id/seeds used
by your program), compare it to rent_sponsor.key(), and return a clear error if
it does not match; place this check immediately after the rent_sponsor binding
in the same function so callers fail fast with a readable message.

Comment on lines +132 to +137
/// Set only the rent_sponsor account (without compressible_config or mint/cmint).
/// Required for create_mint operations to receive the mint creation fee.
pub fn with_rent_sponsor(mut self, rent_sponsor: Pubkey) -> Self {
self.rent_sponsor = Some(rent_sponsor);
self
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against using with_rent_sponsor without compressible_config.

Because account metas place compressible_config before rent_sponsor, setting only rent_sponsor can shift account ordering and break parsing for create_mint/compressible flows. Consider documenting that this must be paired with compressible_config (or use with_compressible_mint).

✍️ Suggested doc tweak
-    /// Set only the rent_sponsor account (without compressible_config or mint/cmint).
-    /// Required for create_mint operations to receive the mint creation fee.
+    /// Set the rent_sponsor account. Must be paired with `compressible_config`
+    /// (use `new_create_mint` / `with_compressible_mint`) to preserve account ordering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk-libs/compressed-token-sdk/src/compressed_token/v2/mint_action/account_metas.rs`
around lines 132 - 137, The with_rent_sponsor setter allows setting rent_sponsor
without compressible_config which can reorder account metas and break
create_mint/compressible parsing; update the implementation to guard against
this by either (A) making with_rent_sponsor also require/attach a
compressible_config (or call into with_compressible_mint) or (B) add a
validation step in the build/validate method that checks if rent_sponsor is
Some(...) while compressible_config is None and then return/error/panic with a
clear message; also update the doc comment on with_rent_sponsor to state it must
be paired with compressible_config (or use with_compressible_mint) and reference
the compressible_config field and with_compressible_mint method for callers.

Removes the restriction that blocked create_mint when
write_to_cpi_context is true. This enables multi-mint creation
(N-1 write-mode CPI calls + 1 execute-mode call).

In write mode, the mint creation fee is charged by validating
rent_sponsor against the hardcoded RENT_SPONSOR_V1 constant
(no compressible_config account needed). The system program is
included as a trailing account to enable the fee transfer CPI.

Entire-Checkpoint: 3c0618ad006f
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 91-102: The rent_sponsor account must be parsed as mutable when
config.create_mint is true because fee transfer writes lamports; replace the
call to iter.next_account("rent_sponsor")? with the mut-validated iterator call
(e.g., iter.next_mut("rent_sponsor")?) so write_mode_rent_sponsor is obtained as
a mutable account; keep the surrounding conditional (config.create_mint) and
leave the rest (CpiContextLightSystemAccounts::new(...) and the system_program
handling) unchanged.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 184-190: The current idempotent early-exit branch uses
ErrorCode::MintActionMissingExecutingAccounts which is misleading for
create_mint; add a new error variant (e.g., CreateMintIdempotentNotAllowed) to
the ErrorCode enum and return that when is_idempotent_early_exit(err) is true
and accounts_config.create_mint is set; update any error display/serialization
as needed and replace the existing return
Err(ErrorCode::MintActionMissingExecutingAccounts.into()) in the create_mint
branch with Err(ErrorCode::CreateMintIdempotentNotAllowed.into()) so the failure
reason is explicit (refer to is_idempotent_early_exit,
accounts_config.create_mint, and the create_mint code path).

---

Duplicate comments:
In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`:
- Around line 476-480: The check that blocks combining create_mint with
CompressAndCloseCMint uses the generic
ErrorCode::CompressAndCloseCMintMustBeOnlyAction which is ambiguous; add a new
dedicated error variant (e.g.,
ErrorCode::CreateMintCannotCombineWithCompressAndCloseCMint) to your ErrorCode
enum and return that here instead of CompressAndCloseCMintMustBeOnlyAction,
updating the msg! string accordingly and ensuring the new error is
documented/converted to ProgramError where needed; change the conditional in the
block that references has_compress_and_close_cmint_action and
parsed_instruction_data.create_mint to return the new error.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`:
- Around line 50-58: The code uses the same ErrorCode variant
(MintActionMissingExecutingAccounts) for multiple missing-option failures; add
distinct error variants (e.g., MintActionMissingRentSponsor and
MintActionMissingCompressibleConfig) to the ErrorCode enum and update the
processor code around validated_accounts.executing: keep the existing check that
returns MintActionMissingExecutingAccounts when executing is None, but change
the rent_sponsor unwrap to return MintActionMissingRentSponsor when
executing.rent_sponsor is None and the compressible_config unwrap to return
MintActionMissingCompressibleConfig when executing.compressible_config is None;
also update any tests or match arms that rely on the old variant.

Comment on lines +91 to +102
let write_mode_rent_sponsor = if config.create_mint {
Some(iter.next_account("rent_sponsor")?)
} else {
None
};
let write_to_cpi_context_system = CpiContextLightSystemAccounts::new(&mut iter)?;
// System program is needed for the fee transfer CPI when creating mint in write mode.
// It's placed after all parsed accounts - the account iterator consumes it here,
// but it's available for the system program CPI via the transaction accounts.
if config.create_mint {
let _system_program = iter.next_account("system_program")?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Require rent_sponsor to be mutable in write mode.
The fee transfer credits lamports to this account, so it must be writable; using next_account doesn’t enforce that. Prefer a mut-validated iterator call.

Suggested fix
-            let write_mode_rent_sponsor = if config.create_mint {
-                Some(iter.next_account("rent_sponsor")?)
-            } else {
-                None
-            };
+            let write_mode_rent_sponsor = if config.create_mint {
+                Some(iter.next_mut("rent_sponsor")?)
+            } else {
+                None
+            };

Based on learnings: "Use specialized AccountIterator methods (next_signer, next_mut, next_signer_mut, etc.) instead of manually calling next_account() followed by separate validation functions".

📝 Committable suggestion

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

Suggested change
let write_mode_rent_sponsor = if config.create_mint {
Some(iter.next_account("rent_sponsor")?)
} else {
None
};
let write_to_cpi_context_system = CpiContextLightSystemAccounts::new(&mut iter)?;
// System program is needed for the fee transfer CPI when creating mint in write mode.
// It's placed after all parsed accounts - the account iterator consumes it here,
// but it's available for the system program CPI via the transaction accounts.
if config.create_mint {
let _system_program = iter.next_account("system_program")?;
}
let write_mode_rent_sponsor = if config.create_mint {
Some(iter.next_mut("rent_sponsor")?)
} else {
None
};
let write_to_cpi_context_system = CpiContextLightSystemAccounts::new(&mut iter)?;
// System program is needed for the fee transfer CPI when creating mint in write mode.
// It's placed after all parsed accounts - the account iterator consumes it here,
// but it's available for the system program CPI via the transaction accounts.
if config.create_mint {
let _system_program = iter.next_account("system_program")?;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs`
around lines 91 - 102, The rent_sponsor account must be parsed as mutable when
config.create_mint is true because fee transfer writes lamports; replace the
call to iter.next_account("rent_sponsor")? with the mut-validated iterator call
(e.g., iter.next_mut("rent_sponsor")?) so write_mode_rent_sponsor is obtained as
a mutable account; keep the surrounding conditional (config.create_mint) and
leave the rest (CpiContextLightSystemAccounts::new(...) and the system_program
handling) unchanged.

Comment on lines +184 to +190
// Check for idempotent early exit - skip CPI and return success.
// create_mint must never use idempotent early exit (fee already charged).
if let Err(ref err) = result {
if is_idempotent_early_exit(err) {
if accounts_config.create_mint {
return Err(ErrorCode::MintActionMissingExecutingAccounts.into());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a clearer error for create_mint idempotent early-exit.
Returning MintActionMissingExecutingAccounts here is misleading; a dedicated error variant (e.g., CreateMintIdempotentNotAllowed) would make failure causes obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@programs/compressed-token/program/src/compressed_token/mint_action/processor.rs`
around lines 184 - 190, The current idempotent early-exit branch uses
ErrorCode::MintActionMissingExecutingAccounts which is misleading for
create_mint; add a new error variant (e.g., CreateMintIdempotentNotAllowed) to
the ErrorCode enum and return that when is_idempotent_early_exit(err) is true
and accounts_config.create_mint is set; update any error display/serialization
as needed and replace the existing return
Err(ErrorCode::MintActionMissingExecutingAccounts.into()) in the create_mint
branch with Err(ErrorCode::CreateMintIdempotentNotAllowed.into()) so the failure
reason is explicit (refer to is_idempotent_early_exit,
accounts_config.create_mint, and the create_mint code path).

The wrapper program doesn't include rent_sponsor in its CPI,
so the error changes from 6035 (CpiContextSetNotUsable) to
20009 (account iterator parse failure).
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.

1 participant