Skip to content

Commit fbbd1fe

Browse files
fix: robust account creation (#2097)
* first version * Update programs/compressed-token/program/src/shared/create_pda_account.rs Co-authored-by: Swen Schäferjohann <swen@lightprotocol.com> * Update programs/compressed-token/program/src/shared/create_pda_account.rs Co-authored-by: Swen Schäferjohann <swen@lightprotocol.com> * Update programs/compressed-token/program/src/shared/create_pda_account.rs Co-authored-by: Swen Schäferjohann <swen@lightprotocol.com> --------- Co-authored-by: Swen Schäferjohann <swen@lightprotocol.com>
1 parent f90dbdd commit fbbd1fe

8 files changed

Lines changed: 305 additions & 140 deletions

File tree

‎program-tests/compressed-token-test/tests/ctoken/create.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ async fn test_create_compressible_token_account_failing() {
206206
&mut context,
207207
compressible_data,
208208
"account_already_initialized",
209-
0, // AlreadyInitialized system program cpi fails (for compressible accounts we create the token accounts via cpi)
209+
78, // AlreadyInitialized (our program checks this after Assign+realloc pattern)
210210
)
211211
.await;
212212
}

‎program-tests/compressed-token-test/tests/ctoken/functional_ata.rs‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,173 @@ async fn test_create_ata_idempotent() {
220220
)
221221
.await;
222222
}
223+
224+
/// Test: DoS prevention for ATA creation
225+
/// 1. Derive ATA address
226+
/// 2. Pre-fund the ATA address with lamports (simulating attacker donation)
227+
/// 3. SUCCESS: Create ATA should succeed despite pre-funded lamports
228+
#[tokio::test]
229+
#[serial]
230+
async fn test_create_ata_with_prefunded_lamports() {
231+
let mut context = setup_account_test().await.unwrap();
232+
let payer_pubkey = context.payer.pubkey();
233+
let owner_pubkey = context.owner_keypair.pubkey();
234+
235+
// Derive ATA address
236+
let (ata, bump) = derive_ctoken_ata(&owner_pubkey, &context.mint_pubkey);
237+
238+
// Pre-fund the ATA address with lamports (simulating attacker donation DoS attempt)
239+
let prefund_amount = 1_000; // 1000 lamports
240+
let transfer_ix = solana_sdk::system_instruction::transfer(&payer_pubkey, &ata, prefund_amount);
241+
242+
context
243+
.rpc
244+
.create_and_send_transaction(&[transfer_ix], &payer_pubkey, &[&context.payer])
245+
.await
246+
.unwrap();
247+
248+
// Verify the ATA address now has lamports
249+
let ata_account = context.rpc.get_account(ata).await.unwrap();
250+
assert!(
251+
ata_account.is_some(),
252+
"ATA address should exist with lamports"
253+
);
254+
assert_eq!(
255+
ata_account.unwrap().lamports,
256+
prefund_amount,
257+
"ATA should have pre-funded lamports"
258+
);
259+
260+
// Now create the ATA - this should succeed despite pre-funded lamports
261+
let instruction = CreateAssociatedTokenAccount {
262+
idempotent: false,
263+
bump,
264+
payer: payer_pubkey,
265+
owner: owner_pubkey,
266+
mint: context.mint_pubkey,
267+
associated_token_account: ata,
268+
compressible: None,
269+
}
270+
.instruction()
271+
.unwrap();
272+
273+
context
274+
.rpc
275+
.create_and_send_transaction(&[instruction], &payer_pubkey, &[&context.payer])
276+
.await
277+
.unwrap();
278+
279+
// Verify ATA was created correctly
280+
assert_create_associated_token_account(
281+
&mut context.rpc,
282+
owner_pubkey,
283+
context.mint_pubkey,
284+
None,
285+
)
286+
.await;
287+
288+
// Verify the ATA now has more lamports (rent-exempt + pre-funded)
289+
let final_ata_account = context.rpc.get_account(ata).await.unwrap().unwrap();
290+
assert!(
291+
final_ata_account.lamports > prefund_amount,
292+
"ATA should have rent-exempt balance plus pre-funded amount"
293+
);
294+
}
295+
296+
/// Test: DoS prevention for token account creation with custom rent payer
297+
/// 1. Generate token account keypair
298+
/// 2. Pre-fund the token account address with lamports (simulating attacker donation)
299+
/// 3. SUCCESS: Create token account should succeed despite pre-funded lamports
300+
#[tokio::test]
301+
#[serial]
302+
async fn test_create_token_account_with_prefunded_lamports() {
303+
let mut context = setup_account_test().await.unwrap();
304+
let payer_pubkey = context.payer.pubkey();
305+
let token_account_pubkey = context.token_account_keypair.pubkey();
306+
307+
// Pre-fund the token account address with lamports (simulating attacker donation DoS attempt)
308+
let prefund_amount = 1_000; // 1000 lamports
309+
let transfer_ix = solana_sdk::system_instruction::transfer(
310+
&payer_pubkey,
311+
&token_account_pubkey,
312+
prefund_amount,
313+
);
314+
315+
context
316+
.rpc
317+
.create_and_send_transaction(&[transfer_ix], &payer_pubkey, &[&context.payer])
318+
.await
319+
.unwrap();
320+
321+
// Verify the token account address now has lamports
322+
let token_account = context.rpc.get_account(token_account_pubkey).await.unwrap();
323+
assert!(
324+
token_account.is_some(),
325+
"Token account address should exist with lamports"
326+
);
327+
assert_eq!(
328+
token_account.unwrap().lamports,
329+
prefund_amount,
330+
"Token account should have pre-funded lamports"
331+
);
332+
333+
// Now create the compressible token account - this should succeed despite pre-funded lamports
334+
let compressible_params = CompressibleParams {
335+
compressible_config: context.compressible_config,
336+
rent_sponsor: context.rent_sponsor,
337+
pre_pay_num_epochs: 0,
338+
lamports_per_write: Some(100),
339+
compress_to_account_pubkey: None,
340+
token_account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat,
341+
};
342+
343+
let create_token_account_ix = CreateCTokenAccount::new(
344+
payer_pubkey,
345+
token_account_pubkey,
346+
context.mint_pubkey,
347+
context.owner_keypair.pubkey(),
348+
)
349+
.with_compressible(compressible_params)
350+
.instruction()
351+
.unwrap();
352+
353+
context
354+
.rpc
355+
.create_and_send_transaction(
356+
&[create_token_account_ix],
357+
&payer_pubkey,
358+
&[&context.payer, &context.token_account_keypair],
359+
)
360+
.await
361+
.unwrap();
362+
363+
// Verify token account was created correctly
364+
assert_create_token_account(
365+
&mut context.rpc,
366+
token_account_pubkey,
367+
context.mint_pubkey,
368+
context.owner_keypair.pubkey(),
369+
Some(CompressibleData {
370+
compression_authority: context.compression_authority,
371+
rent_sponsor: context.rent_sponsor,
372+
num_prepaid_epochs: 0,
373+
lamports_per_write: Some(100),
374+
compress_to_pubkey: false,
375+
account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat,
376+
payer: payer_pubkey,
377+
}),
378+
)
379+
.await;
380+
381+
// Verify the token account now has more lamports (rent-exempt + pre-funded)
382+
let final_token_account = context
383+
.rpc
384+
.get_account(token_account_pubkey)
385+
.await
386+
.unwrap()
387+
.unwrap();
388+
assert!(
389+
final_token_account.lamports > prefund_amount,
390+
"Token account should have rent-exempt balance plus pre-funded amount"
391+
);
392+
}

