Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/security.md
2025-11-30 09:01:25 +08:00

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.