Initial commit
This commit is contained in:
1150
skills/solana-security/references/anchor-security.md
Normal file
1150
skills/solana-security/references/anchor-security.md
Normal file
File diff suppressed because it is too large
Load Diff
386
skills/solana-security/references/caveats.md
Normal file
386
skills/solana-security/references/caveats.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Important Caveats
|
||||
|
||||
Critical limitations, quirks, and gotchas in Solana and Anchor development that every security reviewer must know.
|
||||
|
||||
## Anchor Framework Limitations
|
||||
|
||||
### 1. `init_if_needed` Re-initialization Risk
|
||||
|
||||
```rust
|
||||
// Dangerous: Can bypass initialization logic
|
||||
#[account(init_if_needed, payer = user, space = ...)]
|
||||
pub user_account: Account<'info, UserAccount>,
|
||||
```
|
||||
|
||||
**Issue:** If account already exists, initialization is skipped entirely. Existing malicious or inconsistent data is not validated.
|
||||
|
||||
**When to use:** Only when you explicitly validate existing accounts in instruction logic.
|
||||
|
||||
### 2. `AccountLoader` Missing Discriminator Check
|
||||
|
||||
```rust
|
||||
// Does NOT validate discriminator by default!
|
||||
#[account(mut)]
|
||||
pub user: AccountLoader<'info, User>,
|
||||
```
|
||||
|
||||
**Issue:** `AccountLoader` is for zero-copy accounts and doesn't check the account discriminator automatically. Enables type cosplay attacks.
|
||||
|
||||
**Solution:** Use `Account<'info, T>` when possible, or add manual discriminator check.
|
||||
|
||||
### 3. `close` Constraint Ordering
|
||||
|
||||
```rust
|
||||
// ❌ Wrong: close must be last
|
||||
#[account(
|
||||
close = receiver,
|
||||
mut,
|
||||
has_one = authority
|
||||
)]
|
||||
|
||||
// ✅ Correct: close is last
|
||||
#[account(
|
||||
mut,
|
||||
has_one = authority,
|
||||
close = receiver
|
||||
)]
|
||||
```
|
||||
|
||||
**Issue:** Anchor processes constraints in order. If `close` isn't last, subsequent constraints may check zeroed account.
|
||||
|
||||
### 4. Space Calculation Errors Are Permanent
|
||||
|
||||
```rust
|
||||
// If this space is wrong, account is unusable!
|
||||
#[account(
|
||||
init,
|
||||
payer = user,
|
||||
space = 8 + 32 // Too small = can't deserialize later!
|
||||
)]
|
||||
pub user_account: Account<'info, UserAccount>,
|
||||
```
|
||||
|
||||
**Issue:** Once initialized, account size is fixed. Too small = deserialization fails. Too large = wasted rent.
|
||||
|
||||
**Solution:** Always use `InitSpace` derive macro:
|
||||
```rust
|
||||
#[account]
|
||||
#[derive(InitSpace)]
|
||||
pub struct UserAccount {
|
||||
pub authority: Pubkey,
|
||||
#[max_len(100)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// Then use:
|
||||
space = 8 + UserAccount::INIT_SPACE
|
||||
```
|
||||
|
||||
### 5. `constraint` Expression Limitations
|
||||
|
||||
```rust
|
||||
// constraint expressions can't call functions that return Results!
|
||||
#[account(
|
||||
constraint = some_validation(account.value)? @ ErrorCode::Invalid // Compile error!
|
||||
)]
|
||||
```
|
||||
|
||||
**Issue:** Constraint expressions must be simple boolean checks. Cannot use `?` operator.
|
||||
|
||||
**Solution:** Validate in instruction body for complex checks.
|
||||
|
||||
## Solana Runtime Quirks
|
||||
|
||||
### 1. Account Data Persists After Zeroing Lamports
|
||||
|
||||
```rust
|
||||
// Within same transaction:
|
||||
**account.lamports.borrow_mut() = 0;
|
||||
let data = account.try_borrow_data()?; // Still readable!
|
||||
```
|
||||
|
||||
**Issue:** Account data remains accessible within the transaction even after lamports are zeroed. Only garbage collected after transaction completes.
|
||||
|
||||
**Implication:** Always check lamports before reading account data.
|
||||
|
||||
### 2. Non-Canonical PDA Bumps
|
||||
|
||||
```rust
|
||||
// Multiple PDAs possible with different bumps!
|
||||
let (pda_255, bump_255) = Pubkey::find_program_address(seeds, program_id); // bump = 255
|
||||
let (pda_254, bump_254) = Pubkey::create_program_address(&[seeds, &[254]], program_id); // Also valid!
|
||||
```
|
||||
|
||||
**Issue:** Same seeds can derive multiple PDAs with different bumps. Creates confusion and potential exploits.
|
||||
|
||||
**Solution:** Always use canonical bump (255 counting down to first valid). Anchor's `bump` constraint enforces this.
|
||||
|
||||
### 3. Compute Budget Limits
|
||||
|
||||
| Network | Base Compute Units | With Optimization |
|
||||
|---------|-------------------|-------------------|
|
||||
| Mainnet | 200,000 | Up to 1,400,000 (with request) |
|
||||
| Devnet | 200,000 | Up to 1,400,000 |
|
||||
|
||||
**Issue:** Complex programs can exceed compute budget, causing transaction failure.
|
||||
|
||||
**Optimization strategies:**
|
||||
- Minimize CPIs (each costs ~1000 CU)
|
||||
- Use `AccountLoader` for large accounts
|
||||
- Avoid loops with variable length
|
||||
- Request higher compute budget: `ComputeBudgetProgram::set_compute_unit_limit()`
|
||||
|
||||
### 4. Transaction Size Limit
|
||||
|
||||
**Hard limit:** ~1232 bytes for transaction
|
||||
|
||||
**Implications:**
|
||||
- Limits number of accounts (~35-40 accounts typical max)
|
||||
- Large instructions need Account Compression or chunking
|
||||
- Can't pass large data directly in instruction
|
||||
|
||||
**Solutions:**
|
||||
- Use PDAs to store large data
|
||||
- Break operations into multiple transactions
|
||||
- Use lookup tables for frequent accounts
|
||||
|
||||
### 5. Account Snapshot Loading
|
||||
|
||||
```rust
|
||||
let balance_before = ctx.accounts.vault.balance;
|
||||
// CPI happens here
|
||||
// balance_before is STALE - account was loaded before CPI
|
||||
```
|
||||
|
||||
**Issue:** Accounts are loaded as snapshots at transaction start. Modifications during transaction (via CPIs) don't update the loaded data.
|
||||
|
||||
**Solution:** Call `.reload()` after any CPI that might modify the account.
|
||||
|
||||
## Token Program Gotchas
|
||||
|
||||
### 1. ATA Addresses Are Deterministic But Not Guaranteed
|
||||
|
||||
```rust
|
||||
let ata = get_associated_token_address(&owner, &mint);
|
||||
// ata address is deterministic but account might not exist!
|
||||
```
|
||||
|
||||
**Issue:** ATA address can be calculated but account may not be initialized.
|
||||
|
||||
**Solution:** Check account exists and is initialized before use, or use `init_if_needed` with proper validation.
|
||||
|
||||
### 2. Delegates Don't Automatically Reset
|
||||
|
||||
```rust
|
||||
// After transfer of ownership:
|
||||
token_account.owner = new_owner;
|
||||
// BUT: delegate and delegated_amount are NOT reset!
|
||||
```
|
||||
|
||||
**Issue:** Changing owner doesn't clear delegate/close authority. Old delegate can still spend.
|
||||
|
||||
**Solution:** Explicitly reset authorities when changing ownership:
|
||||
```rust
|
||||
account.delegate = COption::None;
|
||||
account.delegated_amount = 0;
|
||||
if account.is_native() {
|
||||
account.close_authority = COption::None;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Token-2022 Extension Rent
|
||||
|
||||
**Issue:** Each extension adds rent cost. Account size varies by extensions enabled.
|
||||
|
||||
**Extensions and their sizes:**
|
||||
- Transfer Fee: ~83 bytes
|
||||
- Transfer Hook: ~107 bytes
|
||||
- Permanent Delegate: ~36 bytes
|
||||
- Interest Bearing: ~40 bytes
|
||||
|
||||
**Solution:** Calculate rent based on all enabled extensions.
|
||||
|
||||
### 4. Token-2022 Transfer Hooks Can Be Malicious
|
||||
|
||||
```rust
|
||||
// Transfer hook can call arbitrary program!
|
||||
pub struct TransferHookAccount {
|
||||
pub program_id: Pubkey, // Could be malicious
|
||||
}
|
||||
```
|
||||
|
||||
**Issue:** Transfer hook extensions allow calling external program during transfers. Malicious hook can fail transaction or drain funds.
|
||||
|
||||
**Solution:**
|
||||
- Validate transfer hook program if accepting specific tokens
|
||||
- Consider disallowing tokens with transfer hooks
|
||||
- Use Anchor's `TransferChecked` instruction
|
||||
|
||||
## Testing Blind Spots
|
||||
|
||||
### 1. Concurrent Transaction Ordering
|
||||
|
||||
**Issue:** Tests typically run transactions sequentially. In production, concurrent transactions can interleave in unexpected ways.
|
||||
|
||||
**Vulnerability example:**
|
||||
```rust
|
||||
// Transaction 1: Check balance = 100
|
||||
// Transaction 2: Withdraw 80 (balance now 20)
|
||||
// Transaction 1: Withdraw 80 (uses stale check, balance now -60!)
|
||||
```
|
||||
|
||||
**Mitigation:**
|
||||
- Use atomic operations
|
||||
- Reload accounts before critical operations
|
||||
- Design for idempotency
|
||||
|
||||
### 2. Account Rent Reclaim Attacks
|
||||
|
||||
**Issue:** When account rent falls below minimum, validator can reclaim the account. Tests don't simulate this.
|
||||
|
||||
**Solution:** Ensure all accounts are rent-exempt (2+ years of rent).
|
||||
|
||||
### 3. Sysvar Manipulation in Tests
|
||||
|
||||
```rust
|
||||
// In tests, you can set arbitrary clock values
|
||||
ctx.accounts.clock = Clock { unix_timestamp: attacker_value, ... };
|
||||
```
|
||||
|
||||
**Issue:** Tests may not catch reliance on tamper-resistant sysvars.
|
||||
|
||||
**Solution:** In production, always load sysvars from official sysvar accounts:
|
||||
```rust
|
||||
pub clock: Sysvar<'info, Clock>, // Validated address
|
||||
```
|
||||
|
||||
### 4. Devnet vs Mainnet Differences
|
||||
|
||||
| Aspect | Devnet | Mainnet |
|
||||
|--------|--------|---------|
|
||||
| Oracle prices | Often stale/fake | Real-time |
|
||||
| Program versions | May differ | Stable versions |
|
||||
| Compute limits | More lenient | Strict |
|
||||
| Congestion | Minimal | Can be high |
|
||||
| Token availability | Test tokens | Real value |
|
||||
|
||||
**Issue:** Programs tested only on devnet may fail on mainnet.
|
||||
|
||||
**Solution:** Test on mainnet-fork or mainnet with small amounts before full deployment.
|
||||
|
||||
## Rust-Specific Gotchas
|
||||
|
||||
### 1. `unwrap()` Panics
|
||||
|
||||
```rust
|
||||
// Panics kill the entire transaction!
|
||||
let value = some_option.unwrap(); // ❌ Never do this
|
||||
```
|
||||
|
||||
**Solution:** Always use proper error handling:
|
||||
```rust
|
||||
let value = some_option.ok_or(ErrorCode::MissingValue)?;
|
||||
```
|
||||
|
||||
### 2. Integer Division Truncation
|
||||
|
||||
```rust
|
||||
let result = 5 / 2; // result = 2, not 2.5!
|
||||
```
|
||||
|
||||
**Issue:** Integer division truncates, potentially causing precision loss in financial calculations.
|
||||
|
||||
**Solution:** Use `Decimal` type for precise calculations, or multiply before divide:
|
||||
```rust
|
||||
let result = (5 * PRECISION) / 2 / PRECISION;
|
||||
```
|
||||
|
||||
### 3. Overflow in Debug vs Release
|
||||
|
||||
```rust
|
||||
// Debug mode: panics on overflow
|
||||
// Release mode: wraps silently!
|
||||
let x: u8 = 255;
|
||||
let y = x + 1; // Debug: panic, Release: y = 0
|
||||
```
|
||||
|
||||
**Solution:** Always use `checked_*` methods - they work same in debug and release.
|
||||
|
||||
## Cross-Program Invocation (CPI) Gotchas
|
||||
|
||||
### 1. CPI Success Doesn't Guarantee Correct State
|
||||
|
||||
```rust
|
||||
// CPI returns success but state may be unexpected
|
||||
invoke(&transfer_instruction, &accounts)?;
|
||||
// Transfer succeeded but amount might be different due to fees!
|
||||
```
|
||||
|
||||
**Solution:** Reload and validate account state after CPI.
|
||||
|
||||
### 2. Signer Seeds Must Be Exact
|
||||
|
||||
```rust
|
||||
// Seeds for signing must match PDA derivation exactly
|
||||
let seeds = &[
|
||||
b"vault",
|
||||
user.key().as_ref(),
|
||||
&[bump], // Must be same bump used to derive PDA
|
||||
];
|
||||
|
||||
invoke_signed(&instruction, &accounts, &[seeds])?;
|
||||
```
|
||||
|
||||
**Issue:** Wrong seeds = "signature verification failed" error.
|
||||
|
||||
### 3. CPI Depth Limit
|
||||
|
||||
**Limit:** 4 levels of CPI depth
|
||||
|
||||
**Issue:** Program A → Program B → Program C → Program D → Program E (fails!)
|
||||
|
||||
**Solution:** Design programs to minimize CPI depth.
|
||||
|
||||
## Common Misunderstandings
|
||||
|
||||
### 1. "Anchor Prevents All Security Issues"
|
||||
|
||||
**False:** Anchor prevents some common issues (missing discriminators, wrong account types) but doesn't validate business logic, arithmetic, or authorization.
|
||||
|
||||
### 2. "Devnet Testing Is Sufficient"
|
||||
|
||||
**False:** Mainnet has different compute limits, real oracle data, congestion, and MEV considerations.
|
||||
|
||||
### 3. "One Audit Makes Code Secure"
|
||||
|
||||
**False:** Audits find issues in a snapshot. Code changes after audit reintroduce risk. Need continuous security review.
|
||||
|
||||
### 4. "`checked_*` Methods Are Slower"
|
||||
|
||||
**False:** Rust compiler optimizes these similarly to unchecked arithmetic. Always use checked methods.
|
||||
|
||||
### 5. "PDAs Can't Sign"
|
||||
|
||||
**True for external transactions, false for CPIs:** PDAs can sign CPIs using `invoke_signed` but can't sign transactions directly.
|
||||
|
||||
## Version-Specific Issues
|
||||
|
||||
### Anchor Version Compatibility
|
||||
|
||||
- **< 0.28**: No `InitSpace` derive, manual space calculation error-prone
|
||||
- **< 0.29**: Different constraint syntax
|
||||
- **0.30+**: Breaking changes in error handling and account initialization
|
||||
|
||||
**Solution:** Check `Cargo.toml` for version and consult [Anchor Changelog](https://github.com/coral-xyz/anchor/blob/master/CHANGELOG.md).
|
||||
|
||||
### Solana Version Differences
|
||||
|
||||
- **Pre-1.14**: Different fee structure
|
||||
- **Pre-1.16**: No Address Lookup Tables
|
||||
- **Pre-1.17**: No Token-2022
|
||||
|
||||
**Solution:** Verify target Solana version matches deployment network.
|
||||
|
||||
---
|
||||
|
||||
**Key Takeaway:** Many "obvious" assumptions about blockchain behavior don't hold in Solana. Always validate against actual runtime behavior, not assumptions from other chains.
|
||||
1133
skills/solana-security/references/native-security.md
Normal file
1133
skills/solana-security/references/native-security.md
Normal file
File diff suppressed because it is too large
Load Diff
177
skills/solana-security/references/resources.md
Normal file
177
skills/solana-security/references/resources.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Resources
|
||||
|
||||
Comprehensive collection of official documentation, security guides, audit reports, and learning materials for Solana development and security.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
### Solana Core
|
||||
- [Solana Docs](https://solana.com/docs/) - Official Solana documentation
|
||||
- [Solana Cookbook](https://solana.com/developers/cookbook) - Recipes for common Solana tasks
|
||||
- [Solana Courses](https://solana.com/developers/courses/) - Official learning paths
|
||||
- [Program Examples](https://github.com/solana-developers/program-examples) - Multi-framework examples
|
||||
- [Developer Bootcamp 2024](https://github.com/solana-developers/developer-bootcamp-2024)
|
||||
|
||||
### Anchor Framework
|
||||
- [Anchor Docs](https://www.anchor-lang.com/docs) - Official Anchor documentation
|
||||
- [Anchor Book](https://book.anchor-lang.com/) - Comprehensive Anchor guide
|
||||
- [Anchor by Example](https://examples.anchor-lang.com/) - Example programs
|
||||
- [Anchor Lang Docs](https://docs.rs/anchor-lang) - API documentation
|
||||
- [Anchor SPL Docs](https://docs.rs/anchor-spl) - SPL integration helpers
|
||||
|
||||
### SPL Programs
|
||||
- [SPL Documentation](https://spl.solana.com/) - Solana Program Library docs
|
||||
- [Token Program](https://github.com/solana-program/token) - SPL Token source
|
||||
- [Token-2022](https://github.com/solana-program/token-2022) - Next-gen token program
|
||||
- [Associated Token Account](https://github.com/solana-program/associated-token-account)
|
||||
- [Token Metadata](https://github.com/solana-program/token-metadata)
|
||||
- [Metaplex Token Metadata](https://github.com/metaplex-foundation/mpl-token-metadata)
|
||||
|
||||
## Security Resources
|
||||
|
||||
### Curated Security Lists
|
||||
- [Awesome Solana Security (0xMacro)](https://github.com/0xMacro/awesome-solana-security) - **Actively maintained**, comprehensive resource list
|
||||
- [Rektoff Security Roadmap](https://github.com/Rektoff/Security-Roadmap-for-Solana-applications) - Full lifecycle security strategy
|
||||
- [SlowMist Best Practices](https://github.com/slowmist/solana-smart-contract-security-best-practices) - Common pitfalls with examples
|
||||
- [Ackee Solana Handbook](https://ackee.xyz/solana/book/latest/) - Comprehensive development guide
|
||||
|
||||
### Security Guides & Articles
|
||||
- [Helius Security Guide](https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security) - Common vulnerabilities explained
|
||||
- [Neodyme Breakpoint Workshop](https://github.com/neodyme-labs/neodyme-breakpoint-workshop) - Hands-on security training
|
||||
- [Solana Security Course](https://solana.com/developers/courses/program-security) - Official security course
|
||||
- [Asymmetric Research CPI Vulnerabilities](https://blog.asymmetric.re/invocation-security-navigating-vulnerabilities-in-solana-cpis/)
|
||||
- [Ottersec Lamport Transfers](https://osec.io/blog/2025-05-14-king-of-the-sol) - SOL transfer vulnerabilities
|
||||
- [Infect3d Auditing Essentials](https://www.infect3d.xyz/blog/solana-quick-start)
|
||||
|
||||
### Vulnerability Collections
|
||||
- [Urataps Audit Examples](https://github.com/urataps/solana-audit-examples) - Programs with vulnerabilities
|
||||
- [ImmuneBytes Attack Vectors](https://github.com/ImmuneBytes-Security-Audit/Blockchain-Attack-Vectors/tree/main/Solana%20Attack%20Vectors)
|
||||
- [Exvul Security Guide](https://exvul.com/rust-smart-contract-security-guide-in-solana/)
|
||||
- [Nirlin Advanced Vulnerabilities](https://substack.com/inbox/post/164534668)
|
||||
|
||||
### Video Tutorials
|
||||
- [Zigtur Security Walkthrough](https://www.youtube.com/watch?v=xd6qfY-GDYY)
|
||||
- [M4rio Security Walkthrough](https://www.youtube.com/watch?v=q4z8tIi43lg)
|
||||
|
||||
### Token-2022 Security
|
||||
- [Offside Token-2022 Part 1](https://blog.offside.io/p/token-2022-security-best-practices-part-1)
|
||||
- [Offside Token-2022 Part 2](https://blog.offside.io/p/token-2022-security-best-practices-part-2)
|
||||
- [Neodyme Token-2022 Security](https://neodyme.io/en/blog/token-2022)
|
||||
|
||||
### Deep Dives & Research
|
||||
- [r0bre's 100 Daily Solana Tips](https://accretionxyz.substack.com/p/r0bres-100-daily-solana-tips)
|
||||
- [Accretion Hidden IDL Instructions](https://accretionxyz.substack.com/p/hidden-idl-instructions-and-how-to)
|
||||
- [Farouk ELALEM Under the Hood](https://ubermensch.blog/under-the-hood-of-solana-program-execution-from-rust-code-to-sbf-bytecode)
|
||||
- [Lucrative_Panda Security History](https://medium.com/@lucrativepanda/a-comprehensive-analysis-of-solanas-security-history-all-incidents-impacts-and-evolution-up-to-1b1564c7ddfe)
|
||||
|
||||
## Essential Codebases to Study
|
||||
|
||||
Study these production codebases to learn security patterns:
|
||||
|
||||
### Framework & Core Programs
|
||||
- [Anchor Framework](https://github.com/solana-foundation/anchor) - The framework itself
|
||||
- [Solana System Program](https://github.com/solana-program/system)
|
||||
- [SPL Token Program](https://github.com/solana-program/token)
|
||||
- [Token-2022](https://github.com/solana-program/token-2022)
|
||||
|
||||
### Production Protocols
|
||||
- [Raydium AMM](https://github.com/raydium-io/raydium-cp-swap) - DEX protocol
|
||||
- [Kamino Lending](https://github.com/Kamino-Finance/klend) - Lending protocol
|
||||
- [Squads Multisig](https://github.com/Squads-Protocol/v4) - Multisig protocol
|
||||
|
||||
## Audit Reports
|
||||
|
||||
Study real security audits to learn from actual vulnerabilities:
|
||||
|
||||
### Code4rena
|
||||
- [Pump Science](https://code4rena.com/reports/2025-01-pump-science) - 2 High, 3 Medium
|
||||
|
||||
### Sherlock
|
||||
- [Orderly](https://audits.sherlock.xyz/contests/524/report) - 2 High, 1 Medium
|
||||
- [WOOFi](https://audits.sherlock.xyz/contests/535/report) - 2 High, 3 Medium
|
||||
|
||||
### Cantina
|
||||
Contact `0xmorph` in Cantina Discord for read access:
|
||||
- [Grass](https://cantina.xyz/competitions/3211ee0d-133f-43a0-837e-8dc1ecfaa424) - 13 High, 6 Medium
|
||||
- [Olas](https://cantina.xyz/competitions/829164bf-7fba-4b84-a6b8-76652205bd97) - 2 High, 3 Medium
|
||||
- [Tensor](https://cantina.xyz/competitions/21787352-de2c-4a77-af09-cc0a250d1f04) - 5 High, 10 Medium
|
||||
- [ZetaChain](https://cantina.xyz/competitions/80a33cf0-ad69-4163-a269-d27756aacb5e) - 6 High, 27 Medium
|
||||
- [Inclusive Finance](https://cantina.xyz/competitions/3eff5a8f-b73a-4cfe-8c54-546b475548f0) - 45 High, 25 Medium
|
||||
- [Reserve Index](https://cantina.xyz/code/8b94becd-54e7-41cd-88e6-caae7becc76a) - 10 High, 11 Medium
|
||||
|
||||
## Learning Paths
|
||||
|
||||
### For EVM Developers
|
||||
- [RareSkills Solana Course](https://www.rareskills.io/solana-tutorial) - Ethereum to Solana
|
||||
- [0xkowloon Anchor for EVM](https://0xkowloon.gitbook.io/anchor-for-evm-developers)
|
||||
|
||||
### For Rust Learners
|
||||
- [Rust Book](https://doc.rust-lang.org/book/)
|
||||
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/index.html)
|
||||
|
||||
### Native Rust (Non-Anchor)
|
||||
- [Solana Native Rust Docs](https://solana.com/docs/programs/rust)
|
||||
- [Native Development Course](https://solana.com/developers/courses/native-onchain-development)
|
||||
|
||||
### Blueshift Challenges
|
||||
- [Blueshift Courses](https://learn.blueshift.gg/) - Anchor and Pinocchio
|
||||
|
||||
## Tools
|
||||
|
||||
### Development
|
||||
- [Solana Playground](https://beta.solpg.io/) - Browser-based IDE
|
||||
- [Rust Playground](https://play.rust-lang.org/) - Test Rust snippets
|
||||
|
||||
### Security & Analysis
|
||||
- [Trident](https://github.com/Ackee-Blockchain/trident) - Fuzz testing framework
|
||||
- [Certora Prover](https://docs.certora.com/en/latest/docs/solana/index.html) - Formal verification
|
||||
- [Sec3 IDL Guesser](https://github.com/sec3-service/IDLGuesser) - Reverse engineer IDLs
|
||||
- [Anchor X-ray](https://github.com/crytic/anchorx-ray) - Visualize accounts (Trail of Bits)
|
||||
- [Anchor Version Detector](https://github.com/johnsaigle/anchor-version-detector) - Compatibility checker
|
||||
|
||||
### Testing
|
||||
- [Anchor Test Framework](https://book.anchor-lang.com/anchor_in_depth/testing.html)
|
||||
- [Solana Test Validator](https://docs.solana.com/developing/test-validator)
|
||||
|
||||
## CTFs & Practice
|
||||
|
||||
### Capture The Flag
|
||||
- [Ackee Solana CTF](https://github.com/Ackee-Blockchain/Solana-Auditors-Bootcamp/tree/master/Capture-the-Flag)
|
||||
|
||||
### Bootcamps
|
||||
- [Rektoff 6-Week Bootcamp](https://www.rektoff.xyz/bootcamp) - Free, Solana Foundation supported
|
||||
- [Ackee Auditors Bootcamp](https://ackee.xyz/solana-auditors-bootcamp)
|
||||
|
||||
## Community & Support
|
||||
|
||||
### Q&A Platforms
|
||||
- [Solana Stack Exchange](https://solana.stackexchange.com/)
|
||||
|
||||
### Blogs & Newsletters
|
||||
- [Helius Blog](https://www.helius.dev/blog) - Frequent Solana content
|
||||
- [Pine Analytics Substack](https://substack.com/@pineanalytics1) - Protocol deep dives
|
||||
|
||||
## Security Firms
|
||||
|
||||
Top firms for Solana security audits:
|
||||
- [Runtime Verification](https://runtimeverification.com/)
|
||||
- [OtterSec](https://osec.io/)
|
||||
- [Neodyme](https://neodyme.io/en/)
|
||||
- [Sec3](https://www.sec3.dev/)
|
||||
- [Zellic](https://www.zellic.io/)
|
||||
- [Ackee Blockchain](https://ackee.xyz/)
|
||||
- [Hexens](https://hexens.io/)
|
||||
- [Trail of Bits](https://www.trailofbits.com/)
|
||||
- [Kudelski Security](https://kudelskisecurity.com/)
|
||||
- [Cantina](https://cantina.xyz/)
|
||||
- [Certora](https://www.certora.com/)
|
||||
- [Sherlock](https://www.sherlock.xyz/)
|
||||
|
||||
## Version Information
|
||||
|
||||
- Latest Anchor version (as of 2025): 0.30+
|
||||
- Recommended Solana CLI: Latest stable
|
||||
- Rust minimum version: 1.70+
|
||||
|
||||
---
|
||||
|
||||
**Note:** This is a curated collection from the Awesome Solana Security repository and other trusted sources. Resources are selected for their quality, maintenance status, and relevance to modern Solana development practices.
|
||||
291
skills/solana-security/references/security-checklists.md
Normal file
291
skills/solana-security/references/security-checklists.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Security Checklists
|
||||
|
||||
Comprehensive validation checklists for Solana program security reviews.
|
||||
|
||||
## Account Validation Checklist
|
||||
|
||||
For every account in every instruction:
|
||||
|
||||
- [ ] **Signer validation**: Uses `Signer<'info>` or `is_signer` check when needed
|
||||
- [ ] **Owner validation**: Uses `#[account(owner = ...)]` or manual owner check
|
||||
- [ ] **Writable checks**: Properly marked `mut` when account data will be modified
|
||||
- [ ] **Account initialization**: Checks if account is initialized before use
|
||||
- [ ] **PDA validation**: Validates seeds and uses canonical bump
|
||||
- [ ] **Discriminator check**: For `AccountLoader`, validates account type
|
||||
- [ ] **Account relationships**: Uses `has_one` for related accounts
|
||||
|
||||
```rust
|
||||
// Complete account validation example
|
||||
#[derive(Accounts)]
|
||||
pub struct SecureInstruction<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
has_one = authority, // Relationship validation
|
||||
seeds = [b"vault", authority.key().as_ref()],
|
||||
bump, // Canonical bump
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
|
||||
pub authority: Signer<'info>, // Signer required
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
constraint = token_account.owner == authority.key(), // Custom validation
|
||||
)]
|
||||
pub token_account: Account<'info, TokenAccount>,
|
||||
|
||||
pub token_program: Program<'info, Token>, // Program validation
|
||||
}
|
||||
```
|
||||
|
||||
## Arithmetic Safety Checklist
|
||||
|
||||
For all mathematical operations:
|
||||
|
||||
- [ ] **Addition**: Uses `checked_add()` instead of `+`
|
||||
- [ ] **Subtraction**: Uses `checked_sub()` instead of `-`
|
||||
- [ ] **Multiplication**: Uses `checked_mul()` instead of `*`
|
||||
- [ ] **Division**: Uses `checked_div()` instead of `/`
|
||||
- [ ] **Division by zero**: Validates divisor is non-zero
|
||||
- [ ] **Precision loss**: Uses `try_floor_u64()` instead of `try_round_u64()` to prevent arbitrage
|
||||
- [ ] **Avoid saturating**: Does not use `saturating_*` methods (they hide errors)
|
||||
- [ ] **Proper error handling**: All arithmetic wrapped in `ok_or(error)?`
|
||||
|
||||
```rust
|
||||
// Secure arithmetic examples
|
||||
let total = balance
|
||||
.checked_add(amount)
|
||||
.ok_or(ErrorCode::Overflow)?;
|
||||
|
||||
let share = total
|
||||
.checked_div(denominator)
|
||||
.ok_or(ErrorCode::DivisionByZero)?;
|
||||
|
||||
// For Decimal types (token amounts)
|
||||
let liquidity = Decimal::from(collateral_amount)
|
||||
.try_div(rate)?
|
||||
.try_floor_u64()?; // Not try_round_u64()!
|
||||
```
|
||||
|
||||
## PDA and Account Security Checklist
|
||||
|
||||
- [ ] **Canonical bump**: PDAs use `bump` in seeds constraint (not hardcoded)
|
||||
- [ ] **Unique seeds**: Seeds include unique identifier (user pubkey, mint, etc.)
|
||||
- [ ] **No duplicate accounts**: Same account not used twice as mutable
|
||||
- [ ] **Init vs init_if_needed**: Uses `init` with proper validation, not `init_if_needed`
|
||||
- [ ] **has_one constraints**: Related accounts validated with `has_one`
|
||||
- [ ] **Custom constraints**: Complex validation uses `constraint` expression
|
||||
- [ ] **Seed collision**: Seeds designed to prevent collisions
|
||||
|
||||
```rust
|
||||
// Secure PDA patterns
|
||||
#[account(
|
||||
init,
|
||||
payer = authority,
|
||||
space = 8 + UserAccount::INIT_SPACE,
|
||||
seeds = [
|
||||
b"user",
|
||||
authority.key().as_ref(), // Unique to user
|
||||
mint.key().as_ref(), // Unique to mint
|
||||
],
|
||||
bump
|
||||
)]
|
||||
pub user_account: Account<'info, UserAccount>,
|
||||
```
|
||||
|
||||
## CPI Security Checklist
|
||||
|
||||
For all Cross-Program Invocations:
|
||||
|
||||
- [ ] **Program validation**: Target program is validated (uses `Program<'info, T>`)
|
||||
- [ ] **Signer seeds**: PDA signers pass seeds correctly in `invoke_signed`
|
||||
- [ ] **Return value checking**: CPI success doesn't guarantee correct state
|
||||
- [ ] **Account reloading**: Reload accounts after CPI that may modify them
|
||||
- [ ] **No arbitrary CPI**: Program account is not user-controlled
|
||||
- [ ] **Privilege escalation**: CPI doesn't grant unexpected permissions
|
||||
|
||||
```rust
|
||||
// Secure CPI pattern
|
||||
#[derive(Accounts)]
|
||||
pub struct SecureCPI<'info> {
|
||||
pub token_program: Program<'info, Token>, // Type-validated
|
||||
// ... other accounts
|
||||
}
|
||||
|
||||
pub fn secure_cpi(ctx: Context<SecureCPI>) -> Result<()> {
|
||||
// CPI with validated program
|
||||
token::transfer(
|
||||
CpiContext::new(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
Transfer {
|
||||
from: ctx.accounts.from.to_account_info(),
|
||||
to: ctx.accounts.to.to_account_info(),
|
||||
authority: ctx.accounts.authority.to_account_info(),
|
||||
},
|
||||
),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
// Reload account after CPI
|
||||
ctx.accounts.from.reload()?;
|
||||
|
||||
// Validate expected state
|
||||
require!(
|
||||
ctx.accounts.from.amount == expected_amount,
|
||||
ErrorCode::InvalidState
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Oracle and External Data Checklist
|
||||
|
||||
For Pyth, Switchboard, or other oracles:
|
||||
|
||||
- [ ] **Oracle status**: Validates oracle is in valid state (Trading status for Pyth)
|
||||
- [ ] **Price staleness**: Checks timestamp is recent enough
|
||||
- [ ] **Oracle owner**: Validates oracle account owner is correct program
|
||||
- [ ] **Confidence interval**: For Pyth, checks confidence is acceptable
|
||||
- [ ] **Price validity**: Validates price is within reasonable bounds
|
||||
- [ ] **Fallback handling**: Has strategy for oracle failure
|
||||
|
||||
```rust
|
||||
// Pyth oracle validation
|
||||
pub fn validate_pyth_price(
|
||||
pyth_account: &AccountInfo,
|
||||
clock: &Clock,
|
||||
) -> Result<i64> {
|
||||
// Validate owner
|
||||
require_keys_eq!(
|
||||
*pyth_account.owner,
|
||||
PYTH_PROGRAM_ID,
|
||||
ErrorCode::InvalidOracle
|
||||
);
|
||||
|
||||
let price_data = pyth_account.try_borrow_data()?;
|
||||
let price_feed = load_price_feed_from_account_info(pyth_account)?;
|
||||
|
||||
// Check status
|
||||
require!(
|
||||
price_feed.agg.status == PriceStatus::Trading,
|
||||
ErrorCode::InvalidOracleStatus
|
||||
);
|
||||
|
||||
// Check staleness (e.g., max 60 seconds old)
|
||||
let max_age = 60;
|
||||
require!(
|
||||
clock.unix_timestamp - price_feed.agg.publish_time <= max_age,
|
||||
ErrorCode::StalePrice
|
||||
);
|
||||
|
||||
// Check confidence (example: max 1% of price)
|
||||
let confidence_threshold = price_feed.agg.price / 100;
|
||||
require!(
|
||||
price_feed.agg.conf <= confidence_threshold as u64,
|
||||
ErrorCode::OracleConfidenceTooLow
|
||||
);
|
||||
|
||||
Ok(price_feed.agg.price)
|
||||
}
|
||||
```
|
||||
|
||||
## Token Program Security Checklist
|
||||
|
||||
### SPL Token Checks
|
||||
|
||||
- [ ] **ATA validation**: Associated Token Accounts validated correctly
|
||||
- [ ] **Mint authority**: Proper checks on mint authority for minting operations
|
||||
- [ ] **Freeze authority**: Handles frozen accounts appropriately
|
||||
- [ ] **Delegate handling**: Resets delegate when needed
|
||||
- [ ] **Close authority**: Resets close authority on owner change
|
||||
|
||||
### Token-2022 Specific Checks
|
||||
|
||||
- [ ] **Transfer hooks**: Handles transfer hook extensions correctly
|
||||
- [ ] **Extension data**: Validates all active extensions
|
||||
- [ ] **Confidential transfers**: Properly handles confidential transfer extension
|
||||
- [ ] **Transfer fees**: Respects transfer fee extension
|
||||
- [ ] **Permanent delegate**: Checks for permanent delegate extension
|
||||
- [ ] **Additional rent**: Accounts for extension rent requirements
|
||||
|
||||
```rust
|
||||
// Token-2022 with extensions
|
||||
use spl_token_2022::extension::{
|
||||
BaseStateWithExtensions,
|
||||
StateWithExtensions,
|
||||
};
|
||||
|
||||
pub fn safe_token_2022_transfer(
|
||||
/* accounts */
|
||||
) -> Result<()> {
|
||||
// Check for transfer hook
|
||||
let mint_data = mint.try_borrow_data()?;
|
||||
let mint_with_extensions = StateWithExtensions::<Mint>::unpack(&mint_data)?;
|
||||
|
||||
if let Ok(transfer_hook) = mint_with_extensions.get_extension::<TransferHook>() {
|
||||
// Handle transfer hook properly
|
||||
// ... transfer hook logic
|
||||
}
|
||||
|
||||
// Check for transfer fee
|
||||
if let Ok(transfer_fee_config) = mint_with_extensions.get_extension::<TransferFeeConfig>() {
|
||||
// Calculate and handle fees
|
||||
// ... fee logic
|
||||
}
|
||||
|
||||
// Proceed with transfer
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Review Checklist
|
||||
|
||||
- [ ] **PDA design**: PDAs used appropriately vs keypair accounts
|
||||
- [ ] **Account space**: Space calculation uses `InitSpace` derive
|
||||
- [ ] **Error handling**: Custom errors with descriptive messages
|
||||
- [ ] **Event emission**: Critical state changes emit events
|
||||
- [ ] **Rent exemption**: All accounts are rent-exempt
|
||||
- [ ] **Transaction size**: Stays within ~1232 byte limit
|
||||
- [ ] **Compute budget**: Optimized to stay under compute limits
|
||||
- [ ] **Upgradeability**: Considers upgrade path and account versioning
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] **Unit tests**: Each instruction has unit tests
|
||||
- [ ] **Fuzz tests**: Arithmetic operations have fuzz tests (Trident)
|
||||
- [ ] **Integration tests**: Realistic multi-instruction scenarios
|
||||
- [ ] **Negative tests**: Tests for expected failures
|
||||
- [ ] **PDA tests**: Tests for seed collisions
|
||||
- [ ] **Edge cases**: Zero amounts, max values, overflow boundaries
|
||||
- [ ] **Concurrency**: Tests for transaction ordering issues
|
||||
- [ ] **Devnet testing**: Deployed and tested on devnet
|
||||
|
||||
```rust
|
||||
// Example test structure
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_normal_case() {
|
||||
// Test expected behavior
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Overflow")]
|
||||
fn test_overflow() {
|
||||
// Test arithmetic overflow protection
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unauthorized_access() {
|
||||
// Test fails with wrong signer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_case_zero_amount() {
|
||||
// Test zero amount handling
|
||||
}
|
||||
}
|
||||
```
|
||||
1134
skills/solana-security/references/security-fundamentals.md
Normal file
1134
skills/solana-security/references/security-fundamentals.md
Normal file
File diff suppressed because it is too large
Load Diff
525
skills/solana-security/references/vulnerability-patterns.md
Normal file
525
skills/solana-security/references/vulnerability-patterns.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# Common Vulnerability Patterns
|
||||
|
||||
Detailed examples of common Solana smart contract vulnerabilities with exploit scenarios and secure alternatives.
|
||||
|
||||
## 1. Missing Signer Validation
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
|
||||
// No check that caller is authorized!
|
||||
let vault = &mut ctx.accounts.vault;
|
||||
vault.balance -= amount;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Withdraw<'info> {
|
||||
#[account(mut)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
pub user: AccountInfo<'info>, // Not a Signer!
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker can drain the vault by calling `withdraw` with any account as the `user` parameter. No signature verification means anyone can execute the instruction.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct Withdraw<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
has_one = authority, // Ensures vault.authority == authority.key()
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
pub authority: Signer<'info>, // Must sign transaction
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Integer Overflow/Underflow
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
||||
let vault = &mut ctx.accounts.vault;
|
||||
vault.balance = vault.balance + amount; // Can overflow!
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
If `vault.balance = u64::MAX - 100` and `amount = 200`, the addition overflows and wraps to `99`, effectively stealing funds from the vault.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
||||
let vault = &mut ctx.accounts.vault;
|
||||
vault.balance = vault
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.ok_or(ErrorCode::Overflow)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 3. PDA Substitution Attack
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
#[derive(Accounts)]
|
||||
pub struct Transfer<'info> {
|
||||
#[account(mut)]
|
||||
pub config: Account<'info, Config>, // No PDA validation!
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"vault", config.key().as_ref()], // Uses unvalidated config
|
||||
bump
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker creates a fake `config` account with malicious settings. The `vault` PDA is derived from this fake config, potentially accessing wrong vault or bypassing security checks.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct Transfer<'info> {
|
||||
#[account(
|
||||
seeds = [b"config"], // Global config PDA
|
||||
bump,
|
||||
)]
|
||||
pub config: Account<'info, Config>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [b"vault", config.key().as_ref()],
|
||||
bump
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Type Cosplay Attack
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
#[account(mut)]
|
||||
pub user: AccountLoader<'info, User>, // Doesn't check discriminator!
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker passes a `UserAdmin` account instead of `User`. Since `AccountLoader` doesn't check discriminators by default, the program treats the admin account as a regular user, potentially bypassing privilege checks.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[account(mut)]
|
||||
pub user: Account<'info, User>, // Enforces correct discriminator
|
||||
```
|
||||
|
||||
## 5. Account Reloading Issues
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn complex_operation(ctx: Context<ComplexOp>) -> Result<()> {
|
||||
let initial_balance = ctx.accounts.vault.balance;
|
||||
|
||||
// CPI that modifies vault
|
||||
transfer_tokens(&ctx)?;
|
||||
|
||||
// Still using stale balance!
|
||||
require!(
|
||||
ctx.accounts.vault.balance >= initial_balance,
|
||||
ErrorCode::InvalidBalance
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
The `balance` value is cached from before the CPI. If the CPI modified the vault, the check uses stale data, potentially allowing invalid state transitions.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn complex_operation(ctx: Context<ComplexOp>) -> Result<()> {
|
||||
transfer_tokens(&ctx)?;
|
||||
|
||||
// Reload account to get fresh data
|
||||
ctx.accounts.vault.reload()?;
|
||||
|
||||
require!(
|
||||
ctx.accounts.vault.balance >= expected_balance,
|
||||
ErrorCode::InvalidBalance
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Improper Account Closing
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
|
||||
**ctx.accounts.vault.to_account_info().lamports.borrow_mut() = 0;
|
||||
// Data not zeroed, authority not reset!
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Account data remains accessible within the same transaction even after lamports are zeroed. Attacker can read sensitive data or reuse the account in unexpected ways.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseAccount<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
close = receiver // Properly closes: transfers lamports, zeros data
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
#[account(mut)]
|
||||
pub receiver: SystemAccount<'info>,
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Missing Lamports Check
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn process(ctx: Context<Process>) -> Result<()> {
|
||||
let data = ctx.accounts.user_data.load()?; // Can read closed account!
|
||||
// ... use data
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Account was closed earlier in the transaction but data is still readable. Processing closed account data can lead to inconsistent state or bypass business logic.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn process(ctx: Context<Process>) -> Result<()> {
|
||||
require!(
|
||||
**ctx.accounts.user_data.to_account_info().lamports.borrow() > 0,
|
||||
ErrorCode::AccountClosed
|
||||
);
|
||||
|
||||
let data = ctx.accounts.user_data.load()?;
|
||||
// ... use data
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Arbitrary CPI
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
#[derive(Accounts)]
|
||||
pub struct ArbitraryCPI<'info> {
|
||||
pub token_program: AccountInfo<'info>, // Not validated!
|
||||
}
|
||||
|
||||
pub fn transfer(ctx: Context<ArbitraryCPI>) -> Result<()> {
|
||||
invoke(
|
||||
&transfer_instruction,
|
||||
&[
|
||||
ctx.accounts.token_program.clone(), // Could be malicious!
|
||||
// ...
|
||||
]
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker passes malicious program instead of real Token program. Malicious program can emit fake events, return success without transferring, or drain funds.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct SecureCPI<'info> {
|
||||
pub token_program: Program<'info, Token>, // Type-checked!
|
||||
}
|
||||
|
||||
// Or manual validation
|
||||
require_keys_eq!(
|
||||
*ctx.accounts.token_program.key,
|
||||
spl_token::ID,
|
||||
ErrorCode::InvalidTokenProgram
|
||||
);
|
||||
```
|
||||
|
||||
## 9. Duplicate Mutable Accounts
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
#[derive(Accounts)]
|
||||
pub struct Transfer<'info> {
|
||||
#[account(mut)]
|
||||
pub from: Account<'info, TokenAccount>,
|
||||
#[account(mut)]
|
||||
pub to: Account<'info, TokenAccount>,
|
||||
}
|
||||
|
||||
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
|
||||
ctx.accounts.from.amount -= amount;
|
||||
ctx.accounts.to.amount += amount; // Same account = double amount!
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
If `from` and `to` are the same account, the user can double their balance by transferring to themselves.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct Transfer<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
constraint = from.key() != to.key() @ ErrorCode::SameAccount
|
||||
)]
|
||||
pub from: Account<'info, TokenAccount>,
|
||||
#[account(mut)]
|
||||
pub to: Account<'info, TokenAccount>,
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Bump Seed Canonicalization
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn init_vault(ctx: Context<InitVault>, bump: u8) -> Result<()> {
|
||||
// Accepts any bump from user!
|
||||
let seeds = &[b"vault", user.key().as_ref(), &[bump]];
|
||||
// Multiple PDAs possible for same seeds!
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker can create multiple vault PDAs with different bumps for the same user, fragmenting state or confusing off-chain systems.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
#[derive(Accounts)]
|
||||
pub struct InitVault<'info> {
|
||||
#[account(
|
||||
init,
|
||||
payer = user,
|
||||
space = 8 + Vault::INIT_SPACE,
|
||||
seeds = [b"vault", user.key().as_ref()],
|
||||
bump // Anchor derives and stores canonical bump automatically
|
||||
)]
|
||||
pub vault: Account<'info, Vault>,
|
||||
#[account(mut)]
|
||||
pub user: Signer<'info>,
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
```
|
||||
|
||||
## 11. Missing Owner Check
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn read_data(ctx: Context<ReadData>) -> Result<()> {
|
||||
let oracle_data = ctx.accounts.oracle.try_borrow_data()?;
|
||||
// No check that oracle is owned by Pyth program!
|
||||
let price = parse_price(&oracle_data)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker creates fake oracle account owned by their own program, filled with manipulated price data. Program trusts the fake data.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn read_data(ctx: Context<ReadData>) -> Result<()> {
|
||||
require_keys_eq!(
|
||||
*ctx.accounts.oracle.owner,
|
||||
PYTH_PROGRAM_ID,
|
||||
ErrorCode::InvalidOracleOwner
|
||||
);
|
||||
|
||||
let oracle_data = ctx.accounts.oracle.try_borrow_data()?;
|
||||
let price = parse_price(&oracle_data)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Precision Loss / Rounding Errors
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn calculate_shares(collateral: u64, rate: Decimal) -> Result<u64> {
|
||||
Decimal::from(collateral)
|
||||
.try_div(rate)?
|
||||
.try_round_u64() // Rounding can be exploited!
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Attacker repeatedly deposits/withdraws small amounts. Rounding up gives slightly more shares each time, slowly draining the pool.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn calculate_shares(collateral: u64, rate: Decimal) -> Result<u64> {
|
||||
Decimal::from(collateral)
|
||||
.try_div(rate)?
|
||||
.try_floor_u64() // Always round down in user's favor
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Unchecked Error Returns
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
spl_token::instruction::transfer(
|
||||
token_program.key,
|
||||
source.key,
|
||||
destination.key,
|
||||
authority.key,
|
||||
&[],
|
||||
amount,
|
||||
); // Return value ignored!
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Transfer instruction fails silently but program continues as if it succeeded. State becomes inconsistent with actual token balances.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
invoke(
|
||||
&spl_token::instruction::transfer(
|
||||
token_program.key,
|
||||
source.key,
|
||||
destination.key,
|
||||
authority.key,
|
||||
&[],
|
||||
amount,
|
||||
)?, // Propagates error
|
||||
&[
|
||||
source.clone(),
|
||||
destination.clone(),
|
||||
authority.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
// Or use Anchor's CPI helpers
|
||||
token::transfer(ctx, amount)?;
|
||||
```
|
||||
|
||||
## 14. Init If Needed Vulnerability
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
#[account(
|
||||
init_if_needed,
|
||||
payer = user,
|
||||
space = 8 + Account::INIT_SPACE
|
||||
)]
|
||||
pub user_account: Account<'info, UserAccount>,
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
If account already exists, initialization is skipped but existing data might be inconsistent or malicious. Can bypass initialization checks.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE - Explicit initialization
|
||||
#[account(
|
||||
init,
|
||||
payer = user,
|
||||
space = 8 + Account::INIT_SPACE
|
||||
)]
|
||||
pub user_account: Account<'info, UserAccount>,
|
||||
|
||||
// Or if init_if_needed is truly needed, add validation
|
||||
pub fn init_or_validate(ctx: Context<InitAccount>) -> Result<()> {
|
||||
if ctx.accounts.user_account.is_initialized {
|
||||
// Validate existing data
|
||||
require!(
|
||||
ctx.accounts.user_account.owner == ctx.accounts.user.key(),
|
||||
ErrorCode::InvalidOwner
|
||||
);
|
||||
} else {
|
||||
// Initialize new account
|
||||
ctx.accounts.user_account.is_initialized = true;
|
||||
ctx.accounts.user_account.owner = ctx.accounts.user.key();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 15. Stale Oracle Data
|
||||
|
||||
### Vulnerability
|
||||
```rust
|
||||
// ❌ VULNERABLE
|
||||
pub fn get_price(pyth_account: &AccountInfo) -> Result<i64> {
|
||||
let price_feed = load_price_feed(pyth_account)?;
|
||||
Ok(price_feed.agg.price) // No staleness check!
|
||||
}
|
||||
```
|
||||
|
||||
### Exploit Scenario
|
||||
Oracle stopped updating hours ago due to network issues. Attacker exploits stale price to buy/sell at favorable outdated rates.
|
||||
|
||||
### Secure Alternative
|
||||
```rust
|
||||
// ✅ SECURE
|
||||
pub fn get_price(
|
||||
pyth_account: &AccountInfo,
|
||||
clock: &Clock
|
||||
) -> Result<i64> {
|
||||
let price_feed = load_price_feed(pyth_account)?;
|
||||
|
||||
// Check publishing time
|
||||
let max_age_seconds = 60;
|
||||
require!(
|
||||
clock.unix_timestamp - price_feed.agg.publish_time <= max_age_seconds,
|
||||
ErrorCode::StaleOraclePrice
|
||||
);
|
||||
|
||||
// Check status
|
||||
require!(
|
||||
price_feed.agg.status == PriceStatus::Trading,
|
||||
ErrorCode::OracleNotTrading
|
||||
);
|
||||
|
||||
Ok(price_feed.agg.price)
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user