614 lines
14 KiB
Markdown
614 lines
14 KiB
Markdown
# Security Best Practices for Solana Development
|
|
|
|
Essential security principles and defensive programming patterns for building secure Solana programs with Anchor or native Rust.
|
|
|
|
> **Note:** This guide focuses on defensive programming during development. For comprehensive security audits, vulnerability analysis, and attack vectors, use the **`solana-security` skill**.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Security Mindset](#security-mindset)
|
|
2. [Core Security Rules](#core-security-rules)
|
|
3. [Account Validation](#account-validation)
|
|
4. [Arithmetic Safety](#arithmetic-safety)
|
|
5. [PDA Security](#pda-security)
|
|
6. [CPI Security](#cpi-security)
|
|
7. [Common Pitfalls](#common-pitfalls)
|
|
8. [Pre-Deployment Checklist](#pre-deployment-checklist)
|
|
|
|
---
|
|
|
|
## Security Mindset
|
|
|
|
### Think Like an Attacker
|
|
|
|
**Fundamental principle:** Attackers control everything they send to your program.
|
|
|
|
- ❌ Don't assume: "Users won't do that"
|
|
- ❌ Don't assume: "The client validates this"
|
|
- ❌ Don't assume: "This account must be correct"
|
|
- ✅ Do validate: Every account, every parameter, every assumption
|
|
|
|
### You Control Nothing
|
|
|
|
Once deployed, your program:
|
|
- Cannot control which accounts are passed in
|
|
- Cannot control instruction data
|
|
- Cannot control timing or ordering
|
|
- Cannot prevent malicious clients
|
|
|
|
**Your only control:** How your program validates and handles inputs.
|
|
|
|
---
|
|
|
|
## Core Security Rules
|
|
|
|
### Rule 1: Validate Every Account
|
|
|
|
**Always verify:**
|
|
|
|
**Anchor:**
|
|
```rust
|
|
#[derive(Accounts)]
|
|
pub struct SecureInstruction<'info> {
|
|
// ✅ Signer required
|
|
pub authority: Signer<'info>,
|
|
|
|
// ✅ Owner validation + relationship
|
|
#[account(
|
|
mut,
|
|
has_one = authority, // vault.authority == authority.key()
|
|
)]
|
|
pub vault: Account<'info, Vault>,
|
|
|
|
// ✅ Program ID validation
|
|
pub token_program: Program<'info, Token>,
|
|
}
|
|
```
|
|
|
|
**Native Rust:**
|
|
```rust
|
|
// ✅ Signer check
|
|
if !authority.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
|
|
// ✅ Owner check
|
|
if vault.owner != program_id {
|
|
return Err(ProgramError::IllegalOwner);
|
|
}
|
|
|
|
// ✅ Program ID check
|
|
if *token_program.key != spl_token::id() {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
```
|
|
|
|
### Rule 2: Use Checked Arithmetic
|
|
|
|
**Never use:**
|
|
- `+`, `-`, `*`, `/` operators directly
|
|
- `saturating_*` methods (hide errors)
|
|
- `unwrap()` or `expect()` on arithmetic
|
|
|
|
**Always use:**
|
|
```rust
|
|
// ✅ Checked operations
|
|
let total = balance
|
|
.checked_add(amount)
|
|
.ok_or(ErrorCode::Overflow)?;
|
|
|
|
let remaining = total
|
|
.checked_sub(withdrawal)
|
|
.ok_or(ErrorCode::InsufficientFunds)?;
|
|
|
|
let product = price
|
|
.checked_mul(quantity)
|
|
.ok_or(ErrorCode::Overflow)?;
|
|
|
|
let share = total
|
|
.checked_div(parts)
|
|
.ok_or(ErrorCode::DivisionByZero)?;
|
|
```
|
|
|
|
### Rule 3: Validate PDAs Properly
|
|
|
|
**Anchor:**
|
|
```rust
|
|
#[derive(Accounts)]
|
|
pub struct SecurePDA<'info> {
|
|
// ✅ Use canonical bump
|
|
#[account(
|
|
seeds = [b"vault", user.key().as_ref()],
|
|
bump, // Automatically validates canonical bump
|
|
)]
|
|
pub vault: Account<'info, Vault>,
|
|
}
|
|
```
|
|
|
|
**Native Rust:**
|
|
```rust
|
|
// ✅ Find canonical bump
|
|
let (expected_pda, bump) = Pubkey::find_program_address(
|
|
&[b"vault", user.key.as_ref()],
|
|
program_id,
|
|
);
|
|
|
|
// ✅ Validate PDA matches
|
|
if expected_pda != *vault.key {
|
|
return Err(ProgramError::InvalidSeeds);
|
|
}
|
|
|
|
// Store bump for future use with create_program_address
|
|
```
|
|
|
|
### Rule 4: Secure Cross-Program Invocations
|
|
|
|
**Anchor:**
|
|
```rust
|
|
// ✅ Program type validation
|
|
pub token_program: Program<'info, Token>,
|
|
|
|
// ✅ Use CpiContext
|
|
let cpi_ctx = CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
transfer_accounts,
|
|
);
|
|
|
|
token::transfer(cpi_ctx, amount)?;
|
|
```
|
|
|
|
**Native Rust:**
|
|
```rust
|
|
// ✅ Validate program ID before CPI
|
|
if *token_program.key != spl_token::id() {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
|
|
// ✅ Build instruction safely
|
|
let ix = spl_token::instruction::transfer(
|
|
token_program.key,
|
|
source.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
)?;
|
|
|
|
invoke(&ix, &[source, destination, authority, token_program])?;
|
|
```
|
|
|
|
### Rule 5: Handle Errors Gracefully
|
|
|
|
**Never:**
|
|
```rust
|
|
// ❌ Don't panic or unwrap
|
|
let value = some_operation().unwrap();
|
|
|
|
// ❌ Don't ignore errors
|
|
some_operation();
|
|
```
|
|
|
|
**Always:**
|
|
```rust
|
|
// ✅ Propagate errors
|
|
let value = some_operation()
|
|
.ok_or(ErrorCode::OperationFailed)?;
|
|
|
|
// ✅ Or handle explicitly
|
|
let value = match some_operation() {
|
|
Some(v) => v,
|
|
None => return Err(ErrorCode::OperationFailed.into()),
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Account Validation
|
|
|
|
### Essential Checks
|
|
|
|
For every account, verify:
|
|
|
|
1. **Signer** - Does this account need to sign?
|
|
2. **Owner** - Who owns this account? Is it our program?
|
|
3. **Writable** - Does this need `mut`?
|
|
4. **Type** - Is this the right account type?
|
|
5. **Relationships** - Do related accounts match?
|
|
|
|
### Validation Pattern
|
|
|
|
```rust
|
|
// Native Rust comprehensive validation
|
|
pub fn validate_account(
|
|
account: &AccountInfo,
|
|
expected_owner: &Pubkey,
|
|
must_be_signer: bool,
|
|
must_be_writable: bool,
|
|
) -> ProgramResult {
|
|
// Check signer
|
|
if must_be_signer && !account.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
|
|
// Check owner
|
|
if account.owner != expected_owner {
|
|
return Err(ProgramError::IllegalOwner);
|
|
}
|
|
|
|
// Check writable
|
|
if must_be_writable && !account.is_writable {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Arithmetic Safety
|
|
|
|
### Common Vulnerabilities
|
|
|
|
**Overflow example:**
|
|
```rust
|
|
// ❌ VULNERABLE: Can overflow
|
|
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
|
ctx.accounts.vault.balance = ctx.accounts.vault.balance + amount;
|
|
Ok(())
|
|
}
|
|
|
|
// If vault.balance = u64::MAX - 100 and amount = 200
|
|
// Result wraps to 99, losing 18.4 quintillion tokens!
|
|
```
|
|
|
|
**Fix:**
|
|
```rust
|
|
// ✅ SECURE: Checked arithmetic
|
|
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
|
ctx.accounts.vault.balance = ctx.accounts.vault.balance
|
|
.checked_add(amount)
|
|
.ok_or(ErrorCode::Overflow)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Precision Loss
|
|
|
|
**Multiply before divide:**
|
|
```rust
|
|
// ❌ WRONG: Loses precision
|
|
let fee = amount / 100; // 1.5% becomes 1%
|
|
|
|
// ✅ CORRECT: Multiply first
|
|
let fee = amount
|
|
.checked_mul(15)
|
|
.and_then(|v| v.checked_div(1000))
|
|
.ok_or(ErrorCode::Overflow)?; // Exact 1.5%
|
|
```
|
|
|
|
---
|
|
|
|
## PDA Security
|
|
|
|
### Use Canonical Bumps
|
|
|
|
**Always find the canonical bump:**
|
|
|
|
```rust
|
|
// ✅ Find canonical bump
|
|
let (pda, bump) = Pubkey::find_program_address(
|
|
&[b"vault", user.key.as_ref()],
|
|
program_id,
|
|
);
|
|
|
|
// Store bump in account for later use
|
|
vault.bump = bump;
|
|
```
|
|
|
|
**Never hardcode or accept bumps from clients:**
|
|
```rust
|
|
// ❌ VULNERABLE: Accepts any bump
|
|
#[derive(Accounts)]
|
|
pub struct BadPDA<'info> {
|
|
#[account(seeds = [b"vault"], bump = user_provided_bump)]
|
|
pub vault: Account<'info, Vault>,
|
|
}
|
|
```
|
|
|
|
### Unique Seeds
|
|
|
|
Ensure seeds create unique PDAs:
|
|
|
|
```rust
|
|
// ✅ GOOD: Unique per user
|
|
seeds = [b"vault", user.key().as_ref()]
|
|
|
|
// ❌ BAD: Same PDA for everyone
|
|
seeds = [b"vault"]
|
|
```
|
|
|
|
---
|
|
|
|
## CPI Security
|
|
|
|
### Validate Target Programs
|
|
|
|
**Never accept arbitrary program IDs:**
|
|
|
|
```rust
|
|
// ❌ VULNERABLE
|
|
pub fn bad_cpi(ctx: Context<BadCPI>) -> Result<()> {
|
|
// Attacker can pass any program!
|
|
let cpi_ctx = CpiContext::new(
|
|
ctx.accounts.any_program.to_account_info(),
|
|
accounts,
|
|
);
|
|
// ... make CPI
|
|
}
|
|
|
|
// ✅ SECURE
|
|
#[derive(Accounts)]
|
|
pub struct SecureCPI<'info> {
|
|
pub token_program: Program<'info, Token>, // Type-checked!
|
|
}
|
|
```
|
|
|
|
### Reload Accounts After CPIs
|
|
|
|
If a CPI might modify an account you're using:
|
|
|
|
```rust
|
|
// ✅ Reload account after external call
|
|
let balance_before = token_account.amount;
|
|
|
|
// Make CPI that might change the account
|
|
token::transfer(cpi_ctx, amount)?;
|
|
|
|
// Reload to get fresh data
|
|
token_account.reload()?;
|
|
|
|
let balance_after = token_account.amount;
|
|
```
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
### 1. init_if_needed (Anchor)
|
|
|
|
**Dangerous pattern:**
|
|
```rust
|
|
// ❌ Can be exploited
|
|
#[account(init_if_needed, payer = user, space = 8 + 32)]
|
|
pub config: Account<'info, Config>,
|
|
```
|
|
|
|
**Problem:** Attacker creates the account first with malicious data.
|
|
|
|
**Fix:**
|
|
```rust
|
|
// ✅ Use init or check if exists
|
|
#[account(init, payer = user, space = 8 + 32)]
|
|
pub config: Account<'info, Config>,
|
|
|
|
// Or explicitly check
|
|
if config.is_initialized {
|
|
return Err(ErrorCode::AlreadyInitialized.into());
|
|
}
|
|
```
|
|
|
|
### 2. Missing Signer Checks
|
|
|
|
```rust
|
|
// ❌ Anyone can withdraw!
|
|
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
|
|
ctx.accounts.vault.balance -= amount;
|
|
Ok(())
|
|
}
|
|
|
|
// ✅ Authority must sign
|
|
#[derive(Accounts)]
|
|
pub struct Withdraw<'info> {
|
|
#[account(mut, has_one = authority)]
|
|
pub vault: Account<'info, Vault>,
|
|
pub authority: Signer<'info>, // Required!
|
|
}
|
|
```
|
|
|
|
### 3. Account Confusion
|
|
|
|
```rust
|
|
// ❌ No validation - any accounts work!
|
|
pub struct Transfer<'info> {
|
|
pub from: Account<'info, TokenAccount>,
|
|
pub to: Account<'info, TokenAccount>,
|
|
}
|
|
|
|
// ✅ Validate relationships
|
|
pub struct Transfer<'info> {
|
|
#[account(
|
|
mut,
|
|
constraint = from.owner == authority.key(),
|
|
constraint = from.mint == to.mint,
|
|
)]
|
|
pub from: Account<'info, TokenAccount>,
|
|
|
|
#[account(mut)]
|
|
pub to: Account<'info, TokenAccount>,
|
|
|
|
pub authority: Signer<'info>,
|
|
}
|
|
```
|
|
|
|
### 4. Unchecked Account Types
|
|
|
|
```rust
|
|
// ❌ Uses raw AccountInfo - no type safety
|
|
pub fn bad(ctx: Context<Bad>) -> Result<()> {
|
|
let data = ctx.accounts.account.try_borrow_data()?;
|
|
// What if attacker passes wrong account type?
|
|
}
|
|
|
|
// ✅ Use typed Account
|
|
pub fn good(ctx: Context<Good>) -> Result<()> {
|
|
// Anchor verifies discriminator automatically
|
|
let vault = &ctx.accounts.vault;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Pre-Deployment Checklist
|
|
|
|
Before deploying to mainnet:
|
|
|
|
### Code Review
|
|
|
|
- [ ] All accounts validated (signer, owner, writable)
|
|
- [ ] All arithmetic uses `checked_*` methods
|
|
- [ ] All PDAs use canonical bumps
|
|
- [ ] All CPIs validate target programs
|
|
- [ ] No `unwrap()` or `expect()` in production code
|
|
- [ ] No `init_if_needed` without additional checks
|
|
- [ ] All error cases handled gracefully
|
|
|
|
### Testing
|
|
|
|
- [ ] Unit tests cover all instructions
|
|
- [ ] Integration tests cover instruction interactions
|
|
- [ ] Edge cases tested (zero amounts, max values, overflow)
|
|
- [ ] Error conditions tested (invalid accounts, unauthorized access)
|
|
- [ ] Fuzz testing with Trident (if possible)
|
|
|
|
### Security Audit
|
|
|
|
- [ ] Internal code review completed
|
|
- [ ] External security audit (recommended for >$100k TVL)
|
|
- [ ] Use `solana-security` skill for systematic review
|
|
- [ ] All critical/high severity findings resolved
|
|
- [ ] Medium findings assessed and documented
|
|
|
|
### Documentation
|
|
|
|
- [ ] Account structures documented
|
|
- [ ] Instruction requirements documented
|
|
- [ ] Known limitations documented
|
|
- [ ] Upgrade strategy documented
|
|
- [ ] Emergency procedures documented
|
|
|
|
### Deployment
|
|
|
|
- [ ] Tested on devnet extensively
|
|
- [ ] Tested on mainnet-beta with small amounts
|
|
- [ ] Upgrade authority secured (multisig recommended)
|
|
- [ ] Monitoring and alerts configured
|
|
- [ ] Emergency pause mechanism (if applicable)
|
|
|
|
---
|
|
|
|
## When to Use the Security Skill
|
|
|
|
Use the **`solana-security` skill** for:
|
|
|
|
- 🔍 **Comprehensive security audits** - Systematic review of entire codebase
|
|
- 🐛 **Vulnerability analysis** - Identifying exploit scenarios
|
|
- 📋 **Security checklists** - Category-by-category validation
|
|
- ⚠️ **Attack vectors** - Understanding how programs can be exploited
|
|
- 🛡️ **Framework-specific patterns** - Anchor vs native Rust security
|
|
- 📚 **Vulnerability databases** - Learning from past exploits
|
|
|
|
Use **this skill (solana-development)** for:
|
|
|
|
- 💻 **Building programs** - Implementation guidance
|
|
- ✅ **Defensive programming** - Secure coding patterns
|
|
- 🏗️ **Development workflows** - Testing, deployment, optimization
|
|
- 📖 **Framework learning** - Anchor and native Rust how-tos
|
|
|
|
---
|
|
|
|
## Quick Security Reference
|
|
|
|
### Anchor Security Checklist
|
|
|
|
```rust
|
|
#[derive(Accounts)]
|
|
pub struct Secure<'info> {
|
|
// ✅ Signer
|
|
pub authority: Signer<'info>,
|
|
|
|
// ✅ Validation + relationships
|
|
#[account(
|
|
mut,
|
|
has_one = authority,
|
|
seeds = [b"vault", user.key().as_ref()],
|
|
bump,
|
|
)]
|
|
pub vault: Account<'info, Vault>,
|
|
|
|
// ✅ Program validation
|
|
pub token_program: Program<'info, Token>,
|
|
}
|
|
|
|
pub fn secure_fn(ctx: Context<Secure>, amount: u64) -> Result<()> {
|
|
// ✅ Checked arithmetic
|
|
ctx.accounts.vault.balance = ctx.accounts.vault.balance
|
|
.checked_add(amount)
|
|
.ok_or(ErrorCode::Overflow)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Native Rust Security Checklist
|
|
|
|
```rust
|
|
pub fn secure_fn(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
amount: u64,
|
|
) -> ProgramResult {
|
|
let accounts = &mut accounts.iter();
|
|
let authority = next_account_info(accounts)?;
|
|
let vault = next_account_info(accounts)?;
|
|
|
|
// ✅ Signer check
|
|
if !authority.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
|
|
// ✅ Owner check
|
|
if vault.owner != program_id {
|
|
return Err(ProgramError::IllegalOwner);
|
|
}
|
|
|
|
// ✅ PDA validation
|
|
let (expected_pda, _) = Pubkey::find_program_address(
|
|
&[b"vault", authority.key.as_ref()],
|
|
program_id,
|
|
);
|
|
if *vault.key != expected_pda {
|
|
return Err(ProgramError::InvalidSeeds);
|
|
}
|
|
|
|
// ✅ Deserialize
|
|
let mut vault_data = Vault::try_from_slice(&vault.data.borrow())?;
|
|
|
|
// ✅ Checked arithmetic
|
|
vault_data.balance = vault_data.balance
|
|
.checked_add(amount)
|
|
.ok_or(ProgramError::ArithmeticOverflow)?;
|
|
|
|
// ✅ Serialize back
|
|
vault_data.serialize(&mut &mut vault.data.borrow_mut()[..])?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Remember
|
|
|
|
**Security is not optional.** Every line of code is a potential vulnerability. Validate everything, trust nothing, and when in doubt, use the `solana-security` skill for a comprehensive audit.
|