Skip to content

Commit bb2ac1f

Browse files
committed
feat: allow create_mint in CPI context write mode
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
1 parent 0edce45 commit bb2ac1f

11 files changed

Lines changed: 195 additions & 140 deletions

File tree

program-tests/compressed-token-test/tests/mint/cpi_context.rs

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ async fn test_write_to_cpi_context_create_mint() {
125125
output_queue_index,
126126
} = test_setup().await;
127127

128+
let rent_sponsor = rpc.test_accounts.funding_pool_config.rent_sponsor_pda;
129+
128130
// Build instruction data using new builder API
129131
let instruction_data = MintActionCompressedInstructionData::new_mint(
130132
compressed_mint_inputs.root_index,
@@ -148,6 +150,7 @@ async fn test_write_to_cpi_context_create_mint() {
148150
fee_payer: payer.pubkey(),
149151
mint_signer: Some(mint_seed.pubkey()),
150152
authority: mint_authority.pubkey(),
153+
rent_sponsor: Some(rent_sponsor),
151154
cpi_context: cpi_context_pubkey,
152155
};
153156

@@ -181,22 +184,20 @@ async fn test_write_to_cpi_context_create_mint() {
181184
data: wrapper_ix_data.data(),
182185
};
183186

184-
// Execute wrapper instruction - should fail because create_mint + write_to_cpi_context
185-
// is rejected (error 6035: CpiContextSetNotUsable).
186-
let result = rpc
187-
.create_and_send_transaction(
188-
&[wrapper_instruction],
189-
&payer.pubkey(),
190-
&[&payer, &mint_seed, &mint_authority],
191-
)
192-
.await;
193-
194-
assert_rpc_error(result, 0, 6035).unwrap();
187+
// create_mint + write_to_cpi_context is allowed. The mint creation fee is charged
188+
// in write mode against the hardcoded RENT_SPONSOR_V1 constant.
189+
rpc.create_and_send_transaction(
190+
&[wrapper_instruction],
191+
&payer.pubkey(),
192+
&[&payer, &mint_seed, &mint_authority],
193+
)
194+
.await
195+
.expect("create_mint + write_to_cpi_context should succeed");
195196
}
196197