‎program-tests/utils/src/assert_create_token_account.rs‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,14 @@ pub async fn assert_create_token_account_internal(
110110
assert_eq!(actual_token_account, expected_token_account);
111111

112112
// Check if account existed before transaction (for idempotent mode)
113-
let account_existed_before = rpc
114-
.get_pre_transaction_account(&token_account_pubkey)
115-
.is_some();
113+
// Account "existed" only if it had data (was initialized), not just lamports
114+
let pre_tx_account = rpc.get_pre_transaction_account(&token_account_pubkey);
115+
let account_existed_before = pre_tx_account
116+
.as_ref()
117+
.map(|acc| !acc.data.is_empty())
118+
.unwrap_or(false);
119+
// Get pre-existing lamports (e.g., from attacker donation for DoS prevention test)
120+
let pre_existing_lamports = pre_tx_account.map(|acc| acc.lamports).unwrap_or(0);
116121

117122
// Assert payer and rent sponsor balance changes
118123
let payer_balance_before = rpc
@@ -183,12 +188,17 @@ pub async fn assert_create_token_account_internal(
183188
payer_balance_before - payer_balance_after
184189
);
185190

186-
// Rent sponsor pays: rent_exemption only
191+
// Rent sponsor pays: rent_exemption minus any pre-existing lamports
192+
// (pre-existing lamports from attacker donation are kept in the account)
193+
let expected_rent_sponsor_payment =
194+
rent_exemption.saturating_sub(pre_existing_lamports);
187195
assert_eq!(
188196
rent_sponsor_balance_before - rent_sponsor_balance_after,
197+
expected_rent_sponsor_payment,
198+
"Rent sponsor should have paid {} lamports (rent exemption {} - pre-existing {}), but paid {}",
199+
expected_rent_sponsor_payment,
189200
rent_exemption,
190-
"Rent sponsor should have paid {} lamports (rent exemption only), but paid {}",
191-
rent_exemption,
201+
pre_existing_lamports,
192202
rent_sponsor_balance_before - rent_sponsor_balance_after
193203
);
194204
}

