Skip to content

Commit b36a4cc

Browse files
committed
fix: prevent Create Account DoS via transfer-allocate-assign pattern (Issue #4)
1 parent 0ba7526 commit b36a4cc

12 files changed

Lines changed: 3033 additions & 2648 deletions

File tree

Cargo.lock

Lines changed: 2334 additions & 2277 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program/src/processor/create_wallet.rs

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ use assertions::{check_zero_data, sol_assert_bytes_eq};
22
use no_padding::NoPadding;
33
use pinocchio::{
44
account_info::AccountInfo,
5-
instruction::{AccountMeta, Instruction, Seed, Signer},
6-
program::invoke_signed,
5+
instruction::Seed,
76
program_error::ProgramError,
87
pubkey::{find_program_address, Pubkey},
98
ProgramResult,
@@ -140,41 +139,22 @@ pub fn process(
140139
.and_then(|val| val.checked_add(rent_base))
141140
.ok_or(ProgramError::ArithmeticOverflow)?;
142141

143-
let mut create_wallet_ix_data = Vec::with_capacity(52);
144-
create_wallet_ix_data.extend_from_slice(&0u32.to_le_bytes());
145-
create_wallet_ix_data.extend_from_slice(&wallet_rent.to_le_bytes());
146-
create_wallet_ix_data.extend_from_slice(&(wallet_space as u64).to_le_bytes());
147-
create_wallet_ix_data.extend_from_slice(program_id.as_ref());
148-
149-
let wallet_accounts_meta = [
150-
AccountMeta {
151-
pubkey: payer.key(),
152-
is_signer: true,
153-
is_writable: true,
154-
},
155-
AccountMeta {
156-
pubkey: wallet_pda.key(),
157-
is_signer: true, // Must be true even with invoke_signed
158-
is_writable: true,
159-
},
160-
];
161-
let create_wallet_ix = Instruction {
162-
program_id: system_program.key(),
163-
accounts: &wallet_accounts_meta,
164-
data: &create_wallet_ix_data,
165-
};
142+
// Use secure transfer-allocate-assign pattern to prevent DoS (Issue #4)
166143
let wallet_bump_arr = [wallet_bump];
167144
let wallet_seeds = [
168145
Seed::from(b"wallet"),
169146
Seed::from(&args.user_seed),
170147
Seed::from(&wallet_bump_arr),
171148
];
172-
let wallet_signer: Signer = (&wallet_seeds).into();
173149

174-
invoke_signed(
175-
&create_wallet_ix,
176-
&[&payer.clone(), &wallet_pda.clone(), &system_program.clone()],
177-
&[wallet_signer],
150+
crate::utils::initialize_pda_account(
151+
payer,
152+
wallet_pda,
153+
system_program,
154+
wallet_space,
155+
wallet_rent,
156+
program_id,
157+
&wallet_seeds,
178158
)?;
179159

180160
// Write Wallet Data
@@ -209,42 +189,23 @@ pub fn process(
209189
.and_then(|val| val.checked_add(897840))
210190
.ok_or(ProgramError::ArithmeticOverflow)?;
211191

212-
let mut create_auth_ix_data = Vec::with_capacity(52);
213-
create_auth_ix_data.extend_from_slice(&0u32.to_le_bytes());
214-
create_auth_ix_data.extend_from_slice(&auth_rent.to_le_bytes());
215-
create_auth_ix_data.extend_from_slice(&(auth_space as u64).to_le_bytes());
216-
create_auth_ix_data.extend_from_slice(program_id.as_ref());
217-
218-
let auth_accounts_meta = [
219-
AccountMeta {
220-
pubkey: payer.key(),
221-
is_signer: true,
222-
is_writable: true,
223-
},
224-
AccountMeta {
225-
pubkey: auth_pda.key(),
226-
is_signer: true, // Must be true even with invoke_signed
227-
is_writable: true,
228-
},
229-
];
230-
let create_auth_ix = Instruction {
231-
program_id: system_program.key(),
232-
accounts: &auth_accounts_meta,
233-
data: &create_auth_ix_data,
234-
};
192+
// Use secure transfer-allocate-assign pattern to prevent DoS (Issue #4)
235193
let auth_bump_arr = [auth_bump];
236194
let auth_seeds = [
237195
Seed::from(b"authority"),
238196
Seed::from(wallet_key.as_ref()),
239197
Seed::from(id_seed),
240198
Seed::from(&auth_bump_arr),
241199
];
242-
let auth_signer: Signer = (&auth_seeds).into();
243200

244-
invoke_signed(
245-
&create_auth_ix,
246-
&[&payer.clone(), &auth_pda.clone(), &system_program.clone()],
247-
&[auth_signer],
201+
crate::utils::initialize_pda_account(
202+
payer,
203+
auth_pda,
204+
system_program,
205+
auth_space,
206+
auth_rent,
207+
program_id,
208+
&auth_seeds,
248209
)?;
249210

250211
// Write Authority Data

program/src/processor/manage_authority.rs

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ use assertions::{check_zero_data, sol_assert_bytes_eq};
22
use no_padding::NoPadding;
33
use pinocchio::{
44
account_info::AccountInfo,
5-
instruction::{AccountMeta, Instruction, Seed, Signer},
6-
program::invoke_signed,
5+
instruction::Seed,
76
program_error::ProgramError,
87
pubkey::{find_program_address, Pubkey},
98
ProgramResult,
@@ -201,48 +200,23 @@ pub fn process_add_authority(
201200
.and_then(|val| val.checked_add(897840))
202201
.ok_or(ProgramError::ArithmeticOverflow)?;
203202

204-
// ... (create_ix logic same) ...
205-
206-
let mut create_ix_data = Vec::with_capacity(52);
207-
create_ix_data.extend_from_slice(&0u32.to_le_bytes());
208-
create_ix_data.extend_from_slice(&rent.to_le_bytes());
209-
create_ix_data.extend_from_slice(&(space as u64).to_le_bytes());
210-
create_ix_data.extend_from_slice(program_id.as_ref());
211-
212-
let accounts_meta = [
213-
AccountMeta {
214-
pubkey: payer.key(),
215-
is_signer: true,
216-
is_writable: true,
217-
},
218-
AccountMeta {
219-
pubkey: new_auth_pda.key(),
220-
is_signer: true, // Must be true even with invoke_signed
221-
is_writable: true,
222-
},
223-
];
224-
let create_ix = Instruction {
225-
program_id: system_program.key(),
226-
accounts: &accounts_meta,
227-
data: &create_ix_data,
228-
};
203+
// Use secure transfer-allocate-assign pattern to prevent DoS (Issue #4)
229204
let bump_arr = [bump];
230205
let seeds = [
231206
Seed::from(b"authority"),
232207
Seed::from(wallet_pda.key().as_ref()),
233208
Seed::from(id_seed),
234209
Seed::from(&bump_arr),
235210
];
236-
let signer: Signer = (&seeds).into();
237-
238-
invoke_signed(
239-
&create_ix,
240-
&[
241-
&payer.clone(),
242-
&new_auth_pda.clone(),
243-
&system_program.clone(),
244-
],
245-
&[signer],
211+
212+
crate::utils::initialize_pda_account(
213+
payer,
214+
new_auth_pda,
215+
system_program,
216+
space,
217+
rent,
218+
program_id,
219+
&seeds,
246220
)?;
247221

248222
let data = unsafe { new_auth_pda.borrow_mut_data_unchecked() };

program/src/processor/transfer_ownership.rs

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use assertions::{check_zero_data, sol_assert_bytes_eq};
22
use pinocchio::{
33
account_info::AccountInfo,
4-
instruction::{AccountMeta, Instruction, Seed, Signer},
5-
program::invoke_signed,
4+
instruction::Seed,
65
program_error::ProgramError,
76
pubkey::{find_program_address, Pubkey},
87
ProgramResult,
@@ -173,42 +172,23 @@ pub fn process(
173172
.and_then(|val| val.checked_add(897840))
174173
.ok_or(ProgramError::ArithmeticOverflow)?;
175174

176-
let mut create_ix_data = Vec::with_capacity(52);
177-
create_ix_data.extend_from_slice(&0u32.to_le_bytes());
178-
create_ix_data.extend_from_slice(&rent.to_le_bytes());
179-
create_ix_data.extend_from_slice(&(space as u64).to_le_bytes());
180-
create_ix_data.extend_from_slice(program_id.as_ref());
181-
182-
let accounts_meta = [
183-
AccountMeta {
184-
pubkey: payer.key(),
185-
is_signer: true,
186-
is_writable: true,
187-
},
188-
AccountMeta {
189-
pubkey: new_owner.key(),
190-
is_signer: true,
191-
is_writable: true,
192-
},
193-
];
194-
let create_ix = Instruction {
195-
program_id: system_program.key(),
196-
accounts: &accounts_meta,
197-
data: &create_ix_data,
198-
};
175+
// Use secure transfer-allocate-assign pattern to prevent DoS (Issue #4)
199176
let bump_arr = [bump];
200177
let seeds = [
201178
Seed::from(b"authority"),
202179
Seed::from(wallet_pda.key().as_ref()),
203180
Seed::from(id_seed),
204181
Seed::from(&bump_arr),
205182
];
206-
let signer: Signer = (&seeds).into();
207183

208-
invoke_signed(
209-
&create_ix,
210-
&[&payer.clone(), &new_owner.clone(), &system_program.clone()],
211-
&[signer],
184+
crate::utils::initialize_pda_account(
185+
payer,
186+
new_owner,
187+
system_program,
188+
space,
189+
rent,
190+
program_id,
191+
&seeds,
212192
)?;
213193

214194
let data = unsafe { new_owner.borrow_mut_data_unchecked() };

program/src/utils.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
use pinocchio::{
2+
account_info::AccountInfo,
3+
instruction::{AccountMeta, Instruction, Seed, Signer},
4+
program::invoke_signed,
5+
program_error::ProgramError,
6+
pubkey::Pubkey,
7+
ProgramResult,
8+
};
9+
110
/// Wrapper around the `sol_get_stack_height` syscall
211
pub fn get_stack_height() -> u64 {
312
#[cfg(target_os = "solana")]
@@ -7,3 +16,123 @@ pub fn get_stack_height() -> u64 {
716
#[cfg(not(target_os = "solana"))]
817
0
918
}
19+
20+
/// Safely initializes a PDA account using transfer-allocate-assign pattern.
21+
///
22+
/// This prevents DoS attacks where malicious actors pre-fund target accounts
23+
/// with small amounts of lamports, causing the System Program's `create_account`
24+
/// instruction to fail (since it rejects accounts with non-zero balances).
25+
///
26+
/// The transfer-allocate-assign pattern works in three steps:
27+
/// 1. **Transfer**: Add lamports to reach rent-exemption (if needed)
28+
/// 2. **Allocate**: Set the account's data size
29+
/// 3. **Assign**: Transfer ownership to the target program
30+
///
31+
/// # Security
32+
/// - Prevents Issue #4: Create Account DoS vulnerability
33+
/// - Still enforces rent-exemption requirements
34+
/// - Properly assigns ownership to prevent unauthorized access
35+
/// - Works even if account is pre-funded by attacker
36+
///
37+
/// # Arguments
38+
/// * `payer` - Account paying for initialization (must be signer & writable)
39+
/// * `target_pda` - PDA being initialized (will be writable)
40+
/// * `system_program` - System Program account
41+
/// * `space` - Number of bytes to allocate for account data
42+
/// * `rent_lamports` - Minimum lamports for rent-exemption
43+
/// * `owner` - Program that will own this account
44+
/// * `pda_seeds` - Seeds for PDA signing (for allocate & assign)
45+
///
46+
/// # Errors
47+
/// Returns ProgramError if:
48+
/// - Payer has insufficient funds
49+
/// - Any CPI call fails
50+
/// - Account is already owned by another program
51+
pub fn initialize_pda_account(
52+
payer: &AccountInfo,
53+
target_pda: &AccountInfo,
54+
system_program: &AccountInfo,
55+
space: usize,
56+
rent_lamports: u64,
57+
owner: &Pubkey,
58+
pda_seeds: &[Seed],
59+
) -> ProgramResult {
60+
let current_balance = target_pda.lamports();
61+
62+
// Step 1: Transfer lamports if needed to reach rent-exemption
63+
if current_balance < rent_lamports {
64+
let transfer_amount = rent_lamports
65+
.checked_sub(current_balance)
66+
.ok_or(ProgramError::ArithmeticOverflow)?;
67+
68+
// System Program Transfer instruction (discriminator: 2)
69+
let mut transfer_data = Vec::with_capacity(12);
70+
transfer_data.extend_from_slice(&2u32.to_le_bytes());
71+
transfer_data.extend_from_slice(&transfer_amount.to_le_bytes());
72+
73+
let transfer_accounts = [
74+
AccountMeta {
75+
pubkey: payer.key(),
76+
is_signer: true,
77+
is_writable: true,
78+
},
79+
AccountMeta {
80+
pubkey: target_pda.key(),
81+
is_signer: false,
82+
is_writable: true,
83+
},
84+
];
85+
86+
let transfer_ix = Instruction {
87+
program_id: system_program.key(),
88+
accounts: &transfer_accounts,
89+
data: &transfer_data,
90+
};
91+
92+
pinocchio::program::invoke(&transfer_ix, &[&payer, &target_pda, &system_program])?;
93+
}
94+
95+
// Step 2: Allocate space
96+
// System Program Allocate instruction (discriminator: 8)
97+
let mut allocate_data = Vec::with_capacity(12);
98+
allocate_data.extend_from_slice(&8u32.to_le_bytes());
99+
allocate_data.extend_from_slice(&(space as u64).to_le_bytes());
100+
101+
let allocate_accounts = [AccountMeta {
102+
pubkey: target_pda.key(),
103+
is_signer: true,
104+
is_writable: true,
105+
}];
106+
107+
let allocate_ix = Instruction {
108+
program_id: system_program.key(),
109+
accounts: &allocate_accounts,
110+
data: &allocate_data,
111+
};
112+
113+
let signer: Signer = pda_seeds.into();
114+
invoke_signed(&allocate_ix, &[&target_pda, &system_program], &[signer])?;
115+
116+
// Step 3: Assign ownership to target program
117+
// System Program Assign instruction (discriminator: 1)
118+
let mut assign_data = Vec::with_capacity(36);
119+
assign_data.extend_from_slice(&1u32.to_le_bytes());
120+
assign_data.extend_from_slice(owner.as_ref());
121+
122+
let assign_accounts = [AccountMeta {
123+
pubkey: target_pda.key(),
124+
is_signer: true,
125+
is_writable: true,
126+
}];
127+
128+
let assign_ix = Instruction {
129+
program_id: system_program.key(),
130+
accounts: &assign_accounts,
131+
data: &assign_data,
132+
};
133+
134+
let signer: Signer = pda_seeds.into();
135+
invoke_signed(&assign_ix, &[&target_pda, &system_program], &[signer])?;
136+
137+
Ok(())
138+
}

0 commit comments

Comments
 (0)