197198
#[tokio::test]
198199
#[serial]
199-
async fn test_write_to_cpi_context_invalid_address_tree() {
200+
async fn test_write_to_cpi_context_create_mint_invalid_rent_sponsor() {
200201
let TestSetup {
201202
mut rpc,
202203
compressed_mint_inputs,
@@ -205,16 +206,16 @@ async fn test_write_to_cpi_context_invalid_address_tree() {
205206
mint_authority,
206207
compressed_mint_address: _,
207208
cpi_context_pubkey,
208-
address_tree: _,
209+
address_tree,
209210
address_tree_index,
210211
output_queue: _,
211212
output_queue_index,
212213
} = test_setup().await;
213214

214-
// Swap the address tree pubkey to a random one (this should fail validation)
215-
let invalid_address_tree = Pubkey::new_unique();
215+
// Use a random pubkey as rent_sponsor (not the valid RENT_SPONSOR_V1)
216+
let invalid_rent_sponsor = Pubkey::new_unique();
216217

217-
// Build instruction data with invalid address tree
218+
// Build instruction data
218219
let instruction_data = MintActionCompressedInstructionData::new_mint(
219220
compressed_mint_inputs.root_index,
220221
CompressedProof::default(),
@@ -229,14 +230,15 @@ async fn test_write_to_cpi_context_invalid_address_tree() {
229230
token_out_queue_index: 0,
230231
assigned_account_index: 0,
231232
read_only_address_trees: [0; 4],
232-
address_tree_pubkey: invalid_address_tree.to_bytes(),
233+
address_tree_pubkey: address_tree.to_bytes(),
233234
});
234235

235-
// Build account metas using helper
236+
// Build account metas with invalid rent_sponsor
236237
let config = MintActionMetaConfigCpiWrite {
237238
fee_payer: payer.pubkey(),
238239
mint_signer: Some(mint_seed.pubkey()),
239240
authority: mint_authority.pubkey(),
241+
rent_sponsor: Some(invalid_rent_sponsor),
240242
cpi_context: cpi_context_pubkey,
241243
};
242244

@@ -270,8 +272,8 @@ async fn test_write_to_cpi_context_invalid_address_tree() {
270272
data: wrapper_ix_data.data(),
271273
};
272274

273-
// Execute wrapper instruction - should fail because create_mint + write_to_cpi_context
274-
// is rejected (error 6035: CpiContextSetNotUsable) before address tree validation.
275+
// Should fail with InvalidRentSponsor because rent_sponsor doesn't match RENT_SPONSOR_V1
276+
// Error code 6100 = InvalidRentSponsor
275277
let result = rpc
276278
.create_and_send_transaction(
277279
&[wrapper_instruction],
@@ -280,12 +282,12 @@ async fn test_write_to_cpi_context_invalid_address_tree() {
280282
)
281283
.await;
282284

283-
assert_rpc_error(result, 0, 6035).unwrap();
285+
assert_rpc_error(result, 0, 6099).unwrap();
284286
}
285287

286288
#[tokio::test]
287289
#[serial]
288-
async fn test_write_to_cpi_context_invalid_compressed_address() {
290+
async fn test_write_to_cpi_context_create_mint_missing_rent_sponsor() {
289291
let TestSetup {
290292
mut rpc,
291293
compressed_mint_inputs,
@@ -300,18 +302,11 @@ async fn test_write_to_cpi_context_invalid_compressed_address() {
300302
output_queue_index,
301303
} = test_setup().await;
302304

303-
// Swap the mint_signer to an invalid one (this should fail validation)
304-
// The compressed address will be derived from the invalid mint_signer
305-
let invalid_mint_signer = [42u8; 32];
306-
307-
// Build instruction data with invalid mint_signer in metadata
308-
let mut invalid_mint = compressed_mint_inputs.mint.clone().unwrap();
309-
invalid_mint.metadata.mint_signer = invalid_mint_signer;
310-
305+
// Build instruction data with create_mint
311306
let instruction_data = MintActionCompressedInstructionData::new_mint(
312307
compressed_mint_inputs.root_index,
313308
CompressedProof::default(),
314-
invalid_mint,
309+
compressed_mint_inputs.mint.clone().unwrap(),
315310
)
316311
.with_cpi_context(CpiContext {
317312
set_context: false,
@@ -325,11 +320,14 @@ async fn test_write_to_cpi_context_invalid_compressed_address() {
325320
address_tree_pubkey: address_tree.to_bytes(),
326321
});
327322

328-
// Build account metas using helper
323+
// Build account metas WITHOUT rent_sponsor (None).
324+
// The program expects rent_sponsor when create_mint is true in write mode,
325+
// so not providing it will cause the account iterator to misparse accounts.
329326
let config = MintActionMetaConfigCpiWrite {
330327
fee_payer: payer.pubkey(),
331328
mint_signer: Some(mint_seed.pubkey()),
332329
authority: mint_authority.pubkey(),
330+
rent_sponsor: None,
333331
cpi_context: cpi_context_pubkey,
334332
};
335333

@@ -363,8 +361,9 @@ async fn test_write_to_cpi_context_invalid_compressed_address() {
363361
data: wrapper_ix_data.data(),
364362
};
365363

366-
// Execute wrapper instruction - should fail because create_mint + write_to_cpi_context
367-
// is rejected (error 6035: CpiContextSetNotUsable) before mint signer validation.
364+
// Should fail - when rent_sponsor is missing, the account iterator shifts:
365+
// fee_payer is parsed as rent_sponsor, then CpiContextLightSystemAccounts
366+
// runs out of accounts. Error 20009 is from the account iterator.
368367
let result = rpc
369368
.create_and_send_transaction(
370369
&[wrapper_instruction],
@@ -373,7 +372,7 @@ async fn test_write_to_cpi_context_invalid_compressed_address() {
373372
)
374373
.await;
375374

376-
assert_rpc_error(result, 0, 6035).unwrap();
375+
assert_rpc_error(result, 0, 20009).unwrap();
377376
}
378377

379378
#[tokio::test]
@@ -519,6 +518,7 @@ async fn test_write_to_cpi_context_decompressed_mint_fails() {
519518
fee_payer: payer.pubkey(),
520519
mint_signer: None,
521520
authority: mint_authority.pubkey(),
521+
rent_sponsor: None,
522522
cpi_context: cpi_context_pubkey,
523523
};
524524

@@ -611,6 +611,7 @@ async fn test_write_to_cpi_context_mint_to_ctoken_fails() {
611611
fee_payer: payer.pubkey(),
612612
mint_signer: Some(mint_seed.pubkey()),
613613
authority: mint_authority.pubkey(),
614+
rent_sponsor: None,
614615
cpi_context: cpi_context_pubkey,
615616
};
616617

@@ -703,6 +704,7 @@ async fn test_write_to_cpi_context_decompress_mint_action_fails() {
703704
fee_payer: payer.pubkey(),
704705
mint_signer: Some(mint_seed.pubkey()),
705706
authority: mint_authority.pubkey(),
707+
rent_sponsor: None,
706708
cpi_context: cpi_context_pubkey,
707709
};
708710

programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ pub struct MintActionAccounts<'info> {
2121
/// Seed for mint PDA derivation.
2222
/// Required only for compressed mint creation.
2323
/// Note: mint_signer is not in executing accounts since it is parsed
24-
/// before the executing/cpi-write branch. create_mint is NOT allowed
25-
/// in combination with write to cpi context (rejected in AccountsConfig::new).
24+
/// before the executing/cpi-write branch.
2625
pub mint_signer: Option<&'info AccountInfo>,
2726
pub authority: &'info AccountInfo,
2827
/// Required accounts to execute an instruction
@@ -32,6 +31,9 @@ pub struct MintActionAccounts<'info> {
3231
/// Required accounts to write into a cpi context account.
3332
/// - executing is None
3433
pub write_to_cpi_context_system: Option<CpiContextLightSystemAccounts<'info>>,
34+
/// Rent sponsor account in write mode (when create_mint + write_to_cpi_context).
35+
/// Validated against hardcoded RENT_SPONSOR_V1 constant.
36+
pub write_mode_rent_sponsor: Option<&'info AccountInfo>,
3537
/// Packed accounts contain
3638
/// [
3739
/// ..tree_accounts,
@@ -86,7 +88,18 @@ impl<'info> MintActionAccounts<'info> {
8688
// Authority is always required to sign
8789
let authority = iter.next_signer("authority")?;
8890
if config.write_to_cpi_context {
91+
let write_mode_rent_sponsor = if config.create_mint {
92+
Some(iter.next_account("rent_sponsor")?)
93+
} else {
94+
None
95+
};
8996
let write_to_cpi_context_system = CpiContextLightSystemAccounts::new(&mut iter)?;
97+
// System program is needed for the fee transfer CPI when creating mint in write mode.
98+
// It's placed after all parsed accounts - the account iterator consumes it here,
99+
// but it's available for the system program CPI via the transaction accounts.
100+
if config.create_mint {
101+
let _system_program = iter.next_account("system_program")?;
102+
}
90103

91104
if !iter.iterator_is_empty() {
92105
msg!("Too many accounts for write to cpi context.");
@@ -98,6 +111,7 @@ impl<'info> MintActionAccounts<'info> {
98111
authority,
99112
executing: None,
100113
write_to_cpi_context_system: Some(write_to_cpi_context_system),
114+
write_mode_rent_sponsor,
101115
packed_accounts: ProgramPackedAccounts { accounts: &[] },
102116
})
103117
} else {
@@ -156,6 +170,7 @@ impl<'info> MintActionAccounts<'info> {
156170
tokens_out_queue,
157171
}),
158172
write_to_cpi_context_system: None,
173+
write_mode_rent_sponsor: None,
159174
packed_accounts: ProgramPackedAccounts {
160175
accounts: iter.remaining_unchecked()?,
161176
},
@@ -214,6 +229,11 @@ impl<'info> MintActionAccounts<'info> {
214229
offset += 1;
215230
}
216231

232+
// write_mode_rent_sponsor (optional) - when create_mint + write_to_cpi_context
233+
if self.write_mode_rent_sponsor.is_some() {
234+
offset += 1;
235+
}
236+
217237
if let Some(executing) = &self.executing {
218238
// compressible_config (optional) - when creating mint or CMint
219239
if executing.compressible_config.is_some() {
@@ -467,14 +487,6 @@ impl AccountsConfig {
467487
let cmint_decompressed = parsed_instruction_data.mint.is_none();
468488

469489
if write_to_cpi_context {
470-
// Cannot create a compressed mint when writing to CPI context.
471-
// Mint creation charges the creation fee and requires the rent_sponsor account,
472-
// which is not available in the CPI context write path.
473-
if parsed_instruction_data.create_mint.is_some() {
474-
msg!("Compressed mint creation not allowed when writing to cpi context");
475-
return Err(ErrorCode::CpiContextSetNotUsable.into());
476-
}
477-
478490
// Must not have any MintToCToken actions
479491
let has_mint_to_ctoken_actions = parsed_instruction_data
480492
.actions

programs/compressed-token/program/src/compressed_token/mint_action/processor.rs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,39 @@ pub fn process_mint_action(
4545
let validated_accounts =
4646
MintActionAccounts::validate_and_parse(accounts, &accounts_config, cmint_pubkey.as_ref())?;
4747

48-
// Charge mint creation fee. create_mint is rejected in CPI context write path
49-
// (validated in AccountsConfig::new), so executing and rent_sponsor are always present here.
48+
// Charge mint creation fee in both execute and write modes.
5049
if accounts_config.create_mint {
51-
let executing = validated_accounts
52-
.executing
53-
.as_ref()
54-
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
55-
let rent_sponsor = executing
56-
.rent_sponsor
57-
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
58-
// Validate rent_sponsor matches config to prevent fee bypass.
59-
let config = executing
60-
.compressible_config
61-
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
62-
if rent_sponsor.key() != &config.rent_sponsor.to_bytes() {
63-
msg!("Rent sponsor account does not match config");
64-
return Err(ErrorCode::InvalidRentSponsor.into());
50+
if let Some(executing) = validated_accounts.executing.as_ref() {
51+
// Execute mode: validate rent_sponsor against compressible_config.
52+
let rent_sponsor = executing
53+
.rent_sponsor
54+
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
55+
let config = executing
56+
.compressible_config
57+
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
58+
if rent_sponsor.key() != &config.rent_sponsor.to_bytes() {
59+
msg!("Rent sponsor account does not match config");
60+
return Err(ErrorCode::InvalidRentSponsor.into());
61+
}
62+
transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor)
63+
.map_err(convert_program_error)?;
64+
} else {
65+
// Write mode: validate rent_sponsor against hardcoded RENT_SPONSOR_V1.
66+
let rent_sponsor = validated_accounts
67+
.write_mode_rent_sponsor
68+
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?;
69+
if rent_sponsor.key() != &crate::RENT_SPONSOR_V1 {
70+
msg!("Rent sponsor account does not match RENT_SPONSOR_V1");
71+
return Err(ErrorCode::InvalidRentSponsor.into());
72+
}
73+
let fee_payer = validated_accounts
74+
.write_to_cpi_context_system
75+
.as_ref()
76+
.ok_or(ErrorCode::MintActionMissingExecutingAccounts)?
77+
.fee_payer;
78+
transfer_lamports_via_cpi(MINT_CREATION_FEE, fee_payer, rent_sponsor)
79+
.map_err(convert_program_error)?;
6580
}
66-
transfer_lamports_via_cpi(MINT_CREATION_FEE, executing.system.fee_payer, rent_sponsor)
67-
.map_err(convert_program_error)?;
6881
}
6982

7083
// Get mint data based on source:

programs/compressed-token/program/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub const LIGHT_CPI_SIGNER: CpiSigner =
3737

3838
pub const MAX_ACCOUNTS: usize = 30;
3939
pub const MINT_CREATION_FEE: u64 = 50_000;
40+
/// Hardcoded rent sponsor PDA for write-mode mint creation fee validation.
41+
/// Same value as LIGHT_TOKEN_RENT_SPONSOR in sdk-types/src/constants.rs.
42+
pub const RENT_SPONSOR_V1: pinocchio::pubkey::Pubkey =
43+
light_macros::pubkey_array!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti");
4044
pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40;
4145
/// Maximum number of compression operations per instruction.
4246
/// Used for compression_to_input lookup array sizing.

programs/compressed-token/program/tests/mint_action.rs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -325,41 +325,27 @@ fn check_if_config_should_error(instruction_data: &MintActionCompressedInstructi
325325
.unwrap_or_default();
326326

327327
if write_to_cpi_context {
328-
// Check for MintToCToken actions
328+
// Check for MintToCToken actions (not allowed in write mode)
329329
let has_mint_to_ctoken = instruction_data
330330
.actions
331331
.iter()
332332
.any(|action| matches!(action, Action::MintTo(_)));
333333

334-
// Check for MintToCompressed actions
335-
let require_token_output_queue = instruction_data
334+
// Check for DecompressMint actions (not allowed in write mode)
335+
let has_decompress_mint = instruction_data
336336
.actions
337337
.iter()
338-
.any(|action| matches!(action, Action::MintToCompressed(_)));
338+
.any(|action| matches!(action, Action::DecompressMint(_)));
339339

340-
// mint_decompressed is only from metadata flag (matches AccountsConfig::new)
341-
let mint_decompressed = instruction_data
342-
.mint
343-
.as_ref()
344-
.unwrap()
345-
.metadata
346-
.mint_decompressed;
347-
348-
// Check if this is creating a new mint (not from an existing compressed mint)
349-
let is_creating_mint = instruction_data.mint.is_none();
350-
351-
// Check if create_mint is set (create_mint + write_to_cpi_context not allowed)
352-
let create_mint_with_cpi_write = instruction_data.create_mint.is_some();
340+
// cmint_decompressed (mint.is_none()) not allowed in write mode
341+
let cmint_decompressed = instruction_data.mint.is_none();
353342

354343
// Error conditions matching AccountsConfig::new:
355344
// 1. has_mint_to_ctoken (MintToCToken actions not allowed)
356-
// 2. mint_decompressed && require_token_output_queue (mint decompressed + MintToCompressed not allowed)
357-
// 3. is_creating_mint (mint creation not allowed when writing to cpi context)
358-
// 4. create_mint_with_cpi_write (create_mint + write_to_cpi_context not allowed)
359-
has_mint_to_ctoken
360-
|| (mint_decompressed && require_token_output_queue)
361-
|| is_creating_mint
362-
|| create_mint_with_cpi_write
345+
// 2. has_decompress_mint (DecompressMint not allowed)
346+
// 3. cmint_decompressed (decompressed mint not allowed)
347+
// Note: create_mint IS allowed in write mode (fee charged to rent_sponsor)
348+
has_mint_to_ctoken || has_decompress_mint || cmint_decompressed
363349
} else {
364350
false
365351
}

0 commit comments

Comments
 (0)