‎programs/compressed-token/program/src/create_associated_token_account.rs‎

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,19 @@ pub(crate) fn process_create_associated_token_account_inner<const IDEMPOTENT: bo
115115
} else {
116116
// Create the PDA account (with rent-exempt balance only)
117117
let bump_seed = [bump];
118-
let seeds = [
118+
let ata_seeds = [
119119
Seed::from(owner_bytes.as_ref()),
120120
Seed::from(crate::LIGHT_CPI_SIGNER.program_id.as_ref()),
121121
Seed::from(mint_bytes.as_ref()),
122122
Seed::from(bump_seed.as_ref()),
123123
];
124124

125-
let seeds_inputs = [seeds.as_slice()];
126-
127125
create_pda_account(
128126
fee_payer,
129127
associated_token_account,
130128
token_account_size,
131-
seeds_inputs,
129+
None, // fee_payer is keypair
130+
Some(ata_seeds.as_slice()), // ATA is PDA
132131
None,
133132
)?;
134133
(None, None)
@@ -185,7 +184,7 @@ fn process_compressible_config<'info>(
185184
compressible_config_ix_data.rent_payment as u64,
186185
);
187186

188-
// Build ATA seeds
187+
// Build ATA seeds (new_account is always a PDA)
189188
let ata_bump_seed = [ata_bump];
190189
let ata_seeds = [
191190
Seed::from(owner_bytes.as_ref()),
@@ -194,36 +193,30 @@ fn process_compressible_config<'info>(
194193
Seed::from(ata_bump_seed.as_ref()),
195194
];
196195

197-
// Build rent sponsor seeds if needed (must be outside conditional for lifetime)
198-
let rent_sponsor_bump;
199-
let version_bytes;
200-
let rent_sponsor_seeds;
196+
// Build rent sponsor seeds if using rent sponsor PDA as fee_payer
197+
let rent_sponsor_bump = [compressible_config_account.rent_sponsor_bump];
198+
let version_bytes = compressible_config_account.version.to_le_bytes();
199+
let rent_sponsor_seeds = [
200+
Seed::from(b"rent_sponsor".as_ref()),
201+
Seed::from(version_bytes.as_ref()),
202+
Seed::from(rent_sponsor_bump.as_ref()),
203+
];
201204

202-
// Create the PDA account (with rent-exempt balance only)
203-
// rent_payer will be the rent_sponsor PDA for compressible accounts
204-
let seeds_inputs: [&[Seed]; 2] = if custom_rent_payer {
205-
// Only ATA seeds when custom rent payer
206-
[ata_seeds.as_slice(), &[]]
205+
// fee_payer_seeds: Some for rent_sponsor PDA, None for custom keypair
206+
// new_account_seeds: Always Some (ATA is always a PDA)
207+
let fee_payer_seeds = if custom_rent_payer {
208+
None
207209
} else {
208-
// Both rent sponsor PDA seeds and ATA seeds
209-
rent_sponsor_bump = [compressible_config_account.rent_sponsor_bump];
210-
version_bytes = compressible_config_account.version.to_le_bytes();
211-
rent_sponsor_seeds = [
212-
Seed::from(b"rent_sponsor".as_ref()),
213-
Seed::from(version_bytes.as_ref()),
214-
Seed::from(rent_sponsor_bump.as_ref()),
215-
];
216-
217-
[rent_sponsor_seeds.as_slice(), ata_seeds.as_slice()]
210+
Some(rent_sponsor_seeds.as_slice())
218211
};
219-
220212
let additional_lamports = if custom_rent_payer { Some(rent) } else { None };
221213

222214
create_pda_account(
223215
rent_payer,
224216
associated_token_account,
225217
token_account_size,
226-
seeds_inputs,
218+
fee_payer_seeds,
219+
Some(ata_seeds.as_slice()),
227220
additional_lamports,
228221
)?;
229222

0 commit comments

Comments
 (0)