- Writer not validated
- Signer not validated
- Owner not validated
- Account existence check on lamports → data.len()
- Sysvar not validated
- PDA sharing issue
- Pda's ownership or pubkey should be checked by find_program_address
- Reinitialization - Check if the account is initalized
- Transfer lamports in PDAs using signer seeds
- Type Cosplay
- Sysvar Account not validated
- Arbitrary CPI → CPI into unchecked account
- Unvalidated rent
- Unvalidated Token program , spl_token_id should have been checked
- Seed collision issue
- Bump seeds not validated (find program address vs create_program_address)
- Faulty account space validation, should use #derive[initspace]
- creation of Ata using init or explictly creation of ata in ix, if it exists it will fail the whole tx
- Max-depth cpi => should not be
- in between the instruction, creation of a account which would have been done beforehand
- When transfering lamports not adhering to the rent exempt lamports
- when considering lamports as assets always keep in mind that the lamports consisdered as assets are after the rent check
- Mid-transaction account creation DOS
- Lamport asset accounting with rent consideration
- Signer not validated
// ❌ Bad: No signer validation
pub fn handler(ctx: Context<Handler>) -> Result<()> {
// Missing signer check
Ok(())
}
// ✅ Good: Validate signer
if !payer_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
- Writer not validated
// ❌ Bad: No writable check
let state_account = next_account_info(accounts_iter)?;
// ✅ Good: Check if account is writable
let state_account = next_account_info(accounts_iter)?;
if !state_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
- Owner not checked
// ❌ Bad: Using AccountInfo without owner check
pub mint: AccountInfo<'info>
// ✅ Good: Use typed account with ownership check
pub mint: Account<'info, Mint>
// ✅ Good: Explicit owner check
#[account(constraint = authority.owner == token_program::ID)]
- Account initialized check missing
// ❌ Bad: No initialization check
#[account]
pub struct StateAccount {
data: u64,
}
// ✅ Good: Check initialization
#[account(init, payer = user, space = 8 + 8)]
pub struct StateAccount {
data: u64,
}
- Missing check for the permission of caller
// ❌ Bad: No authority check
fn init_market(accounts: &[AccountInfo]) -> ProgramResult {
Ok(())
}
// ✅ Good: Validate authority
fn init_market(accounts: &[AccountInfo]) -> ProgramResult {
let authority = next_account_info(accounts_iter)?;
if *authority.key != AUTHORIZED_KEY {
return Err(ProgramError::InvalidAuthority);
}
Ok(())
}
- Missing system account check
// ❌ Bad: No system account validation
let token_program_id = next_account_info(account_info_iter)?;
// ✅ Good: Validate system account
if *token_program_id.key != spl_token::ID {
return Err(ProgramError::IncorrectProgramId);
}
- Missing check for lamports
// ❌ Bad: Reading account data without lamports check
let data = account.try_borrow_data()?;
// ✅ Good: Check lamports before reading
if account.try_borrow_lamports()? > 0 {
let data = account.try_borrow_data()?;
}
- Improper closing of the account
// ❌ Bad: No proper account closing
#[account(mut)]
pub account_to_close: Account<'info, MyAccount>
// ✅ Good: Proper account closing with rent collection
#[account(mut, close = recipient)]
pub account_to_close: Account<'info, MyAccount>
- Reinitialization of the same account using
init_if_needed
// ❌ Bad: Unsafe reinitialization
#[account(init_if_needed, ...)]
// ✅ Fixed version - remove init_if_needed
#[account(
init, // Requires explicit initialization
payer = payer,
space = 8 + 32,
constraint = !account.is_initialized
)]
- Sysvar not validated
- Remaining accounts not validated
- Bump seed canonicalization missing
- Integer overflow/underflow not checked
// ❌ Bad: Unchecked arithmetic
let balance = account.balance + amount;
// ✅ Good: Checked arithmetic
let balance = account.balance.checked_add(amount)
.ok_or(ProgramError::Overflow)?;
- Loss of precision in calculations
- Division by zero not handled
// ❌ Bad: Unchecked division
let result = total / divisor;
// ✅ Good: Check for zero before division
if divisor == 0 {
return Err(ProgramError::InvalidArgument);
}
let result = total / divisor;
- Error handling missing (unchecked Result types)
- Arbitrary CPI (Cross-Program Invocation) risks
- Type cosplay vulnerabilities
- Account data matching not verified
- Timely state reset missing
- Account data consistency not maintained
- Pyth oracle validation missing
-
Use
checked_add
,checked_sub
,checked_mul
,checked_div
for arithmetic operations -
Hardcode and validate system program addresses
-
Implement proper error handling with
?
operator -
Validate all account permissions before operations
-
Use typed accounts instead of AccountInfo
// ❌ Bad: Using raw AccountInfo
pub struct Instructions<'info> {
pub mint: AccountInfo<'info>
}
// ✅ Good: Using typed Account
pub struct Instructions<'info> {
pub mint: Account<'info, Mint>
}
- Validate PDA derivation
// ✅ Good: Validate PDA derivation
let (pda, bump) = Pubkey::find_program_address(
&[b"seed", authority.key.as_ref()],
program_id
);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
- Implement proper error types
// ✅ Good: Custom error types
#[error_code]
pub enum MyError {
#[msg("Invalid authority provided")]
InvalidAuthority,
#[msg("Account not initialized")]
NotInitialized,
}
// ❌ Bad: No sysvar validation
let clock = Clock::from_account_info(&sysvar_clock)?;
// ✅ Good: Validate sysvar address
#[account(address = sysvar::clock::ID)]
pub sysvar_clock: AccountInfo<'info>
- PDA sharing protection
// ✅ Good: PDA ownership check
#[account(
init,
payer = payer,
space = 8 + 32,
seeds = [b"vault", owner.key.as_ref()],
bump
)]
pub vault: Account<'info, Vault>,
#[account(constraint = authority.key == vault.owner)]
pub authority: Signer<'info>
// ❌ Old version
#[account(mut, constraint = account.get_lamports() >= Rent::get()?.minimum_balance(account.data_len()))]
// ✅ Fixed version
#[account(mut, constraint = account.to_account_info().lamports() >= Rent::get()?.minimum_balance(account.data_len()))]
pub account: Account<'info, MyData>
// ❌ Bad: No token program validation
pub token_program: AccountInfo<'info>
// ✅ Good: Validate token program
#[account(constraint = token_program.key == &spl_token::ID)]
pub token_program: Program<'info, Token>
// ✅ Good: Unique PDA seeds
let (pda, bump) = Pubkey::find_program_address(
&[b"unique_prefix", owner.key.as_ref(), token_mint.key.as_ref()],
program_id
);
// ✅ Good: Safe PDA lamport transfer
invoke_signed(
&system_instruction::transfer(
pda_account.key,
recipient.key,
lamports,
),
&[pda_account.clone(), recipient.clone(), system_program.clone()],
&[&[b"vault", owner.key.as_ref(), &[bump]]]
)?;
// ❌ Bad: Unchecked CPI
let cpi_ctx = CpiContext::new(
arbitrary_program.clone(),
accounts
);
// ✅ Good: Validate CPI program
#[account(constraint = cpi_program.key == &VALID_CPI_PROGRAM_ID)]
pub cpi_program: AccountInfo<'info>
// ❌ Bad: Lamports check
if account.lamports() > 0 { /*...*/ }
// ✅ Good: Data length check
if account.data_len() > 0 { /*...*/ }
// ✅ Good: Reinit protection
#[account(
init, // Requires explicit initialization
payer = payer,
space = 8 + 32,
constraint = !account.is_initialized
)]
pub account: Account<'info, MyData>,
#[account(constraint = signer.key == account.owner)]
pub signer: Signer<'info>
// ✅ Improved version with Anchor's automatic discriminator check
#[account]
pub account: Account<'info, MyAccount> // Anchor automatically checks discriminator