27 KiB
Solana Program Security & Validation
This reference provides comprehensive security guidance for native Rust Solana program development, covering validation patterns, common vulnerabilities, and defensive programming practices.
Table of Contents
- Security Mindset
- Core Validation Patterns
- Common Vulnerabilities
- Input Validation
- State Management Security
- Arithmetic Safety
- Re-entrancy Protection
- Security Checklist
Security Mindset
Think Like an Attacker
The fundamental principle of secure programming: ask "How do I break this?"
Presented at Breakpoint 2021 by Neodyme, this mindset shift is critical:
- Don't just test expected functionality - explore how it can be broken
- All programs can be exploited - the goal is to make it as difficult as possible
- You control nothing - once deployed, you can't control what transactions are sent
- Assume malicious input - every account, every parameter, every edge case
The Harsh Reality
┌─────────────────────────────────────────┐
│ Your Program (Deployed) │
├─────────────────────────────────────────┤
│ • No control over incoming transactions │
│ • No control over accounts passed in │
│ • No control over instruction data │
│ • No control over timing │
└─────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
Legitimate Malicious Buggy
User Attacker Client
Your only control: How your program handles inputs.
Security is Not Optional
Example Impact:
Without proper validation, a simple "update note" function becomes:
- ❌ Anyone can update anyone's notes
- ❌ Drain program funds
- ❌ Corrupt global state
- ❌ Brick the entire program
With validation:
- ✅ Only note author can update
- ✅ Funds are protected
- ✅ State remains consistent
- ✅ Program operates as intended
Core Validation Patterns
1. Signer Checks
Purpose: Verify that an account signed the transaction, authorizing the operation.
When Required:
- Transferring funds from an account
- Modifying user-specific data
- Any privileged operation
Pattern:
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program_error::ProgramError,
msg,
};
pub fn check_signer(account: &AccountInfo) -> ProgramResult {
if !account.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature);
}
Ok(())
}
Real-World Example:
pub fn update_user_profile(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_name: String,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let profile_pda = next_account_info(account_info_iter)?;
// CRITICAL: Verify user signed the transaction
if !user.is_signer {
msg!("User must sign to update profile");
return Err(ProgramError::MissingRequiredSignature);
}
// Validate PDA belongs to this user
let (expected_pda, _) = Pubkey::find_program_address(
&[b"profile", user.key.as_ref()],
program_id,
);
if expected_pda != *profile_pda.key {
msg!("Profile PDA doesn't match user");
return Err(ProgramError::InvalidAccountData);
}
// Safe to update
let mut profile = UserProfile::try_from_slice(&profile_pda.data.borrow())?;
profile.name = new_name;
profile.serialize(&mut &mut profile_pda.data.borrow_mut()[..])?;
Ok(())
}
2. Ownership Checks
Purpose: Verify an account is owned by the expected program.
When Required:
- Before reading/writing account data
- When validating PDAs
- Before performing any account-specific operations
Pattern:
pub fn check_ownership(
account: &AccountInfo,
expected_owner: &Pubkey,
) -> ProgramResult {
if account.owner != expected_owner {
msg!("Account owner mismatch");
return Err(ProgramError::IllegalOwner);
}
Ok(())
}
Common Use Cases:
// 1. Verify program owns its PDA
if note_pda.owner != program_id {
msg!("Note account not owned by this program");
return Err(ProgramError::IllegalOwner);
}
// 2. Verify account owned by System Program (user wallet)
use solana_program::system_program;
if wallet.owner != &system_program::ID {
msg!("Expected a system account (wallet)");
return Err(ProgramError::IllegalOwner);
}
// 3. Verify account owned by Token Program
use spl_token::ID as TOKEN_PROGRAM_ID;
if token_account.owner != &TOKEN_PROGRAM_ID {
msg!("Expected a token account");
return Err(ProgramError::IllegalOwner);
}
3. PDA Validation
Purpose: Ensure a provided PDA matches the expected derivation.
Critical for Security: Multiple bumps can derive different PDAs. Always use canonical bump.
Pattern:
pub fn validate_pda(
pda_account: &AccountInfo,
seeds: &[&[u8]],
program_id: &Pubkey,
) -> Result<u8, ProgramError> {
// Derive expected PDA with canonical bump
let (expected_pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);
// Validate match
if expected_pda != *pda_account.key {
msg!("Invalid PDA derivation");
return Err(ProgramError::InvalidSeeds);
}
Ok(bump_seed)
}
Complete Validation:
pub fn validate_user_vault(
program_id: &Pubkey,
user: &AccountInfo,
vault_pda: &AccountInfo,
) -> ProgramResult {
// 1. Derive expected PDA
let (expected_pda, _bump) = Pubkey::find_program_address(
&[b"vault", user.key.as_ref()],
program_id,
);
// 2. Validate address match
if expected_pda != *vault_pda.key {
msg!("Vault PDA seeds don't match");
return Err(ProgramError::InvalidSeeds);
}
// 3. Validate ownership
if vault_pda.owner != program_id {
msg!("Vault not owned by program");
return Err(ProgramError::IllegalOwner);
}
// 4. Validate initialization
let vault_data = VaultAccount::try_from_slice(&vault_pda.data.borrow())?;
if !vault_data.is_initialized {
msg!("Vault not initialized");
return Err(ProgramError::UninitializedAccount);
}
Ok(())
}
4. Initialization Checks
Purpose: Prevent re-initialization or use of uninitialized accounts.
Pattern: Discriminator Field
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountData {
pub is_initialized: bool,
// ... other fields
}
// On creation - ensure NOT initialized
if account_data.is_initialized {
msg!("Account already initialized");
return Err(ProgramError::AccountAlreadyInitialized);
}
account_data.is_initialized = true;
// On update - ensure IS initialized
if !account_data.is_initialized {
msg!("Account not initialized");
return Err(ProgramError::UninitializedAccount);
}
Advanced: Enum Discriminator
#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AccountState {
Uninitialized,
Initialized,
Frozen,
Closed,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct GameAccount {
pub state: AccountState,
pub player: Pubkey,
pub score: u64,
}
// Validation
let account = GameAccount::try_from_slice(&account_info.data.borrow())?;
match account.state {
AccountState::Uninitialized => {
msg!("Account not initialized");
return Err(ProgramError::UninitializedAccount);
}
AccountState::Frozen => {
msg!("Account is frozen");
return Err(ProgramError::InvalidAccountData);
}
AccountState::Closed => {
msg!("Account is closed");
return Err(ProgramError::InvalidAccountData);
}
AccountState::Initialized => {
// Proceed
}
}
5. Account Type Validation
Purpose: Ensure account contains the expected data structure.
Pattern: Type Discriminator
#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
#[repr(u8)]
pub enum AccountType {
Uninitialized = 0,
UserProfile = 1,
GameState = 2,
Leaderboard = 3,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct GenericAccount {
pub account_type: AccountType,
// ... rest of data varies by type
}
// Validation
pub fn validate_account_type(
account_info: &AccountInfo,
expected_type: AccountType,
) -> ProgramResult {
let account = GenericAccount::try_from_slice(&account_info.data.borrow())?;
if account.account_type != expected_type {
msg!("Unexpected account type");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
6. Writable Validation
Purpose: Ensure accounts that need modification are marked writable.
Pattern:
pub fn check_writable(account: &AccountInfo) -> ProgramResult {
if !account.is_writable {
msg!("Account must be writable");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
Note: Runtime enforces this, but explicit checks improve clarity and error messages.
Common Vulnerabilities
1. Missing Signer Check
Vulnerability:
// ❌ VULNERABLE - no signer check
pub fn withdraw_funds(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
// Anyone can call this to withdraw anyone's funds!
**user.lamports.borrow_mut() += amount;
**vault.lamports.borrow_mut() -= amount;
Ok(())
}
Exploit:
Attacker creates transaction:
- Passes victim's account as user
- Drains vault to victim's account
- Profits by intercepting the transaction or social engineering
Fix:
// ✅ SECURE - with signer check
pub fn withdraw_funds(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let user = &accounts[0];
let vault = &accounts[1];
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**user.lamports.borrow_mut() += amount;
**vault.lamports.borrow_mut() -= amount;
Ok(())
}
2. Missing Ownership Check
Vulnerability:
// ❌ VULNERABLE - no ownership check
pub fn update_score(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_score: u64,
) -> ProgramResult {
let player_account = &accounts[0];
// Could be ANY account with matching data structure!
let mut player = PlayerData::try_from_slice(&player_account.data.borrow())?;
player.score = new_score;
player.serialize(&mut &mut player_account.data.borrow_mut()[..])?;
Ok(())
}
Exploit:
Attacker creates a fake account:
- Owned by attacker's program
- Has same data structure
- Passes it to victim program
- Victim program modifies attacker's account!
Fix:
// ✅ SECURE - with ownership check
pub fn update_score(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_score: u64,
) -> ProgramResult {
let player_account = &accounts[0];
// Verify ownership
if player_account.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
let mut player = PlayerData::try_from_slice(&player_account.data.borrow())?;
player.score = new_score;
player.serialize(&mut &mut player_account.data.borrow_mut()[..])?;
Ok(())
}
3. PDA Substitution Attack
Vulnerability:
// ❌ VULNERABLE - accepts any PDA
pub fn claim_reward(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let reward_pda = &accounts[1];
// No PDA validation!
let mut reward = RewardData::try_from_slice(&reward_pda.data.borrow())?;
reward.claimed = true;
reward.serialize(&mut &mut reward_pda.data.borrow_mut()[..])?;
Ok(())
}
Exploit:
Attacker passes someone else's reward PDA:
- Creates transaction with victim's reward PDA
- Claims victim's rewards
- Victim loses rewards
Fix:
// ✅ SECURE - validates PDA derivation
pub fn claim_reward(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let reward_pda = &accounts[1];
// Validate PDA belongs to this user
let (expected_pda, _) = Pubkey::find_program_address(
&[b"reward", user.key.as_ref()],
program_id,
);
if expected_pda != *reward_pda.key {
return Err(ProgramError::InvalidSeeds);
}
let mut reward = RewardData::try_from_slice(&reward_pda.data.borrow())?;
reward.claimed = true;
reward.serialize(&mut &mut reward_pda.data.borrow_mut()[..])?;
Ok(())
}
4. Non-Canonical Bump
Vulnerability:
// ❌ VULNERABLE - accepts user-provided bump
pub fn update_data(
program_id: &Pubkey,
accounts: &[AccountInfo],
bump: u8, // User provides bump!
) -> ProgramResult {
let user = &accounts[0];
let data_pda = &accounts[1];
// Uses user's bump - could derive DIFFERENT PDA!
let derived_pda = Pubkey::create_program_address(
&[b"data", user.key.as_ref(), &[bump]],
program_id,
)?;
if derived_pda != *data_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Proceeds with potentially wrong PDA
// ...
}
Exploit:
Multiple bumps derive different valid PDAs:
- Canonical bump 254: User A's PDA
- Bump 253: User B's PDA (also valid!)
- Attacker uses bump 253 to access User B's data
Fix:
// ✅ SECURE - uses canonical bump only
pub fn update_data(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let data_pda = &accounts[1];
// Always use find_program_address (canonical bump)
let (expected_pda, _bump) = Pubkey::find_program_address(
&[b"data", user.key.as_ref()],
program_id,
);
if expected_pda != *data_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Safe - validated with canonical bump
// ...
}
5. Type Cosplay Attack
Vulnerability:
// ❌ VULNERABLE - assumes account type
pub fn admin_withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let admin_config = &accounts[0];
// No type validation!
let config = AdminConfig::try_from_slice(&admin_config.data.borrow())?;
// Proceeds assuming it's actually an AdminConfig
// ...
}
Exploit:
Attacker creates fake account:
- UserProfile with same memory layout as AdminConfig
- First field happens to match admin pubkey format
- Deserializes successfully as AdminConfig
- Attacker gains admin privileges!
Fix:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AdminConfig {
pub discriminator: [u8; 8], // Type identifier
pub admin: Pubkey,
// ... other fields
}
const ADMIN_CONFIG_DISCRIMINATOR: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
// ✅ SECURE - validates type
pub fn admin_withdraw(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let admin_config = &accounts[0];
let config = AdminConfig::try_from_slice(&admin_config.data.borrow())?;
// Validate discriminator
if config.discriminator != ADMIN_CONFIG_DISCRIMINATOR {
msg!("Invalid account type");
return Err(ProgramError::InvalidAccountData);
}
// Safe - type validated
// ...
}
6. Uninitialized Account Reuse
Vulnerability:
// ❌ VULNERABLE - no initialization check
pub fn update_balance(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let balance_account = &accounts[0];
let mut balance = BalanceData::try_from_slice(&balance_account.data.borrow())?;
// What if this account was never initialized?
// Default values could lead to undefined behavior
balance.amount += amount;
balance.serialize(&mut &mut balance_account.data.borrow_mut()[..])?;
Ok(())
}
Fix:
// ✅ SECURE - checks initialization
pub fn update_balance(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let balance_account = &accounts[0];
let mut balance = BalanceData::try_from_slice(&balance_account.data.borrow())?;
if !balance.is_initialized {
msg!("Account not initialized");
return Err(ProgramError::UninitializedAccount);
}
balance.amount += amount;
balance.serialize(&mut &mut balance_account.data.borrow_mut()[..])?;
Ok(())
}
Input Validation
Validate All Input Data
Never trust instruction data. Always validate constraints.
pub fn allocate_stat_points(
accounts: &[AccountInfo],
strength: u8,
agility: u8,
intelligence: u8,
) -> ProgramResult {
let character_account = &accounts[0];
let mut character = Character::try_from_slice(&character_account.data.borrow())?;
// 1. Validate individual stat caps
let new_strength = character.strength.checked_add(strength)
.ok_or(ProgramError::ArithmeticOverflow)?;
if new_strength > 100 {
msg!("Strength cannot exceed 100");
return Err(ProgramError::InvalidArgument);
}
// 2. Validate total points spent
let total_spent = (strength as u64)
.checked_add(agility as u64)
.and_then(|sum| sum.checked_add(intelligence as u64))
.ok_or(ProgramError::ArithmeticOverflow)?;
if total_spent > character.available_points {
msg!("Insufficient available points");
return Err(ProgramError::InsufficientFunds);
}
// 3. Safe to apply
character.strength = new_strength;
character.agility += agility;
character.intelligence += intelligence;
character.available_points -= total_spent;
character.serialize(&mut &mut character_account.data.borrow_mut()[..])?;
Ok(())
}
String Length Validation
pub fn set_username(
accounts: &[AccountInfo],
username: String,
) -> ProgramResult {
// Validate length
if username.len() < 3 {
msg!("Username too short (min 3 characters)");
return Err(ProgramError::InvalidArgument);
}
if username.len() > 20 {
msg!("Username too long (max 20 characters)");
return Err(ProgramError::InvalidArgument);
}
// Validate characters (alphanumeric only)
if !username.chars().all(|c| c.is_alphanumeric()) {
msg!("Username must be alphanumeric");
return Err(ProgramError::InvalidArgument);
}
// Safe to use
// ...
}
Enum Validation
#[derive(BorshDeserialize)]
#[repr(u8)]
pub enum Rarity {
Common = 0,
Uncommon = 1,
Rare = 2,
Epic = 3,
Legendary = 4,
}
pub fn create_item(
accounts: &[AccountInfo],
rarity_value: u8,
) -> ProgramResult {
// Validate enum range
if rarity_value > 4 {
msg!("Invalid rarity value");
return Err(ProgramError::InvalidArgument);
}
let rarity: Rarity = unsafe {
std::mem::transmute(rarity_value)
};
// Safe to use
// ...
}
State Management Security
Avoid Race Conditions
Problem: Multiple transactions modifying shared state.
Solution: Use account-level locking and atomic operations.
pub fn claim_limited_reward(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let user = &accounts[0];
let global_pool = &accounts[1];
let user_claim = &accounts[2];
// Load global state
let mut pool = RewardPool::try_from_slice(&global_pool.data.borrow())?;
// Check availability
if pool.claimed >= pool.total_rewards {
msg!("No rewards remaining");
return Err(ProgramError::InsufficientFunds);
}
// Check user hasn't claimed
let mut claim = UserClaim::try_from_slice(&user_claim.data.borrow())?;
if claim.has_claimed {
msg!("User already claimed");
return Err(ProgramError::Custom(0));
}
// Atomically update both accounts
pool.claimed += 1;
claim.has_claimed = true;
pool.serialize(&mut &mut global_pool.data.borrow_mut()[..])?;
claim.serialize(&mut &mut user_claim.data.borrow_mut()[..])?;
Ok(())
}
Note: Solana's account locking prevents true race conditions within a single transaction, but be aware of state assumptions across multiple transactions.
Prevent State Corruption
Always validate state transitions:
#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum GameState {
NotStarted,
InProgress,
Finished,
}
pub fn start_game(
accounts: &[AccountInfo],
) -> ProgramResult {
let game_account = &accounts[0];
let mut game = Game::try_from_slice(&game_account.data.borrow())?;
// Validate current state
if game.state != GameState::NotStarted {
msg!("Game already started or finished");
return Err(ProgramError::InvalidAccountData);
}
// Transition state
game.state = GameState::InProgress;
game.start_time = Clock::get()?.unix_timestamp;
game.serialize(&mut &mut game_account.data.borrow_mut()[..])?;
Ok(())
}
Arithmetic Safety
Always Use Checked Math
Rust default: Integer overflow/underflow panics in debug, wraps in release.
Solana requirement: Use checked operations to prevent wrapping.
// ❌ DANGEROUS - can overflow/underflow
let total = a + b;
let remaining = balance - withdrawal;
// ✅ SAFE - returns error on overflow/underflow
let total = a.checked_add(b)
.ok_or(ProgramError::ArithmeticOverflow)?;
let remaining = balance.checked_sub(withdrawal)
.ok_or(ProgramError::InsufficientFunds)?;
Common Checked Operations
// Addition
let sum = a.checked_add(b)
.ok_or(ProgramError::ArithmeticOverflow)?;
// Subtraction
let diff = a.checked_sub(b)
.ok_or(ProgramError::InsufficientFunds)?;
// Multiplication
let product = a.checked_mul(b)
.ok_or(ProgramError::ArithmeticOverflow)?;
// Division
let quotient = a.checked_div(b)
.ok_or(ProgramError::InvalidArgument)?; // b could be 0
// Power
let power = base.checked_pow(exponent)
.ok_or(ProgramError::ArithmeticOverflow)?;
Compound Operations
// Calculate: (a + b) * c / d
let result = a.checked_add(b)
.and_then(|sum| sum.checked_mul(c))
.and_then(|product| product.checked_div(d))
.ok_or(ProgramError::ArithmeticOverflow)?;
Precision Loss
Be careful with division:
// ❌ Loses precision
let fee = amount / 100; // 1.5% becomes 1%
// ✅ Better - multiply first, then divide
let fee = amount.checked_mul(15)
.and_then(|v| v.checked_div(1000))
.ok_or(ProgramError::ArithmeticOverflow)?;
Re-entrancy Protection
Solana's Built-in Protection
Good news: Solana provides strong protection against traditional re-entrancy:
- Account locking: Accounts are locked during transaction execution
- No concurrent modification: Same account can't be modified by multiple instructions simultaneously
- Atomic transactions: Either all instructions succeed or all fail
Residual Risks
Cross-program state assumptions:
// ❌ RISKY - state can change between checks
pub fn risky_operation(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let vault = &accounts[0];
let mut vault_data = VaultData::try_from_slice(&vault.data.borrow())?;
// Check balance
let balance = **vault.lamports.borrow();
if balance < 1000 {
return Err(ProgramError::InsufficientFunds);
}
// CPI that might modify vault
invoke(&some_instruction, accounts)?;
// Balance might have changed!
// Don't rely on previous check
**vault.lamports.borrow_mut() -= 1000; // Could underflow!
Ok(())
}
✅ Better:
pub fn safe_operation(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let vault = &accounts[0];
// CPI first
invoke(&some_instruction, accounts)?;
// Check and modify atomically
let balance = **vault.lamports.borrow();
let new_balance = balance.checked_sub(1000)
.ok_or(ProgramError::InsufficientFunds)?;
**vault.lamports.borrow_mut() = new_balance;
Ok(())
}
Security Checklist
Pre-Deployment Checklist
Account Validation:
- ✅ All signers verified with
is_signer - ✅ All account owners checked
- ✅ All PDAs validated with canonical bump
- ✅ All accounts checked for initialization
- ✅ Account types validated (discriminators)
- ✅ Writable accounts verified
Input Validation:
- ✅ All numeric inputs range-checked
- ✅ All string inputs length-limited
- ✅ All enum values validated
- ✅ All business logic constraints enforced
Arithmetic:
- ✅ All additions use
checked_add - ✅ All subtractions use
checked_sub - ✅ All multiplications use
checked_mul - ✅ All divisions check for zero
- ✅ No unsafe casting that could overflow
State Management:
- ✅ State transitions validated
- ✅ Initialization flags checked
- ✅ No assumptions across CPI boundaries
- ✅ Atomicity maintained
Error Handling:
- ✅ All errors properly propagated
- ✅ Meaningful error messages
- ✅ No silent failures
- ✅ Proper cleanup on errors
Testing Checklist
Security Testing:
- ✅ Test with missing signers
- ✅ Test with wrong account owners
- ✅ Test with wrong PDAs (non-canonical bumps)
- ✅ Test with uninitialized accounts
- ✅ Test with re-initialized accounts
- ✅ Test integer overflow/underflow
- ✅ Test boundary conditions
- ✅ Test with maximum values
- ✅ Test with malicious input
Fuzzing:
- ✅ Random account combinations
- ✅ Random instruction data
- ✅ Random ordering
- ✅ Edge case values
Summary
Core Security Principles:
- Validate Everything - Assume all inputs are malicious
- Fail Fast - Return errors immediately when validation fails
- Use Checked Math - Prevent integer overflow/underflow
- Think Like an Attacker - Ask "How do I break this?"
- Test Malicious Cases - Don't just test happy paths
The Three Pillars of Account Security:
// 1. Signer Check
if !account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// 2. Ownership Check
if account.owner != expected_owner {
return Err(ProgramError::IllegalOwner);
}
// 3. PDA Validation (if applicable)
let (expected_pda, _) = Pubkey::find_program_address(&seeds, program_id);
if expected_pda != *account.key {
return Err(ProgramError::InvalidSeeds);
}
Remember: Once deployed, you have no control over what transactions are sent to your program. Your only defense is rigorous validation.
Security is not a feature—it's a requirement.