solana-development
Build, test, deploy, and audit Solana programs with Anchor or native Rust, and build with ZK Compression (Light Protocol). Use when developing Solana smart contracts, implementing token operations, optimizing compute, deploying to networks, auditing programs for vulnerabilities,
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/solana-development
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
git clone https://github.com/tenequm/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Solana
Everything for building on Solana: developing programs (Anchor or native Rust), auditing them for security, and building with ZK Compression. All three share the same core model - accounts, PDAs, CPIs, tokens - and differ only in abstraction level and goal.
What this skill covers
| Area | Use when | Jump to |
|---|---|---|
| Development | Writing programs, tokens, tests, deployments | Development |
| Security & Auditing | Reviewing for vulnerabilities, writing exploits, audit reports | Security and Auditing |
| ZK Compression | Rent-free tokens/PDAs at scale via Light Protocol | ZK Compression |
Development
Build Solana programs with Anchor (recommended) or native Rust. Both share accounts, PDAs, CPIs, and tokens; they differ in syntax and abstraction.
Quick Start
Recommended: Anchor Framework
Macros and tooling that cut boilerplate and generate TypeScript clients:
use anchor_lang::prelude::*;
declare_id!("YourProgramID");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
ctx.accounts.account.data = data;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyAccount {
pub data: u64,
}
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install latest && avm use latest
anchor init my_project && cd my_project && anchor build && anchor test
→ See references/anchor.md for the complete Anchor guide
Advanced: Native Rust
Maximum control, optimization potential, and deeper runtime understanding:
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
pubkey::Pubkey, msg,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Processing instruction");
// Manual account parsing, validation, and instruction routing
Ok(())
}
cargo new my_program --lib
cd my_program # configure Cargo.toml (see native-rust.md)
cargo build-sbf
→ See references/native-rust.md for the complete native Rust guide
When to use which
| Your need | Approach | Reason |
|---|---|---|
| Standard DeFi/NFT program | Anchor | Faster, proven patterns |
| TypeScript client needed | Anchor | Auto-generates IDL + types |
| New to Solana | Anchor | Gentler learning curve |
| Compute optimization critical | Native Rust | Direct control, no overhead |
| Smallest program size | Native Rust | No abstraction layer |
| Learning fundamentals | Native Rust | Understand the platform deeply |
You can also start with Anchor for speed, then optimize hot paths with native patterns. Both can coexist in one workspace.
Reference map
Foundations
- accounts.md - Account model, ownership, rent, validation
- pda.md - Program Derived Addresses: derivation, canonical bumps, signing
- cpi.md - Cross-Program Invocations safely
Tokens
- tokens-overview.md - Token accounts and ATAs
- tokens-operations.md - Create, mint, transfer, burn, close
- tokens-validation.md - Account validation patterns
- tokens-2022.md - Token Extensions Program
- tokens-patterns.md - Common patterns and security
Testing
- testing-overview.md - Test pyramid and strategy
- testing-frameworks.md - Mollusk, Anchor test, native Rust
- testing-practices.md - Best practices and patterns
- surfpool.md - Local dev with mainnet forking, cheatcodes, IaC
Deployment
- deployment.md - Deploy, upgrade, verify, manage programs
- production-deployment.md - Verified builds (Anchor 0.32.1)
Client
- client-development.md - dApp client: wallet connections, React hooks, SOL/SPL transfers, transaction management (framework-kit and @solana/kit 6.x)
Implementation details
- serialization.md - Data layout, Borsh, zero-copy
- error-handling.md - Custom errors, propagation, client handling
- security.md - Defensive programming patterns during development
Advanced
- compute-optimization.md - CU optimization and benchmarking
- versioned-transactions.md - Address Lookup Tables for 256+ accounts
- durable-nonces.md - Offline signing with durable nonces
- transaction-lifecycle.md - Submission, retries, confirmations
Low-level
- sysvars.md - Clock, Rent, EpochSchedule, SlotHashes
- builtin-programs.md - System Program, Compute Budget Program
Common tasks
| Task | Pointer |
|---|---|
| New program | Anchor: anchor init / Native: cargo new --lib → anchor.md, native-rust.md |
| Initialize a PDA | pda.md |
| Transfer SPL tokens | tokens-operations.md |
| Fast unit tests | Mollusk → testing-frameworks.md |
| Local mainnet fork | surfpool start → surfpool.md |
| Deploy to devnet | deployment.md |
| Production verified build | solana-verify build → production-deployment.md |
| Optimize compute | compute-optimization.md |
| Handle 40+ accounts | Address Lookup Tables → versioned-transactions.md |
| Offline signing | Durable nonces → durable-nonces.md |
Security and Auditing
Systematic security review for Solana programs (Anchor or native Rust). The core principle: attackers can pass arbitrary accounts to any instruction, so there are no implicit guarantees - validate everything, trust nothing.
Review process
- Initial assessment - Framework (Anchor vs native), Anchor version, dependencies (oracles, external programs), instruction count, account types, program purpose.
- Systematic review - For each instruction, check in order: account validation (signer/owner/writable/init), arithmetic safety (
checked_*), PDA security (canonical bumps, seed uniqueness), CPI security (validated targets), oracle/external data (staleness, status). → security-checklists.md - Vulnerability pattern detection - Type cosplay, account reloading, improper closing, missing lamports/ownership checks, PDA substitution, arbitrary CPI, overflow/underflow. → vulnerability-patterns.md
- Architecture and testing review - PDA design, space/rent, error handling, event emission, compute budget, test coverage (unit/integration/fuzz), upgrade and authority management.
- Generate report - Findings by severity, critical first, quick wins, testing recommendations.
Essential checks (every instruction)
Anchor:
#[derive(Accounts)]
pub struct SecureInstruction<'info> {
#[account(
mut,
has_one = authority, // relationship check
seeds = [b"vault", user.key().as_ref()], bump // canonical bump
)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>, // signer required
pub token_program: Program<'info, Token>, // program validation
}
let total = balance.checked_add(amount).ok_or(ErrorCode::Overflow)?;
Native Rust:
if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); }
if vault.owner != program_id { return Err(ProgramError::IllegalOwner); }
let total = balance.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?;
Critical anti-patterns
❌ Never: saturating_* arithmetic (hides errors), unwrap()/expect() in production, init_if_needed without extra checks, skipped signer validation, unchecked arithmetic, arbitrary CPI targets, forgetting to reload accounts after mutations.
✅ Always: checked_* arithmetic, ok_or(error)? on Options, explicit init with validation, Signer/is_signer checks, Program<'info, T> for CPI targets, reload after external mutations, validate ownership + discriminator + relationships.
Severity and finding format
- 🔴 Critical - Funds stolen/lost, protocol broken
- 🟠 High - Disruption, partial fund loss possible
- 🟡 Medium - Edge cases, griefing, suboptimal behavior
- 🔵 Low - Code quality, gas, best practices
- 💡 Informational - Recommendations, documentation
## 🔴 [CRITICAL] Title
**Location:** `programs/vault/src/lib.rs:45-52`
**Issue:** Brief description.
**Vulnerable Code:** ```rust ... ```
**Exploit Scenario:** How it's exploited, step by step.
**Recommendation:** ```rust ... ```
**References:** Links to docs or similar exploits.
Reference map
- security-fundamentals.md - Security mindset, threat modeling, core validation, input/state/arithmetic safety, re-entrancy
- vulnerability-patterns.md - Each vuln with vulnerable code, exploit scenario, secure fix, references
- security-checklists.md - Account, arithmetic, PDA, CPI, oracle, token checklists
- anchor-security.md - Anchor constraints, CpiContext, events, custom errors
- native-security.md - Manual validation, secure PDA signing, discriminators, rent exemption
- caveats.md - Solana quirks, Anchor limitations, testing blind spots, version issues
Key questions for every audit
- Can an attacker substitute accounts? (PDA validation, program IDs,
has_one) - Can arithmetic overflow/underflow? (checked ops, division by zero)
- Are all accounts validated? (owner, signer, writable, initialized)
- Can the program be drained? (authorization, reentrancy, account confusion)
- What happens in edge cases? (zero amounts, max values, closed accounts, expired data)
- Are external dependencies safe? (oracle staleness/status, CPI targets, token program)
Modern practices (2025)
Anchor 0.30+ • Token-2022 with proper extension handling • InitSpace derive • emit events for critical state changes • fuzz tests with Trident • document invariants • roadmap: Dev → Audit → Testnet → Audit → Mainnet.
ZK Compression
ZK Compression enables rent-free tokens and PDAs by storing state on the ledger (not in accounts), using zero-knowledge proofs to validate state transitions. Built by Light Protocol, indexed by Helius Photon.
When to use
Use when: creating millions of token accounts (~5000x cheaper), airdrops/loyalty/gaming mints to many recipients, many infrequently-updated user accounts, low-update-frequency PDAs.
Use regular accounts when: updated frequently (>1000 lifetime writes), large data accessed on-chain, or compute budget is critical (compression adds ~100k CU).
Quick Start
# TypeScript client
npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
# Rust SDK for programs
cargo add light-sdk
# CLI + local validator
npm install -g @lightprotocol/zk-compression-cli
light test-validator # local validator with compression
light init my-program # new Anchor project with compression
Mint compressed tokens (TypeScript)
import { createRpc } from '@lightprotocol/stateless.js';
import { createMint, mintTo, transfer } from '@lightprotocol/compressed-token';
const rpc = createRpc(); // or createRpc('https://mainnet.helius-rpc.com?api-key=YOUR_KEY')
const { mint } = await createMint(rpc, payer, payer.publicKey, 9);
await mintTo(rpc, payer, mint, recipient, payer, 1_000_000_000);
await transfer(rpc, payer, mint, 500_000_000, owner, recipient);
const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint });
→ See references/compressed-pdas.md for the full Anchor program with compressed PDAs (LightAccount, validity proofs, CPI).
Core concepts
Compressed account model - Like regular accounts but stored on the ledger instead of AccountsDB. No rent. Identified by content hash (changes on every write) or an optional persistent address (PDA-like). State validated by ZK proofs.
State trees - Concurrent Merkle trees (Poseidon hashing); only the root is on-chain. V2 batched trees (mainnet, Jan 2026): ~250x cheaper state-root updates and ~70% less CU vs V1. New deployments use V2 automatically; V1 still supported.
Validity proofs - Groth16 ZK proofs, constant 128 bytes regardless of account count, ~100k CU to verify.
LightAccount operations:
LightAccount::<T>::new_init(&program_id, Some(address), tree_index); // create
LightAccount::<T>::new_mut(&program_id, &account_meta, current_state)?; // modify
LightAccount::<T>::new_close(&program_id, &account_meta, current_state)?; // close
RPC and infrastructure
Query compressed state via Helius RPC or self-hosted Photon. Key methods: getCompressedAccount, getCompressedAccountsByOwner, getCompressedTokenAccountsByOwner, getCompressedTokenBalancesByOwner, getValidityProof, getMultipleCompressedAccounts, getCompressionSignaturesForAccount.
Node types: Photon RPC (indexes state, serves queries - Helius or self-host), Prover (generates proofs), Forester (maintains trees, empties nullifier queues).
cargo install photon-indexer
photon --rpc-url=https://api.devnet.solana.com
Trade-offs
Larger transactions (+128 byte proof + account data) • higher CU (~100k proof + ~6k per account) • each write nullifies old state and appends new • requires a Photon indexer for queries. With V2 batched trees, break-even shifts well past V1's ~1000-write threshold.
Reference map
- compressed-accounts.md - Account model, hashing, addresses
- compressed-tokens.md - Token operations, pools, batch operations
- compressed-pdas.md - Building programs with compressed PDAs
- client-integration.md - TypeScript/Rust client setup, RPC methods
External: ZK Compression Docs • Light Protocol • Helius SDK • Photon Indexer • Program Examples
Resources
Official docs, tools, learning paths, security guides, audit reports, security firms, and community links:
→ See references/resources.md
General best practices
✅ Validate every account (ownership, signers, mutability) • use checked_* arithmetic • test extensively (unit, integration, edge cases) • use PDAs for program-owned accounts • minimize and profile compute • add security.txt so researchers can reach you.
Anchor-specific: InitSpace derive for space • has_one constraints • Program<'info, T> for CPI validation • emit! events • group related constraints.
Native-specific: next_account_info for safe iteration • cache PDA bumps • zero-copy for large structs (50%+ CU savings) • minimize logging (pubkey formatting is expensive) • solana-verify build for production.
Getting help
- Anchor: Discord • Docs
- Solana: Stack Exchange • Discord
Files (skills)
-
references
-
accounts.md 24.8 KB
# Solana Account Model & Validation This reference provides comprehensive coverage of Solana's account model, validation patterns, and rent mechanics for native Rust program development. ## Table of Contents 1. [Account Structure](#account-structure) 2. [Account Types](#account-types) 3. [Account Ownership](#account-ownership) 4. [Rent Mechanics](#rent-mechanics) 5. [Account Validation Patterns](#account-validation-patterns) 6. [Security Best Practices](#security-best-practices) 7. [Common Vulnerabilities](#common-vulnerabilities) --- ## Account Structure Every Solana account is a location on the blockchain that stores data. All accounts have a uniform structure defined by the [`Account`](https://github.com/anza-xyz/agave/blob/v2.1.13/sdk/account/src/lib.rs#L48-L60) struct: ```rust pub struct Account { /// lamports in the account pub lamports: u64, /// data held in this account pub data: Vec<u8>, /// the program that owns this account pub owner: Pubkey, /// this account's data contains a loaded program (and is now read-only) pub executable: bool, /// the epoch at which this account will next owe rent (DEPRECATED) pub rent_epoch: Epoch, } ``` ### Field Details #### `lamports` (u64) - The account's balance in lamports (1 SOL = 1,000,000,000 lamports) - Every account must maintain a minimum balance for rent exemption - Rent works as a **refundable deposit** - recoverable when account is closed - Only the account owner can deduct lamports - Any program can **add** lamports to any account #### `data` (Vec<u8>) - Maximum size: **10 MiB** (10,485,760 bytes) - Can contain any arbitrary sequence of bytes - Structure defined by the owning program - Common patterns: - **Program accounts**: Executable code or pointer to program data account - **Data accounts**: Serialized state (often using Borsh) #### `owner` (Pubkey) - The program ID that owns this account - **Critical security property**: Only the owner can modify `data` or deduct `lamports` - Cannot be changed after account creation (except by System Program for newly created accounts) - Newly created accounts start owned by System Program #### `executable` (bool) - `true`: Account contains executable program code - `false`: Account is a data account - Cannot be changed after being set to `true` #### `rent_epoch` (Epoch) - **DEPRECATED** - no longer used - Remains in struct for backward compatibility - Rent is now a one-time refundable deposit, not periodic payment --- ## Account Types ### 1. Program Accounts (Executable) Program accounts contain executable code and are owned by a [loader program](https://solana.com/docs/core/programs#loader-programs). **Simple Program Account Structure:** ``` ┌─────────────────────────────────────┐ │ Program Account │ ├─────────────────────────────────────┤ │ lamports: 1000000 │ │ data: [executable bytecode] │ │ owner: BPFLoaderUpgradeab1e... │ │ executable: true │ └─────────────────────────────────────┘ ``` **Loader-v3 Program Structure (Upgradeable):** Programs deployed with loader-v3 use a **two-account model**: ``` ┌─────────────────────────────────────┐ │ Program Account │ ├─────────────────────────────────────┤ │ data: [pointer to program data] │ ──┐ │ executable: true │ │ └─────────────────────────────────────┘ │ │ ▼ ┌─────────────────────────────────────┐ │ Program Data Account │ ├─────────────────────────────────────┤ │ data: [actual executable bytecode] │ │ executable: false │ └─────────────────────────────────────┘ ``` This separation enables: - Program upgrades without changing the program address - Buffer accounts for staging uploads - Separate upgrade authority management ### 2. Data Accounts (Non-Executable) Data accounts store program state and are owned by programs (or System Program). #### a) Program State Accounts Accounts created and owned by your program to store application state: ```rust // Example: Note account owned by a note-taking program pub struct NoteAccount { pub is_initialized: bool, pub author: Pubkey, pub note_id: u64, pub content: String, } ``` **Creation Process:** 1. Invoke System Program to create account (allocate space, transfer lamports) 2. System Program transfers ownership to your program 3. Your program initializes the account data ```rust // Step 1: Create account via System Program CPI invoke_signed( &system_instruction::create_account( initializer.key, pda_account.key, rent_lamports, account_len.try_into().unwrap(), program_id, // Transfer ownership to our program ), &[initializer.clone(), pda_account.clone(), system_program.clone()], &[&[seeds, &[bump_seed]]], )?; // Step 2: Initialize the account data let mut account_data = try_from_slice_unchecked::<NoteAccount>(&pda_account.data.borrow())?; account_data.is_initialized = true; account_data.author = *initializer.key; account_data.note_id = note_id; account_data.content = content; account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?; ``` #### b) System Accounts (Wallet Accounts) Accounts owned by the System Program, typically used as user wallets: ``` ┌─────────────────────────────────────┐ │ Wallet Account │ ├─────────────────────────────────────┤ │ lamports: 1000000000 │ │ data: [] │ │ owner: 11111111111111111111... │ ← System Program │ executable: false │ └─────────────────────────────────────┘ ``` **Characteristics:** - Can sign transactions (if you have the private key) - Can pay transaction fees - Can transfer SOL - Created automatically when funded with SOL #### c) Sysvar Accounts Special accounts at predefined addresses that provide cluster state data: | Sysvar | Address | Purpose | |--------|---------|---------| | Clock | `SysvarC1ock11111111111111111111111111111111` | Current slot, epoch, timestamp | | Rent | `SysvarRent111111111111111111111111111111111` | Rent rate calculation | | EpochSchedule | `SysvarEpochSchedu1e111111111111111111111111` | Epoch duration info | | SlotHashes | `SysvarS1otHashes111111111111111111111111111` | Recent slot hashes | **Access Pattern:** ```rust use solana_program::sysvar::{clock::Clock, Sysvar}; let clock = Clock::get()?; let current_timestamp = clock.unix_timestamp; ``` --- ## Account Ownership ### Ownership Rules **The Golden Rule:** Only the account owner can: 1. Modify the account's `data` field 2. Deduct lamports from the account **Critical Security Implication:** Programs must verify account ownership to prevent unauthorized state modifications. ### Ownership in Program Context When a program receives accounts in an instruction: ```rust pub fn process_instruction( program_id: &Pubkey, // Your program's ID accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let data_account = next_account_info(account_info_iter)?; // CRITICAL: Verify ownership before modifying if data_account.owner != program_id { return Err(ProgramError::IllegalOwner); } // Safe to modify - we own this account // ... } ``` ### AccountInfo Structure Programs receive accounts as `AccountInfo` structs: ```rust pub struct AccountInfo<'a> { pub key: &'a Pubkey, // Account address pub is_signer: bool, // Did this account sign the transaction? pub is_writable: bool, // Is this account writable in this instruction? pub lamports: Rc<RefCell<&'a mut u64>>, // Mutable lamport balance pub data: Rc<RefCell<&'a mut [u8]>>, // Mutable data pub owner: &'a Pubkey, // Owner program ID pub executable: bool, // Is this executable? pub rent_epoch: Epoch, // Deprecated } ``` **Key Operations:** ```rust // Read data let data = data_account.data.borrow(); let account_state = MyState::try_from_slice(&data)?; // Write data let mut data = data_account.data.borrow_mut(); account_state.serialize(&mut *data)?; // Modify lamports **data_account.lamports.borrow_mut() += transfer_amount; ``` --- ## Rent Mechanics Rent is a **refundable security deposit** required to store data on-chain. Despite the name "rent", it's not a recurring fee—it's a one-time deposit fully recoverable when the account is closed. ### Rent Calculation Rent is proportional to account size: ```rust use solana_program::rent::Rent; use solana_program::sysvar::Sysvar; // Get current rent rates let rent = Rent::get()?; // Calculate minimum balance for rent exemption let account_size: usize = 1000; // bytes let rent_lamports = rent.minimum_balance(account_size); ``` **Formula:** Based on [agave source](https://github.com/anza-xyz/agave/blob/v2.1.13/sdk/rent/src/lib.rs#L93-L97): ```rust minimum_balance = (LAMPORTS_PER_BYTE_YEAR * account_size) * EXEMPTION_THRESHOLD / slots_per_year ``` **Constants:** - `LAMPORTS_PER_BYTE_YEAR`: 3,480 lamports - `EXEMPTION_THRESHOLD`: 2.0 (200% of annual rent) - Typical cost: ~0.00139536 SOL per 100 bytes ### Rent Exemption **All accounts must be rent-exempt.** This means: - Account lamport balance ≥ `rent.minimum_balance(account.data.len())` - The Solana runtime enforces this requirement - Non-exempt accounts cannot be created ### Practical Example ```rust pub fn create_data_account( program_id: &Pubkey, accounts: &[AccountInfo], data_size: usize, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let payer = next_account_info(account_info_iter)?; let new_account = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Calculate rent-exempt balance let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(data_size); // Create account with rent-exempt balance invoke( &system_instruction::create_account( payer.key, new_account.key, rent_lamports, // Must be rent-exempt data_size as u64, program_id, ), &[payer.clone(), new_account.clone(), system_program.clone()], )?; Ok(()) } ``` ### Closing Accounts (Recovering Rent) To recover rent when an account is no longer needed: ```rust pub fn close_account( account_to_close: &AccountInfo, destination: &AccountInfo, ) -> ProgramResult { // Transfer all lamports to destination let dest_lamports = destination.lamports(); **destination.lamports.borrow_mut() = dest_lamports .checked_add(**account_to_close.lamports.borrow()) .ok_or(ProgramError::ArithmeticOverflow)?; // Zero out lamports in closed account **account_to_close.lamports.borrow_mut() = 0; // Zero out data (security best practice) let mut data = account_to_close.data.borrow_mut(); data.fill(0); Ok(()) } ``` **Important:** The runtime will garbage-collect accounts with 0 lamports. --- ## Account Validation Patterns Proper account validation is **critical for security**. Programs must verify accounts before using them. ### 1. Ownership Check **Purpose:** Ensure an account is owned by the expected program. **When to use:** - Before reading/writing account data - When validating PDAs - When ensuring proper account initialization ```rust // Basic ownership check if account.owner != program_id { msg!("Account not owned by this program"); return Err(ProgramError::IllegalOwner); } // PDA ownership check (essential for security) if note_pda.owner != program_id { msg!("Invalid note account - wrong owner"); return Err(ProgramError::IllegalOwner); } ``` **Why it matters:** Without ownership checks, malicious actors can pass arbitrary accounts that match the expected data format but are controlled by other programs or themselves. ### 2. Signer Check **Purpose:** Verify that an account signed the transaction. **When to use:** - Before transferring funds from an account - Before modifying user-specific data - Before any privileged operation ```rust if !initializer.is_signer { msg!("Missing required signature"); return Err(ProgramError::MissingRequiredSignature); } // Practical example: Only allow note author to update pub fn update_note( program_id: &Pubkey, accounts: &[AccountInfo], new_content: String, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let author = next_account_info(account_info_iter)?; let note_pda = next_account_info(account_info_iter)?; // Verify author signed the transaction if !author.is_signer { msg!("Author must sign to update note"); return Err(ProgramError::MissingRequiredSignature); } // Deserialize and verify author matches let note_data = NoteAccount::try_from_slice(¬e_pda.data.borrow())?; if note_data.author != *author.key { msg!("Author mismatch"); return Err(ProgramError::IllegalOwner); } // Safe to proceed with update // ... } ``` ### 3. Writable Check **Purpose:** Verify an account is marked as writable. **When to use:** - Before modifying account data - Before changing lamport balances - Enforced automatically by runtime, but explicit checks improve clarity ```rust if !account.is_writable { msg!("Account must be writable"); return Err(ProgramError::InvalidAccountData); } ``` ### 4. Initialization Check **Purpose:** Prevent re-initialization or use of uninitialized accounts. **Pattern: Flag-based initialization** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct DataAccount { pub is_initialized: bool, // ... other fields } impl DataAccount { pub fn is_initialized(&self) -> bool { self.is_initialized } } // On creation - check NOT initialized if account_data.is_initialized() { msg!("Account already initialized"); return Err(ProgramError::AccountAlreadyInitialized); } // On update - check IS initialized if !account_data.is_initialized() { msg!("Account not initialized"); return Err(ProgramError::UninitializedAccount); } ``` ### 5. PDA Validation **Purpose:** Verify a provided PDA matches expected derivation. **Critical for security:** Always validate PDAs using canonical bump. ```rust pub fn validate_pda( program_id: &Pubkey, accounts: &[AccountInfo], note_id: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let author = next_account_info(account_info_iter)?; let note_pda = next_account_info(account_info_iter)?; // Derive expected PDA let (expected_pda, _bump) = Pubkey::find_program_address( &[ author.key.as_ref(), note_id.to_le_bytes().as_ref(), ], program_id, ); // Validate match if expected_pda != *note_pda.key { msg!("Invalid PDA - seeds don't match"); return Err(ProgramError::InvalidSeeds); } Ok(()) } ``` **Why use `find_program_address` instead of accepting a bump?** - Prevents bump seed manipulation attacks - Ensures canonical bump is used - Eliminates category of security vulnerabilities ### 6. Account Type Validation **Purpose:** Ensure account contains expected data type. **Pattern: Discriminator/Type Field** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub enum AccountType { Uninitialized, UserProfile, GameState, } #[derive(BorshSerialize, BorshDeserialize)] pub struct AccountData { pub account_type: AccountType, // ... other fields } // Validation let account_data = AccountData::try_from_slice(&account.data.borrow())?; if !matches!(account_data.account_type, AccountType::UserProfile) { msg!("Wrong account type"); return Err(ProgramError::InvalidAccountData); } ``` --- ## Security Best Practices ### 1. Always Validate Before Trusting **Never assume accounts are correct.** Always validate: ```rust pub fn secure_instruction( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let user = next_account_info(account_info_iter)?; let user_data_pda = next_account_info(account_info_iter)?; // ✅ Signer check if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); } // ✅ Ownership check if user_data_pda.owner != program_id { return Err(ProgramError::IllegalOwner); } // ✅ PDA validation let (expected_pda, _) = Pubkey::find_program_address( &[b"user_data", user.key.as_ref()], program_id, ); if expected_pda != *user_data_pda.key { return Err(ProgramError::InvalidSeeds); } // ✅ Initialization check let data = UserData::try_from_slice(&user_data_pda.data.borrow())?; if !data.is_initialized { return Err(ProgramError::UninitializedAccount); } // Now safe to proceed // ... } ``` ### 2. Fail Fast with Meaningful Errors Return errors immediately when validation fails: ```rust // ✅ Good - fail fast if !account.is_signer { msg!("User must sign the transaction"); return Err(ProgramError::MissingRequiredSignature); } // ❌ Bad - continues with invalid state if account.is_signer { // process... } ``` ### 3. Use Type Safety Leverage Rust's type system for compile-time guarantees: ```rust // Define a validated account type pub struct ValidatedUserAccount<'a> { info: &'a AccountInfo<'a>, data: UserAccountData, } impl<'a> ValidatedUserAccount<'a> { pub fn validate( account: &'a AccountInfo<'a>, program_id: &Pubkey, ) -> Result<Self, ProgramError> { // Ownership check if account.owner != program_id { return Err(ProgramError::IllegalOwner); } // Deserialize and validate let data = UserAccountData::try_from_slice(&account.data.borrow())?; if !data.is_initialized { return Err(ProgramError::UninitializedAccount); } Ok(Self { info: account, data }) } } // Usage guarantees validated account pub fn process_with_validated_account( validated: ValidatedUserAccount, ) -> ProgramResult { // No need to re-validate! // ... } ``` ### 4. Check Arithmetic Operations Always use checked math to prevent overflow/underflow: ```rust // ❌ Dangerous - can overflow let total = amount1 + amount2; // ✅ Safe - returns error on overflow let total = amount1 .checked_add(amount2) .ok_or(ProgramError::ArithmeticOverflow)?; ``` ### 5. Validate Data Constraints Check business logic constraints: ```rust pub fn allocate_points( character_account: &AccountInfo, new_strength: u8, ) -> ProgramResult { let mut character = Character::try_from_slice(&character_account.data.borrow())?; // Validate attribute cap if character.strength.checked_add(new_strength).ok_or(ProgramError::ArithmeticOverflow)? > 100 { msg!("Attribute cannot exceed 100"); return Err(ProgramError::InvalidArgument); } // Validate allowance if new_strength > character.available_points { msg!("Insufficient available points"); return Err(ProgramError::InsufficientFunds); } character.strength += new_strength; character.available_points -= new_strength; character.serialize(&mut &mut character_account.data.borrow_mut()[..])?; Ok(()) } ``` --- ## Common Vulnerabilities ### 1. Missing Ownership Check **Vulnerability:** ```rust // ❌ No ownership validation pub fn update_data( program_id: &Pubkey, accounts: &[AccountInfo], new_value: u64, ) -> ProgramResult { let data_account = &accounts[0]; // Dangerous - could be any account! let mut data = MyData::try_from_slice(&data_account.data.borrow())?; data.value = new_value; data.serialize(&mut &mut data_account.data.borrow_mut()[..])?; Ok(()) } ``` **Exploit:** Attacker passes an account they control that happens to deserialize correctly, modifying arbitrary data. **Fix:** ```rust // ✅ With ownership check if data_account.owner != program_id { return Err(ProgramError::IllegalOwner); } ``` ### 2. Missing Signer Check **Vulnerability:** ```rust // ❌ No signer validation pub fn withdraw( accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let user_account = &accounts[0]; let vault = &accounts[1]; // Dangerous - anyone can drain anyone's funds! **user_account.lamports.borrow_mut() += amount; **vault.lamports.borrow_mut() -= amount; Ok(()) } ``` **Exploit:** Attacker calls instruction with victim's account, draining their funds without signature. **Fix:** ```rust // ✅ With signer check if !user_account.is_signer { return Err(ProgramError::MissingRequiredSignature); } ``` ### 3. PDA Substitution Attack **Vulnerability:** ```rust // ❌ Accepts PDA without validation pub fn update_user_data( program_id: &Pubkey, accounts: &[AccountInfo], user: &AccountInfo, user_pda: &AccountInfo, ) -> ProgramResult { // No PDA derivation check! let mut data = UserData::try_from_slice(&user_pda.data.borrow())?; data.balance += 100; data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?; Ok(()) } ``` **Exploit:** Attacker passes a different user's PDA, crediting that user's balance instead. **Fix:** ```rust // ✅ Validate PDA derivation let (expected_pda, _) = Pubkey::find_program_address( &[b"user_data", user.key.as_ref()], program_id, ); if expected_pda != *user_pda.key { return Err(ProgramError::InvalidSeeds); } ``` ### 4. Integer Overflow/Underflow **Vulnerability:** ```rust // ❌ Unchecked arithmetic pub fn add_rewards( account: &AccountInfo, reward: u64, ) -> ProgramResult { let mut user = UserData::try_from_slice(&account.data.borrow())?; user.total_rewards = user.total_rewards + reward; // Can overflow! user.serialize(&mut &mut account.data.borrow_mut()[..])?; Ok(()) } ``` **Exploit:** Overflow wraps around: u64::MAX + 1 = 0, causing balance to reset. **Fix:** ```rust // ✅ Checked arithmetic user.total_rewards = user.total_rewards .checked_add(reward) .ok_or(ProgramError::ArithmeticOverflow)?; ``` ### 5. Unvalidated Account Reuse **Vulnerability:** ```rust // ❌ No initialization check pub fn update_score( accounts: &[AccountInfo], score: u64, ) -> ProgramResult { let score_account = &accounts[0]; let mut data = ScoreData::try_from_slice(&score_account.data.borrow())?; // What if account was never initialized? data.score = score; data.serialize(&mut &mut score_account.data.borrow_mut()[..])?; Ok(()) } ``` **Exploit:** Reusing uninitialized memory can lead to undefined behavior or data corruption. **Fix:** ```rust // ✅ Check initialization if !data.is_initialized { return Err(ProgramError::UninitializedAccount); } ``` --- ## Summary **Critical Account Validation Checklist:** - ✅ **Ownership check**: Verify `account.owner == expected_program_id` - ✅ **Signer check**: Verify `account.is_signer` for privileged operations - ✅ **PDA validation**: Use `find_program_address` with expected seeds - ✅ **Initialization check**: Verify account is initialized before use - ✅ **Type validation**: Ensure account contains expected data structure - ✅ **Rent exemption**: Calculate and enforce rent-exempt balances - ✅ **Arithmetic safety**: Use `checked_add`, `checked_sub`, etc. - ✅ **Data constraints**: Validate business logic rules **Think Like an Attacker:** For every account your program receives, ask: - "What if this is the wrong account?" - "What if this account isn't owned by my program?" - "What if the user didn't sign for this?" - "What if this account is uninitialized?" - "What if these seeds derive a different PDA?" Validate everything. Trust nothing. -
anchor-security.md 26.9 KB
# Anchor Security Reference This document covers security patterns, vulnerabilities, and best practices specific to the Anchor framework for Solana program development. ## 1. Anchor Constraint Security ### 1.1 Account Constraint Basics Anchor's `#[account(...)]` constraints provide declarative validation of accounts passed to instructions. Proper use is critical for security. **Core constraint types:** - `init` - Initialize a new account - `mut` - Mark account as mutable - `has_one` - Verify relationship between accounts - `seeds` and `bump` - Validate PDA derivation - `constraint` - Custom validation expressions - `close` - Close account and return rent - `realloc` - Resize account data ### 1.2 init vs init_if_needed **VULNERABLE - Using init_if_needed:** ```rust #[derive(Accounts)] pub struct UpdateConfig<'info> { #[account( init_if_needed, payer = authority, space = 8 + Config::INIT_SPACE, seeds = [b"config"], bump )] pub config: Account<'info, Config>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } ``` **Issue:** `init_if_needed` allows re-initialization attacks. An attacker can close the account in a previous transaction, then re-initialize it with malicious data. **SECURE - Separate init and update instructions:** ```rust #[derive(Accounts)] pub struct InitConfig<'info> { #[account( init, payer = authority, space = 8 + Config::INIT_SPACE, seeds = [b"config"], bump )] pub config: Account<'info, Config>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct UpdateConfig<'info> { #[account( mut, seeds = [b"config"], bump = config.bump )] pub config: Account<'info, Config>, pub authority: Signer<'info>, } ``` **When init_if_needed is acceptable:** - Idempotent operations where re-initialization is safe - Accounts with no state that matters (pure PDAs used only for signing) - Always combine with additional constraints to prevent misuse ### 1.3 has_one Constraints for Relationships **VULNERABLE - Missing has_one check:** ```rust #[derive(Accounts)] pub struct WithdrawFunds<'info> { #[account(mut)] pub vault: Account<'info, Vault>, #[account(mut)] pub owner: Signer<'info>, #[account(mut)] pub destination: SystemAccount<'info>, } pub fn withdraw_funds(ctx: Context<WithdrawFunds>, amount: u64) -> Result<()> { // Missing validation: anyone can withdraw from any vault! transfer_lamports(&ctx.accounts.vault, &ctx.accounts.destination, amount)?; Ok(()) } ``` **SECURE - Using has_one:** ```rust #[account] pub struct Vault { pub owner: Pubkey, pub bump: u8, } #[derive(Accounts)] pub struct WithdrawFunds<'info> { #[account( mut, has_one = owner, // Validates vault.owner == owner.key() seeds = [b"vault", owner.key().as_ref()], bump = vault.bump )] pub vault: Account<'info, Vault>, #[account(mut)] pub owner: Signer<'info>, #[account(mut)] pub destination: SystemAccount<'info>, } ``` ### 1.4 seeds and bump for PDA Validation **VULNERABLE - Not validating PDA derivation:** ```rust #[derive(Accounts)] pub struct Deposit<'info> { #[account(mut)] pub vault: Account<'info, Vault>, pub depositor: Signer<'info>, } ``` **Issue:** Attacker can pass any account as vault, including one they control. **SECURE - Validate PDA with seeds and bump:** ```rust #[derive(Accounts)] pub struct Deposit<'info> { #[account( mut, seeds = [b"vault", depositor.key().as_ref()], bump = vault.bump )] pub vault: Account<'info, Vault>, pub depositor: Signer<'info>, } ``` **CRITICAL: Always use canonical bump:** ```rust #[account] pub struct Vault { pub bump: u8, // Store canonical bump at initialization } // At initialization, use: #[account( init, payer = payer, space = 8 + Vault::INIT_SPACE, seeds = [b"vault", authority.key().as_ref()], bump // Anchor automatically finds canonical bump )] pub vault: Account<'info, Vault>, // Then store it: vault.bump = ctx.bumps.vault; // ctx.bumps available in Anchor 0.29+ ``` ### 1.5 constraint Expressions and Pitfalls **VULNERABLE - Using constraint without proper checks:** ```rust #[derive(Accounts)] pub struct Transfer<'info> { #[account( mut, constraint = from.amount >= amount @ ErrorCode::InsufficientFunds )] pub from: Account<'info, TokenAccount>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, } ``` **Issue:** Missing check that authority actually owns the from account! **SECURE - Combine constraints appropriately:** ```rust #[derive(Accounts)] pub struct Transfer<'info> { #[account( mut, has_one = authority, // Verify ownership constraint = from.amount >= amount @ ErrorCode::InsufficientFunds )] pub from: Account<'info, TokenAccount>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, } ``` **Constraint expression tips:** - Use `@` to specify custom error codes - Constraints execute after account deserialization - Complex logic should go in instruction handler, not constraints - Prefer built-in constraints (`has_one`, `seeds`) over custom `constraint` ### 1.6 close Constraint Security **VULNERABLE - close without proper authorization:** ```rust #[derive(Accounts)] pub struct CloseAccount<'info> { #[account( mut, close = destination )] pub account_to_close: Account<'info, MyAccount>, #[account(mut)] pub destination: SystemAccount<'info>, } ``` **Issue:** Anyone can close the account and steal the rent! **SECURE - Verify authorization before closing:** ```rust #[derive(Accounts)] pub struct CloseAccount<'info> { #[account( mut, has_one = authority, close = authority // Return rent to authorized party )] pub account_to_close: Account<'info, MyAccount>, #[account(mut)] pub authority: Signer<'info>, } ``` **CRITICAL: close order matters:** ```rust // WRONG - closes account before using it #[account( close = authority, has_one = authority )] pub my_account: Account<'info, MyAccount>, // CORRECT - validates before closing #[account( has_one = authority, close = authority )] pub my_account: Account<'info, MyAccount>, ``` ### 1.7 realloc Security Considerations **VULNERABLE - realloc without validation:** ```rust #[derive(Accounts)] pub struct UpdateData<'info> { #[account( mut, realloc = 8 + 4 + new_data.len(), realloc::payer = payer, realloc::zero = false )] pub data_account: Account<'info, DataAccount>, #[account(mut)] pub payer: Signer<'info>, } ``` **Issues:** - No max size check (DoS via huge allocations) - No authority check (anyone can realloc) - `zero = false` might leak old data **SECURE - Proper realloc constraints:** ```rust #[derive(Accounts)] pub struct UpdateData<'info> { #[account( mut, has_one = authority, realloc = 8 + 4 + new_data.len(), realloc::payer = authority, realloc::zero = true, // Zero out old data constraint = new_data.len() <= MAX_DATA_SIZE @ ErrorCode::DataTooLarge )] pub data_account: Account<'info, DataAccount>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } ``` ## 2. Common Anchor Vulnerabilities ### 2.1 Missing Constraints Leading to Account Substitution **VULNERABLE - No PDA validation:** ```rust #[derive(Accounts)] pub struct Withdraw<'info> { #[account(mut)] pub pool: Account<'info, Pool>, #[account(mut)] pub user_stake: Account<'info, UserStake>, pub user: Signer<'info>, } ``` **Attack:** User passes a fake `user_stake` account they control with inflated balance. **SECURE - Validate PDAs:** ```rust #[derive(Accounts)] pub struct Withdraw<'info> { #[account( mut, seeds = [b"pool"], bump = pool.bump )] pub pool: Account<'info, Pool>, #[account( mut, seeds = [b"stake", pool.key().as_ref(), user.key().as_ref()], bump = user_stake.bump, has_one = user, has_one = pool )] pub user_stake: Account<'info, UserStake>, pub user: Signer<'info>, } ``` ### 2.2 Incorrect Constraint Ordering Anchor evaluates constraints in this order: 1. `init` / `init_if_needed` / `mut` / `close` 2. `seeds` and `bump` 3. `has_one` 4. `constraint` 5. Account deserialization **Implications:** - Can't use deserialized data in `seeds` - `constraint` expressions can use deserialized data - `close` at end ensures account data available for other checks ### 2.3 Over-Reliance on init_if_needed Covered in section 1.2. Key takeaway: **Avoid `init_if_needed` unless absolutely necessary.** ### 2.4 Missing mut on Accounts **VULNERABLE - Missing mut:** ```rust #[derive(Accounts)] pub struct Deposit<'info> { pub vault: Account<'info, Vault>, // Missing mut! pub user: Signer<'info>, } pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> { ctx.accounts.vault.balance += amount; // Runtime error! Ok(()) } ``` **SECURE:** ```rust #[derive(Accounts)] pub struct Deposit<'info> { #[account(mut)] pub vault: Account<'info, Vault>, pub user: Signer<'info>, } ``` ### 2.5 PDA Bump Not Using Canonical Bump **VULNERABLE - Using non-canonical bump:** ```rust pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let (pda, bump) = Pubkey::find_program_address( &[b"vault"], ctx.program_id ); // Storing bump separately is fine, but must validate it ctx.accounts.vault.bump = bump; Ok(()) } // Later, using wrong bump #[account( seeds = [b"vault"], bump = 254 // WRONG - not canonical! )] pub vault: Account<'info, Vault>, ``` **SECURE - Always use canonical bump:** ```rust #[derive(Accounts)] pub struct Initialize<'info> { #[account( init, payer = payer, space = 8 + Vault::INIT_SPACE, seeds = [b"vault"], bump // Anchor finds canonical bump )] pub vault: Account<'info, Vault>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } pub fn initialize(ctx: Context<Initialize>) -> Result<()> { ctx.accounts.vault.bump = ctx.bumps.vault; // Store canonical bump Ok(()) } ``` ### 2.6 Account Reloading After CPI Mutations **VULNERABLE - Stale account data after CPI:** ```rust pub fn compound_rewards(ctx: Context<CompoundRewards>) -> Result<()> { // CPI to claim rewards (mutates user_rewards account) rewards_program::cpi::claim_rewards( CpiContext::new( ctx.accounts.rewards_program.to_account_info(), ClaimRewards { user_rewards: ctx.accounts.user_rewards.to_account_info(), } ) )?; // WRONG - using stale data! let rewards = ctx.accounts.user_rewards.amount; // Reinvest... Ok(()) } ``` **SECURE - Reload account after CPI:** ```rust pub fn compound_rewards(ctx: Context<CompoundRewards>) -> Result<()> { rewards_program::cpi::claim_rewards( CpiContext::new( ctx.accounts.rewards_program.to_account_info(), ClaimRewards { user_rewards: ctx.accounts.user_rewards.to_account_info(), } ) )?; // Reload account to get fresh data ctx.accounts.user_rewards.reload()?; let rewards = ctx.accounts.user_rewards.amount; // Reinvest... Ok(()) } ``` ## 3. Anchor CPI Security ### 3.1 Using Program<'info, T> for Program Validation **VULNERABLE - Using AccountInfo for program:** ```rust #[derive(Accounts)] pub struct CallExternal<'info> { /// CHECK: This is dangerous! pub external_program: AccountInfo<'info>, } ``` **SECURE - Using Program<'info, T>:** ```rust #[derive(Accounts)] pub struct CallExternal<'info> { pub external_program: Program<'info, ExternalProgram>, } ``` `Program<'info, T>` validates: - Account is executable - Account owner is BPF Loader - Account key matches expected program ID ### 3.2 CpiContext Usage Patterns **Basic CPI:** ```rust use anchor_lang::prelude::*; use anchor_spl::token::{self, Transfer}; pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); token::transfer(cpi_ctx, amount)?; Ok(()) } ``` ### 3.3 with_signer for PDA Signing **SECURE - PDA signing with CPI:** ```rust pub fn transfer_from_vault(ctx: Context<TransferFromVault>, amount: u64) -> Result<()> { let authority_bump = ctx.accounts.vault.authority_bump; let authority_seeds = &[ b"vault-authority", &[authority_bump] ]; let signer_seeds = &[&authority_seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.vault_token_account.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_ctx = CpiContext::new_with_signer( cpi_program, cpi_accounts, signer_seeds // PDA can now sign! ); token::transfer(cpi_ctx, amount)?; Ok(()) } ``` ### 3.4 Validating CPI Return Data **SECURE - Check CPI return values:** ```rust pub fn safe_cpi_call(ctx: Context<SafeCpiCall>) -> Result<()> { let result = external_program::cpi::risky_operation( CpiContext::new( ctx.accounts.external_program.to_account_info(), RiskyOperation { /* ... */ } ) )?; // Validate return data require!( result.get().success, ErrorCode::CpiOperationFailed ); Ok(()) } ``` ### 3.5 Avoiding Arbitrary CPI Targets **VULNERABLE - Arbitrary CPI target:** ```rust #[derive(Accounts)] pub struct ArbitraryCpi<'info> { /// CHECK: DANGEROUS - allows any program! pub target_program: AccountInfo<'info>, } ``` **SECURE - Constrained CPI targets:** ```rust #[derive(Accounts)] pub struct SafeCpi<'info> { // Option 1: Type-safe program constraint pub token_program: Program<'info, Token>, // Option 2: Explicit allowlist #[account( constraint = allowed_programs.contains(&other_program.key()) @ ErrorCode::UnauthorizedProgram )] pub other_program: Program<'info, OtherProgram>, } ``` ## 4. Account Type Safety ### 4.1 Account Discriminators Anchor automatically adds an 8-byte discriminator to each account type (first 8 bytes of SHA256 hash of `"account:<AccountName>"`). **How it protects you:** ```rust #[account] pub struct Vault { pub authority: Pubkey, pub balance: u64, } #[account] pub struct UserAccount { pub authority: Pubkey, pub balance: u64, } // Anchor prevents this type confusion: #[derive(Accounts)] pub struct Withdraw<'info> { #[account(mut)] pub vault: Account<'info, Vault>, // Won't deserialize UserAccount! } ``` **Manual discriminator handling:** ```rust impl Vault { pub const DISCRIMINATOR: [u8; 8] = [/* computed at compile time */]; } // Checking discriminator manually let discriminator = &data[0..8]; require!( discriminator == Vault::DISCRIMINATOR, ErrorCode::InvalidAccountType ); ``` ### 4.2 Account<'info, T> vs AccountInfo **Account<'info, T>:** - Type-safe deserialization - Automatic discriminator check - Automatic owner check - Immutable/mutable access control **AccountInfo:** - Raw account data - No automatic validation - Use only when necessary (non-Anchor programs, dynamic account types) **VULNERABLE - Using AccountInfo unnecessarily:** ```rust #[derive(Accounts)] pub struct UpdateVault<'info> { /// CHECK: Missing type safety! pub vault: AccountInfo<'info>, } ``` **SECURE - Use Account<'info, T>:** ```rust #[derive(Accounts)] pub struct UpdateVault<'info> { #[account(mut)] pub vault: Account<'info, Vault>, } ``` ### 4.3 AccountLoader for Zero-Copy Accounts For large accounts (>10KB), use zero-copy deserialization: ```rust #[account(zero_copy)] pub struct LargeAccount { pub data: [u8; 100000], } #[derive(Accounts)] pub struct UpdateLargeAccount<'info> { #[account(mut)] pub large_account: AccountLoader<'info, LargeAccount>, } pub fn update(ctx: Context<UpdateLargeAccount>) -> Result<()> { let mut account = ctx.accounts.large_account.load_mut()?; account.data[0] = 42; Ok(()) } ``` **Security note:** Zero-copy accounts use `RefCell` internally. Must call `load()` or `load_mut()` each time you access data to ensure safety. ### 4.4 Type Cosplay Prevention **Attack:** Creating fake accounts with correct discriminator but wrong program owner. **Anchor's defense:** ```rust #[account] #[derive(Default)] pub struct MyAccount { pub data: u64, } // Anchor checks: // 1. Discriminator matches // 2. Owner is this program's ID // 3. Account is properly sized ``` **Additional validation for external accounts:** ```rust #[derive(Accounts)] pub struct UseExternalAccount<'info> { #[account( constraint = external_account.owner == &external_program::ID @ ErrorCode::InvalidAccountOwner )] pub external_account: AccountInfo<'info>, } ``` ## 5. Error Handling Security ### 5.1 Custom Error Codes **Define clear error codes:** ```rust #[error_code] pub enum ErrorCode { #[msg("Insufficient funds for withdrawal")] InsufficientFunds, #[msg("Unauthorized access attempt")] Unauthorized, #[msg("Invalid configuration parameters")] InvalidConfig, #[msg("Arithmetic overflow occurred")] Overflow, } ``` **Use with require! macro:** ```rust pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { require!( ctx.accounts.vault.balance >= amount, ErrorCode::InsufficientFunds ); require!( ctx.accounts.vault.authority == ctx.accounts.user.key(), ErrorCode::Unauthorized ); // Safe to proceed Ok(()) } ``` ### 5.2 Error Propagation Patterns **WRONG - Silencing errors:** ```rust pub fn risky_operation(ctx: Context<RiskyOp>) -> Result<()> { let _ = dangerous_function(); // WRONG - error silenced! Ok(()) } ``` **CORRECT - Propagate errors:** ```rust pub fn risky_operation(ctx: Context<RiskyOp>) -> Result<()> { dangerous_function()?; // Propagate error Ok(()) } ``` ### 5.3 Avoiding Silent Failures **VULNERABLE - No error on failure:** ```rust pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> { if ctx.accounts.from.balance >= amount { ctx.accounts.from.balance -= amount; ctx.accounts.to.balance += amount; } // Returns Ok even if transfer didn't happen! Ok(()) } ``` **SECURE - Explicit error:** ```rust pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> { require!( ctx.accounts.from.balance >= amount, ErrorCode::InsufficientFunds ); ctx.accounts.from.balance -= amount; ctx.accounts.to.balance += amount; Ok(()) } ``` ## 6. Token Program Integration ### 6.1 anchor_spl Security Patterns **SECURE - Using anchor_spl helpers:** ```rust use anchor_spl::token::{self, Token, TokenAccount, Transfer}; #[derive(Accounts)] pub struct TransferTokens<'info> { #[account(mut)] pub from: Account<'info, TokenAccount>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { 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, )?; Ok(()) } ``` ### 6.2 token_interface Usage For Token-2022 compatibility: ```rust use anchor_spl::token_interface::{self, TokenInterface, TokenAccount}; #[derive(Accounts)] pub struct TransferTokens<'info> { #[account(mut)] pub from: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub to: InterfaceAccount<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } ``` ### 6.3 Associated Token Account Constraints **VULNERABLE - Missing ATA validation:** ```rust #[derive(Accounts)] pub struct DepositTokens<'info> { #[account(mut)] pub user_token_account: Account<'info, TokenAccount>, pub user: Signer<'info>, } ``` **SECURE - Validate ATA:** ```rust use anchor_spl::associated_token::AssociatedToken; #[derive(Accounts)] pub struct DepositTokens<'info> { #[account( mut, associated_token::mint = mint, associated_token::authority = user )] pub user_token_account: Account<'info, TokenAccount>, pub user: Signer<'info>, pub mint: Account<'info, Mint>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, } ``` ### 6.4 Token-2022 Extension Handling **Be aware of extensions:** ```rust pub fn handle_transfer(ctx: Context<HandleTransfer>, amount: u64) -> Result<()> { // Token-2022 may have transfer fees, freeze authority, etc. // Always check actual amount received after transfer let before_balance = ctx.accounts.destination.amount; token_interface::transfer_checked( CpiContext::new(/* ... */), amount, ctx.accounts.mint.decimals, )?; ctx.accounts.destination.reload()?; let actual_amount = ctx.accounts.destination.amount - before_balance; // Use actual_amount for accounting Ok(()) } ``` ## 7. Event Security ### 7.1 When to Emit Events Events are critical for: - Indexing and querying program state - Auditing sensitive operations - Monitoring for security incidents **Always emit events for:** - State changes (deposits, withdrawals, config updates) - Authorization changes (role grants, ownership transfers) - Critical operations (program upgrades, emergency actions) ### 7.2 Event Data Validation **SECURE - Validate before emitting:** ```rust #[event] pub struct WithdrawalEvent { pub user: Pubkey, pub amount: u64, pub timestamp: i64, } pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { // Validate first require!( ctx.accounts.vault.balance >= amount, ErrorCode::InsufficientFunds ); // Perform operation ctx.accounts.vault.balance -= amount; // Emit event AFTER successful operation emit!(WithdrawalEvent { user: ctx.accounts.user.key(), amount, timestamp: Clock::get()?.unix_timestamp, }); Ok(()) } ``` ### 7.3 emit! vs emit_cpi! **emit! - Regular event:** ```rust emit!(MyEvent { data: value, }); ``` **emit_cpi! - Event for CPI callers:** ```rust // Use when program is called via CPI and event should be // visible to the calling program emit_cpi!(MyEvent { data: value, }); ``` ## 8. Anchor-Specific Best Practices ### 8.1 Account Space Calculation with InitSpace **SECURE - Using InitSpace derive macro:** ```rust use anchor_lang::prelude::*; #[account] #[derive(InitSpace)] pub struct UserProfile { pub authority: Pubkey, // 32 bytes #[max_len(50)] pub name: String, // 4 + 50 bytes pub created_at: i64, // 8 bytes pub bump: u8, // 1 byte } #[derive(Accounts)] pub struct CreateProfile<'info> { #[account( init, payer = payer, space = 8 + UserProfile::INIT_SPACE, // 8 for discriminator seeds = [b"profile", authority.key().as_ref()], bump )] pub profile: Account<'info, UserProfile>, pub authority: Signer<'info>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } ``` ### 8.2 Remaining Accounts Handling **SECURE - Validate remaining accounts:** ```rust pub fn process_multiple_accounts( ctx: Context<ProcessAccounts>, count: u8, ) -> Result<()> { let remaining = &ctx.remaining_accounts; // Validate count require!( remaining.len() == count as usize, ErrorCode::InvalidAccountCount ); // Validate each account for account_info in remaining.iter() { require!( account_info.is_writable, ErrorCode::AccountNotWritable ); require!( account_info.owner == ctx.program_id, ErrorCode::InvalidAccountOwner ); // Deserialize and validate type let account = Account::<MyAccount>::try_from(account_info)?; // Process account... } Ok(()) } ``` ### 8.3 Instruction Data Validation **SECURE - Validate all inputs:** ```rust pub fn create_proposal( ctx: Context<CreateProposal>, title: String, description: String, execution_delay: i64, ) -> Result<()> { // Validate string lengths require!( title.len() > 0 && title.len() <= 100, ErrorCode::InvalidTitleLength ); require!( description.len() <= 1000, ErrorCode::DescriptionTooLong ); // Validate numeric ranges require!( execution_delay >= MIN_DELAY && execution_delay <= MAX_DELAY, ErrorCode::InvalidExecutionDelay ); // Validate against overflow let execution_time = Clock::get()? .unix_timestamp .checked_add(execution_delay) .ok_or(ErrorCode::Overflow)?; ctx.accounts.proposal.title = title; ctx.accounts.proposal.description = description; ctx.accounts.proposal.execution_time = execution_time; Ok(()) } ``` ### 8.4 Upgradability Considerations **SECURE - Handle program upgrades safely:** ```rust #[account] #[derive(InitSpace)] pub struct ProgramConfig { pub version: u8, pub upgrade_authority: Pubkey, pub paused: bool, } pub fn migrate(ctx: Context<Migrate>) -> Result<()> { let config = &mut ctx.accounts.config; // Check current version require!( config.version < CURRENT_VERSION, ErrorCode::AlreadyMigrated ); // Perform version-specific migrations match config.version { 0 => { // Migrate from v0 to v1 // Add new fields, transform data, etc. } 1 => { // Migrate from v1 to v2 } _ => return Err(ErrorCode::UnsupportedVersion.into()), } config.version = CURRENT_VERSION; Ok(()) } ``` **Emergency pause pattern:** ```rust #[derive(Accounts)] pub struct SensitiveOperation<'info> { #[account( constraint = !config.paused @ ErrorCode::ProgramPaused )] pub config: Account<'info, ProgramConfig>, // ... other accounts } ``` This ensures you can pause the program in case of emergencies during or after upgrades. -
anchor.md 34.7 KB
# Anchor Framework Reference This reference covers Anchor-specific features and patterns. For general Solana concepts (accounts, PDAs, CPIs, etc.), see the other reference files in this directory. ## Table of Contents - [Installation and Setup](#installation-and-setup) - [Anchor Macros](#anchor-macros) - [Program Structure](#program-structure) - [Account Validation Constraints](#account-validation-constraints) - [IDL (Interface Description Language)](#idl-interface-description-language) - [TypeScript Client](#typescript-client) - [Rust Client](#rust-client) - [Anchor CLI Commands](#anchor-cli-commands) - [Token Integration (anchor-spl)](#token-integration-anchor-spl) - [Testing with Anchor](#testing-with-anchor) - [Anchor Features](#anchor-features) - [Common Patterns](#common-patterns) - [Error Handling](#error-handling) --- ## Installation and Setup ### Quick Install (Mac/Linux) ```bash # Install all dependencies (Rust, Solana CLI, Anchor) curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash ``` ### Install Anchor with AVM Anchor Version Manager (avm) manages multiple Anchor CLI versions: ```bash # Install AVM cargo install --git https://github.com/coral-xyz/anchor avm --force # Install latest Anchor avm install latest avm use latest # Install specific version avm install 0.32.1 avm use 0.32.1 # Install from commit hash avm install 0.30.1-cfe82aa682138f7c6c58bf7a78f48f7d63e9e466 avm use 0.30.1-cfe82aa ``` ### Verify Installation ```bash anchor --version # Should output: anchor-cli 0.32.1 solana --version # Recommended: solana-cli 2.3.0+ rustc --version # Required: 1.89.0+ for IDL builds ``` ### Solana Playground (No Install) Develop in browser at https://beta.solpg.io/ --- ## Anchor Macros ### Core Macros Overview 1. **`declare_id!`** - Declares the program's on-chain address 2. **`#[program]`** - Defines the program module containing instructions 3. **`#[derive(Accounts)]`** - Defines account validation structs 4. **`#[account]`** - Defines custom account types 5. **`#[error_code]`** - Defines custom error enums 6. **`#[event]`** - Defines event structs for logging ### declare_id! Macro ```rust use anchor_lang::prelude::*; // Program ID from /target/deploy/program_name.json declare_id!("11111111111111111111111111111111"); ``` Sync program IDs after building: ```bash anchor keys sync ``` ### #[program] Macro Marks the module containing instruction handlers: ```rust #[program] pub mod my_program { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; msg!("Data set to: {}!", data); Ok(()) } pub fn update(ctx: Context<Update>, new_data: u64) -> Result<()> { ctx.accounts.account.data = new_data; Ok(()) } } ``` **Context<T> provides:** - `ctx.accounts` - Validated accounts (type T) - `ctx.program_id` - Current program's ID - `ctx.remaining_accounts` - Additional accounts not in struct - `ctx.bumps` - PDA bump seeds (struct with fields matching PDA account names) ### #[derive(Accounts)] Macro Defines account validation structs: ```rust #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = signer, space = 8 + 8)] pub new_account: Account<'info, NewAccount>, #[account(mut)] pub signer: Signer<'info>, pub system_program: Program<'info, System>, } ``` **Validation happens in two ways:** 1. **Account Types** - Signer, Account<'info, T>, Program<'info, T>, etc. 2. **Account Constraints** - `#[account(...)]` attribute constraints ### #[account] Macro Defines custom account data structures: ```rust #[account] pub struct NewAccount { pub data: u64, // 8 bytes pub owner: Pubkey, // 32 bytes pub bump: u8, // 1 byte } ``` **Automatically implements:** - Account discriminator (first 8 bytes) - Serialization/deserialization (Borsh) - Owner validation (owned by program) **Account discriminator** = first 8 bytes of SHA256(`"account:NewAccount"`) ### #[error_code] Macro Defines custom program errors: ```rust #[error_code] pub enum ErrorCode { #[msg("Amount must be greater than zero")] InvalidAmount, #[msg("Insufficient funds")] InsufficientFunds, } ``` Usage: ```rust require!(amount > 0, ErrorCode::InvalidAmount); ``` ### #[event] Macro Defines event structs for logging: ```rust #[event] pub struct TransferEvent { pub from: Pubkey, pub to: Pubkey, pub amount: u64, } // Emit via program logs pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> { emit!(TransferEvent { from: ctx.accounts.from.key(), to: ctx.accounts.to.key(), amount, }); Ok(()) } ``` --- ## Program Structure ### Complete Example ```rust use anchor_lang::prelude::*; declare_id!("YourProgramIdHere11111111111111111111111"); #[program] mod my_program { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.new_account.data = data; ctx.accounts.new_account.authority = ctx.accounts.authority.key(); Ok(()) } pub fn update(ctx: Context<Update>, new_data: u64) -> Result<()> { ctx.accounts.account.data = new_data; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = authority, space = 8 + 8 + 32)] pub new_account: Account<'info, MyAccount>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Update<'info> { #[account( mut, has_one = authority )] pub account: Account<'info, MyAccount>, pub authority: Signer<'info>, } #[account] pub struct MyAccount { pub data: u64, pub authority: Pubkey, } ``` ### Space Calculation Use `InitSpace` derive macro: ```rust #[account] #[derive(InitSpace)] pub struct MyAccount { pub data: u64, // 8 bytes #[max_len(50)] pub name: String, // 4 + 50 bytes pub authority: Pubkey, // 32 bytes } // INIT_SPACE = 8 + 4 + 50 + 32 = 94 bytes #[account(init, payer = payer, space = 8 + MyAccount::INIT_SPACE)] pub account: Account<'info, MyAccount>, ``` **Space = 8 (discriminator) + account data size** --- ## Account Validation Constraints ### Common Constraints #### init - Create New Account ```rust #[account( init, payer = payer, space = 8 + 8 )] pub new_account: Account<'info, Counter>, ``` #### init_if_needed - Create if Doesn't Exist ```rust #[account( init_if_needed, payer = payer, space = 8 + 8 )] pub account: Account<'info, Counter>, ``` Requires `init-if-needed` feature in Cargo.toml: ```toml [dependencies] anchor-lang = { version = "0.32.1", features = ["init-if-needed"] } ``` #### mut - Mutable Account ```rust #[account(mut)] pub account: Account<'info, Counter>, ``` #### signer - Requires Signature ```rust #[account(signer)] pub authority: AccountInfo<'info>, // Or use Signer<'info> type pub authority: Signer<'info>, ``` #### close - Close Account ```rust #[account( mut, close = receiver // Send lamports to receiver )] pub account_to_close: Account<'info, MyAccount>, #[account(mut)] pub receiver: SystemAccount<'info>, ``` ### PDA Constraints #### seeds + bump - Validate PDA ```rust #[account( seeds = [b"vault", authority.key().as_ref()], bump )] pub vault: Account<'info, Vault>, ``` Access bump in instruction: ```rust pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let bump = ctx.bumps.vault; ctx.accounts.vault.bump = bump; Ok(()) } ``` #### seeds + bump + init - Create PDA Account ```rust #[account( init, payer = payer, space = 8 + 32 + 1, seeds = [b"vault", authority.key().as_ref()], bump )] pub vault: Account<'info, Vault>, ``` ### Validation Constraints #### has_one - Field Matches Account ```rust #[account( mut, has_one = authority // Checks account.authority == authority.key() )] pub account: Account<'info, MyAccount>, pub authority: Signer<'info>, ``` #### address - Matches Specific Address ```rust #[account(address = admin_pubkey)] pub admin: Signer<'info>, ``` #### owner - Validates Owner Program ```rust #[account(owner = token::ID)] pub token_account: AccountInfo<'info>, ``` #### constraint - Custom Validation ```rust #[account( constraint = account.data > 0 @ ErrorCode::InvalidData )] pub account: Account<'info, MyAccount>, ``` ### Token Constraints #### mint - Create/Validate Mint ```rust use anchor_spl::token_interface::{Mint, TokenInterface}; #[account( init, payer = payer, mint::decimals = 6, mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] pub mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, ``` #### token - Create/Validate Token Account ```rust use anchor_spl::token_interface::{TokenAccount, TokenInterface}; #[account( init, payer = payer, token::mint = mint, token::authority = owner, token::token_program = token_program, seeds = [b"vault"], bump )] pub vault: InterfaceAccount<'info, TokenAccount>, ``` #### associated_token - Create/Validate ATA ```rust use anchor_spl::{ associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface}, }; #[account( init, payer = payer, associated_token::mint = mint, associated_token::authority = owner, associated_token::token_program = token_program, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, pub owner: SystemAccount<'info>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, ``` --- ## IDL (Interface Description Language) ### What is the IDL? The IDL is a JSON file describing your program's interface: - Instructions (name, accounts, arguments) - Account types (structs) - Custom types (enums, type aliases) - Events - Errors - Discriminators ### IDL Generation Enable IDL build feature in `Cargo.toml`: ```toml [features] idl-build = ["anchor-lang/idl-build"] ``` Build program and generate IDL: ```bash anchor build # Builds program + IDL anchor idl build # Only builds IDL ``` IDL output location: `target/idl/<program_name>.json` ### IDL Structure Example ```json { "address": "8HupNBr7SBhBLcBsLhbtes3tCarBm6Bvpqp5AfVjHuj8", "metadata": { "name": "example", "version": "0.1.0", "spec": "0.1.0" }, "instructions": [ { "name": "initialize", "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], "accounts": [ { "name": "new_account", "writable": true, "signer": true }, { "name": "signer", "writable": true, "signer": true }, { "name": "system_program", "address": "11111111111111111111111111111111" } ], "args": [ { "name": "data", "type": "u64" } ] } ], "accounts": [ { "name": "NewAccount", "discriminator": [123, 45, 67, 89, 101, 112, 131, 145] } ], "types": [ { "name": "NewAccount", "type": { "kind": "struct", "fields": [ { "name": "data", "type": "u64" } ] } } ] } ``` ### Instruction Discriminator 8-byte identifier for each instruction: ``` discriminator = SHA256("global:initialize")[0..8] ``` Automatically handled by Anchor client. ### Account Discriminator 8-byte identifier for each account type: ``` discriminator = SHA256("account:NewAccount")[0..8] ``` Used for: - Account type verification on deserialization - Type safety checks ### IDL Deployment Deploy IDL on-chain: ```bash anchor deploy # Deploys program + IDL anchor deploy --no-idl # Deploy program only anchor idl init <PROGRAM_ID> -f target/idl/program.json ``` Fetch IDL from chain: ```bash anchor idl fetch <PROGRAM_ID> ``` --- ## TypeScript Client ### Installation ```bash npm install @coral-xyz/anchor @solana/web3.js # or yarn add @coral-xyz/anchor @solana/web3.js ``` > **Legacy dependency:** Anchor's TS client requires `@solana/web3.js` v1 as a peer dependency. This is the only reason to install web3.js - it is in maintenance mode. For non-Anchor client code, use [`@solana/kit`](https://solanakit.org) instead. Use [`@solana/compat`](https://www.npmjs.com/package/@solana/compat) to bridge between Kit and legacy web3.js types when needed. > > **For new dApps:** Use framework-kit (`@solana/client` + `@solana/react-hooks`) instead of `@solana/wallet-adapter-react` for wallet connections. Anchor's `.methods()` API works with either approach. See [client-development.md](client-development.md). ### Setup Program Instance #### With Wallet (Frontend) > **Legacy pattern** - uses `@solana/wallet-adapter-react`. For new projects, use framework-kit for wallet management. See [client-development.md](client-development.md). The Anchor `.methods()` API below works the same regardless. ```typescript import { Program, AnchorProvider, setProvider } from "@coral-xyz/anchor"; import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"; import type { MyProgram } from "./types/my_program"; import idl from "./idl/my_program.json"; const { connection } = useConnection(); const wallet = useAnchorWallet(); const provider = new AnchorProvider(connection, wallet, {}); setProvider(provider); const program = new Program(idl as MyProgram, { connection }); ``` #### Without Wallet (Read-Only) ```typescript import { Connection, PublicKey } from "@solana/web3.js"; import { Program } from "@coral-xyz/anchor"; import idl from "./idl/my_program.json"; const connection = new Connection("https://api.devnet.solana.com"); const program = new Program(idl, { connection }); ``` ### Invoke Instructions #### Using .rpc() - Send Transaction ```typescript import { Keypair, SystemProgram } from "@solana/web3.js"; import BN from "bn.js"; const newAccountKp = new Keypair(); const data = new BN(42); const txSignature = await program.methods .initialize(data) .accounts({ newAccount: newAccountKp.publicKey, signer: wallet.publicKey, systemProgram: SystemProgram.programId, }) .signers([newAccountKp]) .rpc(); console.log("Transaction:", txSignature); ``` #### Using .instruction() - Build Instruction ```typescript const ix = await program.methods .initialize(data) .accounts({ newAccount: newAccountKp.publicKey, signer: wallet.publicKey, systemProgram: SystemProgram.programId, }) .instruction(); // Add to transaction const tx = new Transaction().add(ix); ``` #### Using .transaction() - Build Transaction ```typescript const tx = await program.methods .initialize(data) .accounts({ /* ... */ }) .transaction(); // Sign and send tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; tx.sign(wallet, newAccountKp); const signature = await connection.sendRawTransaction(tx.serialize()); ``` ### Fetch Accounts #### Fetch Single Account ```typescript const accountData = await program.account.myAccount.fetch(accountPubkey); console.log("Data:", accountData.data.toString()); ``` #### Fetch All Accounts ```typescript const accounts = await program.account.myAccount.all(); accounts.forEach((account) => { console.log("Pubkey:", account.publicKey.toString()); console.log("Data:", account.account.data); }); ``` #### Fetch with Filters ```typescript const accounts = await program.account.myAccount.all([ { memcmp: { offset: 8, // After discriminator bytes: authority.toBase58(), }, }, ]); ``` ### Event Listeners ```typescript const listenerId = program.addEventListener( "TransferEvent", (event, slot) => { console.log("From:", event.from.toString()); console.log("To:", event.to.toString()); console.log("Amount:", event.amount.toString()); } ); // Remove listener program.removeEventListener(listenerId); ``` --- ## Rust Client ### Dependencies Add to `Cargo.toml`: ```toml [dependencies] anchor-client = { version = "0.32.1", features = ["async"] } anchor-lang = "0.32.1" solana-sdk = "2.3.0" tokio = { version = "1.0", features = ["full"] } ``` ### Generate Client with declare_program! Place IDL in `/idls/program_name.json`: ```rust use anchor_lang::prelude::*; declare_program!(example); use example::{ accounts::Counter, client::{accounts, args}, }; ``` ### Example Client ```rust use anchor_client::{ solana_client::rpc_client::RpcClient, solana_sdk::{ commitment_config::CommitmentConfig, signature::Keypair, signer::Signer, system_program, }, Client, Cluster, }; use std::rc::Rc; #[tokio::main] async fn main() -> anyhow::Result<()> { let connection = RpcClient::new_with_commitment( "http://127.0.0.1:8899", CommitmentConfig::confirmed(), ); let payer = Keypair::new(); let counter = Keypair::new(); // Create program client let provider = Client::new_with_options( Cluster::Localnet, Rc::new(payer), CommitmentConfig::confirmed(), ); let program = provider.program(example::ID)?; // Build instruction let ix = program .request() .accounts(accounts::Initialize { counter: counter.pubkey(), payer: program.payer(), system_program: system_program::ID, }) .args(args::Initialize) .instructions()? .remove(0); // Send transaction let signature = program .request() .instruction(ix) .signer(&counter) .send() .await?; println!("Transaction: {}", signature); // Fetch account let account: Counter = program.account::<Counter>(counter.pubkey()).await?; println!("Count: {}", account.count); Ok(()) } ``` --- ## Anchor CLI Commands ### Project Commands ```bash # Initialize new project anchor init my-project anchor init my-project --test-template rust # Rust tests anchor init my-project --test-template mollusk # Mollusk tests # Create new program in workspace anchor new my-program ``` ### Build Commands ```bash # Build all programs anchor build # Build specific program anchor build --program-name my-program # Build without IDL generation anchor build --no-idl # Verifiable build (uses solana-verify) anchor build --verifiable ``` ### Deploy Commands ```bash # Deploy to cluster in Anchor.toml anchor deploy # Deploy specific program anchor deploy --program-name my-program # Deploy without IDL anchor deploy --no-idl # Deploy with additional program args anchor deploy -- --max-len 200000 ``` ### Test Commands ```bash # Build + deploy + test anchor test # Skip local validator (use running validator) anchor test --skip-local-validator # Test specific program anchor test --program-name my-program # Skip IDL build anchor test --no-idl ``` ### IDL Commands ```bash # Build IDL only anchor idl build # Initialize IDL on-chain anchor idl init <PROGRAM_ID> -f target/idl/program.json # Fetch IDL from chain anchor idl fetch <PROGRAM_ID> # Upgrade on-chain IDL anchor idl upgrade <PROGRAM_ID> -f target/idl/program.json # Get IDL authority anchor idl authority <PROGRAM_ID> # Set new IDL authority anchor idl set-authority <PROGRAM_ID> --new-authority <NEW_AUTHORITY> ``` ### Other Commands ```bash # Sync program IDs anchor keys sync # List program keypairs anchor keys list # Expand macros anchor expand anchor expand --program-name my-program # Verify deployed program anchor verify -p <program-name> <PROGRAM_ID> # Run migration script anchor migrate # Close deployed program (reclaim rent) solana program close <PROGRAM_ID> ``` ### Local Validator ```bash # Start local validator solana-test-validator # Start with program loaded solana-test-validator --bpf-program <PROGRAM_ID> target/deploy/program.so # Configure in Anchor.toml [test.validator] url = "https://api.devnet.solana.com" [[test.validator.clone]] address = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" # Clone USDC mint ``` --- ## Token Integration (anchor-spl) ### Dependencies ```toml [dependencies] anchor-spl = { version = "0.32.1", features = ["metadata"] } ``` ### Token Interface (Token + Token-2022) Use `token_interface` for compatibility with both Token Program and Token Extensions: ```rust use anchor_spl::token_interface::{ self, Mint, MintTo, TokenAccount, TokenInterface, TransferChecked }; ``` ### Create Mint ```rust #[derive(Accounts)] pub struct CreateMint<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, mint::decimals = 6, mint::authority = mint_authority, )] pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Mint authority pub mint_authority: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } ``` ### Create Token Account (ATA) ```rust use anchor_spl::associated_token::AssociatedToken; #[derive(Accounts)] pub struct CreateTokenAccount<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, associated_token::mint = mint, associated_token::authority = owner, associated_token::token_program = token_program, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, pub owner: SystemAccount<'info>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } ``` ### Mint Tokens ```rust pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> { let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_context = CpiContext::new(cpi_program, cpi_accounts); token_interface::mint_to(cpi_context, amount)?; Ok(()) } ``` ### Transfer Tokens ```rust pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { let decimals = ctx.accounts.mint.decimals; let cpi_accounts = TransferChecked { from: ctx.accounts.from.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_context = CpiContext::new(cpi_program, cpi_accounts); token_interface::transfer_checked(cpi_context, amount, decimals)?; Ok(()) } ``` ### PDA as Token Authority ```rust #[derive(Accounts)] pub struct MintWithPDA<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( init, payer = payer, mint::decimals = 6, mint::authority = mint, // PDA is authority seeds = [b"mint"], bump )] pub mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } pub fn mint_with_pda(ctx: Context<MintWithPDA>, amount: u64) -> Result<()> { let seeds = &[b"mint".as_ref(), &[ctx.bumps.mint]]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.mint.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ).with_signer(signer_seeds); token_interface::mint_to(cpi_context, amount)?; Ok(()) } ``` --- ## Testing with Anchor ### TypeScript Tests (Default) Test file location: `tests/my-program.ts` ```typescript import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { MyProgram } from "../target/types/my_program"; import { expect } from "chai"; describe("my-program", () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.MyProgram as Program<MyProgram>; it("Initializes account", async () => { const newAccount = anchor.web3.Keypair.generate(); await program.methods .initialize(new anchor.BN(42)) .accounts({ newAccount: newAccount.publicKey, signer: provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([newAccount]) .rpc(); const account = await program.account.myAccount.fetch( newAccount.publicKey ); expect(account.data.toNumber()).to.equal(42); }); }); ``` ### Rust Tests with LiteSVM Initialize project with Rust tests: ```bash anchor init my-project --test-template rust ``` Test file: `tests/src/test_initialize.rs` ```rust use anchor_client::anchor_lang::prelude::*; use anchor_client::anchor_lang::solana_program::system_program; use anchor_lang_lite_svm::LiteSVM; #[test] fn test_initialize() { let mut svm = LiteSVM::new(); let program_id = svm.deploy_program("target/deploy/my_program.so"); let payer = Keypair::new(); let counter = Keypair::new(); svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap(); let ix = my_program::instruction::Initialize { new_account: counter.pubkey(), signer: payer.pubkey(), system_program: system_program::ID, }; let tx = svm.send_transaction(vec![ix], &[&payer, &counter]).unwrap(); // Fetch and verify account let account_data = svm.get_account(&counter.pubkey()).unwrap(); // Verify data... } ``` ### Test Configuration (Anchor.toml) ```toml [test] # Startup timeout for validator startup_wait = 10000 [test.validator] # URL to clone accounts from url = "https://api.mainnet-beta.solana.com" # Clone accounts [[test.validator.clone]] address = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" # Load account from JSON [[test.validator.account]] address = "MyAccount111111111111111111111111111111111" filename = "tests/fixtures/my-account.json" # Set program as upgradeable [test.validator.upgradeable] my_program = true ``` --- ## Anchor Features ### Custom Errors ```rust #[error_code] pub enum ErrorCode { #[msg("Amount must be greater than zero")] InvalidAmount, #[msg("Authority mismatch")] Unauthorized, } // Usage require!(amount > 0, ErrorCode::InvalidAmount); require_keys_eq!(account.owner, authority.key(), ErrorCode::Unauthorized); ``` **Error macros:** - `require!(condition, error)` - Condition must be true - `require_eq!(a, b, error)` - Values must be equal - `require_neq!(a, b, error)` - Values must not be equal - `require_keys_eq!(a, b, error)` - Pubkeys must match - `require_keys_neq!(a, b, error)` - Pubkeys must not match - `require_gt!(a, b, error)` - a > b - `require_gte!(a, b, error)` - a >= b ### Events #### emit! (Program Logs) ```rust #[event] pub struct TransferEvent { pub from: Pubkey, pub to: Pubkey, pub amount: u64, } pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> { emit!(TransferEvent { from: ctx.accounts.from.key(), to: ctx.accounts.to.key(), amount, }); Ok(()) } ``` #### emit_cpi! (CPI Data) Enable feature: ```toml [dependencies] anchor-lang = { version = "0.32.1", features = ["event-cpi"] } ``` Usage: ```rust #[event_cpi] #[derive(Accounts)] pub struct EmitEvent {} pub fn emit_event(ctx: Context<EmitEvent>, msg: String) -> Result<()> { emit_cpi!(CustomEvent { message: msg }); Ok(()) } ``` ### Zero-Copy Accounts For large accounts (>10KB): ```toml [dependencies] bytemuck = { version = "1.20.0", features = ["min_const_generics"] } ``` ```rust #[account(zero_copy)] pub struct LargeData { pub data: [u8; 10000], } #[derive(Accounts)] pub struct Initialize<'info> { #[account( init, payer = payer, space = 8 + std::mem::size_of::<LargeData>() )] pub large_account: AccountLoader<'info, LargeData>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let mut large_account = ctx.accounts.large_account.load_init()?; large_account.data[0] = 42; Ok(()) } pub fn update(ctx: Context<Update>) -> Result<()> { let mut large_account = ctx.accounts.large_account.load_mut()?; large_account.data[0] = 100; Ok(()) } ``` **For >10240 bytes**, use `zero` constraint + create account separately: ```rust #[account(zero)] // Instead of init pub large_account: AccountLoader<'info, LargeData>, ``` ### declare_program! (Dependency-Free CPI) Place IDL in `/idls/program_name.json`: ```rust declare_program!(example); use example::{ accounts::Counter, cpi::{self, accounts::Initialize}, program::Example, }; // CPI to other program pub fn call_example(ctx: Context<CallExample>) -> Result<()> { let cpi_ctx = CpiContext::new( ctx.accounts.example_program.to_account_info(), Initialize { counter: ctx.accounts.counter.to_account_info(), payer: ctx.accounts.payer.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), }, ); cpi::initialize(cpi_ctx)?; Ok(()) } ``` --- ## Common Patterns ### Store Bump Seed ```rust #[account] pub struct Vault { pub authority: Pubkey, pub bump: u8, } pub fn initialize(ctx: Context<Initialize>) -> Result<()> { ctx.accounts.vault.authority = ctx.accounts.authority.key(); ctx.accounts.vault.bump = ctx.bumps.vault; Ok(()) } // Use stored bump let seeds = &[ b"vault", ctx.accounts.vault.authority.as_ref(), &[ctx.accounts.vault.bump] ]; ``` ### Multi-Seed PDAs ```rust #[account( init, payer = payer, space = 8 + UserAccount::INIT_SPACE, seeds = [ b"user", user.key().as_ref(), &counter.to_le_bytes() ], bump )] pub user_account: Account<'info, UserAccount>, ``` ### CPI with PDA Signer ```rust pub fn transfer_with_pda(ctx: Context<Transfer>, amount: u64) -> Result<()> { let seeds = &[ b"vault", &[ctx.bumps.vault] ]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = TransferChecked { from: ctx.accounts.from.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.vault.to_account_info(), }; let cpi_ctx = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ).with_signer(signer_seeds); token_interface::transfer_checked(cpi_ctx, amount, decimals)?; Ok(()) } ``` ### Remaining Accounts ```rust pub fn process_multiple(ctx: Context<Process>) -> Result<()> { for account_info in ctx.remaining_accounts.iter() { let account = Account::<SomeAccount>::try_from(account_info)?; msg!("Processing: {}", account_info.key()); // Process account... } Ok(()) } #[derive(Accounts)] pub struct Process<'info> { pub authority: Signer<'info>, // Additional accounts in remaining_accounts } ``` ### Close Account Pattern ```rust #[derive(Accounts)] pub struct CloseAccount<'info> { #[account( mut, close = receiver, // Sends lamports to receiver has_one = authority )] pub account: Account<'info, MyAccount>, pub authority: Signer<'info>, #[account(mut)] /// CHECK: Receives lamports pub receiver: UncheckedAccount<'info>, } ``` ### Dynamic Account Space ```rust #[derive(Accounts)] #[instruction(name: String)] pub struct Create<'info> { #[account( init, payer = payer, space = 8 + 4 + name.len() + 8 )] pub item: Account<'info, Item>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct Item { pub name: String, pub count: u64, } ``` --- ## Error Handling ### Built-in Anchor Errors Anchor provides built-in errors in `ErrorCode` enum. Examples: - `ConstraintHasOne` - has_one constraint failed - `ConstraintSigner` - Account not a signer - `ConstraintMut` - Account not mutable - `ConstraintSeeds` - Seeds constraint failed - `AccountNotInitialized` - Account discriminator is zero ### Custom Error Implementation ```rust #[error_code] pub enum ErrorCode { #[msg("Amount must be greater than zero")] InvalidAmount, #[msg("Insufficient balance: required {}, available {}")] InsufficientBalance, #[msg("Unauthorized access")] Unauthorized, } pub fn validate_amount(ctx: Context<Validate>, amount: u64) -> Result<()> { require!(amount > 0, ErrorCode::InvalidAmount); require!( ctx.accounts.account.balance >= amount, ErrorCode::InsufficientBalance ); require_keys_eq!( ctx.accounts.account.owner, ctx.accounts.authority.key(), ErrorCode::Unauthorized ); Ok(()) } ``` ### Error Numbers Anchor errors use this numbering: - `0-1000` - Internal Anchor errors - `1000-2000` - Reserved - `2000-3000` - Custom program errors (from #[error_code]) - `3000+` - Additional custom errors ### TypeScript Error Handling ```typescript try { await program.methods .transfer(amount) .accounts({ /* ... */ }) .rpc(); } catch (error) { if (error.code === 6000) { // Custom error code console.log("Custom error:", error.msg); } console.log("Error logs:", error.logs); } ``` --- ## Anchor.toml Configuration ```toml [toolchain] anchor_version = "0.32.1" solana_version = "2.3.0" [features] resolution = true # IDL account resolution seeds = false skip-lint = false [programs.localnet] my_program = "YourProgramIdHere11111111111111111111111" [programs.devnet] my_program = "YourProgramIdHere11111111111111111111111" [programs.mainnet] my_program = "YourProgramIdHere11111111111111111111111" [provider] cluster = "localnet" # or devnet, mainnet-beta wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" [test] startup_wait = 10000 [test.validator] url = "https://api.devnet.solana.com" [[test.validator.clone]] address = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" [test.validator.upgradeable] my_program = true [workspace] types = "app/src/idl/" members = ["programs/*"] package_manager = "yarn" # npm, yarn, pnpm, bun ``` --- ## Additional Resources - **Official Docs**: https://www.anchor-lang.com - **GitHub**: https://github.com/coral-xyz/anchor - **Examples**: https://github.com/coral-xyz/anchor/tree/master/tests - **Discord**: https://discord.gg/anchor For general Solana concepts (accounts, PDAs, CPIs, transactions, etc.), refer to other reference files in this directory. -
builtin-programs.md 21.8 KB
# Built-in Programs This reference provides comprehensive coverage of Solana's built-in programs for native Rust development, focusing on the System Program and Compute Budget Program. ## Table of Contents 1. [Overview of Built-in Programs](#overview-of-built-in-programs) 2. [System Program](#system-program) 3. [Compute Budget Program](#compute-budget-program) 4. [Other Built-in Programs](#other-built-in-programs) 5. [CPI Patterns](#cpi-patterns) 6. [Best Practices](#best-practices) --- ## Overview of Built-in Programs **Built-in programs** (also called native programs) are fundamental Solana programs that provide core blockchain functionality. ### Key Built-in Programs | Program | Program ID | Purpose | |---------|-----------|---------| | **System Program** | `11111111111111111111111111111111` | Account creation, transfers, allocation | | **Compute Budget** | `ComputeBudget111111111111111111111111111111` | CU limits, heap size, priority fees | | **BPF Loader** | Various | Loading and executing programs | | **Config Program** | `Config1111111111111111111111111111111111111` | Validator configuration | | **Stake Program** | `Stake11111111111111111111111111111111111111` | Staking and delegation | | **Vote Program** | `Vote111111111111111111111111111111111111111` | Validator voting | This reference focuses on the two most commonly used in program development: **System Program** and **Compute Budget Program**. --- ## System Program **Program ID:** `solana_program::system_program::ID` (`11111111111111111111111111111111`) The System Program is responsible for account creation, lamport transfers, and account management. ### Core Functionality 1. **Create accounts** (regular and PDAs) 2. **Transfer lamports** between accounts 3. **Allocate space** for account data 4. **Assign ownership** to programs 5. **Create nonce accounts** for durable transactions ### System Program Instructions ```rust use solana_program::system_instruction; pub enum SystemInstruction { CreateAccount, // Create new account Assign, // Assign account to program Transfer, // Transfer lamports CreateAccountWithSeed,// Create account with seed AdvanceNonceAccount, // Advance nonce WithdrawNonceAccount, // Withdraw from nonce InitializeNonceAccount, // Initialize nonce Allocate, // Allocate account space AllocateWithSeed, // Allocate with seed AssignWithSeed, // Assign with seed TransferWithSeed, // Transfer with seed UpgradeNonceAccount, // Upgrade nonce (v4) } ``` --- ### CreateAccount **Creates a new account with lamports and data space.** #### Function Signature ```rust pub fn create_account( from_pubkey: &Pubkey, // Funding account (must be signer) to_pubkey: &Pubkey, // New account address lamports: u64, // Lamports to fund account space: u64, // Bytes of data space owner: &Pubkey, // Program that will own the account ) -> Instruction ``` #### Usage in Native Rust ```rust use solana_program::{ system_instruction, program::invoke, }; pub fn create_new_account( payer: &AccountInfo, new_account: &AccountInfo, system_program: &AccountInfo, program_id: &Pubkey, ) -> ProgramResult { let space = 100; // Account data size let rent = Rent::get()?; let lamports = rent.minimum_balance(space); let create_account_ix = system_instruction::create_account( payer.key, new_account.key, lamports, space as u64, program_id, ); invoke( &create_account_ix, &[ payer.clone(), new_account.clone(), system_program.clone(), ], )?; msg!("Created account with {} bytes", space); Ok(()) } ``` #### Creating PDA Accounts ```rust use solana_program::program::invoke_signed; pub fn create_pda_account( payer: &AccountInfo, pda_account: &AccountInfo, system_program: &AccountInfo, program_id: &Pubkey, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { // Verify PDA let (expected_pda, _bump) = Pubkey::find_program_address(seeds, program_id); if expected_pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } let space = 200; let rent = Rent::get()?; let lamports = rent.minimum_balance(space); let create_account_ix = system_instruction::create_account( payer.key, pda_account.key, lamports, space as u64, program_id, ); // Create full seeds with bump let mut full_seeds = seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; invoke_signed( &create_account_ix, &[payer.clone(), pda_account.clone(), system_program.clone()], signer_seeds, )?; msg!("Created PDA account at {}", pda_account.key); Ok(()) } ``` --- ### Transfer **Transfers lamports from one account to another.** #### Function Signature ```rust pub fn transfer( from_pubkey: &Pubkey, // Source account (must be signer) to_pubkey: &Pubkey, // Destination account lamports: u64, // Amount to transfer ) -> Instruction ``` #### Usage in Native Rust ```rust pub fn transfer_lamports( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { let transfer_ix = system_instruction::transfer( from.key, to.key, amount, ); invoke( &transfer_ix, &[from.clone(), to.clone(), system_program.clone()], )?; msg!("Transferred {} lamports from {} to {}", amount, from.key, to.key); Ok(()) } ``` #### Transfer from PDA ```rust pub fn transfer_from_pda( pda: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let transfer_ix = system_instruction::transfer( pda.key, to.key, amount, ); let mut full_seeds = seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; invoke_signed( &transfer_ix, &[pda.clone(), to.clone(), system_program.clone()], signer_seeds, )?; Ok(()) } ``` --- ### Allocate **Allocates space for an account's data.** #### Function Signature ```rust pub fn allocate( pubkey: &Pubkey, // Account to allocate (must be signer) space: u64, // Bytes to allocate ) -> Instruction ``` #### Usage in Native Rust ```rust pub fn allocate_account_space( account: &AccountInfo, system_program: &AccountInfo, space: u64, ) -> ProgramResult { let allocate_ix = system_instruction::allocate( account.key, space, ); invoke( &allocate_ix, &[account.clone(), system_program.clone()], )?; msg!("Allocated {} bytes for account", space); Ok(()) } ``` **⚠️ Note:** The account must be owned by the System Program before allocating. Most programs use `create_account` instead, which combines allocation with ownership assignment. --- ### Assign **Assigns an account to a program (changes owner).** #### Function Signature ```rust pub fn assign( pubkey: &Pubkey, // Account to assign (must be signer) owner: &Pubkey, // New owner program ) -> Instruction ``` #### Usage in Native Rust ```rust pub fn assign_to_program( account: &AccountInfo, system_program: &AccountInfo, new_owner: &Pubkey, ) -> ProgramResult { let assign_ix = system_instruction::assign( account.key, new_owner, ); invoke( &assign_ix, &[account.clone(), system_program.clone()], )?; msg!("Assigned account to program {}", new_owner); Ok(()) } ``` **⚠️ Note:** Most programs use `create_account` which handles assignment during creation. --- ### Complete Example: Account Lifecycle ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::invoke_signed, pubkey::Pubkey, system_instruction, sysvar::{rent::Rent, Sysvar}, }; #[derive(BorshSerialize, BorshDeserialize)] pub struct UserData { pub user: Pubkey, pub balance: u64, pub created_at: i64, } pub fn create_user_account( program_id: &Pubkey, accounts: &[AccountInfo], user_pubkey: Pubkey, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let payer = next_account_info(account_info_iter)?; let user_account = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // 1. Derive PDA let seeds = &[b"user", user_pubkey.as_ref()]; let (pda, bump) = Pubkey::find_program_address(seeds, program_id); if pda != *user_account.key { return Err(ProgramError::InvalidSeeds); } // 2. Calculate space and rent let space = std::mem::size_of::<UserData>(); let rent = Rent::get()?; let lamports = rent.minimum_balance(space); // 3. Create account via System Program CPI let create_ix = system_instruction::create_account( payer.key, user_account.key, lamports, space as u64, program_id, ); let signer_seeds: &[&[&[u8]]] = &[&[b"user", user_pubkey.as_ref(), &[bump]]]; invoke_signed( &create_ix, &[payer.clone(), user_account.clone(), system_program.clone()], signer_seeds, )?; // 4. Initialize account data let clock = Clock::get()?; let user_data = UserData { user: user_pubkey, balance: 0, created_at: clock.unix_timestamp, }; user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?; msg!("Created user account for {}", user_pubkey); Ok(()) } ``` --- ## Compute Budget Program **Program ID:** `solana_program::compute_budget::ID` (`ComputeBudget111111111111111111111111111111`) The Compute Budget Program allows transactions to request specific compute unit limits, heap sizes, and priority fees. ### Core Functionality 1. **Set compute unit limit** - Maximum CUs for transaction 2. **Set compute unit price** - Priority fee per CU 3. **Request heap size** - Heap memory allocation ### Compute Budget Instructions ```rust use solana_program::compute_budget::ComputeBudgetInstruction; pub enum ComputeBudgetInstruction { RequestUnitsDeprecated, // Deprecated RequestHeapFrame(u32), // Request heap frame (bytes) SetComputeUnitLimit(u32), // Set max CUs SetComputeUnitPrice(u64), // Set priority fee (microlamports per CU) SetLoadedAccountsDataSizeLimit(u32), // Set loaded accounts data limit } ``` --- ### SetComputeUnitLimit **Sets the maximum compute units available to the transaction.** #### Function Signature ```rust pub fn set_compute_unit_limit(units: u32) -> Instruction ``` #### Default Limits - **Default per instruction:** 200,000 CUs - **Default per transaction:** 1,400,000 CUs (with requested CU limit) - **Maximum:** 1,400,000 CUs #### Usage in Native Rust **Important:** Compute Budget instructions are added to the transaction by the **client**, not inside the program. **Client-side example (for reference):** ```rust // This code runs CLIENT-SIDE, not in the program use solana_sdk::{ compute_budget::ComputeBudgetInstruction, transaction::Transaction, }; let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); let transaction = Transaction::new_signed_with_payer( &[ compute_budget_ix, // Must be first your_program_ix, ], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); ``` **⚠️ Note:** Programs cannot modify their own compute budget. These instructions must be added client-side before sending the transaction. --- ### SetComputeUnitPrice **Sets the priority fee per compute unit (for transaction prioritization).** #### Function Signature ```rust pub fn set_compute_unit_price(microlamports: u64) -> Instruction ``` #### Priority Fee Calculation ``` Total Priority Fee = (CUs Used × microlamports) / 1,000,000 ``` **Example:** - CUs used: 50,000 - Price: 10,000 microlamports per CU - Fee: (50,000 × 10,000) / 1,000,000 = 500 lamports #### Usage (Client-side) ```rust // Client-side code let compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(20_000); let transaction = Transaction::new_signed_with_payer( &[ compute_unit_price_ix, // Set priority fee your_program_ix, ], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); ``` **Use cases:** - High-priority transactions (arbitrage, liquidations) - Congested network periods - Time-sensitive operations --- ### RequestHeapFrame **Requests additional heap memory for the transaction.** #### Function Signature ```rust pub fn request_heap_frame(bytes: u32) -> Instruction ``` #### Default Heap - **Default:** 32 KB - **Maximum:** 256 KB #### Usage (Client-side) ```rust // Client-side code let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); // 256 KB let transaction = Transaction::new_signed_with_payer( &[ heap_size_ix, // Request more heap your_program_ix, ], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); ``` **When to use:** - Large data structures - Complex deserialization - Temporary buffers **⚠️ Cost:** Requesting heap increases CU consumption. --- ### SetLoadedAccountsDataSizeLimit **Sets the maximum total size of loaded account data.** #### Function Signature ```rust pub fn set_loaded_accounts_data_size_limit(bytes: u32) -> Instruction ``` #### Default Limit - **Default:** 64 MB per transaction #### Usage (Client-side) ```rust // Client-side code let accounts_data_limit_ix = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(128 * 1024 * 1024); let transaction = Transaction::new_signed_with_payer( &[ accounts_data_limit_ix, your_program_ix, ], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); ``` **Use cases:** - Transactions with many large accounts - Bulk processing operations --- ### Complete Client-side Example ```rust use solana_sdk::{ compute_budget::ComputeBudgetInstruction, transaction::Transaction, signature::{Keypair, Signer}, pubkey::Pubkey, }; pub fn build_optimized_transaction( payer: &Keypair, program_id: &Pubkey, program_ix_data: &[u8], accounts: Vec<AccountMeta>, recent_blockhash: Hash, ) -> Transaction { // 1. Set compute unit limit (if default 200k is insufficient) let compute_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); // 2. Set priority fee (for faster processing) let compute_price_ix = ComputeBudgetInstruction::set_compute_unit_price(10_000); // 3. Request additional heap if needed let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(128 * 1024); // 128 KB // 4. Your program instruction let program_ix = Instruction { program_id: *program_id, accounts, data: program_ix_data.to_vec(), }; // 5. Build transaction (compute budget instructions FIRST) Transaction::new_signed_with_payer( &[ compute_limit_ix, compute_price_ix, heap_size_ix, program_ix, ], Some(&payer.pubkey()), &[payer], recent_blockhash, ) } ``` --- ## Other Built-in Programs ### BPF Loader **Purpose:** Loads and executes Solana programs. **Program IDs:** - `BPFLoader1111111111111111111111111111111111` (deprecated) - `BPFLoader2111111111111111111111111111111111` (upgradeable) - `BPFLoaderUpgradeab1e11111111111111111111111` (current) **Usage:** Primarily used by the runtime. Programs rarely interact with BPF Loader directly. ### Stake Program **Program ID:** `Stake11111111111111111111111111111111111111` **Purpose:** Staking SOL to validators. **Common operations:** - Create stake accounts - Delegate stake - Deactivate stake - Withdraw stake **Use case:** Staking pools, liquid staking protocols. ### Vote Program **Program ID:** `Vote111111111111111111111111111111111111111` **Purpose:** Validator voting and consensus. **Use case:** Validator operations, rarely used by general programs. --- ## CPI Patterns ### System Program CPI Pattern **Standard pattern for calling System Program:** ```rust use solana_program::{ program::invoke, system_instruction, }; pub fn system_program_cpi( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, ) -> ProgramResult { // 1. Verify System Program if system_program.key != &solana_program::system_program::ID { return Err(ProgramError::IncorrectProgramId); } // 2. Create instruction let ix = system_instruction::transfer(from.key, to.key, 1_000_000); // 3. Invoke invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?; Ok(()) } ``` ### PDA Signing Pattern **When PDAs need to sign:** ```rust pub fn pda_system_cpi( pda: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, program_id: &Pubkey, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { // 1. Verify PDA let (expected_pda, _) = Pubkey::find_program_address(seeds, program_id); if expected_pda != *pda.key { return Err(ProgramError::InvalidSeeds); } // 2. Create instruction let ix = system_instruction::transfer(pda.key, to.key, 500_000); // 3. Prepare signer seeds let mut full_seeds = seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; // 4. Invoke with PDA signature invoke_signed( &ix, &[pda.clone(), to.clone(), system_program.clone()], signer_seeds, )?; Ok(()) } ``` ### Validation Pattern **Always validate accounts before CPI:** ```rust pub fn safe_system_cpi( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { // ✅ Validate System Program if system_program.key != &solana_program::system_program::ID { msg!("Invalid System Program"); return Err(ProgramError::IncorrectProgramId); } // ✅ Validate signer if !from.is_signer { msg!("From account must be signer"); return Err(ProgramError::MissingRequiredSignature); } // ✅ Validate sufficient balance if from.lamports() < amount { msg!("Insufficient balance"); return Err(ProgramError::InsufficientFunds); } // Execute CPI let ix = system_instruction::transfer(from.key, to.key, amount); invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?; Ok(()) } ``` --- ## Best Practices ### 1. Always Validate Program IDs ```rust // ✅ Validate before CPI if system_program.key != &solana_program::system_program::ID { return Err(ProgramError::IncorrectProgramId); } ``` ### 2. Use Rent Exemption ```rust // ✅ Always create accounts with rent exemption let rent = Rent::get()?; let lamports = rent.minimum_balance(space); // ❌ Don't use arbitrary amounts let lamports = 1_000_000; // May not be rent-exempt! ``` ### 3. Verify PDA Before Creation ```rust // ✅ Verify PDA derivation let (expected_pda, bump) = Pubkey::find_program_address(seeds, program_id); if expected_pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } ``` ### 4. Use invoke_signed for PDAs ```rust // ✅ PDAs sign with invoke_signed invoke_signed(&ix, accounts, signer_seeds)?; // ❌ Regular invoke won't work for PDA signers invoke(&ix, accounts)?; // Fails if PDA needs to sign ``` ### 5. Set Compute Budget Client-side ```rust // ✅ Add compute budget instructions in client let ixs = vec![ ComputeBudgetInstruction::set_compute_unit_limit(400_000), your_program_ix, ]; // ❌ Cannot set from within program // Programs cannot modify their own compute budget ``` ### 6. Order Compute Budget Instructions First ```rust // ✅ Compute budget instructions FIRST let ixs = vec![ compute_limit_ix, compute_price_ix, heap_size_ix, program_ix, ]; // ❌ Wrong order - may not apply let ixs = vec![ program_ix, compute_limit_ix, // Too late! ]; ``` ### 7. Check Account Ownership Before Transfer ```rust // ✅ Validate ownership for security if from_account.owner != &solana_program::system_program::ID { msg!("Can only transfer from System-owned accounts"); return Err(ProgramError::IllegalOwner); } ``` --- ## Summary **Key Takeaways:** 1. **System Program** handles account creation, transfers, and allocation 2. **Compute Budget Program** instructions are added **client-side**, not in programs 3. **Always validate** program IDs before CPI 4. **Use rent exemption** when creating accounts 5. **PDAs require invoke_signed** for signing operations **Most Common Operations:** | Operation | Instruction | Use Case | |-----------|------------|----------| | Create account | `create_account` | New program accounts | | Transfer lamports | `transfer` | SOL transfers | | Set CU limit | `set_compute_unit_limit` | High-CU transactions | | Set priority fee | `set_compute_unit_price` | Fast transaction processing | | Request heap | `request_heap_frame` | Large data operations | **System Program CPI Template:** ```rust // Validate if system_program.key != &solana_program::system_program::ID { return Err(ProgramError::IncorrectProgramId); } // Create instruction let ix = system_instruction::transfer(from.key, to.key, amount); // Invoke (or invoke_signed for PDAs) invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?; ``` **Compute Budget Client Template:** ```rust // Client-side let ixs = vec![ ComputeBudgetInstruction::set_compute_unit_limit(300_000), ComputeBudgetInstruction::set_compute_unit_price(10_000), your_program_ix, ]; ``` Master these built-in programs for efficient account management and transaction optimization in production Solana programs. -
caveats.md 11.1 KB
# 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. -
client-development.md 8.5 KB
# Client-Side Development Guide for building Solana dApp frontends: wallet connections, balance queries, token transfers, and transaction management. ## Which Library? | Building | Use | Install | |----------|-----|---------| | React/Next.js dApp | framework-kit (`@solana/client` + `@solana/react-hooks`) | `npm i @solana/client @solana/react-hooks` | | Non-React frontend (Svelte, Vue) | `@solana/client` standalone | `npm i @solana/client` | | Server-side scripts, bots, CLIs | `@solana/kit` 6.x | `npm i @solana/kit` | | Anchor program TS client | `@coral-xyz/anchor` (requires `@solana/web3.js` v1) | `npm i @coral-xyz/anchor @solana/web3.js` | | Migrating from web3.js v1 | `@solana/web3-compat` | `npm i @solana/web3-compat` | > **Default recommendation:** Use framework-kit for any frontend. It handles wallet discovery, state management, and caching out of the box. Use `@solana/kit` 6.x only when you need raw RPC control without framework-kit's abstractions (server-side scripts, bots, CLIs). ## Framework-Kit Solana Foundation's official dApp framework. Built on `@solana/kit` 5.x + Zustand (state) + SWR (React caching). - [Documentation](https://www.framework-kit.com/) | [GitHub](https://github.com/solana-foundation/framework-kit) ### Setup (React) ```tsx import { autoDiscover, createClient } from "@solana/client"; import { SolanaProvider, useWalletConnection, useBalance } from "@solana/react-hooks"; const client = createClient({ cluster: "devnet", // or "mainnet" | "testnet" | "localnet" walletConnectors: autoDiscover(), }); export function App() { return ( <SolanaProvider client={client}> <WalletPanel /> </SolanaProvider> ); } ``` Cluster monikers auto-resolve RPC + WebSocket URLs. Use `endpoint` for custom RPC: ```ts const client = createClient({ endpoint: "https://your-rpc.example.com", walletConnectors: autoDiscover(), }); ``` Filter which wallets appear: ```ts import { autoDiscover, filterByNames } from "@solana/client"; const client = createClient({ cluster: "devnet", walletConnectors: autoDiscover({ filter: filterByNames("phantom", "solflare") }), }); ``` > **Next.js / RSC:** Components using these hooks must be marked with `'use client'`. ### Wallet Connection ```tsx function WalletPanel() { const { connectors, connect, disconnect, wallet, status, currentConnector } = useWalletConnection(); const address = wallet?.account.address; const balance = useBalance(address); if (status === "connected") { return ( <div> <p>Connected via {currentConnector?.name}</p> <p>{address?.toString()}</p> <p>Lamports: {balance.lamports?.toString() ?? "loading..."}</p> <button onClick={disconnect}>Disconnect</button> </div> ); } return connectors.map((c) => ( <button key={c.id} onClick={() => connect(c.id)}> Connect {c.name} </button> )); } ``` ### Send SOL ```tsx import { useSolTransfer } from "@solana/react-hooks"; function SendSol({ destination }: { destination: string }) { const { send, isSending, status, signature, error } = useSolTransfer(); return ( <div> <button disabled={isSending} onClick={() => send({ destination, amount: 100_000_000n /* 0.1 SOL */ })} > {isSending ? "Sending..." : "Send 0.1 SOL"} </button> {signature ? <p>Signature: {signature}</p> : null} {error ? <p>Error: {String(error)}</p> : null} </div> ); } ``` ### SPL Token Balance + Transfer ```tsx import { useSplToken } from "@solana/react-hooks"; function TokenPanel({ mint, destinationOwner }: { mint: string; destinationOwner: string }) { const { balance, send, isSending, status, error, sendSignature } = useSplToken(mint); if (status === "disconnected") return <p>Connect wallet to view balance</p>; if (status === "loading") return <p>Loading balance...</p>; return ( <div> <p>Balance: {balance?.uiAmount ?? "0"}</p> <button disabled={isSending} onClick={() => send({ amount: 1n, destinationOwner, amountInBaseUnits: true })} > {isSending ? "Sending..." : "Send 1 token"} </button> {sendSignature ? <p>Signature: {sendSignature}</p> : null} </div> ); } ``` Token 2022 mints: pass `config: { tokenProgram: "auto" }` as second argument to `useSplToken`. ### Arbitrary Transactions ```tsx import type { TransactionInstructionInput } from "@solana/client"; import { useTransactionPool, useWalletSession } from "@solana/react-hooks"; function TransactionFlow({ ix }: { ix: TransactionInstructionInput }) { const session = useWalletSession(); const { addInstruction, prepareAndSend, isSending, sendSignature, sendError } = useTransactionPool(); return ( <div> <button onClick={() => addInstruction(ix)}>Add instruction</button> <button disabled={isSending || !session} onClick={() => prepareAndSend({ authority: session })} > {isSending ? "Sending..." : "Prepare & Send"} </button> {sendSignature ? <p>Signature: {sendSignature}</p> : null} {sendError ? <p>{String(sendError)}</p> : null} </div> ); } ``` For simpler cases where you already have instructions: ```tsx import { useSendTransaction } from "@solana/react-hooks"; const { send, isSending, signature, error } = useSendTransaction(); await send({ instructions }); ``` ### Non-React Usage (`@solana/client` standalone) `@solana/client` works without React in any JS runtime: ```ts import { autoDiscover, createClient } from "@solana/client"; const client = createClient({ cluster: "devnet", walletConnectors: autoDiscover(), }); // Connect wallet await client.actions.connectWallet("wallet-standard:phantom"); // Read balance const wallet = client.store.getState().wallet; if (wallet.status === "connected") { const lamports = await client.actions.fetchBalance(wallet.session.account.address); console.log(`Lamports: ${lamports.toString()}`); } // Send SOL const signature = await client.solTransfer.sendTransfer({ amount: 100_000_000n, authority: wallet.session, destination: "Ff34MXWdgNsEJ1kJFj9cXmrEe7y2P93b95mGu5CJjBQJ", }); // SPL token const usdc = client.splToken({ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }); const balance = await usdc.fetchBalance(wallet.session.account.address); // Arbitrary transaction import { getTransferSolInstruction } from "@solana-program/system"; const prepared = await client.transaction.prepare({ authority: wallet.session, instructions: [ getTransferSolInstruction({ destination: "Ff34MXWdgNsEJ1kJFj9cXmrEe7y2P93b95mGu5CJjBQJ", lamports: 10_000n, source: wallet.session.account.address, }), ], }); const sig = await client.transaction.send(prepared); ``` ### Additional Hooks | Hook | Purpose | |------|---------| | `useAccount(address)` | Fetch + watch account data (lamports, owner, slot) | | `useWaitForSignature(sig, opts)` | Track confirmation status | | `useProgramAccounts(program)` | Query all accounts owned by a program (wrap in `SolanaQueryProvider`) | | `useSimulateTransaction(wire)` | Simulate transaction before sending | | `useLookupTable(address)` | Fetch address lookup table | | `useNonceAccount(address)` | Fetch durable nonce account | | `useClientStore(selector)` | Direct access to Zustand store | ## @solana/kit 6.x (Server-Side / Direct Control) For server-side scripts, bots, and CLIs where you don't need wallet connection UI or React state management. Kit 6.x provides functional, tree-shakeable primitives for RPC, transaction building, and signing. - [Documentation](https://solanakit.org) | [npm](https://www.npmjs.com/package/@solana/kit) This is a lower-level SDK. If you're building a frontend dApp, use framework-kit instead. ## Version Compatibility - **Framework-kit** uses `@solana/kit ^5.0.0` internally. You don't manage this - it's a framework-kit dependency. - **`@solana/kit` 6.x** exists as a standalone SDK for direct use (server-side, scripts). - These don't conflict: framework-kit handles its own Kit 5.x; if you also need Kit 6.x for server-side code, they're separate packages. - **`@solana/web3-compat`** bridges legacy `@solana/web3.js` v1 code to Kit primitives for incremental migration. ## Resources - [Framework-kit docs](https://www.framework-kit.com/) - [Framework-kit GitHub](https://github.com/solana-foundation/framework-kit) - [Vite + React example](https://github.com/solana-foundation/framework-kit/tree/main/examples/vite-react) - [Next.js example](https://github.com/solana-foundation/framework-kit/tree/main/examples/nextjs) - [@solana/kit docs](https://solanakit.org) -
client-integration.md 11.2 KB
# Client Integration Integrate ZK Compression into TypeScript/JavaScript and Rust applications using Light Protocol SDKs and Helius/Photon RPC. ## TypeScript Setup ### Installation ```bash npm install @lightprotocol/stateless.js @lightprotocol/compressed-token helius-sdk ``` ### RPC Connection ```typescript import { createRpc, Rpc } from '@lightprotocol/stateless.js'; // Local development (requires light test-validator) const rpc = createRpc(); // Helius mainnet const rpc = createRpc('https://mainnet.helius-rpc.com?api-key=YOUR_API_KEY'); // Helius devnet const rpc = createRpc('https://devnet.helius-rpc.com?api-key=YOUR_API_KEY'); // Custom Photon endpoint const rpc = createRpc('https://your-photon-instance.com'); ``` ### Using Helius SDK ```typescript import { Helius } from 'helius-sdk'; const helius = new Helius('YOUR_API_KEY'); // All ZK compression methods available via helius.zk const account = await helius.zk.getCompressedAccount({ address }); const accounts = await helius.zk.getCompressedAccountsByOwner(owner); const tokenAccounts = await helius.zk.getCompressedTokenAccountsByOwner(owner); ``` ## RPC Methods Reference ### Account Methods #### getCompressedAccount Fetch a single compressed account by address or hash. ```typescript // By address (for accounts with persistent address) const account = await rpc.getCompressedAccount({ address: new PublicKey('...'), }); // By hash const account = await rpc.getCompressedAccount({ hash: '...', // Base58 encoded hash }); // Response interface CompressedAccount { hash: string; address?: string; owner: string; lamports: number; data?: { discriminator: number[]; data: string; // Base64 encoded dataHash: string; }; leafIndex: number; tree: string; seq: number; } ``` #### getCompressedAccountsByOwner Fetch all compressed accounts owned by a program for a specific owner. ```typescript const accounts = await rpc.getCompressedAccountsByOwner( ownerPubkey, { cursor?: string, // Pagination cursor limit?: number, // Max results (default 100) dataSlice?: { // Return partial data offset: number, length: number, }, } ); // Response interface PaginatedResult<T> { items: T[]; cursor?: string; // For next page } ``` #### getMultipleCompressedAccounts Batch fetch multiple accounts. ```typescript const accounts = await rpc.getMultipleCompressedAccounts({ addresses: [addr1, addr2], // Or use hashes }); ``` ### Token Methods #### getCompressedTokenAccountsByOwner ```typescript const tokenAccounts = await rpc.getCompressedTokenAccountsByOwner( owner, { mint?: PublicKey, // Filter by mint cursor?: string, limit?: number, } ); // Response item interface CompressedTokenAccount { parsed: { mint: string; owner: string; amount: string; // BigInt as string delegate?: string; state: 'initialized' | 'frozen'; }; hash: string; // ... account metadata } ``` #### getCompressedTokenBalancesByOwner Get aggregated balances across all token accounts. ```typescript const balances = await rpc.getCompressedTokenBalancesByOwner(owner); // Response interface TokenBalance { mint: string; balance: string; // Total across all accounts } ``` #### getCompressedMintTokenHolders ```typescript const holders = await rpc.getCompressedMintTokenHolders(mint, { cursor?: string, limit?: number, }); ``` ### Proof Methods #### getValidityProof Fetch ZK proof for account inclusion. ```typescript const proof = await rpc.getValidityProof({ hashes: [hash1, hash2], // Existing accounts newAddresses?: [address1], // New addresses being created newAddressesV2?: [...], // V2 format addresses }); // Response interface ValidityProof { compressedProof: { a: number[]; // 64 bytes b: number[]; // 128 bytes c: number[]; // 64 bytes }; roots: string[]; rootIndices: number[]; leafIndices: number[]; leaves: string[]; merkleTrees: string[]; nullifierQueues: string[]; // Address proof data if newAddresses provided } ``` #### getMultipleCompressedAccountProofs Get Merkle proofs for multiple accounts. ```typescript const proofs = await rpc.getMultipleCompressedAccountProofs({ hashes: [hash1, hash2], }); ``` ### Signature Methods #### getCompressionSignaturesForAccount ```typescript const signatures = await rpc.getCompressionSignaturesForAccount( accountHash, { cursor?: string, limit?: number, } ); ``` #### getCompressionSignaturesForOwner ```typescript const signatures = await rpc.getCompressionSignaturesForOwner(owner); ``` #### getLatestCompressionSignatures ```typescript const signatures = await rpc.getLatestCompressionSignatures({ cursor?: string, limit?: number, }); ``` ### Indexer Health ```typescript // Check indexer status const health = await rpc.getIndexerHealth(); // Get current indexed slot const slot = await rpc.getIndexerSlot(); ``` ## Building Transactions ### Transaction with Compressed Accounts > Light Protocol SDK uses `@solana/web3.js` v1 types internally. These imports are required by the SDK - for non-Light client code, use [`@solana/kit`](https://solanakit.org) instead. ```typescript import { createRpc, buildAndSignTx, sendAndConfirmTx, } from '@lightprotocol/stateless.js'; import { TransactionInstruction, PublicKey, Keypair, } from '@solana/web3.js'; // Required by Light SDK async function executeCompressedTransaction( rpc: Rpc, payer: Keypair, programId: PublicKey, accounts: CompressedAccountMeta[], ) { // 1. Fetch validity proof const hashes = accounts.map(a => a.hash); const { compressedProof, ...proofMetadata } = await rpc.getValidityProof({ hashes, }); // 2. Build instruction with proof and account data const instruction = new TransactionInstruction({ programId, keys: [ { pubkey: payer.publicKey, isSigner: true, isWritable: true }, // Add Light Protocol accounts from proofMetadata ...buildLightAccountMetas(proofMetadata), ], data: Buffer.from([ // Your instruction data + serialized proof ]), }); // 3. Build and send transaction const { blockhash } = await rpc.getLatestBlockhash(); const tx = buildAndSignTx( [instruction], payer, blockhash, ); const signature = await sendAndConfirmTx(rpc, tx); return signature; } ``` ### Using Anchor Client ```typescript import { Program, AnchorProvider } from '@coral-xyz/anchor'; import { createRpc } from '@lightprotocol/stateless.js'; // Setup const rpc = createRpc(); const provider = new AnchorProvider(rpc, wallet, {}); const program = new Program(idl, programId, provider); // Fetch account and proof const account = await rpc.getCompressedAccount({ address }); const proof = await rpc.getValidityProof({ hashes: [account.hash] }); // Call program instruction await program.methods .updateAccount( proof.compressedProof, currentValue, accountMeta, ) .remainingAccounts([ // Light Protocol accounts from proof ]) .rpc(); ``` ## Rust Client ### Setup ```toml [dependencies] light-client = "0.22" solana-sdk = "2.2" tokio = { version = "1", features = ["full"] } ``` ### Usage ```rust use light_client::{ rpc::RpcConnection, indexer::Indexer, }; use solana_sdk::pubkey::Pubkey; #[tokio::main] async fn main() -> Result<()> { // Connect to RPC let rpc = RpcConnection::new("https://devnet.helius-rpc.com?api-key=KEY"); // Fetch compressed accounts let accounts = rpc.get_compressed_accounts_by_owner(&owner).await?; // Get validity proof let hashes: Vec<_> = accounts.iter().map(|a| a.hash).collect(); let proof = rpc.get_validity_proof(&hashes, &[]).await?; // Build and send transaction // ... Ok(()) } ``` ## Error Handling ### Common Errors ```typescript try { const account = await rpc.getCompressedAccount({ address }); } catch (error) { if (error.message.includes('Account not found')) { // Account doesn't exist or was closed } else if (error.message.includes('Invalid proof')) { // Proof verification failed - state may have changed } else if (error.message.includes('Rate limit')) { // Too many requests - implement backoff } } ``` ### Retry Logic ```typescript async function withRetry<T>( fn: () => Promise<T>, maxRetries = 3, delayMs = 1000, ): Promise<T> { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await new Promise(r => setTimeout(r, delayMs * (i + 1))); } } throw new Error('Max retries exceeded'); } // Usage const account = await withRetry(() => rpc.getCompressedAccount({ address }) ); ``` ## Caching Strategies ### Account Cache ```typescript class CompressedAccountCache { private cache = new Map<string, { account: any; timestamp: number }>(); private ttl = 5000; // 5 seconds async get(rpc: Rpc, address: PublicKey): Promise<CompressedAccount> { const key = address.toBase58(); const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < this.ttl) { return cached.account; } const account = await rpc.getCompressedAccount({ address }); this.cache.set(key, { account, timestamp: Date.now() }); return account; } invalidate(address: PublicKey) { this.cache.delete(address.toBase58()); } } ``` ### Proof Caching Proofs should generally NOT be cached because: - Account state (and thus proofs) can change between transactions - Proofs include specific Merkle roots that become stale Instead, always fetch fresh proofs immediately before transaction submission. ## Pagination ```typescript async function fetchAllAccounts(rpc: Rpc, owner: PublicKey) { const allAccounts = []; let cursor: string | undefined; do { const result = await rpc.getCompressedAccountsByOwner(owner, { cursor, limit: 100, }); allAccounts.push(...result.items); cursor = result.cursor; } while (cursor); return allAccounts; } ``` ## Running Local Photon For development without Helius: ```bash # Install Photon cargo install photon-indexer # Run against local validator photon # Or connect to devnet photon --rpc-url=https://api.devnet.solana.com ``` Then use `createRpc('http://localhost:8784')` to connect. ## Best Practices 1. **Batch requests** - Use `getMultipleCompressedAccounts` when fetching multiple accounts 2. **Handle state changes** - Account hashes change on write; re-fetch after transactions 3. **Fresh proofs** - Always fetch proofs immediately before transaction submission 4. **Pagination** - Use cursors for large result sets 5. **Error handling** - Implement retries for transient failures 6. **Rate limiting** - Respect RPC rate limits, implement backoff 7. **Caching** - Cache account data briefly, never cache proofs -
compressed-accounts.md 8.2 KB
# Compressed Account Model Compressed accounts store state on Solana's ledger instead of in the AccountsDB, eliminating rent requirements while maintaining security through zero-knowledge proofs. ## Account Structure ### Compressed Account Layout ```rust pub struct CompressedAccount { pub owner: Pubkey, // Program that owns this account pub lamports: u64, // Lamport balance pub address: Option<[u8; 32]>, // Optional persistent address (PDA-like) pub data: Option<CompressedAccountData>, } pub struct CompressedAccountData { pub discriminator: [u8; 8], // Account type identifier pub data: Vec<u8>, // Serialized account data pub data_hash: [u8; 32], // Poseidon hash of data } ``` ### Comparison with Regular Accounts | Field | Regular Account | Compressed Account | |-------|-----------------|-------------------| | owner | Program ID | Program ID | | lamports | Balance | Balance | | data | Raw bytes | Structured with discriminator + hash | | executable | Boolean | Not applicable | | rent_epoch | Epoch number | Not applicable | | **address** | 32-byte pubkey (permanent) | Optional, hash changes on write | ## Account Identification ### Hash-Based Identification Every compressed account has a unique hash computed from its contents: ```rust // Hash computation (Poseidon) hash = poseidon_hash([ owner, lamports, address, data_hash, ]) ``` **Important**: The hash changes on every write operation, so you cannot use the hash as a permanent identifier. ### Address-Based Identification (Optional) For accounts needing persistent identification (like PDAs), set an address: ```rust // Derive address (similar to PDA derivation) let (address, address_seed) = derive_address( &[b"my_seed", user.key().as_ref()], &address_tree_pubkey, &program_id, ); ``` Addresses: - Are permanent unique identifiers - Cannot be reused after account closure - Are stored in separate address trees - Add computational overhead (use only when needed) ## State Trees ### Merkle Tree Structure Compressed accounts are stored in concurrent Merkle trees: ``` Root (on-chain) / \ Hash Hash / \ / \ Hash Hash Hash Hash / \ / \ / \ / \ A1 A2 A3 A4 A5 A6 A7 A8 (account hashes) ``` - Only the root is stored on-chain - Leaves contain account hashes - Trees use Poseidon hashing (ZK-friendly) - Concurrent access supported via versioned roots ### Tree Types | Tree Type | Purpose | Contents | |-----------|---------|----------| | State Tree (V1) | Store account hashes | Compressed account leaves (individual updates) | | State Tree (V2) | Store account hashes | Compressed account leaves (batched updates, ~70% less CU) | | Address Tree | Track unique addresses | Address leaves (indexed) | | Nullifier Queue | Track spent accounts | Nullified account hashes | V2 batched Merkle trees (mainnet January 2026) batch multiple insertions and verify them with ZK proofs, reducing state root update costs by ~250x. New deployments use V2 trees by default. Tree type is tracked via a `TreeType` enum in the SDK (`StateV1` or `StateV2`); tree selection is handled automatically by SDK helpers like `selectStateTreeInfo()`. ## Account Operations ### Creating Accounts ```rust use light_sdk::account::LightAccount; // new_init creates account with only output state (no prior state) let mut account = LightAccount::<MyAccount>::new_init( &program_id, // Owner program Some(address), // Optional persistent address output_state_tree_index, // Which tree to store in ); // Set initial values account.owner = signer.key(); account.value = 0; // Invoke Light System Program to create LightSystemProgramCpi::new_cpi(CPI_SIGNER, proof) .with_light_account(account)? .with_new_addresses(&[new_address_params]) .invoke(cpi_accounts)?; ``` ### Reading Accounts Accounts are read via the compression RPC API: ```typescript // By address (if set) const account = await rpc.getCompressedAccount({ address }); // By hash const account = await rpc.getCompressedAccount({ hash }); // All accounts for owner const accounts = await rpc.getCompressedAccountsByOwner(owner); ``` ### Updating Accounts ```rust // new_mut creates account with input state (to be consumed) and output state (new) let mut account = LightAccount::<MyAccount>::new_mut( &program_id, &account_meta, // Metadata about existing account MyAccount { // Current state (must match on-chain) owner: signer.key(), value: current_value, }, )?; // Modify the account account.value = account.value.checked_add(1).unwrap(); // Invoke to apply changes LightSystemProgramCpi::new_cpi(CPI_SIGNER, proof) .with_light_account(account)? .invoke(cpi_accounts)?; ``` ### Closing Accounts ```rust // new_close creates account with only input state (consumed, no output) let account = LightAccount::<MyAccount>::new_close( &program_id, &account_meta, MyAccount { owner: signer.key(), value: current_value, }, )?; LightSystemProgramCpi::new_cpi(CPI_SIGNER, proof) .with_light_account(account)? .invoke(cpi_accounts)?; ``` **Note**: Closed account addresses can be reinitialized. Use `LightAccount::new_empty()` to reconstruct the closed account hash, then optionally chain `LightAccount::new_mut()` to set custom values in the same transaction. See [reinitialize guide](https://www.zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts). ## Account Metadata When referencing existing accounts, provide metadata: ```rust pub struct CompressedAccountMeta { pub merkle_context: PackedMerkleContext, pub leaf_index: u32, pub hash: [u8; 32], } pub struct PackedMerkleContext { pub merkle_tree_pubkey_index: u8, pub nullifier_queue_pubkey_index: u8, pub leaf_index: u32, pub queue_index: Option<QueueIndex>, } ``` This metadata is fetched from the indexer and included in transactions. ## Hashing with LightHasher Define hashable account structures: ```rust use light_sdk::{LightDiscriminator, LightHasher}; #[derive(Clone, Debug, Default, LightDiscriminator, LightHasher)] pub struct MyAccount { #[hash] // Include in hash computation pub owner: Pubkey, pub value: u64, // Not hashed (but still serialized) } ``` - `#[hash]` marks fields included in the Poseidon hash - All fields are serialized with Borsh - Discriminator auto-derived from struct name ## Validity Proofs ### What Proofs Verify - Account exists as a leaf in specified state tree - Account hash matches provided data - Address (if set) exists in address tree - State transition is valid ### Proof Structure ```rust pub struct ValidityProof { pub a: [u8; 64], // Groth16 proof element A pub b: [u8; 128], // Groth16 proof element B pub c: [u8; 64], // Groth16 proof element C } // Total: 256 bytes (compressed to 128 bytes in transactions) ``` ### Fetching Proofs ```typescript // Get proof for multiple accounts const proof = await rpc.getValidityProof({ hashes: [accountHash1, accountHash2], newAddresses: [newAddress1], // If creating new addressed accounts }); ``` ## Compute Units | Operation | Approximate CU | |-----------|---------------| | Validity proof verification | ~100,000 | | System program overhead | ~100,000 | | Per account read/write | ~6,000 | | Poseidon hash (syscall) | ~1,500 | **Example**: Transaction with 2 accounts ≈ 212,000 CU ## Trust Assumptions 1. **Data Availability**: At least one node must store raw account data for proof generation 2. **Forester Liveness**: Nullifier queues must be emptied to prevent state tree congestion 3. **Program Upgradeability**: Light System Program is currently upgradeable (will be frozen) ## Best Practices 1. **Use addresses sparingly** - Only when persistent identification needed 2. **Batch operations** - Multiple accounts in one transaction share proof overhead 3. **Cache account state** - Reduce RPC calls by caching recent fetches 4. **Handle hash changes** - Account hashes change on every write; re-fetch after updates 5. **Plan for CU limits** - Account for ~200k CU base + 6k per account -
compressed-pdas.md 12.1 KB
# Compressed PDAs Build Solana programs with compressed Program Derived Addresses (PDAs) for rent-free accounts with persistent identification. ## Overview | Aspect | Regular PDA | Compressed PDA | |--------|-------------|----------------| | 100-byte account | ~1,600,000 lamports | ~15,000 lamports | | Rent | Required | None | | Address derivation | `find_program_address` | `derive_address` | | State updates | In-place mutation | Consume + create new | ## Prerequisites ### Required Versions - Rust: 1.89.0+ - Solana CLI: 2.3.0+ - Anchor CLI: 0.32.1+ - ZK Compression CLI: latest (`npm install -g @lightprotocol/zk-compression-cli`) - Node.js: 23.5.0+ ### Installation ```bash # Install ZK compression CLI npm install -g @lightprotocol/zk-compression-cli # Initialize new project with compression support light init my-program cd my-program ``` ## Project Structure ``` my-program/ ├── Anchor.toml ├── Cargo.toml ├── programs/ │ └── my-program/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── tests/ │ └── my-program.ts └── package.json ``` ### Cargo.toml Dependencies ```toml [dependencies] anchor-lang = "0.32.1" light-sdk = "0.22" [dev-dependencies] light-program-test = "1.2" light-client = "0.22" ``` > **Note**: Light Protocol SDK versions change frequently. Check [crates.io](https://crates.io/crates/light-sdk) for the latest compatible versions. ## Program Structure ### Basic Anchor Program with Compression ```rust #![allow(unexpected_cfgs)] use anchor_lang::prelude::*; use light_sdk::{ account::LightAccount, address::v1::derive_address, cpi::{v1::CpiAccounts, CpiSigner}, derive_light_cpi_signer, instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof}, LightDiscriminator, LightHasher, }; declare_id!("YourProgramID11111111111111111111111111111"); // Derive CPI signer for Light System Program calls pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("YourProgramID11111111111111111111111111111"); #[program] pub mod my_program { use super::*; use light_sdk::cpi::{v1::LightSystemProgramCpi, InvokeLightSystemProgram}; // Instructions here... } // Account context (minimal for compressed accounts) #[derive(Accounts)] pub struct MyAccounts<'info> { #[account(mut)] pub signer: Signer<'info>, } // Compressed account data structure #[event] #[derive(Clone, Debug, Default, LightDiscriminator, LightHasher)] pub struct MyAccount { #[hash] pub owner: Pubkey, pub data: u64, } ``` ## Creating Compressed PDAs ### Create Instruction ```rust pub fn create_account<'info>( ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>, proof: ValidityProof, address_tree_info: PackedAddressTreeInfo, output_state_tree_index: u8, ) -> Result<()> { // 1. Setup CPI accounts from remaining_accounts let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, crate::LIGHT_CPI_SIGNER, ); // 2. Derive the compressed PDA address let (address, address_seed) = derive_address( &[b"my_account", ctx.accounts.signer.key().as_ref()], &address_tree_info .get_tree_pubkey(&light_cpi_accounts) .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, &crate::ID, ); // 3. Prepare address params for new address creation let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); // 4. Create the compressed account let mut account = LightAccount::<MyAccount>::new_init( &crate::ID, Some(address), output_state_tree_index, ); // 5. Initialize account data account.owner = ctx.accounts.signer.key(); account.data = 0; // 6. Invoke Light System Program LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) .with_light_account(account)? .with_new_addresses(&[new_address_params]) .invoke(light_cpi_accounts)?; Ok(()) } ``` ### Address Derivation ```rust use light_sdk::address::v1::derive_address; // Seeds work like regular PDAs let (address, address_seed) = derive_address( &[ b"my_prefix", // Static seed user.key().as_ref(), // Dynamic seed &[counter], // Additional data ], &address_tree_pubkey, // Address tree (from CPI accounts) &program_id, // Your program ID ); ``` ## Updating Compressed Accounts ### Update Instruction ```rust pub fn update_account<'info>( ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>, proof: ValidityProof, // Current state must be provided (fetched from indexer) current_data: u64, account_meta: CompressedAccountMeta, ) -> Result<()> { // 1. Create mutable account (consumes input, creates output) let mut account = LightAccount::<MyAccount>::new_mut( &crate::ID, &account_meta, MyAccount { owner: ctx.accounts.signer.key(), data: current_data, }, )?; // 2. Validate ownership require!( account.owner == ctx.accounts.signer.key(), CustomError::Unauthorized ); // 3. Modify state account.data = account.data.checked_add(1).ok_or(CustomError::Overflow)?; // 4. Setup CPI and invoke let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, crate::LIGHT_CPI_SIGNER, ); LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) .with_light_account(account)? .invoke(light_cpi_accounts)?; Ok(()) } ``` ## Closing Compressed Accounts ### Close Instruction ```rust pub fn close_account<'info>( ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>, proof: ValidityProof, current_data: u64, account_meta: CompressedAccountMeta, ) -> Result<()> { // new_close only has input state (no output created) let account = LightAccount::<MyAccount>::new_close( &crate::ID, &account_meta, MyAccount { owner: ctx.accounts.signer.key(), data: current_data, }, )?; require!( account.owner == ctx.accounts.signer.key(), CustomError::Unauthorized ); let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, crate::LIGHT_CPI_SIGNER, ); LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) .with_light_account(account)? .invoke(light_cpi_accounts)?; Ok(()) } ``` **Important**: Closed account addresses can be reinitialized. Use `LightAccount::new_empty()` to reconstruct the closed account hash, then optionally chain `LightAccount::new_mut()` to set custom values in the same transaction. See [reinitialize guide](https://www.zkcompression.com/compressed-pdas/guides/how-to-reinitialize-compressed-accounts). ## Account Data Structures ### Defining Hashable Structs ```rust use light_sdk::{LightDiscriminator, LightHasher}; #[event] // Makes struct available in IDL #[derive(Clone, Debug, Default, LightDiscriminator, LightHasher)] pub struct GamePlayer { #[hash] pub wallet: Pubkey, // Included in hash #[hash] pub game_id: [u8; 32], // Included in hash pub score: u64, // Not hashed (but serialized) pub level: u8, // Not hashed (but serialized) } ``` ### Hash Attribute - `#[hash]` marks fields for Poseidon hash computation - All fields are Borsh serialized regardless of hash attribute - Hash determines account identity; hashed fields should be immutable or tracked carefully ## Multiple Accounts in One Transaction ```rust pub fn batch_create<'info>( ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>, proof: ValidityProof, address_tree_infos: Vec<PackedAddressTreeInfo>, output_state_tree_index: u8, count: u8, ) -> Result<()> { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, crate::LIGHT_CPI_SIGNER, ); let mut accounts = Vec::new(); let mut address_params = Vec::new(); for i in 0..count { let (address, seed) = derive_address( &[b"batch", &[i]], &address_tree_infos[i as usize].get_tree_pubkey(&light_cpi_accounts)?, &crate::ID, ); address_params.push( address_tree_infos[i as usize].into_new_address_params_packed(seed) ); let mut account = LightAccount::<MyAccount>::new_init( &crate::ID, Some(address), output_state_tree_index, ); account.owner = ctx.accounts.signer.key(); account.data = i as u64; accounts.push(account); } let mut cpi = LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) .with_new_addresses(&address_params); for account in accounts { cpi = cpi.with_light_account(account)?; } cpi.invoke(light_cpi_accounts)?; Ok(()) } ``` ## Testing ### Unit Tests with light-program-test ```rust #[cfg(test)] mod tests { use super::*; use light_program_test::{LightProgramTest, TestRpc}; #[tokio::test] async fn test_create_account() { let mut test = LightProgramTest::new(&[("my_program", crate::ID)]).await; let rpc = test.rpc(); // Create account let result = test.create_compressed_account( &crate::ID, &[b"test"], ).await; assert!(result.is_ok()); } } ``` ### Integration Tests (TypeScript) > Light Protocol SDK uses `@solana/web3.js` v1 types. For non-Light client code, use [`@solana/kit`](https://solanakit.org). ```typescript import { createRpc } from '@lightprotocol/stateless.js'; import { Keypair } from '@solana/web3.js'; // Required by Light SDK describe('My Program', () => { const rpc = createRpc(); it('creates compressed account', async () => { const payer = Keypair.generate(); await rpc.requestAirdrop(payer.publicKey, 1000000000); // Build and send transaction // ... }); }); ``` ## Build and Deploy ```bash # Build program anchor build # Run tests cargo test-sbf # Deploy to devnet anchor deploy --provider.cluster devnet ``` ## Common Patterns ### Read-Only Access For instructions that only read compressed accounts without modifying: ```rust pub fn read_account<'info>( ctx: Context<'_, '_, '_, 'info, MyAccounts<'info>>, proof: ValidityProof, account_data: MyAccount, account_meta: CompressedAccountMeta, ) -> Result<()> { // Validate the account exists with this data let account = LightAccount::<MyAccount>::new_mut( &crate::ID, &account_meta, account_data, )?; // Read-only logic msg!("Account data: {}", account.data); // Re-create same state (no modification) let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, crate::LIGHT_CPI_SIGNER, ); LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) .with_light_account(account)? .invoke(light_cpi_accounts)?; Ok(()) } ``` ### CPI Between Programs ```rust // Call another program that uses compressed accounts pub fn cpi_example<'info>( ctx: Context<'_, '_, '_, 'info, CpiAccounts<'info>>, // ... params ) -> Result<()> { // Build CPI to other program // Include Light System Program accounts in remaining_accounts Ok(()) } ``` ## Error Handling ```rust #[error_code] pub enum CustomError { #[msg("Unauthorized access")] Unauthorized, #[msg("Arithmetic overflow")] Overflow, #[msg("Arithmetic underflow")] Underflow, #[msg("Invalid account state")] InvalidState, } ``` ## Best Practices 1. **Validate ownership** - Always check account owner matches expected signer 2. **Use checked arithmetic** - Prevent overflow/underflow errors 3. **Minimize state size** - Smaller accounts = lower costs 4. **Hash immutable fields** - Fields marked `#[hash]` affect identity 5. **Handle concurrent access** - Account hashes change on every write 6. **Test with real proofs** - Use `light-program-test` for accurate testing -
compressed-tokens.md 8.7 KB
# Compressed Tokens Compressed tokens are SPL tokens stored as compressed accounts, enabling 5000x cheaper token distribution while maintaining full SPL token compatibility. ## Overview | Aspect | Regular SPL Token | Compressed Token | |--------|-------------------|------------------| | Storage | Token account (~0.002 SOL rent) | Compressed account (no rent) | | 100 accounts | ~0.2 SOL | ~0.00004 SOL | | Mint | Standard SPL mint | SPL mint + token pool | | Transfers | Account-to-account | Consume input, create outputs | ## Setup ### Installation ```bash npm install @lightprotocol/stateless.js @lightprotocol/compressed-token ``` ### Create RPC Connection ```typescript import { createRpc } from '@lightprotocol/stateless.js'; // Local development const rpc = createRpc(); // defaults to localhost:8899 // Helius mainnet const rpc = createRpc('https://mainnet.helius-rpc.com?api-key=YOUR_KEY'); // Helius devnet const rpc = createRpc('https://devnet.helius-rpc.com?api-key=YOUR_KEY'); ``` ## Token Operations ### Create Mint with Token Pool A token pool enables compression for an SPL mint: ```typescript import { createMint } from '@lightprotocol/compressed-token'; // Creates SPL mint + token pool in one transaction const { mint, transactionSignature } = await createMint( rpc, payer, // Fee payer and mint authority payer.publicKey, // Mint authority 9, // Decimals // Optional: keypair for deterministic mint address ); console.log('Mint:', mint.toBase58()); ``` ### Add Token Pool to Existing Mint ```typescript import { createTokenPool } from '@lightprotocol/compressed-token'; // Add compression support to existing SPL mint const transactionSignature = await createTokenPool( rpc, payer, existingMint, ); ``` > **Note**: `createTokenPool` is being renamed to `createSplInterface` in newer SDK versions. Both work currently; prefer `createSplInterface` in new code. ### Mint Compressed Tokens ```typescript import { mintTo } from '@lightprotocol/compressed-token'; // Mint to single recipient const sig = await mintTo( rpc, payer, mint, recipient, // toPubkey mintAuthority, // Must have mint authority 1_000_000_000, // Amount (with decimals) ); // Mint to multiple recipients const recipients = [addr1, addr2, addr3]; const amounts = [1_000_000_000, 2_000_000_000, 500_000_000]; const sig = await mintTo( rpc, payer, mint, recipients, mintAuthority, amounts, ); ``` ### Transfer Compressed Tokens ```typescript import { transfer } from '@lightprotocol/compressed-token'; // Basic transfer const sig = await transfer( rpc, payer, mint, amount, owner, // Token owner (keypair) recipient, // Destination address ); ``` **How transfers work**: 1. Input compressed token accounts are consumed (nullified) 2. New output accounts created for recipient and sender (change) 3. Balances recomputed and stored in new accounts ### Query Token Accounts ```typescript // Get all compressed token accounts for owner const accounts = await rpc.getCompressedTokenAccountsByOwner( owner, { mint } // Optional: filter by mint ); for (const account of accounts.items) { console.log('Amount:', account.parsed.amount); console.log('Mint:', account.parsed.mint); console.log('Owner:', account.parsed.owner); } // Get token balances summary const balances = await rpc.getCompressedTokenBalancesByOwner(owner); ``` ### Get Token Account Balance ```typescript // Get balance for specific compressed token account const balance = await rpc.getCompressedTokenAccountBalance(accountHash); ``` ## Compress/Decompress SPL Tokens ### Compress SPL Tokens Convert regular SPL token account to compressed: ```typescript import { compress } from '@lightprotocol/compressed-token'; const sig = await compress( rpc, payer, mint, amount, owner, // SPL token account owner recipient, // Compressed token recipient (can be same) ); ``` ### Decompress to SPL Tokens Convert compressed tokens back to regular SPL: ```typescript import { decompress } from '@lightprotocol/compressed-token'; const sig = await decompress( rpc, payer, mint, amount, owner, // Compressed token owner recipient, // SPL token account (or ATA created) ); ``` ## Delegate Authority ### Approve Delegate ```typescript import { approve } from '@lightprotocol/compressed-token'; const sig = await approve( rpc, payer, mint, amount, // Delegated amount owner, // Token owner delegate, // Delegate pubkey ); ``` ### Revoke Delegate ```typescript import { revoke } from '@lightprotocol/compressed-token'; const sig = await revoke( rpc, payer, mint, owner, ); ``` ### Transfer with Delegate ```typescript import { transfer } from '@lightprotocol/compressed-token'; // When delegate is transferring on behalf of owner const sig = await transfer( rpc, payer, mint, amount, delegate, // Delegate performing transfer recipient, owner, // Original owner (optional, for delegate transfers) ); ``` ## Merge Token Accounts Combine multiple compressed token accounts: ```typescript import { mergeTokenAccounts } from '@lightprotocol/compressed-token'; // Merge all token accounts for a mint into fewer accounts const sig = await mergeTokenAccounts( rpc, payer, mint, owner, ); ``` **When to merge**: - Owner has many small token accounts - Reduce number of inputs needed for transfers - Simplify account management ## Batch Operations ### Airdrop to Many Recipients ```typescript import { mintTo } from '@lightprotocol/compressed-token'; // Prepare recipients and amounts const recipients: PublicKey[] = []; const amounts: number[] = []; for (const user of users) { recipients.push(user.address); amounts.push(airdropAmount); } // Batch mint (limited by transaction size) const BATCH_SIZE = 5; // Adjust based on transaction limits for (let i = 0; i < recipients.length; i += BATCH_SIZE) { const batchRecipients = recipients.slice(i, i + BATCH_SIZE); const batchAmounts = amounts.slice(i, i + BATCH_SIZE); await mintTo(rpc, payer, mint, batchRecipients, mintAuthority, batchAmounts); } ``` ## Token Pool Info ```typescript import { getTokenPoolInfos } from '@lightprotocol/compressed-token'; // Get token pool details for a mint const poolInfo = await getTokenPoolInfos(rpc, mint); console.log('Pool address:', poolInfo.address); console.log('Pool token account:', poolInfo.tokenAccount); ``` ## Common Errors ### TokenPool not found ```typescript // Error: TokenPool not found for mint // Solution: Create token pool first import { createTokenPool } from '@lightprotocol/compressed-token'; await createTokenPool(rpc, payer, mint); ``` ### Insufficient balance ```typescript // Error: Insufficient balance for transfer // Check balance before transfer const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint }); const totalBalance = accounts.items.reduce( (sum, acc) => sum + BigInt(acc.parsed.amount), 0n ); ``` ### Array length mismatch ```typescript // Error: Amount and toPubkey arrays must have same length // Ensure arrays match const recipients = [addr1, addr2, addr3]; const amounts = [100, 200, 300]; // Must be same length ``` ## Rust SDK (On-Chain Programs) For programs that need to interact with compressed tokens: ```rust use light_compressed_token::{ process_transfer, InputTokenDataWithContext, PackedTokenTransferOutputData, }; use light_sdk::instruction::ValidityProof; // Transfer compressed tokens in your program pub fn transfer_compressed_tokens( ctx: Context<MyContext>, inputs: Vec<InputTokenDataWithContext>, outputs: Vec<PackedTokenTransferOutputData>, proof: ValidityProof, ) -> Result<()> { // ... validation logic // CPI to compressed token program process_transfer( ctx.accounts.into(), inputs, outputs, proof, )?; Ok(()) } ``` ## Cost Comparison | Operation | Regular SPL | Compressed Token | |-----------|-------------|------------------| | Create 1 token account | ~0.002 SOL | ~0.000001 SOL | | Create 1000 accounts | ~2 SOL | ~0.001 SOL | | Create 1M accounts | ~2000 SOL | ~1 SOL | | Transfer | ~0.000005 SOL | ~0.00001 SOL | **Note**: Compressed tokens have higher compute costs (~200k CU per transfer) but much lower state costs. ## Best Practices 1. **Use for distribution** - Ideal for airdrops, rewards, gaming items 2. **Batch mints** - Multiple recipients in one transaction 3. **Merge periodically** - Consolidate accounts for cleaner state 4. **Cache balances** - Reduce RPC calls in UI 5. **Handle multiple accounts** - Users may have multiple compressed accounts for same mint -
compute-optimization.md 19.4 KB
# Compute Unit Optimization Guide This guide provides comprehensive techniques for optimizing compute unit (CU) usage in Solana native Rust programs, compiled from official Solana documentation, community repositories, and expert resources. ## Understanding Compute Units ### Compute Limits Solana enforces strict compute budgets to ensure network performance: - **Max CU per block**: 60 million CU - **Max CU per account per block**: 12 million CU - **Max CU per transaction**: 1.4 million CU - **Default soft cap per transaction**: 200,000 CU Programs can request higher compute budgets using the Compute Budget program, up to the 1.4M hard limit. ### Transaction Fees Transaction fees consist of two components: 1. **Base fee**: 5,000 lamports per signature (fixed, independent of CU usage) 2. **Priority fee**: Optional additional fee to prioritize transaction inclusion Priority fees are calculated as: ``` priority_fee = microLamports_per_CU × requested_compute_units ``` ### Why Optimize CU Usage? Even though current fees don't scale with CU usage within the budget, optimization matters: 1. **Block inclusion probability**: Smaller transactions are more likely to fit in congested blocks 2. **Composability**: When your program is called via CPI, it shares the caller's CU budget 3. **Efficient resource usage**: Better utilization of limited block space 4. **Future-proofing**: Fee structures may change to account for actual CU consumption 5. **User experience**: Faster transaction execution and lower rejection rates ## Common Optimization Techniques ### 1. Logging Optimization (Highest Impact) Logging is one of the most expensive operations in Solana programs. **Anti-patterns:** ```rust // EXPENSIVE: 11,962 CU // Base58 encoding + string concatenation msg!("A string {0}", ctx.accounts.counter.to_account_info().key()); // EXPENSIVE: 357 CU // String concatenation msg!("A string {0}", "5w6z5PWvtkCd4PaAV7avxE6Fy5brhZsFdbRLMt8UefRQ"); ``` **Best practices:** ```rust // EFFICIENT: 262 CU // Use .key().log() directly ctx.accounts.counter.to_account_info().key().log(); // BETTER: 206 CU // Store in variable first let pubkey = ctx.accounts.counter.to_account_info().key(); pubkey.log(); // CHEAPEST: 204 CU // Simple string logging msg!("Compute units"); ``` **Recommendation**: Avoid logging in production unless absolutely necessary for debugging. Remove or conditionally compile logging for mainnet deployments. ### 2. Data Type Optimization Smaller data types consume fewer compute units. **Comparison:** ```rust // 618 CU - u64 let mut a: Vec<u64> = Vec::new(); for _ in 0..6 { a.push(1); } // 600 CU - i32 (default integer type) let mut a = Vec::new(); for _ in 0..6 { a.push(1); } // 459 CU - u8 (best for small values) let mut a: Vec<u8> = Vec::new(); for _ in 0..6 { a.push(1); } ``` **Initialization vs pushing:** ```rust // 357 CU - Pushing elements one by one let mut a: Vec<u64> = Vec::new(); for _ in 0..6 { a.push(1); } // 125 CU - Direct initialization (65% savings!) let _a: Vec<u64> = vec![1, 1, 1, 1, 1, 1]; ``` **Best practice**: Use the smallest data type that fits your requirements (u8 > u16 > u32 > u64), and prefer `vec![]` initialization over repeated `push()` calls. ### 3. Serialization: Zero-Copy vs Borsh Zero-copy deserialization can provide massive CU savings for account operations. **Standard Borsh serialization:** ```rust // 6,302 CU - Standard account initialization pub fn initialize(_ctx: Context<InitializeCounter>) -> Result<()> { Ok(()) } // 2,600 CU total for increment (including serialization overhead) pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = counter.count.checked_add(1).unwrap(); // 108 CU for operation Ok(()) } ``` **Zero-copy optimization:** ```rust // 5,020 CU - Zero-copy initialization (20% savings) pub fn initialize_zero_copy(_ctx: Context<InitializeCounterZeroCopy>) -> Result<()> { Ok(()) } // 1,254 CU total for increment (52% savings!) pub fn increment_zero_copy(ctx: Context<IncrementZeroCopy>) -> Result<()> { let counter = &mut ctx.accounts.counter_zero_copy.load_mut()?; counter.count = counter.count.checked_add(1).unwrap(); // 151 CU for operation Ok(()) } ``` **Zero-copy account definition:** ```rust #[account(zero_copy)] #[repr(C)] #[derive(InitSpace)] pub struct CounterZeroCopy { count: u64, authority: Pubkey, big_struct: BigStruct, // Can include large structs without stack overflow } ``` **Benefits of zero-copy:** - 50%+ CU savings on serialization/deserialization - Avoids stack frame violations with large account structures - Direct memory access without intermediate copying - Particularly valuable for frequently updated accounts **Trade-off**: Slightly more complex API (`load()`, `load_mut()`) and requires `#[repr(C)]` for memory layout guarantees. ### 4. Program Derived Addresses (PDAs) PDA operations vary significantly in cost depending on the method used. **Finding PDAs:** ```rust // EXPENSIVE: 12,136 CU // Iterates through nonces to find valid bump seed let (pda, bump) = Pubkey::find_program_address(&[b"counter"], ctx.program_id); // EFFICIENT: 1,651 CU (87% savings!) // Uses known bump seed directly let pda = Pubkey::create_program_address(&[b"counter", &[248_u8]], &program_id).unwrap(); ``` **Optimization strategy:** 1. Use `find_program_address()` **once** during account initialization 2. Save the bump seed in the account data 3. Use `create_program_address()` with the saved bump for all subsequent operations **Anchor implementation:** ```rust // Account structure - save the bump #[account] pub struct CounterData { pub count: u64, pub bump: u8, // Store the bump seed here } // EXPENSIVE: 12,136 CU - Without saved bump #[account( seeds = [b"counter"], bump // Anchor finds it every time )] pub counter_checked: Account<'info, CounterData>, // EFFICIENT: 1,600 CU - With saved bump (87% savings!) #[account( seeds = [b"counter"], bump = counter_checked.bump // Use the saved bump )] pub counter_checked: Account<'info, CounterData>, ``` ### 5. Cross-Program Invocations (CPIs) CPIs add significant overhead compared to direct operations. **CPI to System Program:** ```rust // 2,215 CU - CPI for SOL transfer let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.payer.to_account_info().clone(), to: ctx.accounts.counter.to_account_info().clone(), }, ); system_program::transfer(cpi_context, 1_000_000)?; ``` **Direct lamport manipulation:** ```rust // 251 CU - Direct operation (90% savings!) let counter_account_info = ctx.accounts.counter.to_account_info(); let mut counter_lamports = counter_account_info.try_borrow_mut_lamports()?; **counter_lamports += 1_000_000; let payer_account_info = ctx.accounts.payer.to_account_info(); let mut payer_lamports = payer_account_info.try_borrow_mut_lamports()?; **payer_lamports -= 1_000_000; ``` **Important caveats:** 1. **Error handling overhead**: Error paths add ~1,199 CU if triggered 2. **Safety**: Direct manipulation bypasses safety checks in the System Program 3. **Ownership**: Only safe when you control both accounts 4. **Rent exemption**: You're responsible for maintaining rent exemption **Best practice**: Use CPIs for safety and correctness by default. Only optimize to direct manipulation when: - You have tight CU constraints - You fully understand the safety implications - Both accounts are controlled by your program ### 6. Pass by Reference vs Clone Solana's bump allocator doesn't free memory, making unnecessary cloning particularly problematic. **Comparison:** ```rust let balances = vec![10_u64; 100]; // EFFICIENT: 47,683 CU - Pass by reference fn sum_by_reference(data: &Vec<u64>) -> u64 { data.iter().sum() } for _ in 0..39 { sum_reference += sum_by_reference(&balances); } // INEFFICIENT: 49,322 CU - Clone data (3.5% more expensive) // WARNING: Runs out of memory at 40+ iterations! fn sum_by_value(data: Vec<u64>) -> u64 { data.iter().sum() } for _ in 0..39 { sum_clone += sum_by_value(balances.clone()); } ``` **Memory concern**: Solana programs have a 32KB heap using a bump allocator that **never frees memory** during transaction execution. Excessive cloning leads to out-of-memory errors. **Best practice**: Always pass by reference (`&T`) unless you explicitly need ownership transfer. Use `Copy` types for small data. ### 7. Checked Math vs Unchecked Operations Checked arithmetic adds safety at the cost of compute units. **Comparison:** ```rust let mut count: u64 = 1; // 97,314 CU - Checked multiplication with overflow protection for _ in 0..12000 { count = count.checked_mul(2).expect("overflow"); } // 85,113 CU - Bit shift operation (12% savings) // Equivalent to multiply by 2, but unchecked for _ in 0..12000 { count = count << 1; } ``` **Trade-off**: Unchecked operations are faster but risk overflow bugs that can lead to serious security vulnerabilities. **Best practice**: - Use checked math by default for safety - Profile your program to identify hot paths - Only switch to unchecked math when: - You've proven overflow is impossible - CU savings are critical - You've added overflow tests **Compiler configuration** (in Cargo.toml): ```toml [profile.release] overflow-checks = true # Keep overflow checks even in release mode ``` ## Framework Comparison Different implementation approaches offer varying trade-offs between developer experience, safety, and performance. | Implementation | Binary Size | Deploy Cost | Init CU | Increment CU | |---------------|-------------|-------------|---------|--------------| | **Anchor** | 265,677 bytes | 1.85 SOL | 6,302 | 946 | | **Anchor Zero-Copy** | Same | 1.85 SOL | 5,020 | ~1,254 | | **Native Rust** | 48,573 bytes | 0.34 SOL | - | 843 | | **Unsafe Rust** | 973 bytes | 0.008 SOL | - | 5 | | **Assembly (SBPF)** | 1,389 bytes | 0.01 SOL | - | 4 | | **C** | 1,333 bytes | 0.01 SOL | - | 5 | **Key insights:** - **Anchor**: Best developer experience, automatic account validation, but highest CU and deployment costs - **Anchor Zero-Copy**: Significant CU improvement over standard Anchor with minimal code changes - **Native Rust**: 11% CU savings over Anchor, 82% smaller deployment size, moderate complexity - **Unsafe Rust**: 99% CU savings, minimal size, but requires extreme care and deep expertise - **Assembly/C**: Maximum optimization possible, but very difficult to develop and maintain **Recommendation**: Start with Anchor or native Rust. Optimize hot paths with zero-copy. Only consider unsafe Rust or lower-level languages for critical performance bottlenecks after profiling. ## Advanced Optimization Techniques ### 1. Compiler Flags Configure optimization in `Cargo.toml`: ```toml [profile.release] opt-level = 3 # Maximum optimization lto = "fat" # Full link-time optimization codegen-units = 1 # Single codegen unit for better optimization overflow-checks = true # Keep safety checks despite performance cost ``` **Trade-offs**: - `overflow-checks = false`: Saves CU but removes critical safety checks - Higher `opt-level`: Better performance but slower compilation - `lto = "fat"`: Maximum optimization but much slower builds ### 2. Function Inlining Control function inlining to balance CU usage and stack space: ```rust // Force inlining - saves CU by eliminating function call overhead #[inline(always)] fn add(a: u64, b: u64) -> u64 { a + b } // Prevent inlining - saves stack space at the cost of CU #[inline(never)] pub fn complex_operation() { // Large function body } ``` **Trade-off**: Inlining saves CU but increases stack usage. Solana has a 4KB stack limit, so excessive inlining can cause stack overflow. ### 3. Alternative Entry Points The standard Solana entry point adds overhead. Alternatives: **Standard entry point:** ```rust use solana_program::entrypoint; entrypoint!(process_instruction); ``` **Minimal entry points:** - [solana-nostd-entrypoint](https://github.com/cavemanloverboy/solana-nostd-entrypoint): Ultra-minimal entry using unsafe Rust - [eisodos](https://github.com/anza-xyz/eisodos): Alternative minimal entry point **Warning**: These require deep understanding of Solana internals and unsafe Rust. Only use for extreme optimization needs. ### 4. Custom Heap Allocators Solana's default bump allocator never frees memory during transaction execution. **Problem:** ```rust // This will eventually run out of heap space (32KB limit) for _ in 0..1000 { let v = vec![0u8; 1024]; // Each iteration uses more heap // Memory is never freed! } ``` **Solution - Custom allocators:** - **smalloc**: Used by Metaplex programs, provides better memory management - Prevents out-of-memory errors in memory-intensive operations **Implementation** (advanced): ```rust #[global_allocator] static ALLOCATOR: custom_allocator::CustomAllocator = custom_allocator::CustomAllocator; ``` ### 5. Boxing and Heap Allocation Heap operations cost more CU than stack operations. ```rust // Stack allocation - faster let data = [0u8; 100]; // Heap allocation - slower, uses more CU let data = Box::new([0u8; 100]); ``` **Best practice**: Avoid `Box`, `Vec`, and other heap allocations when stack allocation is possible and doesn't risk overflow. ## Measuring Compute Units ### Using sol_log_compute_units() Built-in logging function to track CU consumption: ```rust use solana_program::log::sol_log_compute_units; pub fn my_instruction(ctx: Context<MyContext>) -> Result<()> { sol_log_compute_units(); // Log remaining CU // ... do some work ... sol_log_compute_units(); // Log remaining CU again Ok(()) } ``` **Output in transaction logs:** ``` Program consumption: 200000 units remaining Program consumption: 195432 units remaining ``` **CU used = 200000 - 195432 = 4,568 CU** ### compute_fn! Macro Convenient macro for measuring specific code blocks (costs 409 CU overhead): ```rust #[macro_export] macro_rules! compute_fn { ($msg:expr=> $($tt:tt)*) => { ::solana_program::msg!(concat!($msg, " {")); ::solana_program::log::sol_log_compute_units(); let res = { $($tt)* }; ::solana_program::log::sol_log_compute_units(); ::solana_program::msg!(concat!(" } // ", $msg)); res }; } ``` **Usage:** ```rust let result = compute_fn! { "My expensive operation" => expensive_computation() }; ``` **Output:** ``` Program log: My expensive operation { Program consumption: 195432 units remaining Program consumption: 180123 units remaining Program log: } // My expensive operation ``` **Actual CU = (195432 - 180123) - 409 (macro overhead) = 14,900 CU** ### Using Mollusk Bencher For native Rust programs, use Mollusk's built-in benchmarking (see main SKILL.md for details). ## Anti-Patterns to Avoid ### 1. Excessive Logging ```rust // BAD: Logging in production msg!("Processing user {}", user_pubkey); msg!("Amount: {}", amount); msg!("Timestamp: {}", Clock::get()?.unix_timestamp); ``` **Solution**: Remove logging or use conditional compilation: ```rust #[cfg(feature = "debug")] msg!("Processing user {}", user_pubkey); ``` ### 2. Large Data Types for Small Values ```rust // BAD: Using u64 when u8 suffices pub struct Config { pub fee_percentage: u64, // Only 0-100 pub max_items: u64, // Only 0-255 } // GOOD: Use smallest type pub struct Config { pub fee_percentage: u8, // 0-100 pub max_items: u8, // 0-255 } ``` ### 3. Cloning Large Structures ```rust // BAD: Unnecessary clone fn process_data(data: Vec<u8>) -> Result<()> { let copy = data.clone(); // Wastes CU and heap // ... } // GOOD: Pass by reference fn process_data(data: &[u8]) -> Result<()> { // Work directly with reference } ``` ### 4. Repeated PDA Derivation ```rust // BAD: Finding bump every time #[account( seeds = [b"vault"], bump // Finds bump on every call! )] pub vault: Account<'info, Vault>, // GOOD: Use saved bump #[account( seeds = [b"vault"], bump = vault.bump // Uses saved bump )] pub vault: Account<'info, Vault>, ``` ### 5. Unnecessary Boxing ```rust // BAD: Boxing adds heap overhead let value = Box::new(calculate_value()); // GOOD: Keep on stack let value = calculate_value(); ``` ### 6. String Operations ```rust // BAD: String concatenation and formatting let message = format!("User {} sent {} tokens", user, amount); msg!(&message); // GOOD: Use separate logs or remove entirely user.log(); amount.log(); ``` ### 7. Deep CPI Chains Each CPI adds significant overhead. Avoid unnecessary indirection: ```rust // BAD: Unnecessary CPI invoke( &my_helper_program::process(), &accounts, )?; // GOOD: Direct implementation process_directly(&accounts)?; ``` ### 8. Not Using Zero-Copy for Large Accounts ```rust // BAD: Large account with standard serialization #[account] pub struct LargeData { pub items: [u64; 1000], // Expensive to serialize/deserialize } // GOOD: Use zero-copy #[account(zero_copy)] #[repr(C)] pub struct LargeData { pub items: [u64; 1000], // Direct memory access } ``` ## Best Practices Summary 1. **Minimize or eliminate logging** in production code 2. **Use zero-copy** for accounts with large data structures 3. **Cache PDA bumps** - derive once, store in account, reuse 4. **Choose smallest data types** that meet your requirements 5. **Pass by reference** instead of cloning data 6. **Profile before optimizing** - measure CU usage to identify bottlenecks 7. **Consider native Rust** over Anchor for performance-critical programs 8. **Use `vec![]` initialization** instead of repeated `push()` calls 9. **Avoid unnecessary CPIs** - use direct operations when safe 10. **Balance safety vs performance** - don't sacrifice security without careful analysis 11. **Test CU usage** regularly - include benchmarks in your test suite 12. **Use checked math by default** - only optimize to unchecked when proven safe 13. **Minimize heap allocations** - prefer stack when possible 14. **Remove or conditionally compile debug code** for production builds 15. **Consider zero-copy for frequently updated accounts** - 50%+ CU savings ## Additional Resources ### Official Documentation - [How to Optimize Compute](https://solana.com/developers/guides/advanced/how-to-optimize-compute) - [Solana Compute Budget Documentation](https://github.com/solana-labs/solana/blob/090e11210aa7222d8295610a6ccac4acda711bb9/program-runtime/src/compute_budget.rs#L26-L87) ### Code Examples and Tools - [solana-developers/cu_optimizations](https://github.com/solana-developers/cu_optimizations) - Official examples with benchmarks - [hetdagli234/optimising-solana-programs](https://github.com/hetdagli234/optimising-solana-programs) - Community optimization examples ### Video Guides - [How to optimize CU in programs](https://www.youtube.com/watch?v=7CbAK7Oq_o4) - [Program optimization Part 1](https://www.youtube.com/watch?v=xoJ-3NkYXfY) - [Program optimization Part 2 - Advanced](https://www.youtube.com/watch?v=Pwly1cOa2hg) - [Writing Solana programs in Assembly](https://www.youtube.com/watch?v=eacDC0VgyxI) ### Technical Articles - [RareSkills: Solana Compute Unit Price](https://rareskills.io/post/solana-compute-unit-price) - [Understanding Solana Compute Units](https://www.helius.dev/blog/priority-fees-understanding-solanas-transaction-fee-mechanics) ### Advanced Tools - [solana-nostd-entrypoint](https://github.com/cavemanloverboy/solana-nostd-entrypoint) - Minimal entry point - [Mollusk](https://github.com/anza-xyz/mollusk) - Fast testing with CU benchmarking -
cpi.md 20.4 KB
# Cross-Program Invocation (CPI) This reference provides comprehensive coverage of Cross-Program Invocation (CPI) for native Rust Solana program development, including invoke patterns, account privilege propagation, and security considerations. ## Table of Contents 1. [What is CPI](#what-is-cpi) 2. [CPI Fundamentals](#cpi-fundamentals) 3. [invoke vs invoke_signed](#invoke-vs-invoke_signed) 4. [Account Privilege Propagation](#account-privilege-propagation) 5. [Common CPI Patterns](#common-cpi-patterns) 6. [CPI Limits and Constraints](#cpi-limits-and-constraints) 7. [Security Considerations](#security-considerations) 8. [Best Practices](#best-practices) --- ## What is CPI **Cross-Program Invocation (CPI) is when one Solana program directly calls instructions on another program.** ### Conceptual Model If you think of a Solana instruction as an API endpoint, a CPI is like one API endpoint internally calling another. ``` User Transaction │ ▼ ┌────────────────────┐ │ Your Program │ │ │ │ ┌──────────────┐ │ │ │ Instruction │ │ │ │ Handler │ │ │ └──────┬───────┘ │ │ │ CPI │ └──────────┼─────────┘ │ ▼ ┌────────────────────┐ │ System Program │ │ create_account │ └────────────────────┘ ``` ### Why CPI is Essential **Composability**: Programs can leverage functionality from other programs without reimplementing it. **Common Use Cases:** - Create accounts (System Program CPI) - Transfer tokens (Token Program CPI) - Interact with DeFi protocols - Call custom program logic - Complex multi-step operations ### CPI vs Direct Instruction | Aspect | Direct Instruction | CPI | |--------|-------------------|-----| | Who initiates | User wallet | Another program | | Signer source | User's private key | Program or PDA | | Call depth | 1 (top-level) | 2-5 (nested) | | Use case | Entry point | Program-to-program | --- ## CPI Fundamentals ### The Two CPI Functions Solana provides two functions for making CPIs: ```rust use solana_program::program::{invoke, invoke_signed}; // 1. invoke: For regular account signers pub fn invoke( instruction: &Instruction, account_infos: &[AccountInfo], ) -> ProgramResult // 2. invoke_signed: For PDA signers pub fn invoke_signed( instruction: &Instruction, account_infos: &[AccountInfo], signers_seeds: &[&[&[u8]]], ) -> ProgramResult ``` ### Required Imports ```rust use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, instruction::{AccountMeta, Instruction}, program::{invoke, invoke_signed}, pubkey::Pubkey, }; ``` ### Instruction Structure Before making a CPI, you must construct an `Instruction`: ```rust pub struct Instruction { /// Program ID of the program being invoked pub program_id: Pubkey, /// Accounts required by the instruction pub accounts: Vec<AccountMeta>, /// Serialized instruction data pub data: Vec<u8>, } pub struct AccountMeta { /// Account public key pub pubkey: Pubkey, /// Is this account a signer? pub is_signer: bool, /// Is this account writable? pub is_writable: bool, } ``` --- ## invoke vs invoke_signed ### invoke: Regular Signers Use `invoke` when all required signers are regular accounts (not PDAs). **Example: User transfers SOL** ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, pubkey::Pubkey, system_instruction, }; pub fn user_transfer_sol( _program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let sender = next_account_info(account_info_iter)?; let recipient = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Verify sender signed the transaction if !sender.is_signer { return Err(ProgramError::MissingRequiredSignature); } // Create transfer instruction let transfer_ix = system_instruction::transfer( sender.key, recipient.key, amount, ); // Execute CPI (sender already signed the transaction) invoke( &transfer_ix, &[ sender.clone(), recipient.clone(), system_program.clone(), ], )?; Ok(()) } ``` **Key Points:** - `sender.is_signer` must be true (verified at transaction level) - No `signers_seeds` needed - `invoke` internally calls `invoke_signed` with empty seeds ### invoke_signed: PDA Signers Use `invoke_signed` when a PDA needs to sign the instruction. **Example: PDA transfers SOL** ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey, system_instruction, }; pub fn pda_transfer_sol( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let pda_account = next_account_info(account_info_iter)?; let recipient = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Derive PDA and verify let (pda, bump_seed) = Pubkey::find_program_address( &[b"vault", recipient.key.as_ref()], program_id, ); if pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } // Create transfer instruction let transfer_ix = system_instruction::transfer( pda_account.key, // From PDA (needs signing!) recipient.key, amount, ); // PDA signing seeds (must match derivation) let signer_seeds: &[&[&[u8]]] = &[&[ b"vault", recipient.key.as_ref(), &[bump_seed], // Critical: bump must be included ]]; // Execute CPI with PDA signature invoke_signed( &transfer_ix, &[ pda_account.clone(), recipient.clone(), system_program.clone(), ], signer_seeds, // Runtime verifies and grants signing authority )?; Ok(()) } ``` **How Runtime Handles PDA Signing:** 1. Runtime receives `signers_seeds` 2. Calls `create_program_address(signers_seeds, calling_program_id)` 3. Verifies derived PDA matches an account in the instruction 4. Grants signing authority for that account 5. Executes the CPI **Critical:** Seeds must exactly match the PDA derivation, including the bump. --- ## Account Privilege Propagation ### Privilege Extension When making a CPI, account privileges **extend** from the caller to the callee. ``` User Transaction │ (provides: signer=true, writable=true) ▼ ┌─────────────────────┐ │ Program A │ │ Receives accounts: │ │ - user (signer) │──┐ Privileges │ - vault (writable) │ │ propagate └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Program B (via CPI)│ │ Can use: │ │ - user (signer) │ │ - vault (writable) │ └─────────────────────┘ ``` ### Propagation Rules **Rule 1:** If an account is a signer in Program A, it remains a signer in Program B (via CPI) **Rule 2:** If an account is writable in Program A, it remains writable in Program B (via CPI) **Rule 3:** Programs can add PDA signers via `invoke_signed` **Rule 4:** Programs cannot escalate privileges (can't make non-signer a signer without PDA derivation) ### Example: Privilege Propagation Chain ```rust // User calls Program A // Accounts: [user (signer, writable), vault (writable), data_account] // Program A → CPI to Program B invoke( &instruction_for_program_b, &[user.clone(), vault.clone()], // Both retain privileges )?; // Program B → CPI to Program C invoke( &instruction_for_program_c, &[user.clone()], // user still a signer! )?; ``` **Depth**: Up to 4 levels of CPI (5 total stack height including initial transaction) --- ## Common CPI Patterns ### 1. System Program: Create Account **Most common CPI**: Creating new accounts. ```rust use solana_program::{ program::invoke_signed, rent::Rent, system_instruction, sysvar::Sysvar, }; pub fn create_pda_account( program_id: &Pubkey, payer: &AccountInfo, pda_account: &AccountInfo, system_program: &AccountInfo, space: usize, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { // Calculate rent let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(space); // Create account instruction let create_account_ix = system_instruction::create_account( payer.key, pda_account.key, rent_lamports, space as u64, program_id, ); // Prepare signer seeds let mut full_seeds = seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; // Execute CPI invoke_signed( &create_account_ix, &[payer.clone(), pda_account.clone(), system_program.clone()], signer_seeds, )?; Ok(()) } ``` ### 2. System Program: Transfer SOL ```rust use solana_program::system_instruction; // From regular account let transfer_ix = system_instruction::transfer(from_key, to_key, lamports); invoke(&transfer_ix, &[from_account, to_account, system_program])?; // From PDA let transfer_ix = system_instruction::transfer(pda_key, to_key, lamports); let signer_seeds: &[&[&[u8]]] = &[&[seeds, &[bump]]]; invoke_signed(&transfer_ix, &[pda_account, to_account, system_program], signer_seeds)?; ``` ### 3. Custom Program CPI **Calling another custom program:** ```rust use borsh::BorshSerialize; #[derive(BorshSerialize)] struct CustomInstructionData { amount: u64, memo: String, } pub fn call_custom_program( custom_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let user = next_account_info(account_info_iter)?; let target_account = next_account_info(account_info_iter)?; let custom_program = next_account_info(account_info_iter)?; // Serialize instruction data let instruction_data = CustomInstructionData { amount, memo: "Hello from CPI".to_string(), }; let data = instruction_data.try_to_vec()?; // Build instruction let instruction = Instruction { program_id: *custom_program_id, accounts: vec![ AccountMeta::new(*user.key, true), // signer, writable AccountMeta::new(*target_account.key, false), // writable ], data, }; // Execute CPI invoke( &instruction, &[user.clone(), target_account.clone(), custom_program.clone()], )?; Ok(()) } ``` ### 4. Multiple PDAs Signing ```rust pub fn multi_pda_cpi( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let pda1_seeds = &[b"pda1", &[bump1]]; let pda2_seeds = &[b"pda2", &[bump2]]; // Multiple PDA signers let signer_seeds: &[&[&[u8]]] = &[ pda1_seeds, // First PDA pda2_seeds, // Second PDA ]; invoke_signed(&instruction, &accounts, signer_seeds)?; Ok(()) } ``` ### 5. Chained CPIs **Program A calls Program B, which calls Program C:** ```rust // In Program A pub fn program_a_handler( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { // Call Program B let instruction_for_b = build_program_b_instruction(); invoke(&instruction_for_b, accounts)?; Ok(()) } // In Program B pub fn program_b_handler( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { // Call Program C let instruction_for_c = build_program_c_instruction(); invoke(&instruction_for_c, accounts)?; Ok(()) } ``` **Depth tracking**: User→A→B→C = stack depth 4 (within limit) --- ## CPI Limits and Constraints ### Stack Depth Limit **Maximum call depth:** 5 (including initial transaction) ``` Depth 1: User Transaction Depth 2: Program A (first CPI) Depth 3: Program B (second CPI) Depth 4: Program C (third CPI) Depth 5: Program D (fourth CPI) Depth 6: ❌ ERROR - MAX_INSTRUCTION_STACK_DEPTH exceeded ``` **Constant:** ```rust // From agave source pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5; ``` **Error when exceeded:** ``` Error: CallDepth(5) ``` ### Account Limits - **Max accounts per instruction:** 256 (practical limit ~64 without ALTs) - **Max writable accounts:** Limited by transaction size - **Duplicate accounts:** Allowed but share state (mutations visible to all references) ### Compute Unit Costs CPI operations consume compute units: | Operation | Approximate CU Cost | |-----------|---------------------| | `invoke` base cost | ~1,000 CU | | `invoke_signed` base cost | ~1,000 CU | | Per account passed | ~50-100 CU | | PDA derivation in runtime | ~1,500 CU | | Actual callee logic | Variable | **Tip:** Pre-derive PDAs and store bumps to save CU. ### Data Size Limits - **Instruction data:** No hard limit, but affects transaction size (1232 bytes max for non-ALT transactions) - **Account data modification:** Accounts can be resized via `realloc` (up to 10 MiB) --- ## Security Considerations ### 1. Validate PDA Derivation Before CPI **❌ Vulnerable:** ```rust pub fn vulnerable_cpi( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let pda_account = &accounts[0]; // No validation! let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]]; invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?; Ok(()) } ``` **✅ Secure:** ```rust pub fn secure_cpi( program_id: &Pubkey, accounts: &[AccountInfo], bump: u8, ) -> ProgramResult { let pda_account = &accounts[0]; // Validate PDA before CPI let (expected_pda, _) = Pubkey::find_program_address(&[b"vault"], program_id); if expected_pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]]; invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?; Ok(()) } ``` ### 2. Verify Signer Requirements **Always check `is_signer` before making CPIs that transfer value:** ```rust if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); } let transfer_ix = system_instruction::transfer(user.key, vault.key, amount); invoke(&transfer_ix, &[user.clone(), vault.clone(), system_program.clone()])?; ``` ### 3. Program ID Verification **Verify the program being called is the expected program:** ```rust const EXPECTED_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; if token_program.key.to_string() != EXPECTED_PROGRAM { return Err(ProgramError::IncorrectProgramId); } ``` ### 4. Privilege Leakage **Be careful about which accounts you pass in CPIs:** ```rust // ❌ Dangerous - passes admin with signer privilege invoke( &untrusted_program_instruction, &[admin.clone(), user_data.clone()], // Admin is a signer! )?; // ✅ Safe - only pass necessary accounts invoke( &untrusted_program_instruction, &[user_data.clone()], // Admin not included )?; ``` ### 5. Reent rancy Considerations **Solana programs are generally safe from reentrancy** because: - Accounts are locked during instruction execution - Runtime prevents concurrent modifications **However, be cautious with:** - State assumptions across CPI boundaries - Read-modify-write patterns split across CPIs ### 6. Error Handling **CPI errors propagate to the caller:** ```rust // If CPI fails, entire transaction reverts match invoke(&instruction, &accounts) { Ok(()) => msg!("CPI succeeded"), Err(e) => { msg!("CPI failed: {:?}", e); return Err(e); // Propagate error } } ``` **All state changes are atomic** - if CPI fails, all changes rollback. --- ## Best Practices ### 1. Derive PDAs Once ```rust // ❌ Wasteful - derives multiple times pub fn wasteful(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id); // ... use pda let (pda_again, bump_again) = Pubkey::find_program_address(&[b"data"], program_id); // ... use pda_again (same as pda!) } // ✅ Efficient - derive once, reuse pub fn efficient(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id); // Reuse pda and bump } ``` ### 2. Store and Reuse Bumps ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct VaultData { pub bump: u8, // Store on creation // ... other fields } // On CPI: use stored bump let vault_data = VaultData::try_from_slice(&vault_pda.data.borrow())?; let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[vault_data.bump]]]; ``` **Benefit:** Saves ~2,700 CU per operation. ### 3. Helper Functions for Common CPIs ```rust pub mod cpi_helpers { use super::*; pub fn transfer_sol( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { let ix = system_instruction::transfer(from.key, to.key, amount); invoke(&ix, &[from.clone(), to.clone(), system_program.clone()]) } pub fn transfer_sol_from_pda( from_pda: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, signer_seeds: &[&[&[u8]]], ) -> ProgramResult { let ix = system_instruction::transfer(from_pda.key, to.key, amount); invoke_signed(&ix, &[from_pda.clone(), to.clone(), system_program.clone()], signer_seeds) } } ``` ### 4. Validate All CPI Inputs **Checklist before CPI:** - ✅ Verify signer requirements (`is_signer`) - ✅ Validate PDA derivation - ✅ Check program IDs match expectations - ✅ Verify account ownership - ✅ Validate data integrity ### 5. Document CPI Dependencies ```rust /// Transfers SOL from program vault to recipient. /// /// # Accounts /// 0. `[writable]` vault_pda - Program vault (PDA, signer) /// 1. `[writable]` recipient - Receives SOL /// 2. `[]` system_program - System Program (11111...) /// /// # CPIs Made /// - System Program: transfer (from vault to recipient) pub fn withdraw_from_vault( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { // ... } ``` ### 6. Error Context ```rust invoke(&instruction, &accounts).map_err(|e| { msg!("CPI to System Program failed"); e })?; ``` ### 7. Minimize CPI Depth **Keep call chains shallow:** - Reduces compute units - Easier to debug - Lower risk of hitting stack limit - Better user experience (simpler transactions) --- ## Summary **Key Takeaways:** 1. **CPI enables composability** - programs call other programs 2. **Use `invoke` for regular signers**, `invoke_signed` for PDAs 3. **Privileges propagate** - signers and writable flags extend through CPIs 4. **Maximum depth is 5** - including initial transaction 5. **Always validate PDAs** before using in `invoke_signed` 6. **Verify signer requirements** to prevent unauthorized operations 7. **Store bumps** in account data to save compute units 8. **CPIs are atomic** - failures rollback all changes **Security Checklist:** - ✅ Validate PDA derivation with canonical bump - ✅ Verify `is_signer` for value transfers - ✅ Check program IDs match expectations - ✅ Only pass necessary accounts (avoid privilege leakage) - ✅ Handle CPI errors appropriately **Common Pattern:** ```rust // 1. Validate inputs if !user.is_signer { return Err(ProgramError::MissingRequiredSignature); } // 2. Derive and validate PDA if needed let (pda, bump) = Pubkey::find_program_address(&seeds, program_id); if pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } // 3. Build instruction let ix = build_instruction(); // 4. Execute CPI invoke_signed(&ix, &accounts, &[&[seeds, &[bump]]])?; ``` CPI is the foundation of program composability on Solana. Master it to build powerful, modular programs that leverage the entire ecosystem. -
deployment.md 40.1 KB
# Solana Program Deployment Reference This reference covers deployment workflows, best practices, and troubleshooting for both Anchor and native Rust Solana programs. ## Table of Contents - [Deployment Overview](#deployment-overview) - [Pre-Deployment Checklist](#pre-deployment-checklist) - [Building Programs](#building-programs) - [Deploying to Networks](#deploying-to-networks) - [Program Upgrades](#program-upgrades) - [Verified Builds](#verified-builds) - [Program Authority Management](#program-authority-management) - [Multisig Deployments](#multisig-deployments) - [Network-Specific Considerations](#network-specific-considerations) - [Post-Deployment](#post-deployment) - [Common Issues and Troubleshooting](#common-issues-and-troubleshooting) - [Best Practices](#best-practices) --- ## Deployment Overview ### Solana Networks Solana has three primary networks: **Localhost (127.0.0.1:8899)** - Local test validator running on your machine - Fastest iteration, no cost - Full control over network state - Use for rapid development and debugging **Devnet** - Public development network - Free SOL via airdrops - Resets periodically - Use for integration testing **Mainnet-beta** - Production network with real economic value - Requires real SOL for deployment and transactions - Immutable deployed programs (unless upgradeable) - Use for production deployments ### Network Configuration **Anchor** - Edit `Anchor.toml`: ```toml [toolchain] [features] seeds = false skip-lint = false [programs.localnet] my_program = "11111111111111111111111111111111" [programs.devnet] my_program = "YourDevnetProgramID" [programs.mainnet] my_program = "YourMainnetProgramID" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" # Change to "Devnet" or "Mainnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ``` **Native Rust** - Use Solana CLI: ```bash # View current config solana config get # Set network solana config set --url https://api.devnet.solana.com # Devnet solana config set --url https://api.mainnet-beta.solana.com # Mainnet solana config set --url http://localhost:8899 # Localnet # Set wallet solana config set --keypair ~/.config/solana/id.json ``` ### Cost Considerations Program deployment requires rent-exempt balance for: 1. **Program Account** - Stores program metadata (small cost) 2. **Program Data Account** - Stores the executable bytecode (major cost) **Calculate deployment cost:** ```bash # Get program size ls -l target/deploy/my_program.so # Example: 363960 bytes # Check rent for program data account (1x program size in newer versions) solana rent 363960 # Output: # Rent-exempt minimum: ~2.5 SOL # Add transaction fees (~0.002 SOL) for deployment transactions ``` **Cost breakdown:** - **363KB program** ≈ 2.5 SOL rent + 0.002 SOL tx fees = **~2.502 SOL** - **800KB program** ≈ 5.5 SOL rent + 0.002 SOL tx fees = **~5.502 SOL** **Important:** Since Solana CLI v1.18+, program accounts are sized to 1x the .so file (previously 2x), reducing costs by approximately 50%. --- ## Pre-Deployment Checklist ### 1. Build Verification **Anchor:** ```bash # Clean build anchor clean anchor build # Verify build succeeded ls -la target/deploy/ # Should see: my_program.so and my_program-keypair.json ``` **Native Rust:** ```bash # Clean build cargo clean cargo build-sbf # Verify build ls -la target/deploy/ # Should see: my_program.so ``` ### 2. Testing Completeness **Anchor:** ```bash # Run all tests on local validator anchor test # Run tests without redeploying anchor test --skip-deploy # Run specific test file anchor test tests/my-test.ts ``` **Native Rust:** ```bash # Run Mollusk unit tests cargo test # Run integration tests cargo test-sbf ``` ### 3. Security Review - [ ] All account validations implemented (owner checks, signer checks) - [ ] No missing arithmetic overflow checks - [ ] PDA seeds properly validated - [ ] No uninitialized account usage - [ ] Authority checks on privileged operations - [ ] CPI security (check target program IDs) - [ ] Consider professional audit for mainnet ### 4. Program Size Optimization **Check current size:** ```bash ls -lh target/deploy/my_program.so ``` **Optimization techniques:** ```toml # Cargo.toml - Release profile optimization [profile.release] overflow-checks = true lto = "fat" # Link-time optimization codegen-units = 1 # Single codegen unit opt-level = "z" # Optimize for size (use "3" for speed) strip = true # Strip symbols [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ``` **Remove unused dependencies:** ```bash # Check dependency tree cargo tree # Remove unused features # Instead of: # solana-program = "2.1.0" # Use: solana-program = { version = "2.1.0", default-features = false } ``` **Current size limits:** - Maximum program size: ~1 MB (actual limit varies by compute budget) - Recommended: Keep under 500KB for reliable deployment ### 5. Rent Calculation ```bash # Calculate exact rent needed PROGRAM_SIZE=$(wc -c < target/deploy/my_program.so) solana rent $PROGRAM_SIZE # Fund deployment wallet solana balance # If insufficient, request airdrop (devnet) or transfer SOL ``` --- ## Building Programs ### Anchor Programs **Standard build:** ```bash anchor build ``` **What it produces:** - `target/deploy/my_program.so` - Executable binary - `target/deploy/my_program-keypair.json` - Program ID keypair - `target/idl/my_program.json` - Interface definition - `target/types/my_program.ts` - TypeScript types **Build specific program in workspace:** ```bash anchor build --program-name my_program ``` **Verifiable build (Docker-based):** ```bash # Install solana-verify CLI cargo install solana-verify # Build verifiably solana-verify build # Build specific program solana-verify build --library-name my_program ``` **Sync program IDs:** ```bash # After first build, sync declared IDs with keypair anchor keys sync ``` ### Native Rust Programs **Standard build:** ```bash cargo build-sbf ``` **What it produces:** - `target/deploy/my_program.so` - Executable binary - No automatic keypair generation (must provide or use deployed ID) **Verifiable build:** ```bash solana-verify build --library-name my_program ``` **Build with specific Solana version:** ```bash # Set platform tools version cargo build-sbf --sbf-sdk ~/.local/share/solana/install/releases/2.1.0/solana-release/bin/sdk/sbf ``` ### Understanding Build Outputs **.so file:** - Compiled BPF bytecode - This is what gets deployed on-chain - Hash determines if program matches source **Program ID keypair (Anchor):** - Generated on first build - Defines program's on-chain address - **CRITICAL:** Back this up before deploying **IDL (Anchor only):** - JSON describing program interface - Used by clients to interact with program - Can be uploaded on-chain for discovery --- ## Deploying to Networks ### Anchor Deployment **Deploy to configured network:** ```bash # Deploy to network specified in Anchor.toml [provider] cluster anchor deploy # Specify network explicitly anchor deploy --provider.cluster devnet anchor deploy --provider.cluster mainnet ``` **Deploy with specific program ID:** ```bash # First deployment - uses keypair from target/deploy/ anchor deploy # Redeploy to same address (upgrade) anchor deploy ``` **Deploy with priority fees (congested networks):** ```bash # Set priority fee in micro-lamports per compute unit anchor deploy --provider.cluster mainnet \ --program-name my_program \ -- --with-compute-unit-price 50000 ``` **What `anchor deploy` does:** 1. Reads program from `target/deploy/my_program.so` 2. Creates or uses existing program account 3. Uploads program data via multiple write transactions 4. Sets executable flag on program account 5. Optionally uploads IDL to on-chain account ### Native Rust Deployment **Deploy new program:** ```bash # Deploy with auto-generated program ID solana program deploy target/deploy/my_program.so # Deploy to specific program ID (first time) solana program deploy target/deploy/my_program.so \ --program-id my_program-keypair.json ``` **Deploy with priority fees:** ```bash solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ --with-compute-unit-price 50000 \ --max-sign-attempts 100 \ --use-rpc ``` **Flags explained:** - `--with-compute-unit-price` - Priority fee (micro-lamports per CU) - `--max-sign-attempts` - Retries for recent blockhash expiration - `--use-rpc` - Send transactions individually vs in batches - `-u <URL>` - Specify RPC endpoint **Check deployment cost before deploying:** ```bash # Dry run to estimate cost solana program deploy target/deploy/my_program.so --dry-run ``` ### Deploying with Specific Program ID **Generate deterministic program ID:** ```bash # Create new keypair solana-keygen new -o my-program-keypair.json # View address solana-keygen pubkey my-program-keypair.json ``` **For Anchor:** ```bash # Update lib.rs with new program ID declare_id!("YourNewProgramID"); # Update Anchor.toml [programs.devnet] my_program = "YourNewProgramID" # Rebuild and deploy anchor build anchor keys sync # Verify IDs match anchor deploy ``` **For Native Rust:** ```bash # Deploy using keypair solana program deploy target/deploy/my_program.so \ --program-id my-program-keypair.json ``` ### Deployment Costs and Funding **Check balance before deployment:** ```bash solana balance ``` **Fund wallet for devnet:** ```bash # Request airdrop (2 SOL max per request) solana airdrop 2 # For larger programs, request multiple times solana airdrop 2 && solana airdrop 2 ``` **Fund wallet for mainnet:** ```bash # Transfer SOL from exchange or another wallet # No airdrops available on mainnet ``` **Cost estimation:** ```bash # Program size PROGRAM_SIZE=$(wc -c < target/deploy/my_program.so) # Rent cost solana rent $PROGRAM_SIZE # Add ~0.002-0.01 SOL for transaction fees # Add priority fees if network is congested ``` --- ## Program Upgrades ### How Upgrades Work Solana programs deployed via `solana program deploy` or `anchor deploy` are **upgradeable by default**. **Upgrade mechanism:** 1. Program data lives in separate account from program account 2. Upgrade authority (wallet) can replace program data 3. Program address stays the same 4. All accounts/PDAs remain valid **Check if program is upgradeable:** ```bash solana program show <PROGRAM_ID> # Output shows: # Program Id: <PROGRAM_ID> # Owner: BPFLoaderUpgradeable1111111111111111111111111 # ProgramData Address: <DATA_ACCOUNT> # Authority: <UPGRADE_AUTHORITY> # If upgradeable # Last Deployed In Slot: ... # Data Length: ... ``` ### Anchor Upgrades **Upgrade deployed program:** ```bash anchor upgrade target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ --provider.cluster devnet ``` **Note:** In newer Anchor versions, `anchor deploy` automatically upgrades if program exists. **Upgrade with priority fees:** ```bash anchor upgrade target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ --provider.cluster mainnet \ -- --with-compute-unit-price 50000 ``` ### Native Rust Upgrades **Upgrade using same deploy command:** ```bash solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ --upgrade-authority ~/.config/solana/id.json ``` ### Extending Program Accounts **Problem:** If new program is larger than allocated space: ``` Error: account data too small for instruction ``` **Solution:** Extend program account before upgrading: ```bash # Check current program size solana program show <PROGRAM_ID> # Shows: Data Length: 363960 bytes # New build is larger NEW_SIZE=$(wc -c < target/deploy/my_program.so) echo $NEW_SIZE # Shows: 380000 bytes # Calculate additional bytes needed ADDITIONAL_BYTES=$((NEW_SIZE - 363960)) echo $ADDITIONAL_BYTES # Shows: 16040 bytes # Extend program account solana program extend <PROGRAM_ID> $ADDITIONAL_BYTES # Check rent cost for extension solana rent $ADDITIONAL_BYTES # Example: 0.2 SOL # Now upgrade works solana program deploy target/deploy/my_program.so --program-id <PROGRAM_ID> ``` **Note:** Program extension support added in Solana CLI v1.18+ ### Data Migration Strategies **Account structure changes:** When upgrading programs that change account layouts: **Option 1: Version field** ```rust #[account] pub struct MyAccount { pub version: u8, // Add version field pub data: u64, // New fields in v2 pub new_field: Option<String>, } // In instruction handler if account.version == 1 { // Migrate from v1 to v2 account.new_field = Some("default".to_string()); account.version = 2; } ``` **Option 2: Separate migration instruction** ```rust pub fn migrate_account_v1_to_v2(ctx: Context<MigrateAccount>) -> Result<()> { let account = &mut ctx.accounts.account; // Perform migration logic account.new_field = compute_new_field(&account.data); account.version = 2; Ok(()) } ``` **Option 3: New program version with migration path** - Deploy new program ID - Create migration instructions that move data - Deprecate old program gradually --- ## Verified Builds Verified builds prove deployed bytecode matches public source code. ### Why Verify? - **Transparency:** Users can audit your program's source - **Trust:** Proves deployed program matches GitHub repository - **Security:** Enables community security reviews - **Ecosystem:** Explorers display verified status - **Wallets:** May whitelist verified programs ### Tools for Verification **Solana Verify CLI:** ```bash cargo install solana-verify ``` **Docker (required for deterministic builds):** - Install Docker: https://docs.docker.com/engine/install/ - Ensure Docker daemon is running ### Building Verifiable Programs **Verifiable build process:** ```bash # Navigate to project root (with Cargo.toml) cd my-project # Build in Docker container for deterministic output solana-verify build # For workspace with specific program solana-verify build --library-name my_program # Get hash of built executable solana-verify get-executable-hash target/deploy/my_program.so ``` **What makes builds verifiable:** 1. **Docker environment:** Ensures consistent build environment 2. **Locked dependencies:** `Cargo.lock` pins exact versions 3. **Same toolchain:** Uses specific Rust/Solana version 4. **Deterministic compilation:** Same input → same output **Project structure requirements:** ``` my-project/ ├── Cargo.toml # Workspace manifest ├── Cargo.lock # Locked dependencies (required!) ├── programs/ │ └── my_program/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ``` **Workspace Cargo.toml example:** ```toml [workspace] members = ["programs/*"] resolver = "2" [workspace.dependencies] solana-program = "2.1.0" [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ``` ### Deploying Verifiable Programs **Deploy verified build:** ```bash # IMPORTANT: Use the binary from solana-verify build # DO NOT run `anchor build` or `cargo build-sbf` after verification build # Deploy with priority fees for reliability solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ -u https://api.mainnet-beta.solana.com \ --with-compute-unit-price 50000 \ --max-sign-attempts 100 \ --use-rpc ``` **Verify deployed program matches built executable:** ```bash # Get on-chain program hash solana-verify get-program-hash -u mainnet-beta <PROGRAM_ID> # Get local executable hash solana-verify get-executable-hash target/deploy/my_program.so # Hashes must match! ``` ### Verifying Against Repository **Verify from GitHub repository:** ```bash solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo \ --commit-hash <COMMIT_HASH> \ --library-name my_program \ --mount-path programs/my_program ``` **Parameters explained:** - `--program-id`: On-chain program address - `--commit-hash`: Git commit to build from (optional, uses latest if omitted) - `--library-name`: Crate name from Cargo.toml `[lib]` section - `--mount-path`: Path to program directory in repo (for workspaces) **Upload verification data on-chain:** When prompted during verification: ``` Would you like to upload verification data on-chain? (y/n) ``` Select **yes** to write verification PDA. This enables: - Solana Explorer verification badge - OtterSec API verification - SolanaFM verification display ### Remote Verification with OtterSec API **Submit verification job:** ```bash solana-verify verify-from-repo \ --remote \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo ``` **Manual job submission:** ```bash solana-verify remote submit-job \ --program-id <PROGRAM_ID> \ --uploader <UPGRADE_AUTHORITY> ``` **Check verification status:** ```bash solana-verify remote get-job-status --job-id <JOB_ID> ``` **Verification API endpoint:** ``` https://verify.osec.io/status/<PROGRAM_ID> ``` **Where verified status appears:** - [Solana Explorer](https://explorer.solana.com) - [SolanaFM](https://solana.fm) - [SolScan](https://solscan.io) - [SolanaVerify.org](https://solanaverify.org) - [OtterSec API](https://verify.osec.io/verified-programs) ### security.txt Integration **Add security contact info:** ```rust #[cfg(not(feature = "no-entrypoint"))] solana_security_txt::security_txt! { name: "My Program", project_url: "https://myproject.com", contacts: "email:security@myproject.com,discord:myproject", policy: "https://github.com/myproject/security/blob/main/SECURITY.md", preferred_languages: "en", source_code: "https://github.com/myproject/program", source_release: "v1.0.0", auditors: "Audit Firm Name" } ``` **Benefits:** - Security researchers know how to contact you - Shows commitment to security - Standard across Solana ecosystem --- ## Program Authority Management ### Viewing Program Information **Check program authority:** ```bash solana program show <PROGRAM_ID> # Output: # Program Id: YourProgramId # Owner: BPFLoaderUpgradeable1111111111111111111111111 # ProgramData Address: <DATA_ACCOUNT_ADDRESS> # Authority: <CURRENT_AUTHORITY> # Current upgrade authority # Last Deployed In Slot: 123456789 # Data Length: 363960 bytes (0x58e38 bytes) ``` **View all your deployed programs:** ```bash solana program show --programs # Shows all programs where your wallet is authority ``` ### Transferring Upgrade Authority **Transfer to new authority:** ```bash solana program set-upgrade-authority <PROGRAM_ID> \ --new-upgrade-authority <NEW_AUTHORITY_PUBKEY> ``` **Common use cases:** - Transfer to multisig (Squads Protocol) - Transfer to governance program - Transfer to team member - Transfer to DAO **Transfer to multisig (Squads):** ```bash # Get Squads vault address from https://v4.squads.so/ SQUADS_VAULT="YourSquadsVaultAddress" solana program set-upgrade-authority <PROGRAM_ID> \ --new-upgrade-authority $SQUADS_VAULT ``` ### Making Programs Immutable **WARNING:** This is irreversible! ```bash solana program set-upgrade-authority <PROGRAM_ID> --final # Confirms immutability - program can NEVER be upgraded ``` **Use cases for immutability:** - DeFi protocols requiring immutable guarantees - After extensive auditing, lock the program - Community trust through code permanence **Considerations:** - Cannot fix bugs after making immutable - Cannot add features - Ensure thorough testing and auditing first - Consider governance/multisig instead ### Buffer Accounts for Deployment **Understanding buffers:** When deploying, the Solana CLI creates temporary buffer accounts: 1. Creates buffer account 2. Writes program data to buffer 3. Deploys buffer to program account 4. Closes buffer (if successful) **View your buffer accounts:** ```bash solana program show --buffers # Output: # Buffer Address | Authority | Balance # Abc123... | YourWallet... | 2.5 SOL ``` **Close buffer manually (failed deployment):** ```bash solana program close <BUFFER_ADDRESS> # Recovers rent SOL back to wallet ``` **Common scenarios:** - Deployment failed mid-process - Want to cancel deployment - Need to reclaim SOL from old buffers --- ## Multisig Deployments Deploying with multisig (Squads Protocol) provides security for production programs. ### Why Use Multisig? - **Security:** No single point of failure - **Governance:** Team/DAO approval for upgrades - **Transparency:** On-chain approval trail - **Best practice:** Standard for serious projects ### Workflow Overview 1. Build verifiable program 2. Deploy with temporary authority 3. Verify against repository 4. Transfer authority to multisig 5. Export PDA transaction for verification upload 6. Submit through Squads UI 7. Remote verification ### Detailed Multisig Deployment **1. Build verifiable program:** ```bash solana-verify build --library-name my_program ``` **2. Deploy with your wallet as initial authority:** ```bash solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ -u mainnet-beta \ --with-compute-unit-price 50000 ``` **3. Verify locally:** ```bash solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/repo \ --commit-hash <COMMIT> ``` **4. Transfer authority to Squads multisig:** Get Squads vault address from https://v4.squads.so/ ```bash SQUADS_VAULT="YourSquadsVaultAddress" solana program set-upgrade-authority <PROGRAM_ID> \ --new-upgrade-authority $SQUADS_VAULT ``` **5. Export verification PDA transaction:** ```bash solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/repo \ --export-pda-tx verification_tx.json ``` **6. Submit transaction in Squads:** - Go to https://v4.squads.so/ - Navigate to your multisig - Create new transaction - Import `verification_tx.json` - Get approval from multisig members - Execute transaction **7. Submit remote verification:** After Squads transaction executes: ```bash solana-verify remote submit-job \ --program-id <PROGRAM_ID> \ --uploader <SQUADS_VAULT> ``` **8. Monitor verification:** ```bash # Check job status solana-verify remote get-job-status --job-id <JOB_ID> # Or visit https://verify.osec.io/status/<PROGRAM_ID> ``` ### Upgrading via Multisig **Create upgrade buffer:** ```bash # Build new version solana-verify build --library-name my_program # Write to buffer (not direct upgrade) solana program write-buffer target/deploy/my_program.so # Output: Buffer address: <BUFFER_ADDRESS> ``` **Transfer buffer authority to multisig:** ```bash solana program set-buffer-authority <BUFFER_ADDRESS> \ --new-buffer-authority <SQUADS_VAULT> ``` **Create Squads transaction for upgrade:** Use Squads CLI or UI to propose: ```bash # Using Squads SDK/CLI npx ts-node scripts/program-upgrade.ts \ --rpc "https://api.mainnet-beta.solana.com" \ --program "<PROGRAM_ID>" \ --buffer "<BUFFER_ADDRESS>" \ --multisig "<MULTISIG_ADDRESS>" \ --member "<YOUR_PUBKEY>" \ --name "Upgrade my_program v2" ``` **Close buffer via Squads (if needed):** ```bash npx ts-node scripts/squad-closebuffer.ts \ --rpc "https://api.mainnet-beta.solana.com" \ --multisig "<MULTISIG_ADDRESS>" \ --buffer "<BUFFER_ADDRESS>" \ --program "<PROGRAM_ID>" ``` --- ## Network-Specific Considerations ### Localhost Development **Start test validator:** ```bash # Basic solana-test-validator # With program pre-deployed solana-test-validator --bpf-program <PROGRAM_ID> target/deploy/my_program.so # With cloned mainnet accounts solana-test-validator \ --clone <ACCOUNT_ADDRESS> \ --url mainnet-beta # Reset ledger on restart solana-test-validator --reset ``` **Deploy to local validator:** ```bash # Anchor anchor localnet # Starts validator and deploys # Or anchor deploy --provider.cluster localnet # Native Rust solana program deploy target/deploy/my_program.so -ul ``` **Benefits:** - Instant transaction confirmation - Unlimited free SOL - Full control over clock and state - Fast iteration **Limitations:** - No network effects - Single validator (no consensus) - State doesn't persist (unless configured) ### Devnet Deployment **Configure network:** ```bash solana config set --url devnet ``` **Fund wallet:** ```bash solana airdrop 2 # Repeat as needed, max 2 SOL per request ``` **Deploy:** ```bash # Anchor anchor deploy --provider.cluster devnet # Native Rust solana program deploy target/deploy/my_program.so \ -u devnet \ --with-compute-unit-price 1000 ``` **Benefits:** - Real network conditions - Free SOL via airdrops - Test integrations with other programs - Longer-lived state than localnet **Limitations:** - Network resets occasionally - Potential rate limiting - Slower than localnet - Public network (anyone can interact) **Best practices:** - Test all upgrade paths on devnet first - Monitor transaction success rates - Test with realistic compute budgets - Validate against cloned mainnet accounts ### Testnet Deployment Testnet is less commonly used but available for staging: ```bash solana config set --url testnet solana airdrop 2 # If available ``` **Use cases:** - Staging environment before mainnet - Testing between devnet and mainnet - Longer-term testing without mainnet costs ### Mainnet Deployment **Pre-deployment checklist:** - [ ] Thoroughly tested on devnet - [ ] Security audit completed (for DeFi/high-value programs) - [ ] Verified build prepared - [ ] Upgrade authority configured (multisig recommended) - [ ] Sufficient SOL for deployment (check `solana rent`) - [ ] Monitoring/alerting setup - [ ] Rollback plan documented - [ ] Team coordination for deployment time **Configure mainnet:** ```bash solana config set --url mainnet-beta # Use paid RPC for reliability (recommended) solana config set --url https://your-rpc-provider.com ``` **Fund wallet:** ```bash # Transfer from exchange or another wallet # Calculate needed SOL: PROGRAM_SIZE=$(wc -c < target/deploy/my_program.so) solana rent $PROGRAM_SIZE # Add ~0.5 SOL buffer for transaction fees and priority fees ``` **Deploy with appropriate priority fees:** Check current priority fee recommendations: - https://www.quicknode.com/gas-tracker/solana - https://solanacompass.com/gas-fees ```bash # Typical priority fee: 50,000-300,000 micro-lamports per CU solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ -u mainnet-beta \ --with-compute-unit-price 100000 \ --max-sign-attempts 100 \ --use-rpc ``` **Post-deployment verification:** ```bash # Verify deployment solana program show <PROGRAM_ID> -u mainnet-beta # Verify hash matches solana-verify get-program-hash -u mainnet-beta <PROGRAM_ID> # Submit verification job solana-verify verify-from-repo \ --remote \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/repo ``` **Cost considerations:** - 200KB program: ~1.5 SOL - 500KB program: ~3.5 SOL - 800KB program: ~5.5 SOL - Plus transaction fees: ~0.01-0.05 SOL - Priority fees during congestion: +0.1-1 SOL --- ## Post-Deployment ### Verifying Deployment Success **Check program was deployed:** ```bash solana program show <PROGRAM_ID> # Verify output shows: # - Correct ProgramData address # - Your authority # - Expected data length # - Recent slot number ``` **Compare program hash:** ```bash # On-chain hash solana-verify get-program-hash -u <NETWORK> <PROGRAM_ID> # Local build hash solana-verify get-executable-hash target/deploy/my_program.so # Must match exactly ``` ### Testing On-Chain **Anchor:** ```bash # Run tests against deployed program anchor test --skip-deploy --provider.cluster devnet # Or run specific test anchor run test-on-devnet ``` **Native Rust / TypeScript client (Anchor - requires legacy `@solana/web3.js` v1):** ```typescript import { Connection, PublicKey } from '@solana/web3.js'; const connection = new Connection('https://api.devnet.solana.com'); const programId = new PublicKey('YourProgramId'); // Send test transaction const tx = await program.methods .yourInstruction() .accounts({ /* ... */ }) .rpc(); console.log('Transaction:', tx); ``` **Smoke tests:** - Call each instruction with valid inputs - Verify account state changes - Check events are emitted correctly - Test error cases return expected errors ### Monitoring Program Usage **View recent transactions:** ```bash # Get recent transactions for program solana transaction-history <PROGRAM_ID> --limit 10 # Or use explorers: # https://explorer.solana.com/address/<PROGRAM_ID> # https://solscan.io/account/<PROGRAM_ID> ``` **Set up monitoring:** Use services like: - [Helius webhooks](https://www.helius.dev/) - [QuickNode functions](https://www.quicknode.com/) - [SolanaFM API](https://solana.fm/) **Monitor for:** - Transaction success rate - Compute unit usage - Error frequency - Unusual activity patterns ### Publishing IDL (Anchor) **Upload IDL to on-chain account:** ```bash anchor idl init <PROGRAM_ID> \ --filepath target/idl/my_program.json \ --provider.cluster devnet ``` **Upgrade IDL after program upgrade:** ```bash anchor idl upgrade <PROGRAM_ID> \ --filepath target/idl/my_program.json \ --provider.cluster devnet ``` **Fetch published IDL:** ```bash anchor idl fetch <PROGRAM_ID> \ --provider.cluster devnet \ --out fetched_idl.json ``` **Benefits of publishing IDL:** - Clients can auto-discover your interface - Explorers can decode instructions - Reduces integration friction - Standard for Anchor programs ### Closing Programs and Reclaiming SOL **Close buffer accounts:** ```bash # View all buffers solana program show --buffers # Close specific buffer solana program close <BUFFER_ADDRESS> # Reclaims rent SOL to wallet ``` **Close entire program (irreversible!):** ```bash solana program close <PROGRAM_ID> # WARNING: This deletes the program permanently # Cannot be undone # Only do this for test programs ``` **When to close:** - Failed test deployments on devnet - Obsolete test programs - Reclaim SOL from old projects **When NOT to close:** - Any program with active users - Programs other contracts depend on - Mainnet programs (almost never) --- ## Common Issues and Troubleshooting ### Insufficient Balance **Error:** ``` Error: Account <WALLET> has insufficient funds for spend (1.5 SOL) + fee (0.002 SOL) ``` **Solution:** ```bash # Check current balance solana balance # Devnet - request airdrop solana airdrop 2 # Mainnet - transfer SOL # Calculate needed amount: PROGRAM_SIZE=$(wc -c < target/deploy/my_program.so) solana rent $PROGRAM_SIZE # Add 0.5 SOL buffer ``` ### Program Too Large **Error:** ``` Error: Program too large. Maximum size: 1048576 bytes ``` **Solutions:** **1. Optimize build:** ```toml [profile.release] lto = "fat" codegen-units = 1 opt-level = "z" # Optimize for size strip = true ``` **2. Remove unused dependencies:** ```bash cargo tree # Identify large dependencies ``` **3. Use feature flags to exclude optional code:** ```toml [dependencies] solana-program = { version = "2.1.0", default-features = false } ``` **4. Split program into multiple programs:** - Separate complex logic into multiple programs - Use CPI to communicate between them ### Account Data Too Small for Instruction **Error:** ``` Error: account data too small for instruction ``` **Cause:** Upgrade would exceed allocated program size. **Solution:** ```bash # Check current size solana program show <PROGRAM_ID> # Data Length: 363960 bytes # Check new size NEW_SIZE=$(wc -c < target/deploy/my_program.so) echo $NEW_SIZE # 380000 bytes # Calculate difference DIFF=$((NEW_SIZE - 363960)) # Extend program solana program extend <PROGRAM_ID> $DIFF # Check rent for extension solana rent $DIFF # Now deploy upgrade solana program deploy target/deploy/my_program.so --program-id <PROGRAM_ID> ``` ### Network Congestion / Blockhash Expiration **Error:** ``` Error: Transaction simulation failed: Blockhash not found ``` **Cause:** High network congestion or large program deployment. **Solutions:** **1. Increase priority fees:** ```bash solana program deploy target/deploy/my_program.so \ --with-compute-unit-price 300000 \ # Higher priority --max-sign-attempts 100 \ --use-rpc ``` **2. Use paid RPC endpoint:** ```bash solana config set --url https://your-premium-rpc.com # Paid RPCs often have: # - Higher rate limits # - Better transaction success rates # - Priority queue access ``` **3. Deploy during low traffic:** - Avoid peak hours (US/Europe daytime) - Early morning UTC often less congested **4. Break into smaller chunks:** For very large programs, manually create buffer and write in batches. ### Keypair Issues **Error:** ``` Error: Dynamic program error: Invalid keypair file ``` **Solutions:** **1. Verify keypair format:** ```bash # Should be JSON array of numbers cat program-keypair.json # [123, 45, 67, ...] # Or base58 string ``` **2. Regenerate if corrupted:** ```bash solana-keygen new -o program-keypair.json --force ``` **3. Check file permissions:** ```bash chmod 600 program-keypair.json ``` ### Anchor Build vs Deploy Mismatch **Error:** ``` Error: Program <ID> does not match declared program id in lib.rs ``` **Solution:** ```bash # Sync program IDs anchor keys sync # Rebuilds and updates declare_id! to match keypair ``` ### Native Rust: Missing Program ID **Error:** ``` Error: No program keypair found ``` **Solution:** Native Rust doesn't auto-generate keypairs. Either: **Option 1: Create keypair:** ```bash solana-keygen new -o target/deploy/my_program-keypair.json solana program deploy target/deploy/my_program.so \ --program-id target/deploy/my_program-keypair.json ``` **Option 2: Use existing program ID:** ```bash solana program deploy target/deploy/my_program.so \ --program-id <EXISTING_PROGRAM_ID> ``` ### Verification Failures **Error:** ``` Verification failed: Hash mismatch On-chain: abc123... Local: def456... ``` **Causes and solutions:** **1. Built outside Docker:** ```bash # Must use solana-verify build for deterministic build solana-verify build ``` **2. Cargo.lock mismatch:** ```bash # Ensure Cargo.lock committed to git git add Cargo.lock git commit -m "Add Cargo.lock for verifiable builds" ``` **3. Rebuild after verification:** ```bash # Don't run `anchor build` or `cargo build-sbf` after solana-verify build # This regenerates binary with different hash # If you did, rebuild verifiably: solana-verify build solana program deploy target/deploy/my_program.so --program-id <PROGRAM_ID> ``` **4. Wrong commit hash:** ```bash # Ensure you're verifying against correct commit git log # Find exact commit used for deployment solana-verify verify-from-repo \ --commit-hash <EXACT_COMMIT> \ ... ``` --- ## Best Practices ### Deployment Workflow **Recommended deployment process:** 1. **Local development** - Develop on localnet - Unit test with Mollusk (native) or Bankrun/Anchor tests - Iterate quickly 2. **Devnet testing** - Deploy to devnet - Integration testing - Test upgrade paths - Load testing if applicable 3. **Devnet verification** - Build verifiable - Deploy and verify on devnet - Ensure verification succeeds 4. **Security review** - Internal code review - Automated analysis (Soteria, Sec3, etc.) - Professional audit (for mainnet) 5. **Mainnet staging** - Build final verifiable version - Generate program ID - Set up multisig/governance 6. **Mainnet deployment** - Deploy during low-traffic period - Use paid RPC - Set appropriate priority fees - Monitor closely 7. **Post-deployment** - Submit verification - Smoke test critical functions - Set up monitoring - Transfer authority to multisig ### Version Control **Always commit:** - `Cargo.lock` (required for verified builds) - Program keypairs (for test programs only) - IDL files - Migration scripts **Never commit:** - Mainnet keypairs (use environment variables) - Wallet private keys - RPC API keys **Tag deployments:** ```bash git tag -a v1.0.0-mainnet -m "Mainnet deployment v1.0.0" git push origin v1.0.0-mainnet ``` **Link verification to tags:** ```bash solana-verify verify-from-repo \ --commit-hash v1.0.0-mainnet \ ... ``` ### Backup Strategies **Critical to back up:** 1. **Program keypairs** ```bash # Mainnet program keypairs cp target/deploy/my_program-keypair.json ~/secure-backup/ # Encrypted backup gpg -c target/deploy/my_program-keypair.json ``` 2. **Upgrade authority keypairs** ```bash # If not using multisig cp ~/.config/solana/id.json ~/secure-backup/upgrade-authority.json ``` 3. **Buffer accounts during deployment** ```bash # Save buffer address immediately after creating echo "Buffer: <ADDRESS>" >> deployment-log.txt ``` 4. **Deployment artifacts** - Built .so files - IDL files - Verification data - Transaction signatures **Backup locations:** - Encrypted cloud storage (Google Drive, Dropbox) - Hardware wallet (for keypairs) - Offline USB drives (encrypted) - Team password manager (1Password, Bitwarden) **Test backup restoration:** ```bash # Periodically verify backups work cp ~/secure-backup/my_program-keypair.json /tmp/test-restore.json solana-keygen pubkey /tmp/test-restore.json # Should output expected program ID ``` ### Framework-Specific Best Practices **Anchor:** - Always run `anchor keys sync` after first build - Keep `Anchor.toml` in source control - Use workspace for multi-program projects - Upload IDL on-chain for discoverability - Version your IDL files alongside code **Native Rust:** - Use `no-entrypoint` feature for testing - Implement security.txt for contact info - Document your instruction format - Provide client SDK (TypeScript/Rust) for integrations - Include example transaction builders ### Testing Before Deployment **Progressive testing strategy:** ```bash # 1. Unit tests (Mollusk for native, Anchor tests) cargo test # 2. Integration tests on localnet anchor test # or cargo test-sbf # 3. Devnet deployment test anchor deploy --provider.cluster devnet # Test all functions on devnet # 4. Upgrade test on devnet # Make small change, rebuild, upgrade anchor build anchor upgrade <PROGRAM_ID> target/deploy/program.so --provider.cluster devnet # Verify upgrade worked # 5. Verified build test solana-verify build solana-verify verify-from-repo --program-id <DEVNET_ID> https://github.com/... # 6. Final smoke tests on devnet # Run critical user flows # 7. Mainnet deployment # Only after all above pass ``` ### Documentation **Document your deployment:** Create `DEPLOYMENT.md` in your repo: ```markdown # Deployment Guide ## Program IDs - Devnet: ABC123... - Mainnet: DEF456... ## Build ```bash solana-verify build --library-name my_program ``` ## Deploy ### Devnet ```bash anchor deploy --provider.cluster devnet ``` ### Mainnet ```bash solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ -u mainnet-beta \ --with-compute-unit-price 100000 ``` ## Verify ```bash solana-verify verify-from-repo \ --remote \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/repo \ --commit-hash v1.0.0 ``` ## Upgrade Authority Mainnet: Squads Multisig `GHI789...` ## Last Deployment - Date: 2025-01-15 - Version: v1.0.0 - Commit: abc123def456 - Deployed by: @deployer - Verification: https://verify.osec.io/status/<PROGRAM_ID> ``` --- ## Summary **Key takeaways:** 1. **Always test on devnet first** - Never deploy untested code to mainnet 2. **Use verified builds for mainnet** - Transparency builds trust 3. **Calculate costs before deploying** - Use `solana rent` to estimate 4. **Set up multisig for mainnet** - Prevents single points of failure 5. **Monitor after deployment** - Watch for errors and unusual activity 6. **Back up keypairs** - Lose keypair = lose upgrade authority 7. **Document your deployments** - Future you will thank you 8. **Use priority fees on mainnet** - Ensures reliable deployment 9. **Test upgrade paths** - Practice on devnet first 10. **Never make programs immutable hastily** - Irreversible decision **Resources:** - Solana CLI docs: https://docs.solana.com/cli - Anchor docs: https://www.anchor-lang.com/ - Solana Verify: https://github.com/Ellipsis-Labs/solana-verifiable-build - OtterSec Verify API: https://verify.osec.io/ - Squads Protocol: https://squads.so/ - Security.txt: https://github.com/neodyme-labs/solana-security-txt Deploy with confidence! -
durable-nonces.md 26.4 KB
# Durable Transaction Nonces This guide covers Solana's durable transaction nonces, which enable transactions to remain valid indefinitely by replacing the time-limited recent blockhash mechanism. Essential for offline signing, multi-signature coordination, and scheduled transaction execution. ## Introduction ### The Expiration Problem Solana transactions normally include a `recent_blockhash` field that serves two purposes: 1. **Double-spend prevention**: Ensures each transaction is unique and can only be processed once 2. **Transaction freshness**: Limits transaction validity to prevent spam and stale transactions **The limitation:** - Recent blockhashes expire after **150 blocks** (~60-90 seconds) - Transactions with expired blockhashes are permanently rejected - Cannot be re-validated, even with identical content **Critical constraint:** ``` Transaction must be: 1. Signed with recent blockhash 2. Submitted to network 3. Processed by validator 4. Confirmed in block All within ~60-90 seconds! ``` ### Problems This Creates **Hardware Wallet Users:** - Fetch blockhash from network - Transfer to air-gapped device - User reviews and signs (can take minutes) - Transfer back to online device - Submit to network - **Risk**: Blockhash expires during manual review **Multi-Signature Wallets (DAOs, Squads, Realms):** - Create transaction with blockhash - Send to signer 1 for approval (hours/days) - Send to signer 2 for approval (hours/days) - Send to signer N for approval - **Risk**: Blockhash expires while collecting signatures **Scheduled Transactions:** - Want to pre-sign transaction for future execution - E.g., vesting unlock, scheduled payment, conditional trade - **Risk**: Cannot pre-sign hours/days in advance **Cross-Chain Bridges:** - Wait for finality on source chain (minutes/hours) - Sign transaction on destination chain - **Risk**: Blockhash expires during cross-chain confirmation ### The Solution: Durable Nonces Durable nonces replace `recent_blockhash` with a **stored on-chain value** that: - ✅ Never expires (remains valid indefinitely) - ✅ Changes with each use (prevents replay attacks) - ✅ Enables offline signing without time pressure - ✅ Supports multi-signature coordination - ✅ Allows pre-signing transactions for future execution **Key insight**: Instead of using the blockchain's recent history (blockhashes) to ensure uniqueness, durable nonces use a **dedicated account** that stores a nonce value and advances it after each transaction. ## How Durable Nonces Work ### Core Mechanism 1. **Nonce Account**: On-chain account (owned by System Program) storing a 32-byte nonce value 2. **Transaction Structure**: Use nonce value as `recent_blockhash` field 3. **Nonce Advancement**: First instruction MUST advance the nonce to prevent replay 4. **Authority Control**: Only nonce authority can advance nonce or authorize transactions ### Transaction Flow ``` Normal Transaction: 1. Fetch recent blockhash (expires in 90s) 2. Build transaction with blockhash 3. Sign transaction 4. Submit (must be within 90s) 5. Process and confirm Durable Nonce Transaction: 1. Create nonce account (one-time setup) 2. Fetch current nonce value (no expiration!) 3. Build transaction with nonce as blockhash 4. Add advance_nonce instruction (MUST be first) 5. Sign transaction (no time pressure) 6. Submit anytime (minutes, hours, days later) 7. Process: advances nonce, executes instructions ``` ### Double-Spend Prevention **Without expiration, how does it prevent double-spending?** The nonce value **changes** after each transaction: ```rust // Transaction 1 with nonce value "ABC123..." { recent_blockhash: "ABC123...", // Current nonce value instructions: [ advance_nonce_account(...), // Changes nonce to "XYZ789..." transfer(...), ] } // If you try to submit Transaction 1 again: // Runtime checks: Is "ABC123..." the current nonce? // NO! It's now "XYZ789..." // Transaction REJECTED (nonce mismatch) ``` **Critical**: The runtime **always** advances the nonce, even if the transaction fails after the advance instruction. This prevents replay attacks. ## Nonce Account Structure ### Account Layout Nonce accounts are owned by the System Program and have this structure: ```rust pub struct NonceState { pub version: NonceVersion, } pub enum NonceVersion { Legacy(Box<NonceData>), Current(Box<NonceData>), } pub struct NonceData { pub authority: Pubkey, // Who can authorize nonce operations pub durable_nonce: Hash, // The actual nonce value (32 bytes) pub fee_calculator: FeeCalculator, // Historic fee data } ``` **Account requirements:** - **Owner**: System Program (`11111111111111111111111111111111`) - **Size**: 80 bytes - **Rent exemption**: Required (minimum ~0.00144768 SOL) - **Authority**: Can be any pubkey (keypair or PDA) ### Nonce Authority The authority pubkey controls the nonce account: - **Can**: Advance nonce, withdraw funds, authorize nonce transactions, change authority - **Cannot**: Execute other instructions without nonce advancement (runtime enforces this) **Authority options:** - **Keypair**: Direct control (hot wallet, cold wallet) - **PDA**: Program-controlled nonces (advanced use case) - **Multisig**: Multiple signers required (DAO wallets) ## Creating Nonce Accounts ### Using Native Rust ```rust use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, sysvar::rent::Rent, transaction::Transaction, }; use solana_client::rpc_client::RpcClient; fn create_nonce_account( rpc_client: &RpcClient, payer: &Keypair, nonce_account: &Keypair, authority: &Pubkey, ) -> Result<(), Box<dyn std::error::Error>> { // Calculate rent-exempt balance for nonce account let rent = rpc_client.get_minimum_balance_for_rent_exemption(80)?; // Create account instruction let create_account_ix = system_instruction::create_account( &payer.pubkey(), &nonce_account.pubkey(), rent, // Lamports (rent-exempt minimum) 80, // Space (nonce account size) &solana_program::system_program::id(), // Owner (System Program) ); // Initialize nonce instruction let initialize_nonce_ix = system_instruction::initialize_nonce_account( &nonce_account.pubkey(), authority, // Nonce authority ); // Build transaction let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[create_account_ix, initialize_nonce_ix], Some(&payer.pubkey()), &[payer, nonce_account], // Both payer and nonce account must sign recent_blockhash, ); // Send transaction let signature = rpc_client.send_and_confirm_transaction(&transaction)?; println!("Created nonce account: {}", signature); Ok(()) } ``` ### Single-Step Creation There's also a convenience function that combines both steps: ```rust let instruction = system_instruction::create_nonce_account( &payer.pubkey(), &nonce_account.pubkey(), authority, rent_lamports, ); // This creates a single instruction that: // 1. Creates the account // 2. Initializes it as a nonce account ``` ### Using CLI ```bash # Generate keypair for nonce account solana-keygen new -o nonce-account.json # Create nonce account solana create-nonce-account nonce-account.json 0.0015 # Verify creation solana nonce nonce-account.json # Output: Current nonce value (32-byte hash) ``` ## Querying Nonce Accounts ### Fetching Nonce Value ```rust use solana_sdk::account::Account; use solana_program::system_program; fn get_nonce_value( rpc_client: &RpcClient, nonce_pubkey: &Pubkey, ) -> Result<Hash, Box<dyn std::error::Error>> { // Fetch account data let account = rpc_client.get_account(nonce_pubkey)?; // Verify it's a nonce account if account.owner != system_program::id() { return Err("Account is not owned by System Program".into()); } // Deserialize nonce data let nonce_data = bincode::deserialize::<NonceState>(&account.data)?; match nonce_data { NonceState::Current(data) => Ok(data.durable_nonce), NonceState::Legacy(data) => Ok(data.durable_nonce), } } ``` ### Parsing Nonce Account ```rust use solana_program::nonce::state::{Data, State}; fn parse_nonce_account(account_data: &[u8]) -> Result<Data, Box<dyn std::error::Error>> { let state: State = bincode::deserialize(account_data)?; match state { State::Initialized(data) => Ok(data), State::Uninitialized => Err("Nonce account not initialized".into()), } } // Access nonce components fn display_nonce_info(nonce_data: &Data) { println!("Authority: {}", nonce_data.authority); println!("Nonce value: {}", nonce_data.blockhash); println!("Fee calculator: {:?}", nonce_data.fee_calculator); } ``` ## Building Transactions with Durable Nonces ### Transaction Structure **Critical requirements:** 1. **First instruction** MUST be `advance_nonce_account` 2. Use nonce value as `recent_blockhash` 3. Sign with nonce authority (in addition to other required signers) ```rust use solana_sdk::{ hash::Hash, instruction::Instruction, message::Message, signature::{Keypair, Signer}, system_instruction, transaction::Transaction, }; fn build_nonce_transaction( nonce_pubkey: &Pubkey, nonce_authority: &Keypair, nonce_value: Hash, instructions: Vec<Instruction>, payer: &Keypair, ) -> Transaction { // 1. Create advance_nonce instruction (MUST BE FIRST) let advance_nonce_ix = system_instruction::advance_nonce_account( nonce_pubkey, &nonce_authority.pubkey(), ); // 2. Combine with your instructions let mut all_instructions = vec![advance_nonce_ix]; all_instructions.extend(instructions); // 3. Build message with nonce as blockhash let message = Message::new_with_blockhash( &all_instructions, Some(&payer.pubkey()), &nonce_value, // Use nonce value instead of recent blockhash! ); // 4. Sign with both payer and nonce authority let mut signers = vec![payer]; if nonce_authority.pubkey() != payer.pubkey() { signers.push(nonce_authority); } Transaction::new(&signers, message, nonce_value) } ``` ### Complete Example: Transfer with Durable Nonce ```rust fn transfer_with_nonce( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, payer: &Keypair, recipient: &Pubkey, amount: u64, ) -> Result<(), Box<dyn std::error::Error>> { // 1. Fetch current nonce value let nonce_value = get_nonce_value(rpc_client, nonce_account)?; // 2. Create transfer instruction let transfer_ix = system_instruction::transfer( &payer.pubkey(), recipient, amount, ); // 3. Build transaction with nonce let transaction = build_nonce_transaction( nonce_account, nonce_authority, nonce_value, vec![transfer_ix], payer, ); // 4. Can now submit immediately or store for later // No expiration pressure! let signature = rpc_client.send_and_confirm_transaction(&transaction)?; println!("Transfer completed: {}", signature); Ok(()) } ``` ### Serializing for Offline Signing ```rust use base58::ToBase58; fn serialize_for_offline_signing(transaction: &Transaction) -> String { // Serialize transaction to bytes let serialized = bincode::serialize(transaction).unwrap(); // Encode as base58 for transport serialized.to_base58() } fn deserialize_signed_transaction(base58_tx: &str) -> Transaction { use base58::FromBase58; let bytes = base58_tx.from_base58().unwrap(); bincode::deserialize(&bytes).unwrap() } ``` ## Managing Nonce Accounts ### Advancing Nonce **Automatic advancement**: When you submit a transaction with a durable nonce, the runtime automatically advances the nonce as part of processing the `advance_nonce_account` instruction. **Manual advancement** (without submitting transaction): ```rust fn advance_nonce_manually( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, payer: &Keypair, ) -> Result<(), Box<dyn std::error::Error>> { let advance_ix = system_instruction::advance_nonce_account( nonce_account, &nonce_authority.pubkey(), ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[advance_ix], Some(&payer.pubkey()), &[payer, nonce_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **When to manually advance:** - Before reusing nonce for a new transaction - To invalidate a previously signed transaction - Regular rotation for security ### Withdrawing from Nonce Account ```rust fn withdraw_from_nonce( rpc_client: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, recipient: &Pubkey, amount: u64, payer: &Keypair, ) -> Result<(), Box<dyn std::error::Error>> { let withdraw_ix = system_instruction::withdraw_nonce_account( nonce_account, &nonce_authority.pubkey(), recipient, amount, ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[withdraw_ix], Some(&payer.pubkey()), &[payer, nonce_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **Important**: Must maintain rent-exempt minimum balance. Can only withdraw to zero if closing the account. ### Changing Nonce Authority ```rust fn change_nonce_authority( rpc_client: &RpcClient, nonce_account: &Pubkey, current_authority: &Keypair, new_authority: &Pubkey, payer: &Keypair, ) -> Result<(), Box<dyn std::error::Error>> { let authorize_ix = system_instruction::authorize_nonce_account( nonce_account, ¤t_authority.pubkey(), new_authority, ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let transaction = Transaction::new_signed_with_payer( &[authorize_ix], Some(&payer.pubkey()), &[payer, current_authority], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&transaction)?; Ok(()) } ``` **Use cases:** - Transfer control to PDA for program-managed nonces - Rotate keys for security - Transfer to multisig for DAO control ## Offline Signing Workflows ### Hardware Wallet Flow **Setup (online device):** ```rust // 1. Create nonce account (one-time) create_nonce_account(&rpc_client, &payer, &nonce_account, &hw_wallet_pubkey)?; // 2. Fetch nonce value let nonce_value = get_nonce_value(&rpc_client, &nonce_account.pubkey())?; // 3. Build unsigned transaction let unsigned_tx = build_nonce_transaction( &nonce_account.pubkey(), &hw_wallet_keypair, // Will be replaced with actual signature nonce_value, vec![transfer_ix], &payer, ); // 4. Serialize for hardware wallet let serialized = serialize_for_offline_signing(&unsigned_tx); // 5. Transfer to hardware wallet (USB, QR code, etc.) ``` **Signing (air-gapped hardware wallet):** ```rust // 1. Receive serialized transaction let tx = deserialize_signed_transaction(&serialized); // 2. Display to user for review (no time pressure!) // User reviews: recipient, amount, etc. // 3. Sign with hardware wallet private key // (Hardware wallet handles this internally) // 4. Export signed transaction let signed_serialized = serialize_for_offline_signing(&signed_tx); // 5. Transfer back to online device ``` **Submission (online device):** ```rust // 1. Receive signed transaction let signed_tx = deserialize_signed_transaction(&signed_serialized); // 2. Submit to network (can be hours/days after signing!) let signature = rpc_client.send_and_confirm_transaction(&signed_tx)?; ``` ### Multi-Signature Coordination **DAO Proposal Execution Flow:** ```rust // 1. Proposer creates transaction with nonce let nonce_value = get_nonce_value(&rpc_client, &dao_nonce_account)?; let proposal_tx = build_nonce_transaction( &dao_nonce_account, &dao_authority, // PDA controlled by governance nonce_value, vec![execute_proposal_ix], &proposer, ); // 2. Serialize and store in DAO state let tx_data = bincode::serialize(&proposal_tx)?; // Store tx_data in proposal account // 3. Members vote over time (hours/days) // Each vote increments approval count // 4. When threshold reached, anyone can execute let stored_tx: Transaction = bincode::deserialize(&proposal.tx_data)?; // 5. Submit (nonce ensures it's still valid!) rpc_client.send_and_confirm_transaction(&stored_tx)?; ``` ### CLI Multi-Sig Example **First co-signer (offline):** ```bash solana transfer \ --from sender.json \ --sign-only \ --nonce nonce-account.json \ --nonce-authority nonce-authority.json \ --blockhash <NONCE_VALUE> \ --fee-payer co-sender.json \ receiver.json 0.1 # Output: # Pubkey=Signature # 5nZ8nY5...=4SBv7Xp... ``` **Second co-signer (online, hours/days later):** ```bash solana transfer \ --from sender.json \ --nonce nonce-account.json \ --nonce-authority nonce-authority.json \ --blockhash <NONCE_VALUE> \ --fee-payer sender.json \ --signer 5nZ8nY5...=4SBv7Xp... \ receiver.json 0.1 ``` ## Security Considerations ### The Neodyme Vulnerability (2020) **Historic issue**: Before Solana v1.3, there was a critical vulnerability in how durable nonce transactions were processed: **The bug:** 1. Transaction with durable nonce starts processing 2. Runtime advances nonce (changes state) 3. Later instruction in transaction fails 4. Runtime rolls back ALL state changes 5. **BUG**: Nonce advancement was rolled back too! 6. Attacker could replay the transaction **The exploit:** ```rust // Malicious transaction: { instructions: [ advance_nonce(...), // Advances nonce write_arbitrary_data(...), // Attacker's payload fail_intentionally(...), // Forces transaction to fail ] } // After rollback: // - Nonce reverted to original value // - Arbitrary data write WAS NOT rolled back // - Can replay transaction infinitely! ``` **Impact**: Could write arbitrary data to any account by replaying failed transactions. **Fix** (Solana v1.3+): Nonce advancement is now **permanent** even on transaction failure. The runtime explicitly handles nonce accounts separately from normal rollback logic. **Lesson**: This demonstrates why nonce advancement MUST happen regardless of transaction success/failure. ### Best Practices **1. Never reuse nonce without advancing** ```rust // BAD: Reusing nonce value let nonce = get_nonce_value(&rpc, &nonce_account)?; let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix1], &payer); let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix2], &payer); // If tx1 fails, tx2 might also fail with "nonce mismatch" // GOOD: Advance between uses let nonce1 = get_nonce_value(&rpc, &nonce_account)?; let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce1, vec![ix1], &payer); rpc.send_and_confirm_transaction(&tx1)?; // Fetch fresh nonce (it was advanced) let nonce2 = get_nonce_value(&rpc, &nonce_account)?; let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce2, vec![ix2], &payer); ``` **2. Protect nonce authority** ```rust // Use cold storage for nonce authority // OR use PDA with program logic to restrict usage let authority_pda = Pubkey::find_program_address( &[b"nonce_authority", dao.key().as_ref()], program_id, ); ``` **3. Maintain rent exemption** ```rust // Check before withdrawal let nonce_account = rpc.get_account(&nonce_pubkey)?; let rent = rpc.get_minimum_balance_for_rent_exemption(80)?; if nonce_account.lamports - withdraw_amount < rent { return Err("Would violate rent exemption".into()); } ``` **4. Verify nonce advancement in transaction** ```rust // In your program that uses nonce transactions: pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // First account should be nonce account let nonce_account = &accounts[0]; // Verify it's a valid nonce account if nonce_account.owner != &system_program::id() { return Err(ProgramError::InvalidAccountData); } // Verify advance_nonce was called // (Runtime enforces this, but you can add checks) Ok(()) } ``` **5. Monitor nonce account balance** ```rust // Periodic check (e.g., daily job) fn check_nonce_health(rpc: &RpcClient, nonce: &Pubkey) -> Result<(), String> { let account = rpc.get_account(nonce) .map_err(|_| "Nonce account not found")?; let rent = rpc.get_minimum_balance_for_rent_exemption(80) .map_err(|_| "Failed to fetch rent")?; if account.lamports < rent { return Err(format!( "Nonce account below rent exemption: {} < {}", account.lamports, rent )); } Ok(()) } ``` ## Use Cases ### 1. Scheduled Payments (Vesting) ```rust // Pre-sign monthly vesting releases fn create_vesting_schedule( rpc: &RpcClient, nonce_account: &Pubkey, nonce_authority: &Keypair, recipient: &Pubkey, amount_per_month: u64, months: usize, ) -> Result<Vec<Transaction>, Box<dyn std::error::Error>> { let mut transactions = Vec::new(); for month in 0..months { // Fetch current nonce let nonce = get_nonce_value(rpc, nonce_account)?; // Create transfer let transfer_ix = system_instruction::transfer( &nonce_authority.pubkey(), recipient, amount_per_month, ); // Build nonce transaction let tx = build_nonce_transaction( nonce_account, nonce_authority, nonce, vec![transfer_ix], nonce_authority, ); transactions.push(tx); // Advance nonce for next month's transaction advance_nonce_manually(rpc, nonce_account, nonce_authority, nonce_authority)?; } Ok(transactions) } // Executor submits each month fn execute_vesting_payment( rpc: &RpcClient, pre_signed_tx: &Transaction, ) -> Result<(), Box<dyn std::error::Error>> { // No time pressure - can submit anytime! rpc.send_and_confirm_transaction(pre_signed_tx)?; Ok(()) } ``` ### 2. Conditional Trades (Limit Orders) ```rust // Pre-sign trade execution at specific price fn create_limit_order( nonce: &Pubkey, authority: &Keypair, swap_instruction: Instruction, // Execute when price reached ) -> Transaction { let nonce_value = /* fetch nonce */; build_nonce_transaction( nonce, authority, nonce_value, vec![swap_instruction], authority, ) } // Bot monitors price and submits when condition met fn execute_limit_order(rpc: &RpcClient, current_price: f64, limit_tx: &Transaction) { if current_price >= target_price { rpc.send_transaction(limit_tx).ok(); // Submit pre-signed transaction } } ``` ### 3. Cross-Chain Bridges ```rust // Sign Solana transaction while waiting for Ethereum finality async fn bridge_from_ethereum_to_solana( eth_tx_hash: H256, solana_mint_ix: Instruction, nonce_account: &Pubkey, nonce_authority: &Keypair, ) -> Result<(), Box<dyn std::error::Error>> { // 1. Pre-sign Solana mint transaction let nonce = get_nonce_value(&solana_rpc, nonce_account)?; let mint_tx = build_nonce_transaction( nonce_account, nonce_authority, nonce, vec![solana_mint_ix], nonce_authority, ); // 2. Wait for Ethereum finality (12+ minutes) wait_for_ethereum_finality(eth_tx_hash).await?; // 3. Submit Solana transaction (still valid!) solana_rpc.send_and_confirm_transaction(&mint_tx)?; Ok(()) } ``` ### 4. DAO Governance Execution Already covered in multi-sig example above - proposals can be voted on over days/weeks, then executed with pre-signed transaction. ## CLI Reference **Create nonce account:** ```bash solana create-nonce-account <KEYPAIR_PATH> <AMOUNT> ``` **Get current nonce:** ```bash solana nonce <NONCE_ACCOUNT> ``` **Manually advance nonce:** ```bash solana new-nonce <NONCE_ACCOUNT> ``` **Get nonce account info:** ```bash solana nonce-account <NONCE_ACCOUNT> ``` **Withdraw from nonce:** ```bash solana withdraw-from-nonce-account <NONCE_ACCOUNT> <DESTINATION> <AMOUNT> ``` **Change nonce authority:** ```bash solana authorize-nonce-account <NONCE_ACCOUNT> <NEW_AUTHORITY> ``` **Sign transaction offline:** ```bash solana <COMMAND> \ --sign-only \ --nonce <NONCE_ACCOUNT> \ --nonce-authority <AUTHORITY_KEYPAIR> \ --blockhash <NONCE_VALUE> ``` **Submit pre-signed transaction:** ```bash solana <COMMAND> \ --nonce <NONCE_ACCOUNT> \ --nonce-authority <AUTHORITY_KEYPAIR> \ --blockhash <NONCE_VALUE> \ --signer <PUBKEY=SIGNATURE> ``` ## Limitations and Considerations **Transaction size:** - Adding `advance_nonce_account` instruction adds ~40 bytes - May push transaction over size limit if already near maximum **Extra signature requirement:** - Nonce authority must sign (if different from fee payer) - Increases transaction complexity **Rent cost:** - Each nonce account requires ~0.0015 SOL rent-exempt minimum - For many scheduled transactions, can become expensive **Nonce advancement overhead:** - Compute units to advance nonce (~few hundred CU) - Minimal but worth considering for CU-constrained transactions **Cannot mix recent blockhashes and nonces:** - Transaction uses either recent blockhash OR durable nonce - Cannot use both in the same transaction ## Resources ### Official Documentation - [Introduction to Durable Nonces](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces) - [Durable Transaction Nonces Proposal](https://docs.anza.xyz/implemented-proposals/durable-tx-nonces) - [CLI Nonce Examples](https://docs.anza.xyz/cli/examples/durable-nonce) ### Code Examples - [Durable Nonces Repository](https://github.com/0xproflupin/solana-durable-nonces) - [System Program Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/system_instruction.rs) ### Security Analysis - [Neodyme: Nonce Upon a Time](https://neodyme.io/en/blog/nonce-upon-a-time/) - Historic vulnerability analysis ### Technical References - [solana-sdk NonceState](https://docs.rs/solana-sdk/latest/solana_sdk/nonce/state/enum.State.html) - [System Program Instructions](https://docs.rs/solana-sdk/latest/solana_sdk/system_instruction/) -
error-handling.md 18.6 KB
# Error Handling in Solana Programs This reference provides comprehensive coverage of error handling patterns for native Rust Solana program development, including custom error types, error propagation, and best practices. ## Table of Contents 1. [Error Handling Fundamentals](#error-handling-fundamentals) 2. [ProgramError](#programerror) 3. [Custom Error Types](#custom-error-types) 4. [Error Propagation](#error-propagation) 5. [Error Context and Logging](#error-context-and-logging) 6. [Client-Side Error Handling](#client-side-error-handling) 7. [Best Practices](#best-practices) --- ## Error Handling Fundamentals ### Why Error Handling Matters **In Solana programs, errors serve multiple purposes:** 1. **Security:** Prevent invalid state transitions 2. **User Experience:** Provide meaningful feedback 3. **Debugging:** Identify issues quickly 4. **Transaction Validation:** Fail fast when invariants are violated **Key Principle:** Errors should cause the entire transaction to fail and rollback, maintaining atomicity. ### The Result Type All Solana program instructions return `ProgramResult`: ```rust use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, }; pub type ProgramResult = Result<(), ProgramError>; // Success pub fn successful_operation() -> ProgramResult { Ok(()) } // Failure pub fn failed_operation() -> ProgramResult { Err(ProgramError::Custom(42)) } ``` **When an instruction returns `Err`:** - Transaction fails immediately - All state changes rollback - Error code returned to client - Transaction fee still charged (for processing cost) --- ## ProgramError ### The Built-in Error Type Solana provides `ProgramError` enum with common error variants: ```rust use solana_program::program_error::ProgramError; pub enum ProgramError { // Common errors Custom(u32), // Custom error code InvalidArgument, // Invalid instruction argument InvalidInstructionData, // Failed to deserialize instruction data InvalidAccountData, // Invalid account data AccountDataTooSmall, // Account data too small InsufficientFunds, // Not enough lamports IncorrectProgramId, // Wrong program ID MissingRequiredSignature, // Required signer missing AccountAlreadyInitialized, // Account already initialized UninitializedAccount, // Account not initialized NotEnoughAccountKeys, // Not enough accounts provided AccountBorrowFailed, // Failed to borrow account data MaxSeedLengthExceeded, // PDA seed too long InvalidSeeds, // Invalid PDA derivation BorshIoError(String), // Borsh serialization error AccountNotRentExempt, // Account not rent-exempt IllegalOwner, // Wrong account owner ArithmeticOverflow, // Arithmetic overflow // ... and more } ``` ### Common ProgramError Usage ```rust use solana_program::program_error::ProgramError; pub fn validate_inputs( amount: u64, max_amount: u64, ) -> ProgramResult { // InvalidArgument: Input doesn't meet requirements if amount == 0 { return Err(ProgramError::InvalidArgument); } // InsufficientFunds: Not enough balance if amount > max_amount { return Err(ProgramError::InsufficientFunds); } // ArithmeticOverflow: Math operation failed let _result = amount.checked_mul(2) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` --- ## Custom Error Types ### Why Custom Errors? **Built-in `ProgramError` is generic.** Custom errors provide: - **Specific error codes** for different failure modes - **Better debugging** with descriptive messages - **Client clarity** - clients know exactly what went wrong - **Documentation** - errors serve as API documentation ### Defining Custom Errors Use the `thiserror` crate to define custom error enums: ```rust use solana_program::program_error::ProgramError; use thiserror::Error; #[derive(Error, Debug, Copy, Clone)] pub enum NoteError { #[error("You do not own this note")] Forbidden, #[error("Note text is too long")] InvalidLength, #[error("Rating must be between 1 and 5")] InvalidRating, #[error("Note title cannot be empty")] EmptyTitle, #[error("Maximum notes limit reached")] MaxNotesExceeded, } ``` **Attributes explained:** - `#[derive(Error)]` - Implements `std::error::Error` trait - `#[derive(Debug)]` - Allows `{:?}` formatting - `#[derive(Copy, Clone)]` - Makes errors copyable (recommended) - `#[error("...")]` - Error message string ### Converting to ProgramError Implement `From<CustomError> for ProgramError`: ```rust impl From<NoteError> for ProgramError { fn from(e: NoteError) -> Self { ProgramError::Custom(e as u32) } } ``` **How it works:** 1. Custom error is converted to `u32` (using `as u32` cast) 2. Wrapped in `ProgramError::Custom(u32)` 3. Returned to client as error code **Error code mapping:** ```rust NoteError::Forbidden → ProgramError::Custom(0) NoteError::InvalidLength → ProgramError::Custom(1) NoteError::InvalidRating → ProgramError::Custom(2) NoteError::EmptyTitle → ProgramError::Custom(3) NoteError::MaxNotesExceeded → ProgramError::Custom(4) ``` ### Using Custom Errors ```rust pub fn create_note( program_id: &Pubkey, accounts: &[AccountInfo], title: String, content: String, rating: u8, ) -> ProgramResult { // Validation with custom errors if title.is_empty() { return Err(NoteError::EmptyTitle.into()); } if content.len() > 1000 { return Err(NoteError::InvalidLength.into()); } if rating < 1 || rating > 5 { return Err(NoteError::InvalidRating.into()); } // Continue processing... Ok(()) } ``` **The `.into()` method** automatically converts `NoteError` to `ProgramError`. ### Advanced Custom Error Types **With additional context:** ```rust #[derive(Error, Debug)] pub enum GameError { #[error("Insufficient mana: have {current}, need {required}")] InsufficientMana { current: u32, required: u32 }, #[error("Invalid move: {0}")] InvalidMove(String), #[error("Player not found: {0}")] PlayerNotFound(String), } ``` **Note:** Errors with fields cannot derive `Copy`, only `Clone`. --- ## Error Propagation ### The `?` Operator The `?` operator is Rust's error propagation mechanism: ```rust pub fn complex_operation( accounts: &[AccountInfo], ) -> ProgramResult { // If validation fails, error is returned immediately validate_accounts(accounts)?; // If deserialization fails, error is propagated let data = AccountData::try_from_slice(&accounts[0].data.borrow())?; // If checked math fails, ArithmeticOverflow is returned let result = data.value.checked_add(100) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` **What `?` does:** 1. If `Result` is `Ok(value)`, unwraps to `value` 2. If `Result` is `Err(e)`, converts `e` and returns early 3. Conversion happens via `From` trait ### Error Conversion Chain ```rust // Step 1: Borsh deserialization fails let data = MyData::try_from_slice(bytes)?; // Returns: Err(std::io::Error) // Step 2: ? operator converts via From trait // std::io::Error → ProgramError::BorshIoError // Step 3: Custom error conversion return Err(MyError::InvalidData.into()); // MyError → ProgramError::Custom(n) ``` ### Manual Error Handling ```rust // Without ? pub fn manual_error_handling( account: &AccountInfo, ) -> ProgramResult { match validate_account(account) { Ok(()) => { // Continue processing } Err(e) => { msg!("Validation failed: {:?}", e); return Err(e); } } Ok(()) } // With ? (equivalent) pub fn automatic_error_handling( account: &AccountInfo, ) -> ProgramResult { validate_account(account)?; Ok(()) } ``` ### Mapping Errors Transform one error type to another: ```rust pub fn map_errors( account: &AccountInfo, ) -> ProgramResult { // Map generic error to custom error let data = AccountData::try_from_slice(&account.data.borrow()) .map_err(|_| NoteError::InvalidLength)?; // Map to different ProgramError variant let value = data.amount.checked_add(100) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` ### Combining Multiple Operations ```rust pub fn chain_operations( accounts: &[AccountInfo], ) -> ProgramResult { // All operations must succeed or transaction fails let account1 = validate_and_load_account(&accounts[0])?; let account2 = validate_and_load_account(&accounts[1])?; let combined = account1.value .checked_add(account2.value) .ok_or(ProgramError::ArithmeticOverflow)?; update_account(&accounts[2], combined)?; Ok(()) } ``` --- ## Error Context and Logging ### Adding Context with `msg!` Use `msg!` macro to log context before returning errors: ```rust use solana_program::msg; pub fn transfer_tokens( from: &AccountInfo, to: &AccountInfo, amount: u64, ) -> ProgramResult { if amount == 0 { msg!("Transfer amount cannot be zero"); return Err(ProgramError::InvalidArgument); } let from_balance = get_balance(from)?; if from_balance < amount { msg!("Insufficient balance: have {}, need {}", from_balance, amount); return Err(ProgramError::InsufficientFunds); } // Perform transfer... Ok(()) } ``` ### Logging Best Practices **✅ Good logging:** ```rust msg!("Invalid rating: got {}, expected 1-5", rating); msg!("PDA derivation failed: expected {}, got {}", expected, actual); msg!("Account {} not owned by program {}", account.key, program_id); ``` **❌ Poor logging:** ```rust msg!("Error"); // Not helpful msg!("Failed"); // What failed? // (no logging) // Can't debug issues ``` ### Conditional Logging ```rust pub fn debug_operation( account: &AccountInfo, debug_mode: bool, ) -> ProgramResult { if debug_mode { msg!("Processing account: {}", account.key); msg!("Owner: {}", account.owner); msg!("Lamports: {}", account.lamports()); } // Process... Ok(()) } ``` ### Error with Recovery ```rust pub fn try_with_fallback( accounts: &[AccountInfo], ) -> ProgramResult { // Try primary method match process_primary(accounts) { Ok(()) => { msg!("Primary method succeeded"); Ok(()) } Err(e) => { msg!("Primary method failed: {:?}, trying fallback", e); // Try fallback process_fallback(accounts).map_err(|fallback_err| { msg!("Fallback also failed: {:?}", fallback_err); fallback_err }) } } } ``` --- ## Client-Side Error Handling ### Error Code Interpretation **Client receives:** ```json { "error": { "InstructionError": [ 0, { "Custom": 2 } ] } } ``` **Decoding:** - Instruction index: `0` (first instruction) - Error type: `Custom` - Error code: `2` ### TypeScript Error Mapping ```typescript // Define error codes matching Rust enum enum NoteError { Forbidden = 0, InvalidLength = 1, InvalidRating = 2, EmptyTitle = 3, MaxNotesExceeded = 4, } // Error messages const NOTE_ERROR_MESSAGES = { [NoteError.Forbidden]: "You do not own this note", [NoteError.InvalidLength]: "Note text is too long", [NoteError.InvalidRating]: "Rating must be between 1 and 5", [NoteError.EmptyTitle]: "Note title cannot be empty", [NoteError.MaxNotesExceeded]: "Maximum notes limit reached", }; // Parse error function parseNoteError(error: any): string { if (error?.InstructionError) { const [_, instructionError] = error.InstructionError; if (instructionError?.Custom !== undefined) { const errorCode = instructionError.Custom; return NOTE_ERROR_MESSAGES[errorCode] || `Unknown error: ${errorCode}`; } } return "Transaction failed"; } // Usage try { await program.methods.createNote(title, content, rating).rpc(); } catch (error) { const message = parseNoteError(error); console.error(message); } ``` ### Anchor Error Handling **With Anchor framework:** ```typescript import { AnchorError } from "@coral-xyz/anchor"; try { await program.methods.createNote(title, content, rating).rpc(); } catch (error) { if (error instanceof AnchorError) { console.error("Error code:", error.error.errorCode.code); console.error("Error message:", error.error.errorMessage); console.error("Error number:", error.error.errorCode.number); } } ``` --- ## Best Practices ### 1. Fail Fast **Return errors immediately when validation fails:** ```rust // ✅ Good - fails fast pub fn validate_input(rating: u8) -> ProgramResult { if rating < 1 || rating > 5 { return Err(NoteError::InvalidRating.into()); } // Continue only if valid Ok(()) } // ❌ Bad - continues with invalid state pub fn validate_input_bad(rating: u8) -> ProgramResult { if rating >= 1 && rating <= 5 { // Valid branch } // Continues regardless! Ok(()) } ``` ### 2. Meaningful Error Messages ```rust // ✅ Good - specific and actionable #[error("Username must be 3-20 characters, got {0}")] InvalidUsernameLength(usize), #[error("Insufficient mana: need {required}, have {current}")] InsufficientMana { required: u32, current: u32 }, // ❌ Bad - vague #[error("Invalid input")] InvalidInput, #[error("Error")] GenericError, ``` ### 3. Organize Errors by Category ```rust #[derive(Error, Debug, Copy, Clone)] pub enum GameError { // Validation errors (0-99) #[error("Invalid player name")] InvalidPlayerName, #[error("Invalid move")] InvalidMove, // State errors (100-199) #[error("Game not started")] GameNotStarted, #[error("Game already finished")] GameFinished, // Resource errors (200-299) #[error("Insufficient gold")] InsufficientGold, #[error("Inventory full")] InventoryFull, } ``` ### 4. Consistent Error Handling Pattern ```rust pub fn standard_operation_pattern( program_id: &Pubkey, accounts: &[AccountInfo], params: Params, ) -> ProgramResult { // 1. Parse accounts let account_info_iter = &mut accounts.iter(); let user = next_account_info(account_info_iter)?; let data_account = next_account_info(account_info_iter)?; // 2. Validate signers if !user.is_signer { msg!("User must sign the transaction"); return Err(ProgramError::MissingRequiredSignature); } // 3. Validate ownership if data_account.owner != program_id { msg!("Data account not owned by program"); return Err(ProgramError::IllegalOwner); } // 4. Validate input parameters if params.amount == 0 { msg!("Amount cannot be zero"); return Err(ProgramError::InvalidArgument); } // 5. Load and validate account data let mut data = AccountData::try_from_slice(&data_account.data.borrow())?; if !data.is_initialized { msg!("Account not initialized"); return Err(ProgramError::UninitializedAccount); } // 6. Perform operation // ... Ok(()) } ``` ### 5. Document Error Codes ```rust /// Error codes for the Note program. /// /// | Code | Error | Description | /// |------|-------|-------------| /// | 0 | Forbidden | Caller does not own the note | /// | 1 | InvalidLength | Note text exceeds maximum length | /// | 2 | InvalidRating | Rating not in range 1-5 | /// | 3 | EmptyTitle | Note title is empty | /// | 4 | MaxNotesExceeded | User has reached note limit | #[derive(Error, Debug, Copy, Clone)] #[repr(u32)] pub enum NoteError { #[error("You do not own this note")] Forbidden = 0, #[error("Note text is too long")] InvalidLength = 1, #[error("Rating must be between 1 and 5")] InvalidRating = 2, #[error("Note title cannot be empty")] EmptyTitle = 3, #[error("Maximum notes limit reached")] MaxNotesExceeded = 4, } ``` ### 6. Error Testing ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_invalid_rating() { let result = validate_rating(0); assert_eq!( result.unwrap_err(), NoteError::InvalidRating.into() ); let result = validate_rating(6); assert_eq!( result.unwrap_err(), NoteError::InvalidRating.into() ); } #[test] fn test_valid_rating() { for rating in 1..=5 { assert!(validate_rating(rating).is_ok()); } } } ``` ### 7. Avoid Silent Failures ```rust // ❌ Bad - errors ignored pub fn bad_error_handling(accounts: &[AccountInfo]) -> ProgramResult { let _ = validate_accounts(accounts); // Ignores error! if let Ok(data) = load_data(accounts) { process(data); // What if load_data failed? } Ok(()) // Returns success even if operations failed! } // ✅ Good - errors propagated pub fn good_error_handling(accounts: &[AccountInfo]) -> ProgramResult { validate_accounts(accounts)?; let data = load_data(accounts)?; process(data)?; Ok(()) } ``` --- ## Summary **Key Takeaways:** 1. **Always return `ProgramResult`** from instruction handlers 2. **Use custom errors** for specific failure modes 3. **Implement `From` trait** to convert custom errors to `ProgramError` 4. **Use `?` operator** for clean error propagation 5. **Add context with `msg!`** for better debugging 6. **Fail fast** - return errors immediately 7. **Document error codes** for client developers 8. **Test error cases** as thoroughly as success cases **Error Handling Pattern:** ```rust use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, msg, }; use thiserror::Error; // 1. Define custom errors #[derive(Error, Debug, Copy, Clone)] pub enum MyError { #[error("Descriptive error message")] SpecificError, } // 2. Implement From conversion impl From<MyError> for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } // 3. Use in program pub fn my_instruction(accounts: &[AccountInfo]) -> ProgramResult { // Validate if invalid_condition { msg!("Detailed error context"); return Err(MyError::SpecificError.into()); } // Propagate errors with ? let data = load_data(accounts)?; Ok(()) } ``` **Remember:** Good error handling is not optional—it's essential for security, debugging, and user experience. -
native-rust.md 44 KB
# Native Rust Solana Programs Reference This reference covers native Rust-specific implementation patterns and workflows for building Solana programs without the Anchor framework. For general concepts (what PDAs/CPIs are), see the other reference files. ## Table of Contents - [Project Setup](#project-setup) - [Entrypoint Patterns](#entrypoint-patterns) - [Manual Account Handling](#manual-account-handling) - [Manual Serialization](#manual-serialization) - [Instruction Definition](#instruction-definition) - [State Management](#state-management) - [Manual CPI Patterns](#manual-cpi-patterns) - [Build and Deploy Workflow](#build-and-deploy-workflow) - [Testing with Mollusk](#testing-with-mollusk) - [Verified Builds](#verified-builds) - [Program Management](#program-management) - [Common Native Patterns](#common-native-patterns) --- ## Project Setup ### Cargo.toml Configuration Basic program configuration: ```toml [package] name = "my_program" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "lib"] # cdylib for .so, lib for tests name = "my_program" [features] no-entrypoint = [] # Disable entrypoint for testing/CPI [dependencies] solana-program = "2.1.0" borsh = "1.5.1" borsh-derive = "1.5.1" [dev-dependencies] mollusk-svm = "0.3.0" solana-sdk = "2.1.0" [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ``` ### Dependency Versions **Production Dependencies:** - `solana-program = "2.1.0"` - Core program runtime APIs - `borsh = "1.5.1"` - Serialization framework - `borsh-derive = "1.5.1"` - Derive macros for Borsh **Development Dependencies:** - `mollusk-svm = "0.3.0"` - Fast testing framework - `solana-sdk = "2.1.0"` - Client-side SDK for tests - `mollusk-svm-bencher = "0.3.0"` - Compute unit benchmarking **Optional Helpers:** - `thiserror = "2.0"` - Error type definitions - `num-derive = "0.4"` - Derive numeric traits - `num-traits = "0.2"` - Numeric trait support - `spl-token = "6.0"` - Token program integration - `spl-associated-token-account = "5.0"` - ATA integration - `bytemuck = "1.20"` - Zero-copy type conversions ### Workspace Setup Pattern For multi-program projects: ```toml # Workspace Cargo.toml [workspace] members = [ "programs/program-one", "programs/program-two", ] resolver = "2" [workspace.dependencies] solana-program = "2.1.0" borsh = "1.5.1" # Program Cargo.toml [dependencies] solana-program = { workspace = true } borsh = { workspace = true } ``` ### Project Structure ``` my-program/ ├── Cargo.toml ├── src/ │ ├── lib.rs # Entrypoint and routing │ ├── instruction.rs # Instruction definitions │ ├── state.rs # Account state structs │ ├── processor.rs # Instruction handlers │ ├── error.rs # Custom errors │ └── utils.rs # Helper functions ├── tests/ │ └── test.rs # Mollusk tests └── target/ └── deploy/ ├── program.so # Built program binary └── program-keypair.json # Program keypair ``` --- ## Entrypoint Patterns ### Basic Entrypoint The `entrypoint!` macro sets up the program entry: ```rust use solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, }; // Declare the entrypoint entrypoint!(process_instruction); // Process instruction function signature pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Route to handlers Ok(()) } ``` ### Conditional Entrypoint (for testing/CPI) Disable entrypoint when used as a dependency: ```rust #[cfg(not(feature = "no-entrypoint"))] use solana_program::entrypoint; #[cfg(not(feature = "no-entrypoint"))] entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Implementation Ok(()) } ``` ### Instruction Routing Pattern Route to different handlers based on instruction type: ```rust use borsh::BorshDeserialize; pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { // Deserialize instruction let instruction = MyInstruction::try_from_slice(instruction_data)?; // Route to handler match instruction { MyInstruction::Initialize { data } => { process_initialize(program_id, accounts, data) } MyInstruction::Update { new_data } => { process_update(program_id, accounts, new_data) } MyInstruction::Close => { process_close(program_id, accounts) } } } ``` ### Multi-Module Routing For larger programs, organize handlers in modules: ```rust mod processor; pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let instruction = MyInstruction::try_from_slice(instruction_data)?; match instruction { MyInstruction::Initialize { data } => { processor::initialize::process(program_id, accounts, data) } MyInstruction::Update { new_data } => { processor::update::process(program_id, accounts, new_data) } MyInstruction::Close => { processor::close::process(program_id, accounts) } } } ``` --- ## Manual Account Handling ### Using next_account_info Iterator The standard pattern for accessing accounts: ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, }; fn process_transfer(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // Get accounts in order let payer = next_account_info(account_info_iter)?; let recipient = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Use accounts... Ok(()) } ``` ### AccountInfo Structure and Methods Key fields and methods: ```rust pub struct AccountInfo<'a> { pub key: &'a Pubkey, // Account public key pub is_signer: bool, // Signed transaction? pub is_writable: bool, // Writable account? pub lamports: Rc<RefCell<&'a mut u64>>, // Account balance pub data: Rc<RefCell<&'a mut [u8]>>, // Account data pub owner: &'a Pubkey, // Owner program pub executable: bool, // Is executable? pub rent_epoch: Epoch, // Rent epoch } // Common methods impl<'a> AccountInfo<'a> { // Check if account signed the transaction pub fn is_signer(&self) -> bool; // Check if account is writable pub fn is_writable(&self) -> bool; // Borrow account data immutably pub fn data(&self) -> Ref<&mut [u8]>; // Borrow account data mutably pub fn data_mut(&self) -> RefMut<&mut [u8]>; // Borrow lamports immutably pub fn lamports(&self) -> Ref<&mut u64>; // Borrow lamports mutably pub fn lamports_mut(&self) -> RefMut<&mut u64>; // Get data length pub fn data_len(&self) -> usize; // Check if owned by program pub fn is_owned_by(&self, program_id: &Pubkey) -> bool; // Deserialize account data pub fn deserialize_data<T: BorshDeserialize>(&self) -> Result<T, Error>; // Serialize data into account pub fn serialize_data<T: BorshSerialize>(&self, state: &T) -> Result<(), Error>; } ``` ### Explicit Account Validation Patterns **Signer Check:** ```rust if !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } ``` **Writable Check:** ```rust if !account.is_writable { return Err(ProgramError::InvalidAccountData); } ``` **Owner Check:** ```rust if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } ``` **Specific Owner Check:** ```rust use solana_program::system_program; if account.owner != &system_program::ID { return Err(ProgramError::InvalidAccountOwner); } ``` **Combined Validation:** ```rust fn validate_account( account: &AccountInfo, expected_owner: &Pubkey, must_sign: bool, must_write: bool, ) -> ProgramResult { if must_sign && !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } if must_write && !account.is_writable { return Err(ProgramError::InvalidAccountData); } if account.owner != expected_owner { return Err(ProgramError::IncorrectProgramId); } Ok(()) } ``` **PDA Validation:** ```rust fn validate_pda( account: &AccountInfo, seeds: &[&[u8]], program_id: &Pubkey, ) -> ProgramResult { let (expected_key, _bump) = Pubkey::find_program_address(seeds, program_id); if account.key != &expected_key { return Err(ProgramError::InvalidSeeds); } Ok(()) } ``` **Rent Exemption Check:** ```rust use solana_program::sysvar::{rent::Rent, Sysvar}; fn check_rent_exempt(account: &AccountInfo) -> ProgramResult { let rent = Rent::get()?; if !rent.is_exempt(account.lamports(), account.data_len()) { return Err(ProgramError::AccountNotRentExempt); } Ok(()) } ``` ### Account Data Access Patterns **Immutable Borrow:** ```rust let data = account.data.borrow(); let state = MyState::try_from_slice(&data)?; ``` **Mutable Borrow:** ```rust let mut data = account.data.borrow_mut(); let mut state = MyState::try_from_slice(&data)?; state.counter += 1; state.serialize(&mut &mut data[..])?; ``` **Lamport Access:** ```rust // Read lamports let balance = account.lamports(); println!("Balance: {}", *balance); // Modify lamports (for transfers) **account.lamports.borrow_mut() = new_balance; ``` **Zero-Copy Data Access:** ```rust use bytemuck::{Pod, Zeroable}; #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct FastState { value: u64, flag: u8, } fn read_fast_state(account: &AccountInfo) -> Result<&FastState, ProgramError> { let data = account.try_borrow_data()?; bytemuck::try_from_bytes(&data[..std::mem::size_of::<FastState>()]) .map_err(|_| ProgramError::InvalidAccountData) } ``` --- ## Manual Serialization ### Borsh Derive Use `BorshSerialize` and `BorshDeserialize` for most cases: ```rust use borsh::{BorshSerialize, BorshDeserialize}; #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct MyState { pub is_initialized: bool, pub counter: u64, pub authority: Pubkey, pub data: Vec<u8>, } ``` ### Manual Borsh Implementation For custom serialization logic: ```rust use borsh::io::{Read, Write, Result as BorshResult}; #[derive(Debug)] pub struct CustomState { pub flag: bool, pub value: u64, } impl BorshSerialize for CustomState { fn serialize<W: Write>(&self, writer: &mut W) -> BorshResult<()> { self.flag.serialize(writer)?; self.value.serialize(writer)?; Ok(()) } } impl BorshDeserialize for CustomState { fn deserialize_reader<R: Read>(reader: &mut R) -> BorshResult<Self> { let flag = bool::deserialize_reader(reader)?; let value = u64::deserialize_reader(reader)?; Ok(Self { flag, value }) } } ``` ### Account Data Layout Planning Calculate and document exact byte offsets: ```rust // Account layout documentation // [0] is_initialized: bool (1 byte) // [1-8] counter: u64 (8 bytes) // [9-40] authority: Pubkey (32 bytes) // Total: 41 bytes #[derive(BorshSerialize, BorshDeserialize)] pub struct Counter { pub is_initialized: bool, // 1 byte pub counter: u64, // 8 bytes pub authority: Pubkey, // 32 bytes } impl Counter { pub const LEN: usize = 1 + 8 + 32; // 41 bytes } ``` ### Packing and Unpacking Account Data **Deserialize (unpack):** ```rust use borsh::BorshDeserialize; fn get_state(account: &AccountInfo) -> Result<MyState, ProgramError> { let data = account.try_borrow_data()?; MyState::try_from_slice(&data) .map_err(|_| ProgramError::InvalidAccountData) } ``` **Serialize (pack):** ```rust use borsh::BorshSerialize; fn save_state(account: &AccountInfo, state: &MyState) -> ProgramResult { let mut data = account.try_borrow_mut_data()?; state.serialize(&mut &mut data[..]) .map_err(|_| ProgramError::InvalidAccountData)?; Ok(()) } ``` **Combined Pattern:** ```rust fn update_counter(account: &AccountInfo, increment: u64) -> ProgramResult { // Deserialize let mut data = account.try_borrow_mut_data()?; let mut state = MyState::try_from_slice(&data)?; // Modify state.counter += increment; // Serialize back state.serialize(&mut &mut data[..])?; Ok(()) } ``` ### Zero-Copy Patterns with Bytemuck For high-performance, use zero-copy with bytemuck: ```rust use bytemuck::{Pod, Zeroable, from_bytes_mut, bytes_of}; #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct ZeroCopyState { pub is_initialized: u8, // bool as u8 pub counter: u64, pub authority: [u8; 32], // Pubkey as bytes } impl ZeroCopyState { pub const LEN: usize = std::mem::size_of::<Self>(); } // Read zero-copy fn get_state(account: &AccountInfo) -> Result<&ZeroCopyState, ProgramError> { let data = account.try_borrow_data()?; bytemuck::try_from_bytes(&data[..ZeroCopyState::LEN]) .map_err(|_| ProgramError::InvalidAccountData) } // Write zero-copy fn update_state(account: &AccountInfo, new_counter: u64) -> ProgramResult { let mut data = account.try_borrow_mut_data()?; let state = bytemuck::try_from_bytes_mut::<ZeroCopyState>( &mut data[..ZeroCopyState::LEN] ).map_err(|_| ProgramError::InvalidAccountData)?; state.counter = new_counter; Ok(()) } ``` ### Variable-Length Data For dynamic data, use a header + data pattern: ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct VarLenState { pub is_initialized: bool, pub data_len: u32, // Followed by data_len bytes } impl VarLenState { pub const HEADER_LEN: usize = 1 + 4; // bool + u32 pub fn unpack(data: &[u8]) -> Result<(Self, &[u8]), ProgramError> { if data.len() < Self::HEADER_LEN { return Err(ProgramError::InvalidAccountData); } let header = Self::try_from_slice(&data[..Self::HEADER_LEN])?; let data_slice = &data[Self::HEADER_LEN..Self::HEADER_LEN + header.data_len as usize]; Ok((header, data_slice)) } } ``` --- ## Instruction Definition ### Borsh-Serializable Instruction Enums Define instructions as enums: ```rust use borsh::{BorshSerialize, BorshDeserialize}; use solana_program::pubkey::Pubkey; #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub enum MyInstruction { /// Initialize a new account /// /// Accounts expected: /// 0. `[writable, signer]` Account to initialize /// 1. `[signer]` Authority /// 2. `[]` System Program Initialize { initial_value: u64, }, /// Update account data /// /// Accounts expected: /// 0. `[writable]` Account to update /// 1. `[signer]` Authority Update { new_value: u64, }, /// Transfer ownership /// /// Accounts expected: /// 0. `[writable]` Account /// 1. `[signer]` Current authority /// 2. `[]` New authority TransferOwnership { new_authority: Pubkey, }, /// Close account and reclaim rent /// /// Accounts expected: /// 0. `[writable]` Account to close /// 1. `[writable]` Rent recipient /// 2. `[signer]` Authority Close, } ``` ### Instruction Data Layout **Fixed-Size Instructions:** ```rust // Discriminator (1 byte) + data // [0] = 0 -> Initialize // [1] = 1 -> Update // etc. #[derive(BorshSerialize, BorshDeserialize)] pub enum SimpleInstruction { Initialize = 0, Update = 1, Close = 2, } ``` **Instructions with Parameters:** ```rust // Manual discriminator pattern pub enum MyInstruction { // Discriminator 0: [0, value_bytes[0..8]] Initialize { value: u64 }, // Discriminator 1: [1, amount_bytes[0..8]] Transfer { amount: u64 }, } impl MyInstruction { pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> { let (&discriminator, rest) = input.split_first() .ok_or(ProgramError::InvalidInstructionData)?; Ok(match discriminator { 0 => { let value = u64::from_le_bytes(rest[..8].try_into().unwrap()); Self::Initialize { value } } 1 => { let amount = u64::from_le_bytes(rest[..8].try_into().unwrap()); Self::Transfer { amount } } _ => return Err(ProgramError::InvalidInstructionData), }) } } ``` ### Dispatching Instructions **Pattern 1: Direct Match** ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let instruction = MyInstruction::try_from_slice(instruction_data)?; match instruction { MyInstruction::Initialize { initial_value } => { msg!("Instruction: Initialize"); process_initialize(program_id, accounts, initial_value) } MyInstruction::Update { new_value } => { msg!("Instruction: Update"); process_update(program_id, accounts, new_value) } MyInstruction::Close => { msg!("Instruction: Close"); process_close(program_id, accounts) } } } ``` **Pattern 2: Handler Functions** ```rust impl MyInstruction { pub fn process( &self, program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { match self { Self::Initialize { initial_value } => { Self::process_initialize(program_id, accounts, *initial_value) } Self::Update { new_value } => { Self::process_update(program_id, accounts, *new_value) } Self::Close => { Self::process_close(program_id, accounts) } } } fn process_initialize( program_id: &Pubkey, accounts: &[AccountInfo], initial_value: u64, ) -> ProgramResult { // Implementation Ok(()) } } ``` --- ## State Management ### Defining Account State Structs ```rust use borsh::{BorshSerialize, BorshDeserialize}; use solana_program::pubkey::Pubkey; #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct UserAccount { pub is_initialized: bool, pub authority: Pubkey, pub balance: u64, pub last_updated: i64, } impl UserAccount { pub const LEN: usize = 1 + 32 + 8 + 8; // 49 bytes } ``` ### Calculating Account Sizes **Fixed-Size Accounts:** ```rust impl MyState { // Method 1: Manual calculation pub const LEN: usize = 1 + // is_initialized: bool 32 + // authority: Pubkey 8 + // counter: u64 4 + // data_len: u32 100; // data: [u8; 100] // Method 2: Use size_of pub const LEN_ALT: usize = std::mem::size_of::<Self>(); } ``` **Variable-Size Accounts:** ```rust impl DynamicState { pub const BASE_LEN: usize = 1 + 32 + 8; // Fixed fields pub fn calculate_size(data_len: usize) -> usize { Self::BASE_LEN + 4 + data_len // +4 for length prefix } } ``` **With Borsh:** ```rust use borsh::BorshSerialize; let state = MyState { /* ... */ }; let serialized = state.try_to_vec()?; let size = serialized.len(); // Actual size needed ``` ### Initializing Accounts Manually with System Program CPI **Complete Initialization Pattern:** ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::invoke, pubkey::Pubkey, rent::Rent, system_instruction, sysvar::Sysvar, }; fn process_initialize( program_id: &Pubkey, accounts: &[AccountInfo], initial_value: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let new_account = next_account_info(account_info_iter)?; let payer = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Calculate space needed let space = MyState::LEN; // Calculate rent let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(space); // Create account via CPI to System Program invoke( &system_instruction::create_account( payer.key, // Funding account new_account.key, // New account rent_lamports, // Lamports space as u64, // Space program_id, // Owner ), &[ payer.clone(), new_account.clone(), system_program.clone(), ], )?; // Initialize account data let mut data = new_account.try_borrow_mut_data()?; let state = MyState { is_initialized: true, counter: initial_value, authority: *payer.key, }; state.serialize(&mut &mut data[..])?; Ok(()) } ``` **Initialize PDA Pattern:** ```rust use solana_program::program::invoke_signed; fn initialize_pda( program_id: &Pubkey, accounts: &[AccountInfo], seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let pda = next_account_info(account_info_iter)?; let payer = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Verify PDA let (expected_pda, expected_bump) = Pubkey::find_program_address(seeds, program_id); if pda.key != &expected_pda || bump != expected_bump { return Err(ProgramError::InvalidSeeds); } // Create PDA account let space = MyState::LEN; let rent = Rent::get()?; let lamports = rent.minimum_balance(space); let bump_seed = &[bump]; let seeds_with_bump = &[seeds, &[bump_seed.as_slice()]].concat(); invoke_signed( &system_instruction::create_account( payer.key, pda.key, lamports, space as u64, program_id, ), &[payer.clone(), pda.clone(), system_program.clone()], &[seeds_with_bump], // Signer seeds )?; // Initialize data let mut data = pda.try_borrow_mut_data()?; let state = MyState::default(); state.serialize(&mut &mut data[..])?; Ok(()) } ``` ### Account Reallocation Resize account data: ```rust use solana_program::program::invoke; fn reallocate_account( account: &AccountInfo, payer: &AccountInfo, new_size: usize, program_id: &Pubkey, ) -> ProgramResult { // Verify ownership if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } // Reallocate account.realloc(new_size, false)?; // Fund additional rent if needed let rent = Rent::get()?; let new_minimum_balance = rent.minimum_balance(new_size); let current_balance = account.lamports(); if *current_balance < new_minimum_balance { let additional = new_minimum_balance - *current_balance; **payer.lamports.borrow_mut() -= additional; **account.lamports.borrow_mut() += additional; } Ok(()) } ``` --- ## Manual CPI Patterns ### Using invoke For CPIs without PDA signers: ```rust use solana_program::{ account_info::AccountInfo, instruction::{AccountMeta, Instruction}, program::invoke, pubkey::Pubkey, system_instruction, }; fn transfer_sol( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &system_instruction::transfer(from.key, to.key, amount), &[from.clone(), to.clone(), system_program.clone()], ) } ``` ### Using invoke_signed For CPIs with PDA signers: ```rust use solana_program::program::invoke_signed; fn pda_transfer( pda: &AccountInfo, recipient: &AccountInfo, system_program: &AccountInfo, amount: u64, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let bump_seed = &[bump]; let signer_seeds: &[&[&[u8]]] = &[ &[seeds, &[bump_seed]].concat() ]; invoke_signed( &system_instruction::transfer(pda.key, recipient.key, amount), &[pda.clone(), recipient.clone(), system_program.clone()], signer_seeds, ) } ``` ### Building AccountMeta Arrays Manually construct account metadata: ```rust use solana_program::instruction::AccountMeta; let account_metas = vec![ AccountMeta::new(*writable_account.key, false), // Writable, not signer AccountMeta::new(*writable_signer.key, true), // Writable, signer AccountMeta::new_readonly(*readonly_account.key, false), // Read-only, not signer AccountMeta::new_readonly(*readonly_signer.key, true), // Read-only, signer ]; ``` ### Creating Instruction Structs Build instructions for CPI: ```rust use solana_program::instruction::Instruction; fn build_custom_instruction( program_id: &Pubkey, account1: &Pubkey, account2: &Pubkey, data: Vec<u8>, ) -> Instruction { Instruction { program_id: *program_id, accounts: vec![ AccountMeta::new(*account1, true), AccountMeta::new(*account2, false), ], data, } } // Use in CPI fn call_custom_program( program: &AccountInfo, account1: &AccountInfo, account2: &AccountInfo, data: Vec<u8>, ) -> ProgramResult { let instruction = build_custom_instruction( program.key, account1.key, account2.key, data, ); invoke( &instruction, &[account1.clone(), account2.clone()], ) } ``` ### SPL Token CPI Pattern Transfer tokens via CPI: ```rust use spl_token::instruction as token_instruction; fn transfer_tokens( token_program: &AccountInfo, source: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &token_instruction::transfer( token_program.key, source.key, destination.key, authority.key, &[], // No multisig signers amount, )?, &[source.clone(), destination.clone(), authority.clone()], ) } fn transfer_tokens_with_pda( token_program: &AccountInfo, source: &AccountInfo, destination: &AccountInfo, pda_authority: &AccountInfo, amount: u64, seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let bump_seed = &[bump]; let signer_seeds: &[&[&[u8]]] = &[ &[seeds, &[bump_seed]].concat() ]; invoke_signed( &token_instruction::transfer( token_program.key, source.key, destination.key, pda_authority.key, &[], amount, )?, &[source.clone(), destination.clone(), pda_authority.clone()], signer_seeds, ) } ``` --- ## Build and Deploy Workflow ### cargo build-sbf Command Build the program for Solana: ```bash # Basic build cargo build-sbf # Build with specific Solana version cargo build-sbf --solana-version 2.1.0 # Build for mainnet (with optimizations) cargo build-sbf --release # Specify output directory cargo build-sbf --sbf-out-dir ./output ``` ### Understanding .so and -keypair.json Files After building: ``` target/deploy/ ├── my_program.so # Compiled program binary └── my_program-keypair.json # Program's keypair (address) ``` **Program ID:** ```bash # Get program ID from keypair solana address -k target/deploy/my_program-keypair.json ``` **Update Program ID in Code:** ```rust // In lib.rs declare_id!("YourProgramID11111111111111111111111111111"); ``` ### solana program deploy Commands **Deploy to Devnet:** ```bash # Set cluster solana config set --url devnet # Fund deployer account solana airdrop 2 # Deploy program solana program deploy target/deploy/my_program.so # Deploy to specific program ID solana program deploy \ target/deploy/my_program.so \ --program-id target/deploy/my_program-keypair.json # Deploy with custom keypair solana program deploy \ target/deploy/my_program.so \ --program-id custom-keypair.json \ --upgrade-authority ~/.config/solana/id.json ``` **Deploy to Mainnet:** ```bash solana config set --url mainnet-beta # Deploy (costs SOL based on program size) solana program deploy target/deploy/my_program.so ``` ### Program Size and Cost Calculation **Check Program Size:** ```bash ls -lh target/deploy/my_program.so # Or get detailed info solana program show <PROGRAM_ID> ``` **Calculate Deployment Cost:** Program cost formula: `rent_exemption(program_size)` ```bash # Get rent for specific size solana rent <SIZE_IN_BYTES> # Example for 200KB program solana rent 204800 # Output: Rent-exempt minimum: 1.42607328 SOL ``` **Typical Sizes:** - Simple programs: 50-100 KB - Medium programs: 100-300 KB - Large programs: 300-500 KB - Maximum: ~1 MB (hard limit) **Reduce Program Size:** ```toml # In Cargo.toml [profile.release] opt-level = "z" # Optimize for size lto = true # Link-time optimization codegen-units = 1 # Better optimization strip = true # Strip symbols ``` --- ## Testing with Mollusk ### Test Structure with mollusk-svm Basic test setup: ```rust #[cfg(test)] mod tests { use { mollusk_svm::Mollusk, solana_sdk::{ account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }, }; #[test] fn test_initialize() { // Create Mollusk instance let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); // Test implementation... } } ``` ### Creating Test Accounts **System-Owned Account:** ```rust let user = Pubkey::new_unique(); let user_account = Account { lamports: 1_000_000, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }; ``` **Program-Owned Account:** ```rust let state_account = Pubkey::new_unique(); let state = Account { lamports: rent_lamports, data: vec![0; MyState::LEN], owner: program_id, executable: false, rent_epoch: 0, }; ``` **Pre-Initialized Account:** ```rust use borsh::BorshSerialize; let mut data = vec![0; MyState::LEN]; let initial_state = MyState { is_initialized: true, counter: 42, authority: user, }; initial_state.serialize(&mut data.as_mut_slice()).unwrap(); let initialized_account = Account { lamports: rent_lamports, data, owner: program_id, executable: false, rent_epoch: 0, }; ``` ### Process Instructions and Validate Results **Basic Process and Check:** ```rust use mollusk_svm::result::Check; #[test] fn test_instruction() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let instruction = Instruction::new_with_bytes( program_id, &[0], // Instruction data vec![ AccountMeta::new(user, true), ], ); let accounts = vec![ (user, Account { lamports: 1_000_000, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }), ]; let checks = vec![ Check::success(), Check::account(&user) .lamports(1_000_000) .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` **Validate Account Data:** ```rust let expected_data = MyState { is_initialized: true, counter: 10, authority: user, }.try_to_vec().unwrap(); let checks = vec![ Check::success(), Check::account(&state_account) .data(&expected_data) .lamports(rent_lamports) .owner(&program_id) .build(), ]; ``` **Check Specific Data Slice:** ```rust let checks = vec![ Check::success(), Check::account(&account) .data_slice(0, &[1]) // Check first byte is 1 (initialized) .data_slice(8, &10u64.to_le_bytes()) // Check counter at offset 8 .build(), ]; ``` **Test Error Conditions:** ```rust use solana_sdk::instruction::InstructionError; let checks = vec![ Check::instruction_err(InstructionError::InvalidInstructionData), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); ``` ### Compute Unit Benchmarking **Basic Benchmark:** ```rust use mollusk_svm_bencher::MolluskComputeUnitBencher; fn main() { let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let instruction = /* build instruction */; let accounts = /* setup accounts */; MolluskComputeUnitBencher::new(mollusk) .bench(("my_instruction", &instruction, &accounts)) .must_pass(true) .out_dir("./benches") .execute(); } ``` **Run Benchmark:** ```bash # Build first cargo build-sbf # Run benchmark cargo run --bin bench ``` **Benchmark Output:** ``` ╭──────────────────────────────┬────────────────────╮ │ Instruction │ Compute Units │ ├──────────────────────────────┼────────────────────┤ │ my_instruction │ 1,234 │ ╰──────────────────────────────┴────────────────────╯ Results written to: ./benches/compute_units.json ``` --- ## Verified Builds ### solana-verify Workflow Verify programs on-chain match source code: **Install solana-verify:** ```bash cargo install solana-verify ``` **Verify a Program:** ```bash # Verify remote build solana-verify verify-from-repo \ --program-id <PROGRAM_ID> \ --remote https://github.com/user/repo \ --commit-hash <COMMIT_HASH> \ --library-name program_name # Verify with mount path (for workspace) solana-verify verify-from-repo \ --program-id <PROGRAM_ID> \ --remote https://github.com/user/repo \ --commit-hash <COMMIT_HASH> \ --mount-path programs/my-program \ --library-name my_program ``` ### Docker-Based Builds Build in Docker for reproducibility: **Dockerfile:** ```dockerfile FROM --platform=linux/amd64 projectserum/build:v0.29.0 WORKDIR /build COPY . . RUN cargo build-sbf --release ``` **Build Command:** ```bash docker build -t my-program-build . docker create --name extract my-program-build docker cp extract:/build/target/deploy/my_program.so ./my_program-verifiable.so docker rm extract ``` **Verify Deterministic:** ```bash # Compare hashes sha256sum target/deploy/my_program.so sha256sum my_program-verifiable.so # Should match! ``` ### Buffer Uploads for Multisig Deploy via buffer for multisig upgrade authority: ```bash # Write program to buffer solana program write-buffer target/deploy/my_program.so # Output: Buffer: <BUFFER_ADDRESS> # Set buffer authority to multisig solana program set-buffer-authority <BUFFER_ADDRESS> --new-buffer-authority <MULTISIG_ADDRESS> # Later: Deploy from buffer (requires multisig) solana program deploy --buffer <BUFFER_ADDRESS> --program-id <PROGRAM_ID> ``` **Squads Multisig Example:** ```bash # 1. Write buffer BUFFER=$(solana program write-buffer target/deploy/my_program.so | grep "Buffer:" | awk '{print $2}') # 2. Transfer buffer authority to Squads solana program set-buffer-authority $BUFFER --new-buffer-authority <SQUADS_ADDRESS> # 3. Create proposal in Squads UI to deploy from buffer ``` --- ## Program Management ### solana program show Get program information: ```bash # Show program details solana program show <PROGRAM_ID> # Output: # Program Id: <PROGRAM_ID> # Owner: BPFLoaderUpgradeab1e11111111111111111111111 # ProgramData Address: <DATA_ADDRESS> # Authority: <UPGRADE_AUTHORITY> # Last Deployed In Slot: 123456789 # Data Length: 204800 bytes # Balance: 1.42607328 SOL ``` **Show Program Data:** ```bash # Get upgrade authority solana program show <PROGRAM_ID> | grep Authority # Get program size solana program show <PROGRAM_ID> | grep "Data Length" ``` ### Authority Transfers **Transfer Upgrade Authority:** ```bash # Transfer to new authority solana program set-upgrade-authority \ <PROGRAM_ID> \ --new-upgrade-authority <NEW_AUTHORITY> # Transfer to multisig solana program set-upgrade-authority \ <PROGRAM_ID> \ --new-upgrade-authority <MULTISIG_ADDRESS> ``` ### Making Programs Immutable Remove upgrade authority to make program immutable: ```bash # Make immutable (IRREVERSIBLE!) solana program set-upgrade-authority <PROGRAM_ID> --final # Verify immutability solana program show <PROGRAM_ID> # Authority: none ``` **Warning:** This is permanent. The program can never be upgraded again. ### Closing Programs Reclaim rent from closed programs: ```bash # Close program and reclaim rent solana program close <PROGRAM_ID> # Close and send rent to specific recipient solana program close <PROGRAM_ID> --recipient <RECIPIENT_ADDRESS> # Close program buffer solana program close --buffers ``` **Requirements:** - Must be upgrade authority - Program must not be marked as final - Recipient receives all lamports from program account --- ## Common Native Patterns ### PDA Derivation and Signing **Find PDA:** ```rust use solana_program::pubkey::Pubkey; fn get_user_pda(user: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address( &[ b"user", user.as_ref(), ], program_id, ) } ``` **Verify PDA:** ```rust fn validate_pda( pda: &AccountInfo, seeds: &[&[u8]], bump: u8, program_id: &Pubkey, ) -> ProgramResult { let expected_pda = Pubkey::create_program_address( &[seeds, &[&[bump]]].concat(), program_id, )?; if pda.key != &expected_pda { return Err(ProgramError::InvalidSeeds); } Ok(()) } ``` **Sign with PDA:** ```rust use solana_program::program::invoke_signed; fn pda_invoke( instruction: &Instruction, accounts: &[AccountInfo], user: &Pubkey, bump: u8, ) -> ProgramResult { let signer_seeds: &[&[&[u8]]] = &[ &[b"user", user.as_ref(), &[bump]] ]; invoke_signed(instruction, accounts, signer_seeds) } ``` ### Rent Calculation **Calculate Minimum Balance:** ```rust use solana_program::{ rent::Rent, sysvar::Sysvar, }; fn get_rent_exempt_balance(data_len: usize) -> Result<u64, ProgramError> { let rent = Rent::get()?; Ok(rent.minimum_balance(data_len)) } ``` **Check if Rent Exempt:** ```rust fn is_rent_exempt(account: &AccountInfo) -> Result<bool, ProgramError> { let rent = Rent::get()?; Ok(rent.is_exempt(account.lamports(), account.data_len())) } ``` ### Lamport Transfers **Direct Transfer (modify lamports):** ```rust fn transfer_lamports( from: &AccountInfo, to: &AccountInfo, amount: u64, ) -> ProgramResult { // Borrow and update lamports **from.try_borrow_mut_lamports()? -= amount; **to.try_borrow_mut_lamports()? += amount; Ok(()) } ``` **Via System Program:** ```rust use solana_program::{ program::invoke, system_instruction, }; fn transfer_via_system_program( from: &AccountInfo, to: &AccountInfo, system_program: &AccountInfo, amount: u64, ) -> ProgramResult { invoke( &system_instruction::transfer(from.key, to.key, amount), &[from.clone(), to.clone(), system_program.clone()], ) } ``` ### Error Handling with ProgramError **Using Built-in Errors:** ```rust use solana_program::program_error::ProgramError; if !account.is_signer { return Err(ProgramError::MissingRequiredSignature); } if account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } if account.data_len() < MyState::LEN { return Err(ProgramError::AccountDataTooSmall); } ``` **Custom Errors:** ```rust use solana_program::program_error::ProgramError; use thiserror::Error; #[derive(Error, Debug, Copy, Clone)] pub enum MyError { #[error("Account already initialized")] AlreadyInitialized, #[error("Invalid authority")] InvalidAuthority, #[error("Arithmetic overflow")] Overflow, } impl From<MyError> for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } // Usage if state.is_initialized { return Err(MyError::AlreadyInitialized.into()); } ``` **With num_derive:** ```rust use num_derive::FromPrimitive; use num_traits::FromPrimitive; use solana_program::{ decode_error::DecodeError, program_error::{PrintProgramError, ProgramError}, }; use thiserror::Error; #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum MyError { #[error("Already initialized")] AlreadyInitialized, #[error("Invalid authority")] InvalidAuthority, } impl From<MyError> for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } impl<T> DecodeError<T> for MyError { fn type_of() -> &'static str { "MyError" } } impl PrintProgramError for MyError { fn print<E>(&self) where E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive, { match self { MyError::AlreadyInitialized => msg!("Error: Already initialized"), MyError::InvalidAuthority => msg!("Error: Invalid authority"), } } } ``` ### Logging and Debugging **Basic Logging:** ```rust use solana_program::msg; msg!("Processing instruction"); msg!("Counter value: {}", counter); msg!("Account: {}, balance: {}", account.key, account.lamports()); ``` **Compute Units Logging:** ```rust use solana_program::log::sol_log_compute_units; sol_log_compute_units(); // Log current compute units used ``` **Data Logging:** ```rust use solana_program::log::sol_log_data; // Log data for off-chain processing sol_log_data(&[b"event", &event_data]); ``` ### Clock Access Get current timestamp and slot: ```rust use solana_program::{ clock::Clock, sysvar::Sysvar, }; fn get_current_time() -> Result<i64, ProgramError> { let clock = Clock::get()?; Ok(clock.unix_timestamp) } fn get_current_slot() -> Result<u64, ProgramError> { let clock = Clock::get()?; Ok(clock.slot) } ``` ### Account Closure Pattern Properly close accounts and reclaim rent: ```rust fn close_account( account_to_close: &AccountInfo, destination: &AccountInfo, ) -> ProgramResult { // Transfer all lamports let dest_starting_lamports = destination.lamports(); **destination.lamports.borrow_mut() = dest_starting_lamports .checked_add(account_to_close.lamports()) .ok_or(ProgramError::ArithmeticOverflow)?; **account_to_close.lamports.borrow_mut() = 0; // Zero out data let mut data = account_to_close.try_borrow_mut_data()?; data.fill(0); Ok(()) } ``` ### Discriminator Pattern Add discriminator to distinguish account types: ```rust #[derive(BorshSerialize, BorshDeserialize)] pub enum AccountType { Uninitialized, User, Config, } #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub account_type: AccountType, // Discriminator pub authority: Pubkey, pub balance: u64, } impl UserAccount { pub const LEN: usize = 1 + 32 + 8; pub fn validate_type(account: &AccountInfo) -> ProgramResult { let data = account.try_borrow_data()?; let account_type = AccountType::try_from_slice(&data[..1])?; match account_type { AccountType::User => Ok(()), _ => Err(ProgramError::InvalidAccountData), } } } ``` --- ## Additional Resources - **Solana Program Examples**: https://github.com/solana-developers/program-examples - **Mollusk Testing**: https://github.com/anza-xyz/mollusk - **solana-program Docs**: https://docs.rs/solana-program - **Solana Cookbook**: https://solanacookbook.com/ - **SPL Token**: https://spl.solana.com/token - **Solana Verify**: https://github.com/Ellipsis-Labs/solana-verifiable-build --- *This reference focuses on native Rust implementation patterns. For conceptual understanding of Solana primitives (PDAs, CPIs, accounts, etc.), see the other reference files in this directory.* -
native-security.md 27.6 KB
# Native Rust Security Patterns for Solana Programs This reference covers security vulnerabilities and best practices specific to Solana programs built with native Rust (without Anchor framework). ## Table of Contents 1. [Manual Account Validation](#manual-account-validation) 2. [Account Discriminator Patterns](#account-discriminator-patterns) 3. [PDA Security in Native Rust](#pda-security-in-native-rust) 4. [Manual CPI Security](#manual-cpi-security) 5. [Manual Serialization Security](#manual-serialization-security) 6. [Rent and Space Management](#rent-and-space-management) 7. [Error Handling in Native Rust](#error-handling-in-native-rust) 8. [Token Program Integration](#token-program-integration) 9. [Low-Level Security Patterns](#low-level-security-patterns) 10. [Native Rust Best Practices](#native-rust-best-practices) --- ## Manual Account Validation In native Rust programs, ALL account validation must be performed manually. Missing any check can lead to critical vulnerabilities. ### Signer Checks **Vulnerable:** ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let authority = next_account_info(account_info_iter)?; // Missing signer check - anyone can call this! // Perform privileged operation Ok(()) } ``` **Secure:** ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let authority = next_account_info(account_info_iter)?; if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } // Now safe to perform privileged operation Ok(()) } ``` ### Owner Validation **Vulnerable:** ```rust pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let config_account = next_account_info(account_info_iter)?; // Missing owner check - could be any account! let mut config_data = Config::try_from_slice(&config_account.data.borrow())?; config_data.value = 42; config_data.serialize(&mut *config_account.data.borrow_mut())?; Ok(()) } ``` **Secure:** ```rust pub fn update_config( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let config_account = next_account_info(account_info_iter)?; // Verify this account is owned by our program if config_account.owner != program_id { return Err(ProgramError::IncorrectProgramId); } let mut config_data = Config::try_from_slice(&config_account.data.borrow())?; config_data.value = 42; config_data.serialize(&mut *config_account.data.borrow_mut())?; Ok(()) } ``` ### Writable Checks **Vulnerable:** ```rust pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let source = next_account_info(account_info_iter)?; // Missing writable check - runtime will panic! let mut data = source.try_borrow_mut_data()?; // Modify data... Ok(()) } ``` **Secure:** ```rust pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let source = next_account_info(account_info_iter)?; if !source.is_writable { return Err(ProgramError::InvalidAccountData); } let mut data = source.try_borrow_mut_data()?; // Safe to modify data Ok(()) } ``` ### Comprehensive Validation Function **Best Practice:** ```rust pub struct AccountValidation<'a, 'info> { account: &'a AccountInfo<'info>, } impl<'a, 'info> AccountValidation<'a, 'info> { pub fn new(account: &'a AccountInfo<'info>) -> Self { Self { account } } pub fn owner(self, expected_owner: &Pubkey) -> Result<Self, ProgramError> { if self.account.owner != expected_owner { return Err(ProgramError::IncorrectProgramId); } Ok(self) } pub fn signer(self) -> Result<Self, ProgramError> { if !self.account.is_signer { return Err(ProgramError::MissingRequiredSignature); } Ok(self) } pub fn writable(self) -> Result<Self, ProgramError> { if !self.account.is_writable { return Err(ProgramError::InvalidAccountData); } Ok(self) } pub fn key(self, expected_key: &Pubkey) -> Result<Self, ProgramError> { if self.account.key != expected_key { return Err(ProgramError::InvalidAccountData); } Ok(self) } pub fn initialized(self) -> Result<Self, ProgramError> { if self.account.data_is_empty() { return Err(ProgramError::UninitializedAccount); } Ok(self) } } // Usage: pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let authority = next_account_info(account_info_iter)?; let config = next_account_info(account_info_iter)?; AccountValidation::new(authority) .signer()?; AccountValidation::new(config) .owner(program_id)? .writable()? .initialized()?; // All validations passed Ok(()) } ``` --- ## Account Discriminator Patterns Without Anchor's automatic discriminators, you must manually implement account type safety. ### Why Discriminators Matter **Vulnerable:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct ConfigAccount { pub admin: Pubkey, pub value: u64, } #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub owner: Pubkey, pub balance: u64, } pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult { let config = next_account_info(&mut accounts.iter())?; // No discriminator check - UserAccount has same layout! let mut data = ConfigAccount::try_from_slice(&config.data.borrow())?; data.value = 999; // Could be writing to a UserAccount! Ok(()) } ``` ### Implementing Discriminators **Secure:** ```rust use borsh::{BorshDeserialize, BorshSerialize}; pub const CONFIG_DISCRIMINATOR: u64 = 0x1234567890ABCDEF; pub const USER_DISCRIMINATOR: u64 = 0xFEDCBA0987654321; #[derive(BorshSerialize, BorshDeserialize)] pub struct ConfigAccount { pub discriminator: u64, pub admin: Pubkey, pub value: u64, } #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub discriminator: u64, pub owner: Pubkey, pub balance: u64, } impl ConfigAccount { pub const LEN: usize = 8 + 32 + 8; pub fn new(admin: Pubkey, value: u64) -> Self { Self { discriminator: CONFIG_DISCRIMINATOR, admin, value, } } pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> { let data = account.data.borrow(); if data.len() < 8 { return Err(ProgramError::InvalidAccountData); } let discriminator = u64::from_le_bytes(data[0..8].try_into().unwrap()); if discriminator != CONFIG_DISCRIMINATOR { return Err(ProgramError::InvalidAccountData); } Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData) } } pub fn update_config( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let config_account = next_account_info(&mut accounts.iter())?; // Discriminator validated during deserialization let mut config = ConfigAccount::from_account_info(config_account)?; config.value = 999; config.serialize(&mut *config_account.data.borrow_mut())?; Ok(()) } ``` ### Alternative: String-Based Discriminators ```rust pub const ACCOUNT_TYPE_LEN: usize = 8; #[derive(BorshSerialize, BorshDeserialize)] pub struct TaggedAccount { pub account_type: [u8; ACCOUNT_TYPE_LEN], // "CONFIG\0\0" pub data: AccountData, } impl TaggedAccount { pub fn new_config(data: AccountData) -> Self { let mut account_type = [0u8; ACCOUNT_TYPE_LEN]; account_type[..6].copy_from_slice(b"CONFIG"); Self { account_type, data } } pub fn assert_config(&self) -> ProgramResult { let mut expected = [0u8; ACCOUNT_TYPE_LEN]; expected[..6].copy_from_slice(b"CONFIG"); if self.account_type != expected { return Err(ProgramError::InvalidAccountData); } Ok(()) } } ``` --- ## PDA Security in Native Rust ### find_program_address vs create_program_address **Vulnerable:** ```rust pub fn init_pda( program_id: &Pubkey, accounts: &[AccountInfo], bump: u8, ) -> ProgramResult { let pda_account = next_account_info(&mut accounts.iter())?; // Using user-provided bump without validation! let pda = Pubkey::create_program_address( &[b"config", &[bump]], program_id, )?; if pda_account.key != &pda { return Err(ProgramError::InvalidAccountData); } // Attacker could find non-canonical bump Ok(()) } ``` **Secure:** ```rust pub fn init_pda( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let pda_account = next_account_info(&mut accounts.iter())?; // Always use find_program_address to get canonical bump let (pda, bump) = Pubkey::find_program_address( &[b"config"], program_id, ); if pda_account.key != &pda { return Err(ProgramError::InvalidAccountData); } // Store the canonical bump for later use let mut data = ConfigPda::new(bump); data.serialize(&mut *pda_account.data.borrow_mut())?; Ok(()) } ``` ### Storing and Using Canonical Bumps **Best Practice:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct VaultPda { pub discriminator: u64, pub bump: u8, pub authority: Pubkey, pub balance: u64, } impl VaultPda { pub fn seeds<'a>(&'a self, authority: &'a Pubkey) -> [&'a [u8]; 3] { [b"vault", authority.as_ref(), &[self.bump]] } pub fn verify_pda( &self, pda_account: &AccountInfo, authority: &Pubkey, program_id: &Pubkey, ) -> ProgramResult { let expected_pda = Pubkey::create_program_address( &self.seeds(authority), program_id, )?; if pda_account.key != &expected_pda { return Err(ProgramError::InvalidSeeds); } Ok(()) } } ``` ### PDA Signing with invoke_signed **Secure Pattern:** ```rust use solana_program::program::invoke_signed; pub fn transfer_from_pda( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let vault_pda = next_account_info(account_info_iter)?; let destination = next_account_info(account_info_iter)?; let authority = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Load and validate PDA data let vault = VaultPda::from_account_info(vault_pda)?; vault.verify_pda(vault_pda, authority.key, program_id)?; // Sign with PDA's seeds let seeds = vault.seeds(authority.key); let signer_seeds = &[&seeds[..]]; let ix = solana_program::system_instruction::transfer( vault_pda.key, destination.key, amount, ); invoke_signed( &ix, &[vault_pda.clone(), destination.clone(), system_program.clone()], signer_seeds, )?; Ok(()) } ``` ### Preventing PDA Substitution **Vulnerable:** ```rust pub fn withdraw(accounts: &[AccountInfo]) -> ProgramResult { let vault = next_account_info(&mut accounts.iter())?; // No validation that this is the CORRECT vault PDA let vault_data = VaultPda::from_account_info(vault)?; // Attacker could substitute a different vault! Ok(()) } ``` **Secure:** ```rust pub fn withdraw( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let vault = next_account_info(account_info_iter)?; let authority = next_account_info(account_info_iter)?; // Derive expected PDA let (expected_vault, _bump) = Pubkey::find_program_address( &[b"vault", authority.key.as_ref()], program_id, ); // Validate this is the correct PDA if vault.key != &expected_vault { return Err(ProgramError::InvalidAccountData); } let vault_data = VaultPda::from_account_info(vault)?; // Safe to proceed Ok(()) } ``` --- ## Manual CPI Security ### Building AccountMeta Arrays Securely **Vulnerable:** ```rust pub fn dangerous_cpi(accounts: &[AccountInfo]) -> ProgramResult { let target_program = next_account_info(&mut accounts.iter())?; let account1 = next_account_info(&mut accounts.iter())?; // Missing validation - could be any program! let ix = Instruction { program_id: *target_program.key, accounts: vec![ AccountMeta::new(*account1.key, false), // Wrong signer flag! ], data: vec![], }; invoke(&ix, &[target_program.clone(), account1.clone()])?; Ok(()) } ``` **Secure:** ```rust use solana_program::program::invoke; pub const EXPECTED_PROGRAM_ID: Pubkey = solana_program::pubkey!("YourProgramID111111111111111111111111111111"); pub fn secure_cpi(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let target_program = next_account_info(account_info_iter)?; let account1 = next_account_info(account_info_iter)?; // Validate target program ID if target_program.key != &EXPECTED_PROGRAM_ID { return Err(ProgramError::IncorrectProgramId); } // Correctly propagate signer/writable flags let account_metas = vec![ AccountMeta { pubkey: *account1.key, is_signer: account1.is_signer, is_writable: account1.is_writable, }, ]; let ix = Instruction { program_id: *target_program.key, accounts: account_metas, data: vec![], }; invoke(&ix, &[target_program.clone(), account1.clone()])?; Ok(()) } ``` ### Checking CPI Success **Best Practice:** ```rust pub fn cpi_with_validation(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let token_program = next_account_info(account_info_iter)?; let source = next_account_info(account_info_iter)?; let destination = next_account_info(account_info_iter)?; let authority = next_account_info(account_info_iter)?; // Get balances before CPI let source_before = source.lamports(); let dest_before = destination.lamports(); let ix = spl_token::instruction::transfer( token_program.key, source.key, destination.key, authority.key, &[], 1000, )?; invoke(&ix, &[source.clone(), destination.clone(), authority.clone()])?; // Verify state changed as expected (for native SOL transfers) // Note: For SPL tokens, you'd need to deserialize token accounts Ok(()) } ``` --- ## Manual Serialization Security ### Borsh Serialization Pitfalls **Vulnerable:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct Config { pub value: u64, pub items: Vec<Item>, // Variable length! } pub fn deserialize_config(account: &AccountInfo) -> ProgramResult { // No size validation - could run out of compute! let config = Config::try_from_slice(&account.data.borrow())?; // Attacker could create huge Vec causing OOM for item in &config.items { // Process item } Ok(()) } ``` **Secure:** ```rust pub const MAX_ITEMS: usize = 100; #[derive(BorshSerialize, BorshDeserialize)] pub struct Config { pub value: u64, pub item_count: u32, pub items: Vec<Item>, } impl Config { pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> { let data = account.data.borrow(); // Validate minimum size if data.len() < 8 + 4 { return Err(ProgramError::InvalidAccountData); } let config = Self::try_from_slice(&data) .map_err(|_| ProgramError::InvalidAccountData)?; // Validate item count matches actual length if config.item_count as usize != config.items.len() { return Err(ProgramError::InvalidAccountData); } // Enforce maximum items if config.items.len() > MAX_ITEMS { return Err(ProgramError::InvalidAccountData); } Ok(config) } } ``` ### Account Data Layout Validation **Best Practice:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub discriminator: u64, pub owner: Pubkey, pub balance: u64, pub created_at: i64, } impl UserAccount { pub const LEN: usize = 8 + 32 + 8 + 8; pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> { let data = account.data.borrow(); // Exact size check prevents truncation attacks if data.len() != Self::LEN { return Err(ProgramError::InvalidAccountData); } Self::try_from_slice(&data) .map_err(|_| ProgramError::InvalidAccountData) } pub fn to_account_info(&self, account: &AccountInfo) -> ProgramResult { let mut data = account.data.borrow_mut(); if data.len() != Self::LEN { return Err(ProgramError::InvalidAccountData); } self.serialize(&mut *data) .map_err(|_| ProgramError::InvalidAccountData) } } ``` --- ## Rent and Space Management ### Rent Exemption Validation **Secure Pattern:** ```rust use solana_program::rent::Rent; use solana_program::sysvar::Sysvar; pub fn create_account( program_id: &Pubkey, accounts: &[AccountInfo], space: usize, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let new_account = next_account_info(account_info_iter)?; let payer = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Get rent sysvar let rent = Rent::get()?; // Calculate required lamports for rent exemption let required_lamports = rent.minimum_balance(space); // Validate account has enough lamports if new_account.lamports() < required_lamports { return Err(ProgramError::AccountNotRentExempt); } // Additional validation: account is rent exempt if !rent.is_exempt(new_account.lamports(), new_account.data_len()) { return Err(ProgramError::AccountNotRentExempt); } Ok(()) } ``` ### Account Size Calculation **Vulnerable:** ```rust pub fn init_account(space: usize) -> ProgramResult { // No validation - attacker could request huge space let ix = solana_program::system_instruction::create_account( &payer.key, &new_account.key, lamports, space as u64, // Could overflow! program_id, ); Ok(()) } ``` **Secure:** ```rust pub const MIN_ACCOUNT_SIZE: usize = 128; pub const MAX_ACCOUNT_SIZE: usize = 10_240; // 10KB pub fn init_account( program_id: &Pubkey, accounts: &[AccountInfo], requested_space: usize, ) -> ProgramResult { // Validate space within reasonable bounds if requested_space < MIN_ACCOUNT_SIZE || requested_space > MAX_ACCOUNT_SIZE { return Err(ProgramError::InvalidAccountData); } // Ensure space alignment let space = requested_space .checked_next_multiple_of(8) .ok_or(ProgramError::InvalidAccountData)?; let rent = Rent::get()?; let lamports = rent.minimum_balance(space); // Safe to create account Ok(()) } ``` --- ## Error Handling in Native Rust ### Custom Error Types **Best Practice:** ```rust use thiserror::Error; use solana_program::program_error::ProgramError; #[derive(Error, Debug, Copy, Clone)] pub enum MyProgramError { #[error("Invalid authority")] InvalidAuthority, #[error("Insufficient balance")] InsufficientBalance, #[error("Account already initialized")] AlreadyInitialized, #[error("Arithmetic overflow")] ArithmeticOverflow, } impl From<MyProgramError> for ProgramError { fn from(e: MyProgramError) -> Self { ProgramError::Custom(e as u32) } } pub fn process(accounts: &[AccountInfo]) -> Result<(), MyProgramError> { let authority = next_account_info(&mut accounts.iter()) .map_err(|_| MyProgramError::InvalidAuthority)?; if !authority.is_signer { return Err(MyProgramError::InvalidAuthority); } Ok(()) } ``` ### Avoiding unwrap() and expect() **Vulnerable:** ```rust pub fn process(accounts: &[AccountInfo]) -> ProgramResult { let account = accounts.get(0).unwrap(); // Panics if no accounts! let data = account.data.borrow(); let value = u64::from_le_bytes(data[0..8].try_into().unwrap()); // Panics if not 8 bytes! Ok(()) } ``` **Secure:** ```rust pub fn process(accounts: &[AccountInfo]) -> ProgramResult { let account = accounts .get(0) .ok_or(ProgramError::NotEnoughAccountKeys)?; let data = account.data.borrow(); if data.len() < 8 { return Err(ProgramError::InvalidAccountData); } let value = u64::from_le_bytes( data[0..8] .try_into() .map_err(|_| ProgramError::InvalidAccountData)? ); Ok(()) } ``` --- ## Token Program Integration ### Manual Token CPI Construction **Secure Pattern:** ```rust use spl_token::instruction as token_instruction; pub fn transfer_tokens( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let source_token = next_account_info(account_info_iter)?; let dest_token = next_account_info(account_info_iter)?; let authority = next_account_info(account_info_iter)?; let token_program = next_account_info(account_info_iter)?; // Validate token program if token_program.key != &spl_token::id() { return Err(ProgramError::IncorrectProgramId); } // Validate authority is signer if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } // Build transfer instruction let transfer_ix = token_instruction::transfer( token_program.key, source_token.key, dest_token.key, authority.key, &[], amount, )?; invoke( &transfer_ix, &[ source_token.clone(), dest_token.clone(), authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Token Account Validation **Secure Pattern:** ```rust use spl_token::state::Account as TokenAccount; pub fn validate_token_account( token_account_info: &AccountInfo, expected_owner: &Pubkey, expected_mint: &Pubkey, ) -> Result<TokenAccount, ProgramError> { // Verify owned by token program if token_account_info.owner != &spl_token::id() { return Err(ProgramError::IncorrectProgramId); } // Deserialize token account let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?; // Validate owner if &token_account.owner != expected_owner { return Err(ProgramError::InvalidAccountData); } // Validate mint if &token_account.mint != expected_mint { return Err(ProgramError::InvalidAccountData); } Ok(token_account) } ``` --- ## Low-Level Security Patterns ### Account Reloading After External Calls **Vulnerable:** ```rust pub fn vulnerable_pattern(accounts: &[AccountInfo]) -> ProgramResult { let account = next_account_info(&mut accounts.iter())?; let balance_before = account.lamports(); // External CPI call invoke(&some_instruction, &[account.clone()])?; // Account data not reloaded - still using stale reference! let balance_after = account.lamports(); Ok(()) } ``` **Secure:** ```rust pub fn secure_pattern(accounts: &[AccountInfo]) -> ProgramResult { let account = next_account_info(&mut accounts.iter())?; let balance_before = account.lamports(); // External CPI call invoke(&some_instruction, &[account.clone()])?; // AccountInfo automatically reflects changes - lamports(), data, etc. // are fresh after CPI let balance_after = account.lamports(); // But if you cached deserialized data, you must reload: let fresh_data = MyData::from_account_info(account)?; Ok(()) } ``` ### Clock and Timestamp Validation **Secure Pattern:** ```rust use solana_program::clock::Clock; use solana_program::sysvar::Sysvar; pub fn time_locked_operation( accounts: &[AccountInfo], unlock_timestamp: i64, ) -> ProgramResult { // Get clock sysvar let clock = Clock::get()?; // Validate unlock time has passed if clock.unix_timestamp < unlock_timestamp { return Err(ProgramError::InvalidArgument); } // Proceed with operation Ok(()) } ``` --- ## Native Rust Best Practices ### Account Iteration Patterns **Best Practice:** ```rust pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let authority = next_account_info(account_info_iter)?; let config = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Validate all expected accounts consumed if account_info_iter.next().is_some() { return Err(ProgramError::InvalidAccountData); } // Validate accounts AccountValidation::new(authority).signer()?; AccountValidation::new(config) .owner(program_id)? .writable()?; Ok(()) } ``` ### State Management Patterns **Best Practice:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct ProgramState { pub version: u8, pub is_initialized: bool, pub authority: Pubkey, // Add new fields at the end for upgradability pub feature_flags: u64, } impl ProgramState { pub const CURRENT_VERSION: u8 = 1; pub fn initialize(authority: Pubkey) -> Self { Self { version: Self::CURRENT_VERSION, is_initialized: true, authority, feature_flags: 0, } } pub fn validate(&self) -> ProgramResult { if !self.is_initialized { return Err(ProgramError::UninitializedAccount); } if self.version != Self::CURRENT_VERSION { return Err(ProgramError::InvalidAccountData); } Ok(()) } } ``` ### Security.txt Integration **Best Practice:** ```rust #[cfg(not(feature = "no-entrypoint"))] solana_security_txt::security_txt! { name: "My Solana Program", project_url: "https://github.com/myorg/myprogram", contacts: "email:security@myorg.com,discord:myorg", policy: "https://github.com/myorg/myprogram/blob/main/SECURITY.md", preferred_languages: "en", source_code: "https://github.com/myorg/myprogram", auditors: "Auditor1, Auditor2" } ``` --- ## Summary Native Rust Solana programs require meticulous manual validation of all security properties: 1. **Always validate**: signer, owner, writable, key equality 2. **Use discriminators** to prevent account type confusion 3. **Store canonical bumps** and validate PDA derivation 4. **Validate CPI targets** and propagate account flags correctly 5. **Validate sizes** before deserialization 6. **Check rent exemption** for all accounts 7. **Use Result types** - never unwrap or expect 8. **Validate token accounts** completely before use 9. **Reload account data** after external calls if cached 10. **Version your state** and validate initialization For each pattern, create reusable validation functions and leverage Rust's type system to enforce security invariants at compile time where possible. -
pda.md 20.5 KB
# Program Derived Addresses (PDAs) This reference provides comprehensive coverage of Program Derived Addresses (PDAs) for native Rust Solana program development, including derivation mechanics, security implications, and best practices. ## Table of Contents 1. [What are PDAs](#what-are-pdas) 2. [PDA Derivation Mechanics](#pda-derivation-mechanics) 3. [Canonical Bump Seeds](#canonical-bump-seeds) 4. [Creating PDA Accounts](#creating-pda-accounts) 5. [PDA Signing](#pda-signing) 6. [Common PDA Patterns](#common-pda-patterns) 7. [Security Considerations](#security-considerations) 8. [Best Practices](#best-practices) --- ## What are PDAs **Program Derived Addresses (PDAs) are deterministic account addresses derived from a program ID and optional seeds.** ### Key Characteristics 1. **Deterministic**: Same inputs always produce the same PDA 2. **No private key**: PDAs are intentionally off the Ed25519 curve 3. **Program-signable**: The deriving program can sign for PDAs 4. **Hashmap-like**: Enable key-value storage patterns on-chain ### Why PDAs Exist PDAs solve critical problems in Solana program development: **Problem 1: State Storage** - How do you store program state without tracking account addresses? - Solution: Derive addresses from user pubkeys + seeds **Problem 2: Program Signing** - How can a program sign transactions without a private key? - Solution: Runtime enables programs to sign for their PDAs **Problem 3: Account Discovery** - How do clients find accounts created by programs? - Solution: Derive PDAs client-side using known seeds ### PDA vs Regular Account | Property | Regular Account | PDA | |----------|----------------|-----| | Address derivation | Random (from keypair) | Deterministic (from seeds) | | Has private key | ✅ Yes | ❌ No (off-curve) | | Can sign transactions | ✅ Yes (with private key) | ✅ Yes (via program) | | Who can sign | Holder of private key | Only the deriving program | | Use case | User wallets | Program state storage | --- ## PDA Derivation Mechanics ### How PDAs are Derived PDAs are created using a hash function that combines: 1. Program ID 2. Optional seeds (strings, numbers, pubkeys) 3. Bump seed (0-255) The process intentionally finds an address that falls **off** the Ed25519 elliptic curve. ``` ┌──────────────────────────────────────────────┐ │ Input Seeds │ ├──────────────────────────────────────────────┤ │ - Program ID │ │ - Optional Seed 1 (e.g., "user_data") │ │ - Optional Seed 2 (e.g., user pubkey) │ │ - Bump seed (starts at 255) │ └──────────────────────────────────────────────┘ │ ▼ ┌──────────────────────┐ │ Hash Function │ │ (SHA256 + checks) │ └──────────────────────┘ │ ▼ ┌──────────────────────┐ │ Is address off-curve?│ └──────────────────────┘ │ │ │ No │ Yes ▼ ▼ Decrement bump Return (PDA, bump) ``` ### Native Rust API ```rust use solana_program::pubkey::Pubkey; // Find PDA with canonical bump let (pda, bump_seed) = Pubkey::find_program_address( &[ b"user_data", // Seed 1: static string user_pubkey.as_ref(), // Seed 2: user's public key ], program_id, ); // pda: The derived address (off-curve) // bump_seed: The canonical bump (first valid bump found, starting from 255) ``` ### Manual PDA Creation (Advanced) You can manually create a PDA with a specific bump using `create_program_address`: ```rust use solana_program::pubkey::Pubkey; // This may fail if the bump doesn't produce a valid off-curve address let pda = Pubkey::create_program_address( &[ b"user_data", user_pubkey.as_ref(), &[bump_seed], // Specific bump ], program_id, )?; ``` **⚠️ Warning:** Only use `create_program_address` when you're certain the bump is valid. Prefer `find_program_address` for safety. --- ## Canonical Bump Seeds ### What is a Canonical Bump? The **canonical bump** is the first bump seed (starting from 255, decrementing) that produces a valid off-curve address. ```rust // Example: Finding all valid bumps for bump in (0..=255).rev() { if let Ok(pda) = Pubkey::create_program_address( &[b"data", user.as_ref(), &[bump]], program_id, ) { println!("Bump {}: {}", bump, pda); } } // Typical output: // Bump 255: Error (on-curve) // Bump 254: AValidPDAAddress... ← CANONICAL BUMP // Bump 253: AnotherValidPDA... // Bump 252: AThirdValidPDA... // ... ``` ### Why Use the Canonical Bump? **Security Reason:** Multiple bumps can derive different valid PDAs for the same seeds. Accepting arbitrary bumps enables PDA substitution attacks. **Attack Scenario:** ```rust // ❌ Vulnerable - accepts any bump pub fn update_user_balance( program_id: &Pubkey, accounts: &[AccountInfo], bump: u8, // User provides bump ) -> ProgramResult { let user = &accounts[0]; let user_pda = &accounts[1]; // Creates PDA with user-provided bump let expected_pda = Pubkey::create_program_address( &[b"balance", user.key.as_ref(), &[bump]], program_id, )?; // Attacker can provide bump 253 instead of canonical 254 // This derives a DIFFERENT PDA the attacker controls! // ... } ``` **Secure Pattern:** ```rust // ✅ Secure - uses canonical bump only pub fn update_user_balance( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let user = &accounts[0]; let user_pda = &accounts[1]; // Derive with canonical bump let (expected_pda, _bump) = Pubkey::find_program_address( &[b"balance", user.key.as_ref()], program_id, ); // Validate if expected_pda != *user_pda.key { return Err(ProgramError::InvalidSeeds); } // Safe to proceed // ... } ``` ### Storing the Canonical Bump **Best Practice:** Store the canonical bump in the account data: ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct UserAccount { pub bump: u8, // Store canonical bump pub user: Pubkey, pub balance: u64, } // On creation let (pda, bump) = Pubkey::find_program_address(&[b"user", user.key.as_ref()], program_id); let account_data = UserAccount { bump, // Save for future operations user: *user.key, balance: 0, }; ``` **Why store it?** - Saves compute units on subsequent operations - `find_program_address` iterates from 255, costs ~3,000 CU - Using stored bump with `create_program_address` costs ~300 CU (10x cheaper!) --- ## Creating PDA Accounts ### Creation Process PDAs cannot create themselves. Accounts at PDA addresses must be created by: 1. Invoking the System Program via CPI 2. Using `invoke_signed` to sign with the PDA 3. The System Program creates the account and transfers ownership ### Native Rust Pattern ```rust use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, program::{invoke_signed}, program_error::ProgramError, pubkey::Pubkey, rent::Rent, system_instruction, sysvar::Sysvar, }; pub fn create_user_account( program_id: &Pubkey, accounts: &[AccountInfo], user_id: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let payer = next_account_info(account_info_iter)?; // Pays for account let user_pda = next_account_info(account_info_iter)?; // PDA to create let system_program = next_account_info(account_info_iter)?; // System Program // Signer check if !payer.is_signer { return Err(ProgramError::MissingRequiredSignature); } // Derive PDA let user_id_bytes = user_id.to_le_bytes(); let (pda, bump_seed) = Pubkey::find_program_address( &[b"user", payer.key.as_ref(), user_id_bytes.as_ref()], program_id, ); // Validate provided PDA matches derivation if pda != *user_pda.key { return Err(ProgramError::InvalidSeeds); } // Calculate space and rent let account_size: usize = 1 + 32 + 8; // bump + pubkey + u64 let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(account_size); // Create account via CPI let create_account_ix = system_instruction::create_account( payer.key, // Payer user_pda.key, // New account address (the PDA) rent_lamports, // Lamports account_size as u64, // Space program_id, // Owner (our program) ); // Sign with PDA using bump seed let signer_seeds: &[&[&[u8]]] = &[&[ b"user", payer.key.as_ref(), user_id_bytes.as_ref(), &[bump_seed], // Critical: Include bump in signer seeds ]]; invoke_signed( &create_account_ix, &[payer.clone(), user_pda.clone(), system_program.clone()], signer_seeds, // PDA signs here )?; // Initialize account data let mut account_data = UserAccount::try_from_slice(&user_pda.data.borrow())?; account_data.bump = bump_seed; account_data.owner = *payer.key; account_data.user_id = user_id; account_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?; Ok(()) } #[derive(BorshSerialize, BorshDeserialize)] struct UserAccount { bump: u8, owner: Pubkey, user_id: u64, } ``` ### Key Points 1. **Signer Seeds Format**: `&[&[&[u8]]]` (3 levels of slicing) - Outer: Array of seed sets (for multiple PDAs) - Middle: Single seed set (one PDA) - Inner: Individual seed slices 2. **Bump Must Be Included**: Always append `&[bump_seed]` to signer seeds 3. **System Program Required**: Must pass System Program account for CPI 4. **Ownership Transfer**: Account starts owned by System Program, transfers to your program --- ## PDA Signing ### How Programs Sign for PDAs When a program makes a CPI with `invoke_signed`, the runtime: 1. Receives the signer seeds 2. Derives the PDA using seeds + calling program's ID 3. Verifies the derived PDA matches an account in the instruction 4. Grants signing authority to that PDA ### invoke_signed vs invoke ```rust // invoke: No PDA signing pub fn invoke( instruction: &Instruction, account_infos: &[AccountInfo], ) -> ProgramResult // invoke_signed: With PDA signing pub fn invoke_signed( instruction: &Instruction, account_infos: &[AccountInfo], signers_seeds: &[&[&[u8]]], // PDA seeds ) -> ProgramResult ``` ### Practical Example: PDA Transfers SOL ```rust pub fn pda_transfer_sol( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let pda_account = next_account_info(account_info_iter)?; let recipient = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; // Derive PDA and verify let (pda, bump_seed) = Pubkey::find_program_address( &[b"vault", recipient.key.as_ref()], program_id, ); if pda != *pda_account.key { return Err(ProgramError::InvalidSeeds); } // Create transfer instruction let transfer_ix = system_instruction::transfer( pda_account.key, // From: PDA (needs signing!) recipient.key, // To: recipient amount, ); // PDA signs the transfer let signer_seeds: &[&[&[u8]]] = &[&[ b"vault", recipient.key.as_ref(), &[bump_seed], ]]; invoke_signed( &transfer_ix, &[pda_account.clone(), recipient.clone(), system_program.clone()], signer_seeds, // Runtime verifies and grants signing authority )?; Ok(()) } ``` ### Multiple PDA Signers You can sign with multiple PDAs in a single CPI: ```rust let signer_seeds: &[&[&[u8]]] = &[ &[b"pda1", &[bump1]], // First PDA &[b"pda2", &[bump2]], // Second PDA ]; invoke_signed(&instruction, &accounts, signer_seeds)?; ``` --- ## Common PDA Patterns ### 1. User-Specific Accounts **Pattern:** One PDA per user for storing user data. ```rust // Seeds: ["user_data", user_pubkey] let (user_pda, bump) = Pubkey::find_program_address( &[b"user_data", user.key.as_ref()], program_id, ); ``` **Use case:** User profiles, balances, inventory **Advantages:** - Easy client-side discovery - One account per user - User's pubkey acts as unique identifier ### 2. Global State **Pattern:** Single PDA for program-wide state. ```rust // Seeds: ["global_state"] let (global_pda, bump) = Pubkey::find_program_address( &[b"global_state"], program_id, ); ``` **Use case:** Program configuration, global counters, admin settings **Advantages:** - Single source of truth - Easy to find (no variable seeds) - Reduced account proliferation ### 3. Association Pattern **Pattern:** PDA associates two entities. ```rust // Seeds: ["escrow", seller_pubkey, buyer_pubkey] let (escrow_pda, bump) = Pubkey::find_program_address( &[b"escrow", seller.key.as_ref(), buyer.key.as_ref()], program_id, ); ``` **Use case:** Escrow accounts, peer-to-peer trades, relationships **Advantages:** - Unique per relationship - Deterministic discovery - Prevents duplicate associations ### 4. Index/Counter Pattern **Pattern:** PDA with numeric index for multiple instances. ```rust // Seeds: ["note", author_pubkey, note_id] let note_id: u64 = 42; let (note_pda, bump) = Pubkey::find_program_address( &[b"note", author.key.as_ref(), note_id.to_le_bytes().as_ref()], program_id, ); ``` **Use case:** Notes, posts, items, sequential data **Advantages:** - Multiple accounts per user - Enumerable (iterate by incrementing ID) - Scalable **Implementation:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct UserState { pub note_count: u64, // Track next available ID } pub fn create_note( program_id: &Pubkey, accounts: &[AccountInfo], content: String, ) -> ProgramResult { let user = &accounts[0]; let user_state_pda = &accounts[1]; let note_pda = &accounts[2]; // Load user state let mut user_state = UserState::try_from_slice(&user_state_pda.data.borrow())?; // Derive PDA for new note let note_id = user_state.note_count; let (expected_note_pda, bump) = Pubkey::find_program_address( &[b"note", user.key.as_ref(), note_id.to_le_bytes().as_ref()], program_id, ); if expected_note_pda != *note_pda.key { return Err(ProgramError::InvalidSeeds); } // Create note account... // Initialize note data... // Increment counter user_state.note_count += 1; user_state.serialize(&mut &mut user_state_pda.data.borrow_mut()[..])?; Ok(()) } ``` ### 5. Vault/Treasury Pattern **Pattern:** PDA holds funds for the program. ```rust // Seeds: ["vault"] let (vault_pda, bump) = Pubkey::find_program_address( &[b"vault"], program_id, ); ``` **Use case:** Staking pools, treasuries, escrow **Advantages:** - Program controls funds - No external keypair needed - Can't lose "private key" --- ## Security Considerations ### 1. Always Validate PDAs **❌ Vulnerable:** ```rust pub fn update_balance( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let user_pda = &accounts[0]; // No PDA validation! let mut user_data = UserData::try_from_slice(&user_pda.data.borrow())?; user_data.balance += 100; user_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?; Ok(()) } ``` **✅ Secure:** ```rust pub fn update_balance( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let user = &accounts[0]; let user_pda = &accounts[1]; // Derive and validate let (expected_pda, _) = Pubkey::find_program_address( &[b"user", user.key.as_ref()], program_id, ); if expected_pda != *user_pda.key { return Err(ProgramError::InvalidSeeds); } // Safe to proceed let mut user_data = UserData::try_from_slice(&user_pda.data.borrow())?; user_data.balance += 100; user_data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?; Ok(()) } ``` ### 2. Non-Canonical Bump Attack **Vulnerability:** Accepting user-provided bumps allows PDA substitution. **Impact:** Attacker can manipulate which account is used. **Prevention:** - Always use `find_program_address` (canonical bump) - Never accept bump as instruction parameter - Store bump in account data after creation ### 3. Seed Confusion **Vulnerability:** Ambiguous seed ordering can create collisions. ```rust // ❌ Problematic - seeds can collide let seed1 = "hello"; let seed2 = "world"; // These derive the SAME PDA: Pubkey::find_program_address(&[b"helloworld"], program_id); Pubkey::find_program_address(&[b"hello", b"world"], program_id); ``` **Prevention:** ```rust // ✅ Use fixed-size types and clear separators Pubkey::find_program_address( &[ b"prefix_", // Fixed prefix user.key.as_ref(), // 32 bytes (fixed) &id.to_le_bytes(), // 8 bytes (fixed) ], program_id, ); ``` ### 4. Ownership Verification **Always verify PDA ownership:** ```rust // ✅ Check ownership after PDA validation if user_pda.owner != program_id { return Err(ProgramError::IllegalOwner); } ``` --- ## Best Practices ### 1. Seed Design **Good Seed Patterns:** - Use descriptive prefixes: `b"user_profile"`, `b"escrow"`, `b"vault"` - Include entity identifiers: user pubkeys, IDs - Use fixed-size types: `u64.to_le_bytes()`, `Pubkey::as_ref()` - Maintain logical ordering: most general → most specific **Example:** ```rust &[ b"note", // What type of account author.key.as_ref(), // Who owns it note_id.to_le_bytes(), // Which instance ] ``` ### 2. Always Store the Bump ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct PdaAccount { pub bump: u8, // Always first field for efficiency // ... other fields } ``` **Benefits:** - Saves ~2,700 CU per operation - Enables efficient re-derivation - Documents canonical bump ### 3. Validate Everything **Security Checklist:** - ✅ Derive PDA with canonical bump - ✅ Compare derived PDA to provided account - ✅ Verify PDA owner is your program - ✅ Check initialization status - ✅ Validate signer requirements ### 4. Document Your Seed Schema ```rust /// Derives a user profile PDA. /// /// Seeds: ["user_profile", user_pubkey] /// Bump: Stored in account.bump pub fn derive_user_profile_pda( user: &Pubkey, program_id: &Pubkey, ) -> (Pubkey, u8) { Pubkey::find_program_address( &[b"user_profile", user.as_ref()], program_id, ) } ``` ### 5. Use Helper Functions ```rust pub struct PdaDerivation; impl PdaDerivation { pub fn user_profile(user: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[b"user", user.as_ref()], program_id) } pub fn note( author: &Pubkey, note_id: u64, program_id: &Pubkey, ) -> (Pubkey, u8) { Pubkey::find_program_address( &[b"note", author.as_ref(), note_id.to_le_bytes().as_ref()], program_id, ) } } // Usage let (user_pda, bump) = PdaDerivation::user_profile(user.key, program_id); ``` --- ## Summary **Key Takeaways:** 1. **PDAs are deterministic addresses** derived from program ID + seeds 2. **No private key exists** for PDAs (they're off-curve by design) 3. **Only the deriving program can sign** for its PDAs 4. **Always use canonical bump** to prevent substitution attacks 5. **Validate PDAs before use** - never trust client-provided accounts 6. **Store the bump** in account data for compute efficiency 7. **Design clear seed schemas** to prevent collisions and confusion **Security Mantra:** ```rust // Always follow this pattern let (expected_pda, bump) = Pubkey::find_program_address(&seeds, program_id); if expected_pda != *provided_pda.key { return Err(ProgramError::InvalidSeeds); } if provided_pda.owner != program_id { return Err(ProgramError::IllegalOwner); } ``` PDAs are the foundation of state management in Solana programs. Master them, validate them religiously, and your programs will be secure and efficient. -
production-deployment.md 13.1 KB
# Production Deployment Guide for Solana Programs **Best practices for deploying verified, production-ready Solana programs to mainnet and serious devnet environments.** --- ## Overview Production deployments require verified builds that prove deployed bytecode matches public source code. This guide covers the proper workflow for production deployments, particularly with Anchor framework. **Key principle:** Transparency and verifiability build trust. Always use deterministic builds for production. --- ## Why Verified Builds Matter **Without verified builds:** - Users cannot verify deployed code matches GitHub source - Audits cannot confirm they reviewed the exact deployed binary - No transparency into what code actually runs on-chain - Security researchers cannot validate the program **With verified builds:** - ✅ Provably deterministic builds (Docker-based) - ✅ Anyone can verify deployed bytecode matches source - ✅ Explorer verification badges (Solana Explorer, SolanaFM) - ✅ Audit reports apply to exact deployed binary - ✅ Standard for all serious Solana projects **All major Solana protocols use verified builds:** Jupiter, Marinade, Orca, Metaplex, etc. --- ## The Problem with `anchor deploy` ### Anchor 0.32.1 and Earlier **⚠️ CRITICAL: Do NOT use `anchor deploy` for production deployments** **Why `anchor deploy` is unsuitable for production:** 1. **Non-deterministic builds** - Build output varies by local Rust version - Different on macOS vs Linux - Depends on installed toolchain - Same source → different binaries on different machines 2. **Cannot be verified** - No way to prove deployed code matches GitHub - Verification tools cannot reproduce the build - Breaks audit trail 3. **Lacks transparency** - Users must trust deployer - No verification badges on explorers - Goes against Solana ecosystem standards **When Anchor v1 may improve this:** - Anchor v1 is expected to have better support for verified builds - May integrate `solana-verify` directly - Check Anchor docs for updates when v1 releases **For now (Anchor 0.32.1):** Use the verified deployment workflow below. --- ## Production Deployment Workflow ### Step 1: Build Verifiably Use `solana-verify build` instead of `anchor build` for the final production build: ```bash # Install solana-verify if not already installed cargo install solana-verify # Navigate to project root (where Cargo.toml with workspace is) cd my-project # Build verifiably in Docker (deterministic) solana-verify build --library-name my_program # Verify the build succeeded ls -la target/deploy/my_program.so ``` **What this does:** - Builds in Docker container (consistent environment) - Uses exact dependencies from `Cargo.lock` - Same input → same output (deterministic) - Anyone can reproduce this exact binary **Important:** Do NOT run `anchor build` after `solana-verify build` - it will regenerate a different binary! ### Step 2: Deploy the Verified Binary Use `solana program deploy` directly (NOT `anchor deploy`): **For devnet:** ```bash solana program deploy target/deploy/my_program.so \ --program-id target/deploy/my_program-keypair.json \ -u devnet \ --with-compute-unit-price 1000 ``` **For mainnet:** ```bash # Use your deployer keypair and appropriate priority fees solana program deploy target/deploy/my_program.so \ --program-id target/deploy/my_program-keypair.json \ --keypair ~/.config/solana/deployer.json \ -u mainnet-beta \ --with-compute-unit-price 100000 \ --max-sign-attempts 100 \ --use-rpc ``` **Why use `solana program deploy` directly:** - Works with verified builds - More control over deployment parameters - Standard across all Solana programs - Same tool for Anchor and native Rust ### Step 3: Verify Against Repository After deployment, verify the on-chain program matches your source: ```bash solana-verify verify-from-repo \ -u devnet \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo \ --library-name my_program # Or specify exact commit solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo \ --commit-hash <COMMIT_HASH> \ --library-name my_program ``` **When prompted, upload verification data on-chain:** ``` Would you like to upload verification data on-chain? (y/n) ``` Select **yes** to enable: - Verification badge on Solana Explorer - OtterSec verification API listing - SolanaFM verification display ### Step 4: Verify Hash Match (Sanity Check) Before step 3, you can manually verify hashes match: ```bash # Get on-chain program hash solana-verify get-program-hash -u devnet <PROGRAM_ID> # Get local executable hash solana-verify get-executable-hash target/deploy/my_program.so # These MUST match exactly ``` --- ## Complete Production Deployment Checklist ### Pre-Deployment - [ ] All tests pass (`cargo test`, `anchor test`) - [ ] Security audit completed (for mainnet) - [ ] `Cargo.lock` committed to git - [ ] Git tag created for release (e.g., `v1.0.0`) - [ ] Sufficient SOL in deployer wallet - [ ] Multisig or governance ready (mainnet) ### Build - [ ] Run `solana-verify build --library-name my_program` - [ ] Verify `.so` file exists in `target/deploy/` - [ ] Do NOT run `anchor build` after this - [ ] Get hash: `solana-verify get-executable-hash target/deploy/my_program.so` ### Deploy - [ ] Use `solana program deploy` (NOT `anchor deploy`) - [ ] Specify correct program ID keypair - [ ] Use appropriate priority fees - [ ] Verify deployment: `solana program show <PROGRAM_ID>` ### Verify - [ ] Run `solana-verify verify-from-repo` with your GitHub URL - [ ] Upload verification data on-chain when prompted - [ ] Check verification appears on explorer - [ ] Optional: Submit remote verification job ### Post-Deployment - [ ] Transfer upgrade authority to multisig (mainnet) - [ ] Smoke test critical instructions on-chain - [ ] Set up monitoring - [ ] Announce deployment with verification link --- ## Example: Complete Mainnet Deployment ```bash # 1. Prepare git tag v1.0.0 git push origin v1.0.0 # 2. Build verifiably solana-verify build --library-name cascade_splits # 3. Check hash solana-verify get-executable-hash target/deploy/cascade_splits.so # Output: abc123def456... # 4. Deploy to mainnet solana program deploy target/deploy/cascade_splits.so \ --program-id target/deploy/cascade_splits-keypair.json \ --keypair ~/.config/solana/mainnet-deployer.json \ -u mainnet-beta \ --with-compute-unit-price 100000 \ --max-sign-attempts 100 \ --use-rpc # Output: Program Id: SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB # 5. Verify on-chain hash matches solana-verify get-program-hash -u mainnet-beta SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB # Output: abc123def456... (must match step 3!) # 6. Verify against repository solana-verify verify-from-repo \ -u mainnet-beta \ --program-id SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB \ https://github.com/cascade-protocol/splits \ --commit-hash v1.0.0 \ --library-name cascade_splits # When prompted: Upload verification data on-chain? → YES # 7. Transfer authority to multisig SQUADS_VAULT="YourSquadsVaultAddress" solana program set-upgrade-authority SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB \ --new-upgrade-authority $SQUADS_VAULT # 8. Verify on explorer # Visit: https://explorer.solana.com/address/SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB # Should show verification badge # 9. Check OtterSec verification # Visit: https://verify.osec.io/status/SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB ``` --- ## Program Upgrades with Verified Builds ### Upgrade Workflow ```bash # 1. Make changes, test, commit git add . git commit -m "feat: add new feature" git tag v1.1.0 git push origin main v1.1.0 # 2. Build verifiably solana-verify build --library-name my_program # 3. Check if program size increased OLD_SIZE=$(solana program show <PROGRAM_ID> | grep "Data Length" | awk '{print $3}') NEW_SIZE=$(wc -c < target/deploy/my_program.so) # 4. Extend if needed if [ $NEW_SIZE -gt $OLD_SIZE ]; then DIFF=$((NEW_SIZE - OLD_SIZE)) solana program extend <PROGRAM_ID> $DIFF fi # 5. Deploy upgrade solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> \ --upgrade-authority ~/.config/solana/deployer.json \ -u mainnet-beta \ --with-compute-unit-price 100000 # 6. Verify new version solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo \ --commit-hash v1.1.0 \ --library-name my_program ``` ### Upgrades via Multisig If upgrade authority is a Squads multisig: ```bash # 1. Build verifiably solana-verify build --library-name my_program # 2. Create buffer (not direct upgrade) solana program write-buffer target/deploy/my_program.so # Output: Buffer: <BUFFER_ADDRESS> # 3. Transfer buffer to multisig solana program set-buffer-authority <BUFFER_ADDRESS> \ --new-buffer-authority <SQUADS_VAULT> # 4. Create upgrade proposal in Squads UI # - Navigate to https://v4.squads.so/ # - Create transaction for BPF Upgradeable Loader upgrade # - Reference buffer address # - Get approval from multisig members # - Execute # 5. After execution, verify solana-verify verify-from-repo \ -u mainnet-beta \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo \ --commit-hash v1.1.0 \ --library-name my_program ``` --- ## Troubleshooting ### Hash Mismatch After Deployment **Problem:** On-chain hash doesn't match local hash **Causes:** 1. Ran `anchor build` or `cargo build-sbf` after `solana-verify build` 2. Deployed wrong file 3. `Cargo.lock` not committed or out of sync **Solution:** ```bash # 1. Clean everything cargo clean # 2. Ensure Cargo.lock is committed git add Cargo.lock git commit -m "Add Cargo.lock" # 3. Rebuild verifiably solana-verify build --library-name my_program # 4. Redeploy solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> # 5. Verify again solana-verify verify-from-repo ... ``` ### Verification Fails: "Could not build from repository" **Problem:** `solana-verify verify-from-repo` cannot build **Causes:** 1. Missing `Cargo.lock` in repository 2. Wrong commit hash 3. Workspace configuration issue 4. Missing dependencies in Docker build **Solution:** ```bash # 1. Verify Cargo.lock exists in git git ls-files | grep Cargo.lock # 2. Check commit hash is correct git log --oneline # 3. Ensure workspace Cargo.toml exists at root cat Cargo.toml # Should have [workspace] # 4. Try local verification first solana-verify verify-from-repo \ --program-id <PROGRAM_ID> \ file://$(pwd) \ --library-name my_program ``` ### "anchor deploy" Used by Accident **Problem:** Deployed with `anchor deploy` instead of verified build **Solution:** Redeploy properly: ```bash # 1. Build verifiably solana-verify build --library-name my_program # 2. Redeploy (upgrade) with verified binary solana program deploy target/deploy/my_program.so \ --program-id <PROGRAM_ID> # 3. Verify solana-verify verify-from-repo \ -u <NETWORK> \ --program-id <PROGRAM_ID> \ https://github.com/your-org/your-repo ``` --- ## Version-Specific Notes ### Anchor 0.32.1 - **Status:** Current stable version as of November 2024 - **Issue:** `anchor deploy` does not produce verifiable builds - **Workaround:** Use workflow in this guide (solana-verify + solana program deploy) - **Uses:** Solana SDK 2.2.x ### Anchor 0.30.x - **Status:** Older stable version - **Issue:** Same as 0.32.1 - **Workaround:** Same workflow applies - **Uses:** Solana SDK 2.1.x ### Future: Anchor 1.0.0 - **Expected:** Better integration with verified builds - **Possible:** `anchor deploy --verifiable` flag - **Check:** Official Anchor docs when v1 releases - **Until then:** Use this guide --- ## Best Practices Summary ### Always ✅ - Use `solana-verify build` for production builds - Commit `Cargo.lock` to git - Tag releases with git tags - Deploy with `solana program deploy` directly - Verify against repository after deployment - Upload verification data on-chain - Transfer mainnet authority to multisig - Test entire flow on devnet first ### Never ❌ - Use `anchor deploy` for production/mainnet - Run `anchor build` or `cargo build-sbf` after `solana-verify build` - Deploy without verifying - Deploy mainnet without devnet testing first - Deploy mainnet without security audit - Keep upgrade authority as individual wallet (mainnet) - Skip uploading verification data ### Development Only `anchor deploy` is fine for: - Local validator testing - Rapid iteration during development - Devnet experiments - Non-production testing --- ## Additional Resources - **Solana Verify CLI**: https://github.com/Ellipsis-Labs/solana-verifiable-build - **Verified Programs List**: https://verify.osec.io/verified-programs - **Solana Explorer**: https://explorer.solana.com - **Squads Protocol**: https://squads.so - **Anchor Documentation**: https://www.anchor-lang.com/docs --- ## Summary **For production Solana program deployments:** 1. Use `solana-verify build` (NOT `anchor deploy`) 2. Deploy with `solana program deploy` directly 3. Verify with `solana-verify verify-from-repo` 4. Upload verification data on-chain This ensures transparency, verifiability, and trust in your deployed programs. -
resources.md 15.3 KB
# Resources Comprehensive collection of official documentation, development tools, security guides, audit reports, learning paths, and community resources for Solana program 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 - [Anchor GitHub](https://github.com/coral-xyz/anchor) - Framework source code ### 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) ## Example Programs ### Official Examples - [Program Examples](https://github.com/solana-developers/program-examples) - Comprehensive examples in multiple frameworks - [Anchor Examples](https://github.com/coral-xyz/anchor/tree/master/tests) - Official Anchor test programs - [Developer Bootcamp](https://github.com/solana-developers/developer-bootcamp-2024) - Workshop materials ### Production Protocols (for studying) - [Anchor Framework](https://github.com/coral-xyz/anchor) - The framework source itself - [Raydium AMM](https://github.com/raydium-io/raydium-cp-swap) - DEX protocol example - [Kamino Lending](https://github.com/Kamino-Finance/klend) - Lending protocol - [Squads Multisig](https://github.com/Squads-Protocol/v4) - Multisig protocol ## Development Tools ### IDEs & Playgrounds - [Solana Playground](https://beta.solpg.io/) - Browser-based IDE for Solana programs - [Anchor Playground](https://www.anchor-lang.com/playground) - Test Anchor programs online - [Rust Playground](https://play.rust-lang.org/) - Test Rust snippets ### CLI & Tooling - [Solana CLI](https://docs.solana.com/cli) - Command-line tools reference - [Anchor CLI](https://www.anchor-lang.com/docs/cli) - Anchor command reference - [Solana Explorer](https://explorer.solana.com/) - View transactions and accounts - [Solana FM](https://solana.fm/) - Alternative explorer with better UX - [Solscan](https://solscan.io/) - Popular block explorer - [XRAY](https://xray.helius.dev/) - Transaction viewer by Helius ### Testing Frameworks - [Mollusk](https://github.com/anza-xyz/mollusk) - Lightweight test harness for SVM programs - [Mollusk Docs](https://solana.com/docs/programs/testing/mollusk) - Official Mollusk documentation - [Solana Test Validator](https://docs.solana.com/developing/test-validator) - Local validator for testing - [Anchor Testing](https://book.anchor-lang.com/anchor_in_depth/testing.html) - Anchor test framework ### Deployment & Verification - [Solana Verify](https://github.com/Ellipsis-Labs/solana-verifiable-build) - Verifiable builds - [Verified Builds Docs](https://solana.com/docs/programs/verified-builds) - Official guide ## Learning Paths ### Official Courses - [Native Rust Development](https://solana.com/developers/courses/native-onchain-development) - Build with native Rust - [Anchor Development](https://solana.com/developers/courses/onchain-development) - Build with Anchor - [Program Security](https://solana.com/developers/courses/program-security) - Security fundamentals ### Community Tutorials - [RareSkills Solana Course](https://www.rareskills.io/solana-tutorial) - Comprehensive course for EVM developers - [Anchor for EVM Developers](https://0xkowloon.gitbook.io/anchor-for-evm-developers) - Quick Anchor intro - [Ackee Solana Handbook](https://ackee.xyz/solana/book/latest/) - Development guide ### Rust Learning - [Rust Book](https://doc.rust-lang.org/book/) - Official Rust programming language book - [Rust by Example](https://doc.rust-lang.org/rust-by-example/) - Learn Rust through examples ### Advanced Topics - [Solana Architecture](https://docs.solana.com/cluster/overview) - How Solana works - [Sealevel Runtime](https://docs.solana.com/developing/programming-model/overview) - SVM execution model - [Account Model](https://solana.com/docs/core/accounts) - Deep dive into accounts ## Community & Support ### Q&A Platforms - [Solana Stack Exchange](https://solana.stackexchange.com/) - Q&A for Solana development - [Anchor Discussions](https://github.com/coral-xyz/anchor/discussions) - GitHub discussions ### Chat & Forums - [Solana Discord](https://discord.gg/solana) - Official Solana community - [Anchor Discord](https://discord.gg/srmqvxf) - Anchor-specific support - [Solana Tech Discord](https://discord.gg/solana) - Technical discussions ### Blogs & Newsletters - [Helius Blog](https://www.helius.dev/blog) - Frequent Solana developer content - [Solana Foundation Blog](https://solana.com/news) - Official updates - [Pine Analytics Substack](https://substack.com/@pineanalytics1) - Protocol deep dives ## Developer Tools & Libraries ### Rust Crates - [solana-program](https://docs.rs/solana-program) - Core program library - [anchor-lang](https://docs.rs/anchor-lang) - Anchor framework - [anchor-spl](https://docs.rs/anchor-spl) - SPL token integration - [borsh](https://docs.rs/borsh) - Binary serialization - [spl-token](https://docs.rs/spl-token) - Token program library - [spl-token-2022](https://docs.rs/spl-token-2022) - Token Extensions program ### TypeScript/JavaScript **dApp Client (recommended for frontends):** - [@solana/client](https://www.npmjs.com/package/@solana/client) + [@solana/react-hooks](https://www.npmjs.com/package/@solana/react-hooks) - Framework-kit: wallet connection, React hooks, SOL/SPL transfers, transaction pool ([GitHub](https://github.com/solana-foundation/framework-kit)) - [@solana/web3-compat](https://www.npmjs.com/package/@solana/web3-compat) - Drop-in bridge for migrating `@solana/web3.js` v1 code to Kit primitives **Low-level SDK:** - [@solana/kit](https://solanakit.org) - Solana JavaScript SDK v6 (functional, tree-shakeable). Use for server-side scripts, bots, CLIs - [@solana/compat](https://www.npmjs.com/package/@solana/compat) - Bridge between Kit 6.x and legacy `@solana/web3.js` v1 types **Framework-specific:** - [@coral-xyz/anchor](https://www.npmjs.com/package/@coral-xyz/anchor) - Anchor TypeScript client (requires `@solana/web3.js` v1 as peer dependency) - [@solana/spl-token](https://www.npmjs.com/package/@solana/spl-token) - SPL Token JS library - [Umi Framework](https://github.com/metaplex-foundation/umi) - Modular framework by Metaplex > **Note:** `@solana/web3.js` v1 is in maintenance mode. For new dApps, use framework-kit (`@solana/client` + `@solana/react-hooks`). For server-side/CLI code, use `@solana/kit` 6.x. The only reason to install `@solana/web3.js` is as a peer dependency required by Anchor or Light Protocol SDKs. See [client-development.md](client-development.md) for the full decision guide. ### Python - [solana-py](https://github.com/michaelhly/solana-py) - Solana Python SDK - [anchorpy](https://github.com/kevinheavey/anchorpy) - Anchor Python client ## RPC Providers ### Free Tier Available - [Helius](https://www.helius.dev/) - Developer-friendly RPC with generous free tier - [QuickNode](https://www.quicknode.com/) - Global RPC network - [Alchemy](https://www.alchemy.com/solana) - RPC with enhanced APIs - [Triton](https://triton.one/) - High-performance RPC - [Public RPC Endpoints](https://docs.solana.com/cluster/rpc-endpoints) - Free public endpoints ## Developer Communities ### Learning Communities - [Solana Developers](https://github.com/solana-developers) - Official developer org - [Superteam](https://superteam.fun/) - Global Solana community - [Blueshift](https://learn.blueshift.gg/) - Interactive learning platform ### Regional Communities - [Superteam Germany](https://superteam.fun/germany) - [Superteam India](https://superteam.fun/india) - [Superteam Vietnam](https://superteam.fun/vietnam) - [Superteam LatAm](https://superteam.fun/latam) ## Additional Resources ### Developer Guides - [Solana Developer Guide](https://solana.com/developers/guides) - How-to guides - [Solana Bootcamp](https://github.com/solana-developers/developer-bootcamp-2024) - Workshop materials - [Anchor Examples Repo](https://github.com/coral-xyz/anchor/tree/master/tests) - Anchor test programs ### Tool Documentation - [Cargo Build SBF](https://docs.solana.com/cli/deploy-a-program) - Building programs - [Solana Program Deploy](https://docs.solana.com/cli/deploy-a-program) - Deployment guide - [Solana Keygen](https://docs.solana.com/cli/wallets/paper) - Keypair management ### Ecosystem Tools - [Metaplex](https://www.metaplex.com/) - NFT infrastructure - [Squads](https://squads.so/) - Multisig and treasury management - [Dialect](https://www.dialect.to/) - Messaging and notifications ## 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) ## 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 ## Security & Analysis Tools - [Trident](https://github.com/Ackee-Blockchain/trident) - Fuzz testing framework (v0.12.0+: Token-2022 program support, Manually Guided Fuzzing for targeted instruction sequences, solana-sdk 2.3+ compatibility) - [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 ## 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) ## 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 **Current versions (as of February 2026):** - Latest Anchor: 0.32.1 - Recommended Solana CLI: 2.3.0+ (Agave) - Rust minimum: 1.89.0+ (required for Anchor 0.32.0+) - @solana/client (framework-kit): 1.7.0 - @solana/react-hooks: 1.4.1 - @solana/kit: 6.1.0 (standalone) / 5.x (used internally by framework-kit) - Solana program library: 2.0+ **Updating tools:** ```bash # Update Solana CLI solana-install update # Update Anchor avm install latest avm use latest # Update Rust rustup update ``` --- **Note:** Security resources above are a curated collection from the Awesome Solana Security repository and other trusted sources, selected for quality, maintenance status, and relevance to modern Solana development and auditing. -
security-checklists.md 9 KB
# 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 } } ``` -
security-fundamentals.md 26.9 KB
# 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 1. [Security Mindset](#security-mindset) 2. [Core Validation Patterns](#core-validation-patterns) 3. [Common Vulnerabilities](#common-vulnerabilities) 4. [Input Validation](#input-validation) 5. [State Management Security](#state-management-security) 6. [Arithmetic Safety](#arithmetic-safety) 7. [Re-entrancy Protection](#re-entrancy-protection) 8. [Security Checklist](#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](https://workshop.neodyme.io/), 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:** ```rust 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:** ```rust 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:** ```rust 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:** ```rust // 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:** ```rust 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:** ```rust 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** ```rust #[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** ```rust #[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** ```rust #[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:** ```rust 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:** ```rust // ❌ 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:** ```rust // ✅ 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:** ```rust // ❌ 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:** ```rust // ✅ 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:** ```rust // ❌ 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:** ```rust // ✅ 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:** ```rust // ❌ 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:** ```rust // ✅ 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:** ```rust // ❌ 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:** ```rust #[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:** ```rust // ❌ 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:** ```rust // ✅ 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. ```rust 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 ```rust 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 ```rust #[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. ```rust 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:** ```rust #[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. ```rust // ❌ 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 ```rust // 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 ```rust // 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:** ```rust // ❌ 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:** ```rust // ❌ 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:** ```rust 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:** 1. **Validate Everything** - Assume all inputs are malicious 2. **Fail Fast** - Return errors immediately when validation fails 3. **Use Checked Math** - Prevent integer overflow/underflow 4. **Think Like an Attacker** - Ask "How do I break this?" 5. **Test Malicious Cases** - Don't just test happy paths **The Three Pillars of Account Security:** ```rust // 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. -
security.md 14.2 KB
# 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, see the auditing references in this skill: [security-fundamentals.md](security-fundamentals.md), [vulnerability-patterns.md](vulnerability-patterns.md), [security-checklists.md](security-checklists.md), [anchor-security.md](anchor-security.md), and [native-security.md](native-security.md). ## 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) - [ ] Run the systematic audit workflow (see [security-checklists.md](security-checklists.md) and [vulnerability-patterns.md](vulnerability-patterns.md)) - [ ] 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) --- ## Development vs Auditing This guide covers **defensive programming during development** - secure coding patterns, implementation guidance, and development workflows (testing, deployment, optimization). For **comprehensive security auditing**, use the auditing references in this skill: - 🔍 **Systematic audits** - [security-checklists.md](security-checklists.md): category-by-category validation - 🐛 **Vulnerability analysis** - [vulnerability-patterns.md](vulnerability-patterns.md): exploit scenarios and attack vectors - 🛡️ **Framework-specific patterns** - [anchor-security.md](anchor-security.md) and [native-security.md](native-security.md) - 📚 **Threat modeling and fundamentals** - [security-fundamentals.md](security-fundamentals.md) - ⚠️ **Known gotchas** - [caveats.md](caveats.md) --- ## 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. -
serialization.md 13.9 KB
# Serialization and Data Handling This reference provides comprehensive coverage of data serialization and deserialization patterns for native Rust Solana program development, focusing on Borsh and account data layout best practices. ## Table of Contents 1. [Why Borsh for Solana](#why-borsh-for-solana) 2. [Basic Borsh Usage](#basic-borsh-usage) 3. [Account Data Layout Design](#account-data-layout-design) 4. [Serialization Patterns](#serialization-patterns) 5. [Zero-Copy Deserialization](#zero-copy-deserialization) 6. [Data Versioning](#data-versioning) 7. [Performance Considerations](#performance-considerations) 8. [Common Pitfalls](#common-pitfalls) --- ## Why Borsh for Solana **Borsh (Binary Object Representation Serializer for Hashing)** is the recommended serialization format for Solana programs. ### Advantages 1. **Deterministic:** Same data always produces same bytes 2. **Compact:** Efficient binary encoding 3. **Fast:** Lower compute unit cost than alternatives 4. **Strict Schema:** Type-safe serialization/deserialization 5. **No Metadata:** Unlike JSON, no field names in output ### vs Alternatives | Format | CU Cost | Size | Type Safety | Deterministic | |--------|---------|------|-------------|---------------| | **Borsh** | ✅ Low | ✅ Compact | ✅ Yes | ✅ Yes | | bincode | ❌ High | ✅ Compact | ✅ Yes | ⚠️ Config-dependent | | JSON | ❌ Very High | ❌ Large | ❌ No | ❌ No | | MessagePack | ⚠️ Medium | ✅ Compact | ⚠️ Partial | ⚠️ Mostly | **Recommendation:** Use Borsh for all program account data. --- ## Basic Borsh Usage ### Dependencies ```toml [dependencies] borsh = { version = "1.5", features = ["derive"] } ``` ### Deriving Borsh Traits ```rust use borsh::{BorshDeserialize, BorshSerialize}; #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct UserAccount { pub user: Pubkey, pub balance: u64, pub created_at: i64, } ``` ### Serialization **To bytes:** ```rust let account_data = UserAccount { user: Pubkey::new_unique(), balance: 1000, created_at: 1234567890, }; // Serialize to Vec<u8> let bytes = account_data.try_to_vec()?; // Serialize to existing buffer let mut buffer = vec![0u8; 100]; account_data.serialize(&mut buffer.as_mut_slice())?; ``` ### Deserialization **From bytes:** ```rust // Deserialize from slice let account_data = UserAccount::try_from_slice(&bytes)?; // Deserialize with BorshDeserialize let mut cursor = &bytes[..]; let account_data = UserAccount::deserialize(&mut cursor)?; ``` --- ## Account Data Layout Design ### Basic Structure ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct AccountData { // 1. Discriminator / Type Field (1 byte) pub account_type: u8, // 2. Flags / State (1 byte) pub is_initialized: bool, // 3. Fixed-size fields (predictable layout) pub owner: Pubkey, // 32 bytes pub created_at: i64, // 8 bytes pub counter: u64, // 8 bytes // 4. Variable-size fields (at end) pub name: String, // 4 + length pub metadata: Vec<u8>, // 4 + length } ``` **Size calculation:** ``` 1 (type) + 1 (flag) + 32 (pubkey) + 8 (i64) + 8 (u64) + 4 (string len) + N (string) + 4 (vec len) + M (vec) = 58 + N + M bytes ``` ### Size Calculation Helper ```rust impl AccountData { pub const FIXED_SIZE: usize = 58; // All fixed fields pub fn calculate_size(name_len: usize, metadata_len: usize) -> usize { Self::FIXED_SIZE + name_len + metadata_len } pub fn max_size(max_name: usize, max_metadata: usize) -> usize { Self::calculate_size(max_name, max_metadata) } } // Usage let account_size = AccountData::max_size(32, 256); // 346 bytes ``` ### Fixed-Size Accounts **Best for performance:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct FixedAccount { pub is_initialized: bool, pub owner: Pubkey, pub balance: u64, pub last_updated: i64, // Fixed-size array instead of Vec pub data: [u8; 256], } impl FixedAccount { pub const SIZE: usize = 1 + 32 + 8 + 8 + 256; // 305 bytes } ``` --- ## Serialization Patterns ### Pattern 1: try_from_slice (Recommended) **Most common pattern for account deserialization:** ```rust use borsh::BorshDeserialize; pub fn load_account_data( account_info: &AccountInfo, ) -> Result<UserAccount, ProgramError> { let data = UserAccount::try_from_slice(&account_info.data.borrow())?; Ok(data) } ``` **Error handling:** ```rust let data = UserAccount::try_from_slice(&account_info.data.borrow()) .map_err(|e| { msg!("Failed to deserialize account: {}", e); ProgramError::InvalidAccountData })?; ``` ### Pattern 2: Unchecked Deserialization **Use when you've already validated the account:** ```rust use borsh::try_from_slice_unchecked; // After validation checks let mut data = try_from_slice_unchecked::<UserAccount>(&account_info.data.borrow()) .unwrap(); // Safe because we validated ``` **⚠️ Warning:** Only use after thorough validation. Skips some safety checks. ### Pattern 3: Partial Deserialization **Read only what you need:** ```rust #[derive(BorshDeserialize)] pub struct AccountHeader { pub account_type: u8, pub is_initialized: bool, pub owner: Pubkey, } // Deserialize just the header let header = AccountHeader::try_from_slice(&account_info.data.borrow()[..42])?; if !header.is_initialized { return Err(ProgramError::UninitializedAccount); } ``` ### Pattern 4: In-Place Modification **Efficient for large accounts:** ```rust pub fn update_balance( account_info: &AccountInfo, new_balance: u64, ) -> ProgramResult { let mut data = account_info.data.borrow_mut(); // Deserialize let mut account = UserAccount::try_from_slice(&data)?; // Modify account.balance = new_balance; account.last_updated = Clock::get()?.unix_timestamp; // Serialize back account.serialize(&mut &mut data[..])?; Ok(()) } ``` ### Pattern 5: Bulk Operations **Processing multiple accounts:** ```rust pub fn process_accounts( accounts: &[AccountInfo], ) -> ProgramResult { let account_data: Vec<UserAccount> = accounts .iter() .map(|acc| UserAccount::try_from_slice(&acc.data.borrow())) .collect::<Result<Vec<_>, _>>()?; // Process all accounts for (i, data) in account_data.iter().enumerate() { msg!("Account {}: balance = {}", i, data.balance); } Ok(()) } ``` --- ## Zero-Copy Deserialization ### When to Use Zero-Copy **Benefits:** - Avoids memory allocation - Reduces compute units (50%+ savings for large structs) - Direct access to account data **Use when:** - Account data is large (> 100 bytes) - Frequent reads - Performance-critical paths ### Bytemuck Pattern ```toml [dependencies] bytemuck = { version = "1.14", features = ["derive"] } ``` ```rust use bytemuck::{Pod, Zeroable}; #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct ZeroCopyAccount { pub is_initialized: u8, // bool as u8 pub owner: [u8; 32], // Pubkey as bytes pub balance: u64, pub counter: u64, } impl ZeroCopyAccount { pub const SIZE: usize = std::mem::size_of::<Self>(); pub fn from_account_info(account_info: &AccountInfo) -> Result<&Self, ProgramError> { let data = account_info.data.borrow(); bytemuck::try_from_bytes(&data) .map_err(|_| ProgramError::InvalidAccountData) } pub fn from_account_info_mut( account_info: &AccountInfo, ) -> Result<&mut Self, ProgramError> { let data = account_info.data.borrow_mut(); bytemuck::try_from_bytes_mut(&mut data) .map_err(|_| ProgramError::InvalidAccountData) } } // Usage let account = ZeroCopyAccount::from_account_info(account_info)?; msg!("Balance: {}", account.balance); // Mutable access let account = ZeroCopyAccount::from_account_info_mut(account_info)?; account.balance += 100; ``` **⚠️ Limitations:** - Only works with types that are `Pod` (Plain Old Data) - No `String`, `Vec`, or other heap-allocated types - Must be `#[repr(C)]` for stable layout --- ## Data Versioning ### Pattern 1: Version Field ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct VersionedAccount { pub version: u8, pub data: AccountDataEnum, } #[derive(BorshSerialize, BorshDeserialize)] pub enum AccountDataEnum { V1(AccountDataV1), V2(AccountDataV2), } #[derive(BorshSerialize, BorshDeserialize)] pub struct AccountDataV1 { pub balance: u64, } #[derive(BorshSerialize, BorshDeserialize)] pub struct AccountDataV2 { pub balance: u64, pub last_updated: i64, // New field } // Deserialization with version handling pub fn load_versioned_account( account_info: &AccountInfo, ) -> ProgramResult { let versioned = VersionedAccount::try_from_slice(&account_info.data.borrow())?; match versioned.data { AccountDataEnum::V1(data_v1) => { msg!("V1 account: balance = {}", data_v1.balance); } AccountDataEnum::V2(data_v2) => { msg!("V2 account: balance = {}, updated = {}", data_v2.balance, data_v2.last_updated); } } Ok(()) } ``` ### Pattern 2: Optional Fields ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct Account { pub balance: u64, // V2: Added optional field pub metadata: Option<Metadata>, } #[derive(BorshSerialize, BorshDeserialize)] pub struct Metadata { pub name: String, pub url: String, } // Old accounts: metadata = None // New accounts: metadata = Some(Metadata { ... }) ``` ### Pattern 3: Migration Function ```rust pub fn migrate_account_v1_to_v2( account_info: &AccountInfo, ) -> ProgramResult { // Load V1 let data_v1 = AccountDataV1::try_from_slice(&account_info.data.borrow())?; // Convert to V2 let data_v2 = AccountDataV2 { balance: data_v1.balance, last_updated: Clock::get()?.unix_timestamp, }; // Reallocate if needed let new_size = data_v2.try_to_vec()?.len(); account_info.realloc(new_size, false)?; // Serialize V2 data_v2.serialize(&mut &mut account_info.data.borrow_mut()[..])?; Ok(()) } ``` --- ## Performance Considerations ### Compute Unit Costs **Serialization costs (approximate):** | Operation | CU Cost | |-----------|---------| | Serialize small struct (< 100 bytes) | ~500 CU | | Serialize large struct (> 1KB) | ~2,000 CU | | Deserialize small struct | ~800 CU | | Deserialize large struct | ~3,000 CU | | Zero-copy access | ~100 CU | ### Optimization Tips **1. Minimize serialization frequency:** ```rust // ❌ Wasteful - serializes twice let mut data = load_data(account)?; data.field1 = value1; save_data(account, &data)?; data.field2 = value2; save_data(account, &data)?; // Serialize again! // ✅ Efficient - serialize once let mut data = load_data(account)?; data.field1 = value1; data.field2 = value2; save_data(account, &data)?; ``` **2. Use fixed-size fields:** ```rust // ❌ Variable size - more expensive pub struct Account { pub name: String, // 4 + N bytes } // ✅ Fixed size - cheaper pub struct Account { pub name: [u8; 32], // Exactly 32 bytes } ``` **3. Order fields by size:** ```rust // ✅ Optimized layout (largest first) #[derive(BorshSerialize, BorshDeserialize)] #[repr(C)] pub struct OptimizedAccount { pub pubkey1: Pubkey, // 32 bytes pub pubkey2: Pubkey, // 32 bytes pub amount: u64, // 8 bytes pub timestamp: i64, // 8 bytes pub flags: u8, // 1 byte } ``` --- ## Common Pitfalls ### 1. Buffer Too Small ```rust // ❌ Error: buffer too small let mut buffer = vec![0u8; 10]; large_struct.serialize(&mut buffer.as_mut_slice())?; // Fails! // ✅ Correct: proper size let size = large_struct.try_to_vec()?.len(); let mut buffer = vec![0u8; size]; large_struct.serialize(&mut buffer.as_mut_slice())?; ``` ### 2. Forgetting to Borrow ```rust // ❌ Error: data moved let data = account_info.data; UserAccount::try_from_slice(&data)?; // Fails! // ✅ Correct: borrow data let data = account_info.data.borrow(); UserAccount::try_from_slice(&data)?; ``` ### 3. Mismatched Schema ```rust // Account created with V1 #[derive(BorshSerialize)] pub struct AccountV1 { pub balance: u64, } // Later, trying to deserialize as V2 #[derive(BorshDeserialize)] pub struct AccountV2 { pub balance: u64, pub timestamp: i64, // New field! } // ❌ Fails: not enough bytes let data = AccountV2::try_from_slice(&bytes)?; // Error! ``` **Solution:** Use versioning or optional fields. ### 4. String/Vec Limits ```rust // ❌ No validation #[derive(BorshSerialize, BorshDeserialize)] pub struct Account { pub name: String, // Could be 10MB! } // ✅ Validate before deserializing pub fn validate_name(name: &str) -> ProgramResult { if name.len() > 32 { return Err(ProgramError::InvalidArgument); } Ok(()) } ``` ### 5. Incorrect Size Calculation ```rust // ❌ Wrong: ignores vector length prefix let size = my_vec.len(); // ✅ Correct: includes 4-byte length prefix let size = 4 + my_vec.len(); ``` --- ## Summary **Key Takeaways:** 1. **Use Borsh** for all Solana program serialization 2. **Design fixed-size layouts** when possible for predictability 3. **Validate before deserializing** to prevent errors 4. **Use zero-copy** for large, frequently-accessed data 5. **Plan for versioning** from the start 6. **Minimize serialization frequency** to save compute units **Common Patterns:** ```rust // Deserialize let data = AccountData::try_from_slice(&account_info.data.borrow())?; // Modify let mut data = data; data.field = new_value; // Serialize data.serialize(&mut &mut account_info.data.borrow_mut()[..])?; ``` **Size Calculation:** ```rust // Fixed fields const FIXED_SIZE: usize = 1 + 32 + 8; // Variable fields let total_size = FIXED_SIZE + 4 + string.len() + 4 + vec.len(); ``` Proper serialization patterns are fundamental to efficient and correct Solana programs. Master Borsh for production-ready data handling. -
surfpool.md 12 KB
# Surfpool Local Development Surfpool is a drop-in replacement for `solana-test-validator` built on LiteSVM. It provides instant mainnet forking, powerful cheatcodes for testing, and Infrastructure as Code for reproducible deployments. ## Installation ```bash # macOS (Homebrew) brew install txtx/taps/surfpool # Update existing installation brew tap txtx/taps brew reinstall surfpool # From source git clone https://github.com/txtx/surfpool.git cd surfpool cargo surfpool-install # Docker docker run surfpool/surfpool --version # Verify surfpool --version ``` ## Quick Start ```bash # Start local network (in Anchor project directory) surfpool start # Start with custom options surfpool start --port 8899 --slot-time 400 # Start with airdrops to specific addresses surfpool start --airdrop <PUBKEY1> --airdrop <PUBKEY2> --airdrop-amount 100000000000 ``` When run in an Anchor project, Surfpool automatically: - Generates Infrastructure as Code runbooks - Deploys programs to the local network - Provides structured environment for iteration ## CLI Options ```bash surfpool start [OPTIONS] Options: -m, --manifest-file-path <PATH> Path to manifest [default: ./Surfpool.toml] -p, --port <PORT> RPC port [default: 8899] -o, --host <HOST> Host address [default: 127.0.0.1] -s, --slot-time <MS> Slot time in ms [default: 400] -u, --rpc-url <URL> Mainnet RPC for forking [default: https://api.mainnet-beta.solana.com] --no-tui Disable terminal UI, show log streams --no-deploy Disable auto deployments --watch Watch programs for changes -r, --runbook <ID> Runbooks to execute [default: deployment] -a, --airdrop <PUBKEY> Addresses to airdrop SOL -q, --airdrop-amount <LAMPORTS> Airdrop amount [default: 10000000000000] -k, --airdrop-keypair-path <PATH> Keypair paths to airdrop [default: ~/.config/solana/id.json] -g, --geyser-plugin-config <PATH> Geyser plugins to load --no-explorer Disable explorer ``` ## Mainnet Forking (Just-in-Time) Surfpool fetches mainnet accounts on-demand without downloading snapshots: ```bash # Fork mainnet state surfpool start --rpc-url https://api.mainnet-beta.solana.com # Use custom RPC (recommended for rate limits) surfpool start --rpc-url https://mainnet.helius-rpc.com?api-key=YOUR_KEY ``` **Use cases:** - Test CPIs into Jupiter, Raydium, Meteora without manual account dumps - Simulate swaps with real liquidity pool state - Test oracle integrations with live price feeds ## Cheatcodes Special RPC methods for testing. Call via any Solana RPC client: ### Account Manipulation **`surfnet_setAccount`** - Set any account's state: ```typescript await connection._rpcRequest('surfnet_setAccount', [ pubkey.toBase58(), { lamports: 1_000_000_000, data: Buffer.from([...]).toString('hex'), owner: SystemProgram.programId.toBase58(), executable: false, } ]); ``` **`surfnet_setTokenAccount`** - Set token balances directly: ```typescript await connection._rpcRequest('surfnet_setTokenAccount', [ owner.toBase58(), mint.toBase58(), { amount: 1_000_000_000_000, // Set balance without minting delegate: null, state: 'initialized', } ]); ``` **`surfnet_cloneProgramAccount`** - Clone programs between IDs: ```typescript await connection._rpcRequest('surfnet_cloneProgramAccount', [ sourceProgramId.toBase58(), destinationProgramId.toBase58() ]); ``` **`surfnet_setProgramAuthority`** - Modify upgrade authority: ```typescript await connection._rpcRequest('surfnet_setProgramAuthority', [ programId.toBase58(), newAuthority.toBase58() // or null for immutable ]); ``` ### Time Manipulation **`surfnet_timeTravel`** - Jump to future epoch/slot/timestamp: ```typescript // Move to specific slot await connection._rpcRequest('surfnet_timeTravel', [{ slot: 300_000_000 }]); // Move to specific epoch await connection._rpcRequest('surfnet_timeTravel', [{ epoch: 500 }]); ``` **`surfnet_pauseClock`** / **`surfnet_resumeClock`** - Control time: ```typescript await connection._rpcRequest('surfnet_pauseClock', []); // ... perform tests at frozen time await connection._rpcRequest('surfnet_resumeClock', []); ``` ### Transaction Profiling **`surfnet_profileTransaction`** - Detailed CU analysis: ```typescript const result = await connection._rpcRequest('surfnet_profileTransaction', [ transaction.serialize().toString('base64'), 'my-test-tag', // optional tag for grouping { depth: 'instruction', encoding: 'base64' } ]); // Result includes: // - Total CU consumed // - Per-instruction breakdown // - Account changes // - Execution logs ``` **`surfnet_getProfileResults`** - Retrieve tagged profiles: ```typescript const profiles = await connection._rpcRequest('surfnet_getProfileResults', [ 'my-test-tag' ]); ``` **`surfnet_getTransactionProfile`** - Get profile by signature: ```typescript const profile = await connection._rpcRequest('surfnet_getTransactionProfile', [ { signature: txSignature } ]); ``` ### IDL Management **`surfnet_registerIdl`** - Register IDL for account parsing: ```typescript await connection._rpcRequest('surfnet_registerIdl', [ idlJson, // Full IDL object slot // Optional: slot when IDL becomes active ]); ``` **`surfnet_getIdl`** - Retrieve registered IDL: ```typescript const idl = await connection._rpcRequest('surfnet_getIdl', [ programId.toBase58() ]); ``` ## Scenarios (Protocol Overrides) Test against specific protocol states with slot-by-slot account overrides. **Natively supported protocols (v1.0.0-rc1):** - **Pyth v2** - Price feeds (SOL/USD, BTC/USD, ETH/BTC, ETH/USD) - **Jupiter v6** - DEX aggregator, TokenLedger manipulation - **Switchboard On-Demand** - Oracle quote overrides - **Kamino** - Lending reserve liquidity, risk config, obligation health - **Drift v2** - Perp/spot markets, user state, global state - **Raydium AMM-v3** - AMM pool states - **Meteora DLMM** - Dynamic liquidity market maker - **Whirlpool** - Concentrated liquidity pools **Register custom scenarios:** ```typescript await connection._rpcRequest('surfnet_registerScenario', [ { name: 'liquidation-test', description: 'Test liquidation at 80% LTV', overrides: [ { slot: 1, accounts: { [obligationPubkey]: { borrowedValue: 80_000_000, depositedValue: 100_000_000 } } } ] } ]); ``` **Use cases:** - Simulate oracle price crashes - Test liquidation scenarios - Replay historical market conditions - Stress test protocol edge cases ## Infrastructure as Code (IaC) Surfpool uses `.tx` runbooks for declarative deployments: ```hcl # deployment.tx addon "svm" { network_id = input.network_id rpc_api_url = input.rpc_api_url } signer "deployer" "svm::web_wallet" { expected_address = input.deployer_address } action "deploy_program" "svm::deploy_program" { description = "Deploy the program" program = svm::get_program_from_anchor_project("my_program") authority = signer.deployer payer = signer.deployer } output "program_id" { value = action.deploy_program.program_id } ``` **Run runbooks:** ```bash # Supervised mode (web UI) surfpool run deployment.tx # Unsupervised mode (CI/CD) surfpool run deployment.tx --unsupervised # With environment surfpool run deployment.tx --env mainnet ``` **Manifest file (`txtx.yml`):** ```yaml name: my-protocol runbooks: - name: Deploy Protocol location: ./deployment state: location: states environments: devnet: network_id: devnet rpc_api_url: https://api.devnet.solana.com mainnet: network_id: mainnet rpc_api_url: https://api.mainnet-beta.solana.com ``` ## IDL-to-SQL Auto-generate SQL schemas from program IDLs: ```bash # Surfpool automatically generates tables for registered IDLs # Query account data via local SQLite or Postgres # Example: Query all token accounts SELECT * FROM token_accounts WHERE owner = 'YOUR_PUBKEY'; ``` ## MCP Integration Add Surfpool as an MCP server for AI-assisted development: **Claude Code / Cursor configuration:** ```json { "mcpServers": { "surfpool": { "command": "surfpool", "args": ["mcp"] } } } ``` **Available MCP tools:** - `start_surfnet` - Start a local network - `set_token_account` - Set token balances for testing **Example prompt:** > "Start a local network with 10 users loaded with SOL, USDC, JUP and TRUMP tokens" ## Integration with Anchor Surfpool works seamlessly with existing Anchor workflows: ```bash # In Anchor project directory cd my-anchor-project # Start Surfpool (auto-deploys programs) surfpool start # Run Anchor tests against Surfpool anchor test --skip-local-validator # Watch for program changes surfpool start --watch ``` **anchor.toml configuration:** ```toml [provider] cluster = "localnet" # Points to Surfpool on 8899 [programs.localnet] my_program = "YourProgramID" ``` ## Integration with Native Rust ```bash # Build program cargo build-sbf # Start Surfpool surfpool start --no-deploy # Deploy manually solana program deploy target/deploy/my_program.so # Or use runbook surfpool run deploy.tx ``` ## Surfpool Studio Local web dashboard for visualization: ```bash # Start with Studio enabled (default) surfpool start # Access at http://localhost:8899/studio ``` **Features:** - Real-time transaction monitoring - Account state inspection - Scenario builder (drag-and-drop protocol overrides) - IDL-parsed account views - Transaction profiling results ## Best Practices ### Testing with Cheatcodes ```typescript describe('Liquidation Tests', () => { beforeEach(async () => { // Set up specific account states await connection._rpcRequest('surfnet_setTokenAccount', [ userWallet.toBase58(), usdcMint.toBase58(), { amount: 1_000_000_000 } ]); // Pause clock for deterministic tests await connection._rpcRequest('surfnet_pauseClock', []); }); it('should liquidate at 80% LTV', async () => { // Time travel to simulate price change await connection._rpcRequest('surfnet_timeTravel', [{ slot: currentSlot + 1000 }]); // Test liquidation logic // ... }); afterEach(async () => { await connection._rpcRequest('surfnet_resumeClock', []); }); }); ``` ### CU Optimization Workflow 1. **Profile baseline:** ```typescript const baseline = await connection._rpcRequest('surfnet_profileTransaction', [ tx.serialize().toString('base64'), 'optimization-baseline' ]); ``` 2. **Make optimizations** (see [compute-optimization.md](compute-optimization.md)) 3. **Profile again and compare:** ```typescript const optimized = await connection._rpcRequest('surfnet_profileTransaction', [ optimizedTx.serialize().toString('base64'), 'optimization-v1' ]); ``` 4. **Review in Studio** for per-instruction breakdown ### CI/CD Integration ```yaml # GitHub Actions example jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Surfpool run: cargo install --git https://github.com/txtx/surfpool surfpool-cli - name: Start Surfpool run: surfpool start --no-tui & - name: Wait for Surfpool run: sleep 5 - name: Run tests run: anchor test --skip-local-validator ``` ## Troubleshooting **Port already in use:** ```bash surfpool start --port 8900 ``` **Mainnet RPC rate limits:** ```bash # Use dedicated RPC surfpool start --rpc-url https://mainnet.helius-rpc.com?api-key=YOUR_KEY # Or set environment variable export SURFPOOL_DATASOURCE_RPC_URL=https://your-rpc.com ``` **Program not deploying:** ```bash # Check deployment logs surfpool start --debug # Manually deploy surfpool start --no-deploy solana program deploy target/deploy/program.so ``` ## Resources - [Surfpool Documentation](https://docs.surfpool.run) - [GitHub Repository](https://github.com/txtx/surfpool) - [Surfpool 101 Video Series](https://www.youtube.com/playlist?list=PL0FMgRjJMRzO1FdunpMS-aUS4GNkgyr3T) - [Discord Community](https://discord.gg/rqXmWsn2ja) - [Solana Docs: Surfpool CLI Basics](https://solana.com/docs/intro/installation/surfpool-cli-basics) -
sysvars.md 23.1 KB
# Sysvars (System Variables) This reference provides comprehensive coverage of Solana System Variables (sysvars) for native Rust program development, including access patterns, use cases, and performance implications. ## Table of Contents 1. [What are Sysvars](#what-are-sysvars) 2. [Clock Sysvar](#clock-sysvar) 3. [Rent Sysvar](#rent-sysvar) 4. [EpochSchedule Sysvar](#epochschedule-sysvar) 5. [SlotHashes Sysvar](#slothashes-sysvar) 6. [Other Sysvars](#other-sysvars) 7. [Access Patterns](#access-patterns) 8. [Performance Implications](#performance-implications) 9. [Best Practices](#best-practices) --- ## What are Sysvars **System Variables (sysvars)** are special accounts that provide programs with access to blockchain state and cluster information. ### Key Characteristics 1. **Cluster-wide state:** Same values for all programs in the same slot 2. **Updated automatically:** Runtime maintains values 3. **Predictable addresses:** Well-known pubkeys 4. **Read-only:** Programs cannot modify sysvars 5. **Low CU cost:** Cheaper than account reads ### When to Use Sysvars **Use sysvars when you need:** - Current timestamp or slot number - Rent exemption calculations - Epoch and slot timing information - Recent block hashes (for verification) - Stake history or epoch rewards **Don't use sysvars for:** - User-specific data (use accounts) - Program state (use PDAs) - Cross-program communication (use CPIs) --- ## Clock Sysvar **Address:** `solana_program::sysvar::clock::ID` The Clock sysvar provides timing information about the blockchain. ### Clock Structure ```rust use solana_program::clock::Clock; pub struct Clock { pub slot: Slot, // Current slot pub epoch_start_timestamp: i64, // Timestamp of epoch start (approximate) pub epoch: Epoch, // Current epoch pub leader_schedule_epoch: Epoch, // Epoch for which leader schedule is valid pub unix_timestamp: UnixTimestamp, // Estimated wall-clock Unix timestamp } ``` ### Accessing Clock **Pattern 1: get() (Recommended)** ```rust use solana_program::clock::Clock; use solana_program::sysvar::Sysvar; pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8], ) -> ProgramResult { // Get Clock directly (no account needed) let clock = Clock::get()?; msg!("Current slot: {}", clock.slot); msg!("Current timestamp: {}", clock.unix_timestamp); msg!("Current epoch: {}", clock.epoch); Ok(()) } ``` **Pattern 2: From account** ```rust use solana_program::sysvar::clock; pub fn process_with_account( accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let clock_account = next_account_info(account_info_iter)?; // Verify it's the Clock sysvar if clock_account.key != &clock::ID { return Err(ProgramError::InvalidArgument); } let clock = Clock::from_account_info(clock_account)?; msg!("Timestamp: {}", clock.unix_timestamp); Ok(()) } ``` **⚠️ Recommendation:** Use `Clock::get()` unless you specifically need the account for validation. ### Common Clock Use Cases **1. Timestamping events:** ```rust use solana_program::clock::Clock; use solana_program::sysvar::Sysvar; #[derive(BorshSerialize, BorshDeserialize)] pub struct Event { pub created_at: i64, pub data: Vec<u8>, } pub fn create_event( event_account: &AccountInfo, data: Vec<u8>, ) -> ProgramResult { let clock = Clock::get()?; let event = Event { created_at: clock.unix_timestamp, data, }; event.serialize(&mut &mut event_account.data.borrow_mut()[..])?; Ok(()) } ``` **2. Time-based logic (vesting, expiration):** ```rust pub fn check_vesting( vesting_account: &AccountInfo, ) -> ProgramResult { let clock = Clock::get()?; let vesting = VestingSchedule::try_from_slice(&vesting_account.data.borrow())?; if clock.unix_timestamp < vesting.unlock_timestamp { msg!("Tokens still locked until {}", vesting.unlock_timestamp); return Err(ProgramError::Custom(1)); // Locked } msg!("Vesting unlocked!"); Ok(()) } ``` **3. Slot-based mechanics:** ```rust pub fn process_epoch_transition( state_account: &AccountInfo, ) -> ProgramResult { let clock = Clock::get()?; let mut state = State::try_from_slice(&state_account.data.borrow())?; if clock.epoch > state.last_processed_epoch { msg!("Processing epoch transition: {} -> {}", state.last_processed_epoch, clock.epoch); // Process epoch rewards, resets, etc. state.last_processed_epoch = clock.epoch; state.serialize(&mut &mut state_account.data.borrow_mut()[..])?; } Ok(()) } ``` ### Clock Gotchas **⚠️ unix_timestamp is approximate:** ```rust // ❌ Don't use for precise timing if clock.unix_timestamp == expected_timestamp { // Risky! // Might miss by seconds } // ✅ Use ranges for time checks if clock.unix_timestamp >= unlock_time { // Safe } ``` **⚠️ Timestamps can vary across validators:** The `unix_timestamp` is based on validator voting and may differ slightly between validators in the same slot. Don't assume exact precision. --- ## Rent Sysvar **Address:** `solana_program::sysvar::rent::ID` The Rent sysvar provides rent calculation parameters. ### Rent Structure ```rust use solana_program::rent::Rent; pub struct Rent { pub lamports_per_byte_year: u64, // Base rent rate pub exemption_threshold: f64, // Multiplier for exemption (2.0 = 2 years) pub burn_percent: u8, // Percentage of rent burned } ``` ### Accessing Rent **Pattern 1: get() (Recommended)** ```rust use solana_program::rent::Rent; use solana_program::sysvar::Sysvar; pub fn calculate_rent_exemption( data_size: usize, ) -> Result<u64, ProgramError> { let rent = Rent::get()?; // Calculate minimum balance for rent exemption let min_balance = rent.minimum_balance(data_size); msg!("Minimum balance for {} bytes: {} lamports", data_size, min_balance); Ok(min_balance) } ``` **Pattern 2: From account** ```rust use solana_program::sysvar::rent; pub fn check_rent_exemption( accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let data_account = next_account_info(account_info_iter)?; let rent_account = next_account_info(account_info_iter)?; if rent_account.key != &rent::ID { return Err(ProgramError::InvalidArgument); } let rent = Rent::from_account_info(rent_account)?; if !rent.is_exempt(data_account.lamports(), data_account.data_len()) { msg!("Account is not rent-exempt!"); return Err(ProgramError::AccountNotRentExempt); } Ok(()) } ``` ### Common Rent Use Cases **1. Account creation with rent exemption:** ```rust use solana_program::rent::Rent; use solana_program::system_instruction; use solana_program::program::invoke_signed; pub fn create_account_rent_exempt( payer: &AccountInfo, new_account: &AccountInfo, system_program: &AccountInfo, program_id: &Pubkey, seeds: &[&[u8]], space: usize, ) -> ProgramResult { let rent = Rent::get()?; let min_balance = rent.minimum_balance(space); msg!("Creating account with {} lamports for {} bytes", min_balance, space); let create_account_ix = system_instruction::create_account( payer.key, new_account.key, min_balance, space as u64, program_id, ); invoke_signed( &create_account_ix, &[payer.clone(), new_account.clone(), system_program.clone()], &[seeds], )?; Ok(()) } ``` **2. Validating account has sufficient balance:** ```rust pub fn validate_rent_exempt_account( account: &AccountInfo, ) -> ProgramResult { let rent = Rent::get()?; if !rent.is_exempt(account.lamports(), account.data_len()) { let required = rent.minimum_balance(account.data_len()); let current = account.lamports(); msg!("Account not rent-exempt: has {} lamports, needs {}", current, required); return Err(ProgramError::AccountNotRentExempt); } Ok(()) } ``` **3. Calculating required lamports for reallocation:** ```rust pub fn reallocate_account( account: &AccountInfo, new_size: usize, ) -> ProgramResult { let rent = Rent::get()?; let old_size = account.data_len(); let current_lamports = account.lamports(); let new_min_balance = rent.minimum_balance(new_size); if new_size > old_size { // Growing account - ensure sufficient lamports if current_lamports < new_min_balance { msg!("Need {} more lamports for reallocation", new_min_balance - current_lamports); return Err(ProgramError::InsufficientFunds); } } account.realloc(new_size, false)?; Ok(()) } ``` --- ## EpochSchedule Sysvar **Address:** `solana_program::sysvar::epoch_schedule::ID` The EpochSchedule sysvar provides information about epoch timing and slot calculations. ### EpochSchedule Structure ```rust use solana_program::epoch_schedule::EpochSchedule; pub struct EpochSchedule { pub slots_per_epoch: u64, // Slots per epoch after warmup pub leader_schedule_slot_offset: u64, // Offset for leader schedule pub warmup: bool, // Whether in warmup period pub first_normal_epoch: Epoch, // First non-warmup epoch pub first_normal_slot: Slot, // First slot of first normal epoch } ``` ### Accessing EpochSchedule ```rust use solana_program::sysvar::epoch_schedule::EpochSchedule; use solana_program::sysvar::Sysvar; pub fn get_epoch_info() -> ProgramResult { let epoch_schedule = EpochSchedule::get()?; msg!("Slots per epoch: {}", epoch_schedule.slots_per_epoch); msg!("First normal epoch: {}", epoch_schedule.first_normal_epoch); msg!("Warmup: {}", epoch_schedule.warmup); Ok(()) } ``` ### Common EpochSchedule Use Cases **1. Calculating epoch from slot:** ```rust use solana_program::clock::Clock; use solana_program::epoch_schedule::EpochSchedule; pub fn calculate_epoch_from_slot( slot: u64, ) -> Result<u64, ProgramError> { let epoch_schedule = EpochSchedule::get()?; let epoch = epoch_schedule.get_epoch(slot); msg!("Slot {} is in epoch {}", slot, epoch); Ok(epoch) } ``` **2. Determining slots remaining in epoch:** ```rust pub fn slots_until_epoch_end() -> Result<u64, ProgramError> { let clock = Clock::get()?; let epoch_schedule = EpochSchedule::get()?; let current_slot = clock.slot; let current_epoch = clock.epoch; // Get first slot of next epoch let next_epoch_start = epoch_schedule.get_first_slot_in_epoch(current_epoch + 1); let remaining = next_epoch_start - current_slot; msg!("Slots remaining in epoch: {}", remaining); Ok(remaining) } ``` **3. Epoch-based reward distribution:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct RewardState { pub last_distribution_epoch: u64, pub total_distributed: u64, } pub fn distribute_epoch_rewards( reward_state_account: &AccountInfo, ) -> ProgramResult { let clock = Clock::get()?; let mut state = RewardState::try_from_slice(&reward_state_account.data.borrow())?; if clock.epoch > state.last_distribution_epoch { let epochs_passed = clock.epoch - state.last_distribution_epoch; msg!("Distributing rewards for {} epochs", epochs_passed); // Distribute rewards let reward_amount = epochs_passed * 1000; // Example state.total_distributed += reward_amount; state.last_distribution_epoch = clock.epoch; state.serialize(&mut &mut reward_state_account.data.borrow_mut()[..])?; } Ok(()) } ``` --- ## SlotHashes Sysvar **Address:** `solana_program::sysvar::slot_hashes::ID` The SlotHashes sysvar contains recent slot hashes for verification purposes. ### SlotHashes Structure ```rust use solana_program::slot_hashes::SlotHashes; // SlotHashes contains up to 512 recent (slot, hash) pairs pub struct SlotHashes { // Vector of (slot, hash) tuples // Most recent first, up to MAX_ENTRIES (512) } ``` ### Accessing SlotHashes ```rust use solana_program::sysvar::slot_hashes::SlotHashes; use solana_program::sysvar::Sysvar; pub fn verify_recent_slot( claimed_slot: u64, claimed_hash: &[u8; 32], ) -> ProgramResult { let slot_hashes = SlotHashes::get()?; // Check if slot is in recent history for (slot, hash) in slot_hashes.iter() { if *slot == claimed_slot { if hash.as_ref() == claimed_hash { msg!("Slot hash verified!"); return Ok(()); } else { msg!("Slot hash mismatch!"); return Err(ProgramError::InvalidArgument); } } } msg!("Slot not found in recent history"); Err(ProgramError::InvalidArgument) } ``` ### Common SlotHashes Use Cases **1. Verifying transaction recency:** ```rust pub fn verify_transaction_recent( slot_hashes_account: &AccountInfo, claimed_slot: u64, ) -> ProgramResult { let slot_hashes = SlotHashes::from_account_info(slot_hashes_account)?; // Check if claimed slot is in recent 512 slots let is_recent = slot_hashes.iter().any(|(slot, _)| *slot == claimed_slot); if !is_recent { msg!("Transaction too old or slot invalid"); return Err(ProgramError::Custom(1)); } Ok(()) } ``` **2. Preventing replay attacks:** ```rust #[derive(BorshSerialize, BorshDeserialize)] pub struct ProcessedSlot { pub slot: u64, pub hash: [u8; 32], } pub fn process_once_per_slot( state_account: &AccountInfo, ) -> ProgramResult { let slot_hashes = SlotHashes::get()?; let mut state = ProcessedSlot::try_from_slice(&state_account.data.borrow())?; // Get current slot and hash let (current_slot, current_hash) = slot_hashes.iter().next() .ok_or(ProgramError::InvalidArgument)?; if state.slot == *current_slot { msg!("Already processed in this slot!"); return Err(ProgramError::Custom(2)); // Already processed } // Update state state.slot = *current_slot; state.hash = current_hash.to_bytes(); state.serialize(&mut &mut state_account.data.borrow_mut()[..])?; Ok(()) } ``` **⚠️ Note:** SlotHashes only maintains the most recent 512 slots. For older verification, use a different approach. --- ## Other Sysvars ### StakeHistory **Address:** `solana_program::sysvar::stake_history::ID` Provides historical stake activation and deactivation information. ```rust use solana_program::sysvar::stake_history::StakeHistory; pub fn get_stake_history() -> ProgramResult { let stake_history = StakeHistory::get()?; // Access historical stake data by epoch msg!("Stake history available"); Ok(()) } ``` **Use cases:** - Stake pool programs - Historical stake analysis - Reward calculations ### EpochRewards **Address:** `solana_program::sysvar::epoch_rewards::ID` Provides information about epoch rewards distribution (if active). ```rust use solana_program::sysvar::epoch_rewards::EpochRewards; pub fn check_epoch_rewards() -> ProgramResult { let epoch_rewards = EpochRewards::get()?; msg!("Epoch rewards data available"); Ok(()) } ``` **Use cases:** - Stake reward programs - Validator reward tracking ### Instructions **Address:** `solana_program::sysvar::instructions::ID` Provides access to instructions in the current transaction. ```rust use solana_program::sysvar::instructions; pub fn validate_transaction_instructions( instructions_account: &AccountInfo, ) -> ProgramResult { // Check if current instruction is not the first let current_index = instructions::load_current_index_checked(instructions_account)?; msg!("Current instruction index: {}", current_index); // Load a specific instruction if current_index > 0 { let prev_ix = instructions::load_instruction_at_checked( (current_index - 1) as usize, instructions_account, )?; msg!("Previous instruction program: {}", prev_ix.program_id); } Ok(()) } ``` **Use cases:** - Cross-instruction validation - Ensuring instruction order - Detecting sandwich attacks --- ## Access Patterns ### Pattern 1: get() - Direct Access (Recommended) **Advantages:** - No account needed in instruction - Saves account space - Lower CU cost (~100 CU) - Cleaner code **Disadvantages:** - Not supported for all sysvars - Can't be passed to CPIs ```rust use solana_program::sysvar::Sysvar; pub fn use_sysvar_direct() -> ProgramResult { let clock = Clock::get()?; let rent = Rent::get()?; msg!("Clock: {}", clock.unix_timestamp); msg!("Rent: {}", rent.lamports_per_byte_year); Ok(()) } ``` **Supported sysvars:** - Clock - Rent - EpochSchedule - EpochRewards - Fees (deprecated) ### Pattern 2: from_account_info - Account Access **Advantages:** - Works for all sysvars - Can be validated - Can be passed to CPIs - Required for some sysvars (SlotHashes, Instructions) **Disadvantages:** - Account must be passed in instruction - Slightly higher CU cost (~300 CU) - More boilerplate ```rust use solana_program::sysvar::clock; pub fn use_sysvar_from_account( clock_account: &AccountInfo, ) -> ProgramResult { // Validate account address if clock_account.key != &clock::ID { return Err(ProgramError::InvalidArgument); } let clock = Clock::from_account_info(clock_account)?; msg!("Clock: {}", clock.unix_timestamp); Ok(()) } ``` **Required for:** - SlotHashes - StakeHistory - Instructions - Any sysvar passed to CPI ### Pattern 3: Hybrid Approach **Use get() when possible, account when needed:** ```rust pub fn hybrid_sysvar_access( accounts: &[AccountInfo], need_cpi: bool, ) -> ProgramResult { if need_cpi { // Need account for CPI let account_info_iter = &mut accounts.iter(); let clock_account = next_account_info(account_info_iter)?; let clock = Clock::from_account_info(clock_account)?; // Can pass clock_account to CPI msg!("Using account access"); } else { // Direct access is cheaper let clock = Clock::get()?; msg!("Using direct access"); } Ok(()) } ``` --- ## Performance Implications ### Compute Unit Costs | Access Method | Approximate CU Cost | |--------------|---------------------| | Clock::get() | ~100 CU | | Rent::get() | ~100 CU | | EpochSchedule::get() | ~100 CU | | Clock::from_account_info() | ~300 CU | | SlotHashes::from_account_info() | ~500 CU | ### Optimization Tips **1. Use get() when possible:** ```rust // ✅ Efficient - 100 CU let clock = Clock::get()?; // ❌ Wasteful - 300 CU (unless needed for CPI) let clock = Clock::from_account_info(clock_account)?; ``` **2. Cache sysvar values:** ```rust // ❌ Wasteful - calls get() multiple times for i in 0..10 { let clock = Clock::get()?; // 100 CU × 10 = 1000 CU process_item(i, clock.unix_timestamp)?; } // ✅ Efficient - call once let clock = Clock::get()?; // 100 CU let timestamp = clock.unix_timestamp; for i in 0..10 { process_item(i, timestamp)?; } ``` **3. Avoid unnecessary sysvar access:** ```rust // ❌ Wasteful - reading sysvar in every call pub fn update_balance(account: &AccountInfo, amount: u64) -> ProgramResult { let clock = Clock::get()?; // Not needed! // ... no clock usage Ok(()) } // ✅ Efficient - only access when needed pub fn update_with_timestamp(account: &AccountInfo, amount: u64) -> ProgramResult { let clock = Clock::get()?; // Used below let timestamp = clock.unix_timestamp; // ... use timestamp Ok(()) } ``` --- ## Best Practices ### 1. Prefer get() Over from_account_info() **Unless you need the account for CPI or validation:** ```rust // ✅ Default choice let clock = Clock::get()?; // Only if needed for CPI let clock = Clock::from_account_info(clock_account)?; invoke(&ix, &[..., clock_account])?; ``` ### 2. Validate Sysvar Accounts **When accepting sysvar accounts, always validate:** ```rust pub fn validate_clock_account( clock_account: &AccountInfo, ) -> ProgramResult { // ✅ Always validate sysvar address if clock_account.key != &solana_program::sysvar::clock::ID { msg!("Invalid Clock account"); return Err(ProgramError::InvalidArgument); } Ok(()) } ``` ### 3. Use Clock for Timestamps, Not Slot Hashes **For simple time-based logic:** ```rust // ✅ Simple and efficient let clock = Clock::get()?; if clock.unix_timestamp >= unlock_time { // unlock } // ❌ Overkill - SlotHashes is for verification, not timing let slot_hashes = SlotHashes::get()?; // Complex slot-based timing logic ``` ### 4. Cache Sysvar Values **Read once, use multiple times:** ```rust pub fn process_multiple_accounts( accounts: &[AccountInfo], ) -> ProgramResult { // ✅ Read once let clock = Clock::get()?; let timestamp = clock.unix_timestamp; for account in accounts { update_account_timestamp(account, timestamp)?; } Ok(()) } ``` ### 5. Document Sysvar Dependencies **Be explicit about which sysvars your program uses:** ```rust /// Processes user staking /// /// # Sysvars /// - Clock: for stake timestamp /// - Rent: for account validation /// /// # Accounts /// - `[writable]` stake_account /// - `[signer]` user pub fn process_stake( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, ) -> ProgramResult { let clock = Clock::get()?; let rent = Rent::get()?; // ... Ok(()) } ``` ### 6. Handle Clock Drift **Don't assume unix_timestamp is perfectly accurate:** ```rust // ❌ Risky - exact timestamp match if clock.unix_timestamp == expected_time { // May never trigger } // ✅ Safe - use ranges if clock.unix_timestamp >= expected_time { // Reliable } // ✅ Best - add tolerance for early/late const TOLERANCE: i64 = 60; // 60 seconds if clock.unix_timestamp >= expected_time - TOLERANCE { // Handles clock drift } ``` --- ## Summary **Key Takeaways:** 1. **Use get() when possible** for lower CU costs and simpler code 2. **Use from_account_info()** when passing to CPIs or for sysvars without get() 3. **Always validate** sysvar account addresses when accepting them 4. **Cache sysvar values** to avoid redundant reads 5. **Understand timing limitations** - unix_timestamp is approximate **Most Common Sysvars:** | Sysvar | Primary Use | Access Method | |--------|------------|---------------| | **Clock** | Timestamps, epochs, slots | `Clock::get()` | | **Rent** | Rent exemption calculations | `Rent::get()` | | **EpochSchedule** | Epoch/slot calculations | `EpochSchedule::get()` | | **SlotHashes** | Recent slot verification | `from_account_info()` only | | **Instructions** | Transaction introspection | `from_account_info()` only | **Common Patterns:** ```rust // Timestamp current event let clock = Clock::get()?; event.created_at = clock.unix_timestamp; // Validate rent exemption let rent = Rent::get()?; if !rent.is_exempt(account.lamports(), account.data_len()) { return Err(ProgramError::AccountNotRentExempt); } // Calculate rent for new account let rent = Rent::get()?; let min_balance = rent.minimum_balance(space); ``` Sysvars provide essential cluster state to your programs. Master their access patterns for efficient, production-ready Solana development. -
testing-frameworks.md 29.7 KB
# Solana Program Testing Frameworks **Detailed guide for Mollusk, LiteSVM, and Anchor testing frameworks** This file provides comprehensive documentation for the main testing frameworks used in Solana program development. For an overview of the testing strategy and pyramid, see the related files. --- ## Related Testing Documentation - **[Testing Overview](./testing-overview.md)** - Testing pyramid structure and types of tests - **[Testing Best Practices](./testing-practices.md)** - Best practices, common patterns, and additional resources --- ## Table of Contents 1. [Mollusk Testing](#mollusk-testing) 2. [Anchor-Specific Testing](#anchor-specific-testing) 3. [Native Rust Testing](#native-rust-testing) --- ## Mollusk Testing ### What is Mollusk? Mollusk is a lightweight test harness that provides a minified Solana Virtual Machine (SVM) environment for program testing. It creates a program execution pipeline directly from low-level SVM components without the overhead of a full validator. **Key characteristics:** - No validator runtime (no AccountsDB, Bank, or other large components) - Exceptionally fast test execution - Direct program ELF execution via BPF Loader - Requires explicit account lists (can't load from storage) - Configurable compute budget, feature set, and sysvars ### Setup and Dependencies #### Version Compatibility **IMPORTANT:** Mollusk versions must match your Solana SDK version. **For Anchor 0.32.1 (Solana SDK 2.2.x):** ```toml [dev-dependencies] mollusk-svm = "0.5.1" mollusk-svm-bencher = "0.5.1" mollusk-svm-programs-token = "0.5.1" solana-sdk = "2.2" spl-token = "7.0" spl-associated-token-account = "6.0" ``` **Why 0.5.1?** - Anchor 0.32.1 uses Solana SDK 2.2.x internally - Mollusk 0.5.1 is the last version compatible with Solana 2.x - Mollusk 0.6.0+ uses Solana 3.0 and won't compile with Anchor 0.32.1 **For Native Rust programs (Solana SDK 2.1.x or 2.2.x):** ```toml [dev-dependencies] mollusk-svm = "0.5.1" solana-sdk = "2.2" # Or "2.1" depending on your program ``` **For newer Solana versions (3.0+):** ```toml [dev-dependencies] mollusk-svm = "0.9" # Latest version solana-sdk = "3.0" ``` **How to check your Solana SDK version:** ```bash # For Anchor projects grep solana-program programs/*/Cargo.toml # For native Rust grep solana-program Cargo.toml # Check Anchor's internal SDK version cargo tree | grep solana-sdk ``` #### Standard Dependencies For testing with Token program: ```toml [dev-dependencies] mollusk-svm-programs-token = "0.5.1" # Match mollusk-svm version spl-token = "7.0" # For Solana 2.x ``` For compute unit benchmarking: ```toml [dev-dependencies] mollusk-svm-bencher = "0.5.1" # Match mollusk-svm version ``` ### Basic Test Structure ```rust use { mollusk_svm::Mollusk, solana_sdk::{ account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }, }; #[test] fn test_my_instruction() { // 1. Initialize Mollusk with your program let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); // 2. Setup accounts let user = Pubkey::new_unique(); let accounts = vec![ (user, Account { lamports: 1_000_000, data: vec![], owner: program_id, executable: false, rent_epoch: 0, }), ]; // 3. Create instruction let instruction = Instruction::new_with_bytes( program_id, &[0, 1, 2, 3], // instruction data vec![AccountMeta::new(user, true)], ); // 4. Process instruction let result = mollusk.process_instruction(&instruction, &accounts); // 5. Assert success assert!(result.is_ok()); } ``` ### Four Main API Methods Mollusk provides four core testing methods: **1. `process_instruction`** - Execute single instruction, return result ```rust let result = mollusk.process_instruction(&instruction, &accounts); ``` **2. `process_and_validate_instruction`** - Execute and validate with checks ```rust mollusk.process_and_validate_instruction( &instruction, &accounts, &checks, ); ``` **3. `process_instruction_chain`** - Execute multiple instructions sequentially ```rust let result = mollusk.process_instruction_chain( &[instruction1, instruction2, instruction3], &accounts, ); ``` **4. `process_and_validate_instruction_chain`** - Execute chain with per-instruction checks ```rust mollusk.process_and_validate_instruction_chain( &[ (&instruction1, &[Check::success()]), (&instruction2, &[Check::success()]), ], &accounts, ); ``` ### Creating Test Accounts Test accounts must be created explicitly with all required fields: ```rust use solana_sdk::account::Account; // Basic account let account = Account { lamports: 1_000_000, // Account balance data: vec![0; 100], // Account data owner: program_id, // Owner program executable: false, // Not executable rent_epoch: 0, // Rent epoch }; // System account let system_account = Account { lamports: 1_000_000, data: vec![], owner: system_program::id(), executable: false, rent_epoch: 0, }; // Rent-exempt account let rent = mollusk.sysvars.rent; let rent_exempt_account = Account { lamports: rent.minimum_balance(data_len), data: vec![0; data_len], owner: program_id, executable: false, rent_epoch: 0, }; ``` ### Processing Instructions **Simple execution:** ```rust let result = mollusk.process_instruction(&instruction, &accounts); assert!(result.is_ok()); ``` **With result inspection:** ```rust let result = mollusk.process_instruction(&instruction, &accounts); match result { Ok(result) => { println!("Compute units: {}", result.compute_units_consumed); // Access modified accounts from result } Err(err) => panic!("Instruction failed: {:?}", err), } ``` ### Validation with Check API The `Check` enum provides common validation patterns: **Success checks:** ```rust use mollusk_svm::result::Check; let checks = vec![ Check::success(), // Instruction succeeded Check::compute_units(5000), // Exact compute units ]; ``` **Account state checks:** ```rust let checks = vec![ Check::account(&pubkey) .lamports(1_000_000) // Check lamports .data(&[1, 2, 3, 4]) // Check full data .data_slice(8, &[1, 2, 3, 4]) // Check data slice at offset .owner(&program_id) // Check owner .executable(false) // Check executable flag .space(100) // Check data length .rent_exempt() // Check rent exempt .build(), ]; ``` **Error checks:** ```rust use solana_sdk::instruction::InstructionError; let checks = vec![ Check::instruction_err(InstructionError::InvalidInstructionData), ]; ``` **Complete validation example:** ```rust use { mollusk_svm::{Mollusk, result::Check}, solana_sdk::{ account::Account, instruction::Instruction, pubkey::Pubkey, system_instruction, system_program, }, }; #[test] fn test_system_transfer() { let sender = Pubkey::new_unique(); let recipient = Pubkey::new_unique(); let base_lamports = 100_000_000; let transfer_amount = 42_000; let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount); let accounts = [ ( sender, Account::new(base_lamports, 0, &system_program::id()), ), ( recipient, Account::new(base_lamports, 0, &system_program::id()), ), ]; let checks = vec![ Check::success(), Check::account(&sender) .lamports(base_lamports - transfer_amount) .build(), Check::account(&recipient) .lamports(base_lamports + transfer_amount) .build(), ]; Mollusk::default().process_and_validate_instruction( &instruction, &accounts, &checks, ); } ``` ### Compute Unit Benchmarking Monitor compute unit usage to catch performance regressions: **Basic benchmark:** ```rust use mollusk_svm_bencher::MolluskComputeUnitBencher; fn main() { let program_id = Pubkey::new_unique(); let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); MolluskComputeUnitBencher::new(mollusk) .bench(("my_instruction", &instruction, &accounts)) .must_pass(true) .out_dir("./target/benches") .execute(); } ``` **Benchmark multiple instructions:** ```rust fn main() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let bencher = MolluskComputeUnitBencher::new(mollusk); bencher.bench(("initialize", &init_ix, &init_accounts)) .must_pass(true); bencher.bench(("update", &update_ix, &update_accounts)) .must_pass(true); bencher.bench(("close", &close_ix, &close_accounts)) .must_pass(true) .out_dir("./target/benches") .execute(); } ``` Run benchmarks with: ```bash cargo bench ``` Output includes: - Current compute units consumed - Previous benchmark value - Delta (increase/decrease) - Pass/fail status ### Advanced Patterns #### Stateful Context Testing Use `MolluskContext` to persist account state across multiple instructions: ```rust use std::collections::HashMap; #[test] fn test_sequential_transfers() { let mollusk = Mollusk::default(); // Create initial account store let mut account_store = HashMap::new(); let alice = Pubkey::new_unique(); let bob = Pubkey::new_unique(); account_store.insert( alice, Account { lamports: 1_000_000, data: vec![], owner: system_program::id(), executable: false, rent_epoch: 0, }, ); account_store.insert( bob, Account { lamports: 0, data: vec![], owner: system_program::id(), executable: false, rent_epoch: 0, }, ); // Create stateful context let context = mollusk.with_context(account_store); // First transfer - state persists automatically let instruction1 = system_instruction::transfer(&alice, &bob, 200_000); context.process_instruction(&instruction1); // Second transfer - uses updated state from first transfer let instruction2 = system_instruction::transfer(&alice, &bob, 100_000); context.process_instruction(&instruction2); // Access final account state let store = context.account_store.borrow(); assert_eq!(store.get(&alice).unwrap().lamports, 700_000); assert_eq!(store.get(&bob).unwrap().lamports, 300_000); } ``` #### Instruction Chains with Validation Process multiple instructions and validate state after each: ```rust #[test] fn test_instruction_chain_with_checks() { let mollusk = Mollusk::default(); let alice = Pubkey::new_unique(); let bob = Pubkey::new_unique(); let carol = Pubkey::new_unique(); let starting_lamports = 1_000_000; mollusk.process_and_validate_instruction_chain( &[ ( &system_instruction::transfer(&alice, &bob, 300_000), &[ Check::success(), Check::account(&alice).lamports(700_000).build(), Check::account(&bob).lamports(300_000).build(), ], ), ( &system_instruction::transfer(&bob, &carol, 100_000), &[ Check::success(), Check::account(&bob).lamports(200_000).build(), Check::account(&carol).lamports(100_000).build(), ], ), ], &[ (alice, system_account(starting_lamports)), (bob, system_account(0)), (carol, system_account(0)), ], ); } ``` **Important:** Instruction chains are NOT equivalent to Solana transactions. Mollusk doesn't impose transaction constraints like loaded account keys or size limits. Chains are primarily for testing program execution flows. #### Time-Dependent Testing with warp_to_slot Test logic that depends on clock or slot: ```rust use solana_sdk::clock::Clock; #[test] fn test_time_dependent_logic() { let mut mollusk = Mollusk::default(); // Warp to a specific slot mollusk.warp_to_slot(1000); // Test logic that depends on clock.slot let result1 = mollusk.process_instruction(&time_check_ix, &accounts); assert!(result1.is_ok()); // Warp forward in time mollusk.warp_to_slot(2000); // Test again with new slot let result2 = mollusk.process_instruction(&time_check_ix, &accounts); assert!(result2.is_ok()); } ``` #### Custom Sysvar Configuration Modify sysvars to test specific conditions: ```rust use solana_sdk::rent::Rent; #[test] fn test_with_custom_rent() { let mut mollusk = Mollusk::default(); // Customize rent parameters mollusk.sysvars.rent = Rent { lamports_per_byte_year: 1, exemption_threshold: 1.0, burn_percent: 0, }; // Test with custom rent configuration let result = mollusk.process_instruction(&instruction, &accounts); assert!(result.is_ok()); } ``` #### Testing with Built-in Programs **Default builtins:** ```rust // Mollusk::default() includes subset of builtin programs let mollusk = Mollusk::default(); // Includes System, BPF Loader, etc. ``` **All builtins:** ```toml [dev-dependencies] mollusk-svm = { version = "0.9", features = ["all-builtins"] } ``` **Adding specific programs:** ```rust use mollusk_svm_programs_token::token; let mut mollusk = Mollusk::default(); token::add_program(&mut mollusk); // Add Token program ``` --- ## Anchor-Specific Testing ### anchor test Command and Workflow Anchor provides integrated testing via the `anchor test` command: ```bash # Run all tests anchor test # Run tests without rebuilding anchor test --skip-build # Run tests without deploying (use existing deployment) anchor test --skip-deploy # Run specific test file anchor test -- --test test_initialize # Show program logs anchor test -- --nocapture ``` **Standard workflow:** 1. `anchor build` - Build program 2. `anchor test` - Deploy to local validator and run TypeScript tests 3. Test files run against deployed program 4. Validator shuts down after tests complete ### TypeScript Tests with @coral-xyz/anchor **Basic test structure:** ```typescript import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { MyProgram } from "../target/types/my_program"; import { expect } from "chai"; describe("my-program", () => { // Configure the client to use the local cluster anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.MyProgram as Program<MyProgram>; it("Initializes the program", async () => { // Test implementation }); }); ``` ### Setting Up Test Environment ```typescript describe("my-program", () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.MyProgram as Program<MyProgram>; const wallet = provider.wallet as anchor.Wallet; // Generate keypairs const user = anchor.web3.Keypair.generate(); const account = anchor.web3.Keypair.generate(); before(async () => { // Airdrop SOL for testing const airdropSig = await provider.connection.requestAirdrop( user.publicKey, 2 * anchor.web3.LAMPORTS_PER_SOL ); await provider.connection.confirmTransaction(airdropSig); }); it("runs test", async () => { // Test code }); }); ``` ### Invoking Instructions ```typescript it("initializes account", async () => { const [pda, bump] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("seed"), user.publicKey.toBuffer()], program.programId ); const tx = await program.methods .initialize(bump) .accounts({ user: user.publicKey, account: pda, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([user]) .rpc(); console.log("Transaction signature:", tx); }); ``` **With custom transaction options:** ```typescript const tx = await program.methods .initialize(bump) .accounts({ /* ... */ }) .signers([user]) .rpc({ skipPreflight: false, commitment: "confirmed", }); ``` ### Reading Account State ```typescript it("reads account data", async () => { // Fetch account data const accountData = await program.account.myAccount.fetch(accountPubkey); // Assert values expect(accountData.value).to.equal(42); expect(accountData.owner.toString()).to.equal(user.publicKey.toString()); }); // Fetch multiple accounts const accounts = await program.account.myAccount.all(); console.log("Found accounts:", accounts.length); // Fetch with filters const filtered = await program.account.myAccount.all([ { memcmp: { offset: 8, // Skip discriminator bytes: user.publicKey.toBase58(), }, }, ]); ``` ### Event Listeners ```typescript it("listens for events", async () => { let eventReceived = false; // Set up event listener const listener = program.addEventListener( "MyEvent", (event, slot) => { console.log("Event received in slot:", slot); console.log("Event data:", event); eventReceived = true; } ); // Trigger event await program.methods .triggerEvent() .accounts({ /* ... */ }) .rpc(); // Wait for event await new Promise((resolve) => setTimeout(resolve, 1000)); expect(eventReceived).to.be.true; // Clean up listener await program.removeEventListener(listener); }); ``` ### LiteSVM for Fast Anchor Tests LiteSVM provides a faster alternative to the full validator for Anchor tests: **Installation:** ```bash cargo add litesvm --dev ``` **Basic usage:** ```rust use { litesvm::LiteSVM, solana_sdk::{ message::Message, pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction::transfer, transaction::Transaction, }, }; #[test] fn test_with_litesvm() { let from_keypair = Keypair::new(); let from = from_keypair.pubkey(); let to = Pubkey::new_unique(); let mut svm = LiteSVM::new(); svm.airdrop(&from, 10_000).unwrap(); let instruction = transfer(&from, &to, 64); let tx = Transaction::new( &[&from_keypair], Message::new(&[instruction], Some(&from)), svm.latest_blockhash(), ); let tx_res = svm.send_transaction(tx).unwrap(); let from_account = svm.get_account(&from); let to_account = svm.get_account(&to); assert_eq!(from_account.unwrap().lamports, 4936); assert_eq!(to_account.unwrap().lamports, 64); } ``` **Deploying programs:** ```rust use solana_sdk::pubkey; #[test] fn test_program() { let program_id = pubkey!("Logging111111111111111111111111111111111111"); let mut svm = LiteSVM::new(); // Load program from file let bytes = include_bytes!("../target/deploy/my_program.so"); svm.add_program(program_id, bytes); // Test program // ... } ``` **Time travel with LiteSVM:** ```rust use solana_sdk::clock::Clock; #[test] fn test_set_clock() { let mut svm = LiteSVM::new(); // Get current clock let mut clock = svm.get_sysvar::<Clock>(); // Set specific timestamp clock.unix_timestamp = 1735689600; // January 1st 2025 svm.set_sysvar::<Clock>(&clock); // Test time-dependent logic // ... // Warp to specific slot svm.warp_to_slot(1000); } ``` **Writing arbitrary accounts:** ```rust use { solana_sdk::account::Account, spl_token::state::Account as TokenAccount, }; #[test] fn test_with_token_account() { let mut svm = LiteSVM::new(); let user = Pubkey::new_unique(); let usdc_mint = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // Create fake USDC balance let token_account_data = /* serialize TokenAccount with balance */; svm.set_account( user, Account { lamports: 1_000_000, data: token_account_data, owner: spl_token::id(), executable: false, rent_epoch: 0, }, ); // Test with USDC balance // ... } ``` ### Anchor.toml Test Configuration Configure testing behavior in `Anchor.toml`: ```toml [toolchain] anchor_version = "0.30.1" [features] resolution = true skip-lint = false [programs.localnet] my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" [test] startup_wait = 5000 # Wait before running tests (ms) shutdown_wait = 2000 # Wait before shutting down validator (ms) upgradeable = false # Deploy as upgradeable program [test.validator] url = "https://api.mainnet-beta.solana.com" # Clone from mainnet ledger = ".anchor/test-ledger" bind_address = "0.0.0.0" [[test.validator.clone]] address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" # Clone Metaplex [[test.validator.clone]] address = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" # Clone Token program [[test.validator.account]] address = "..." # Clone specific account filename = "account.json" ``` ### Anchor Testing Best Practices 1. **Use `anchor.workspace`**: Automatically loads program IDL 2. **Airdrop SOL in `before()` hooks**: Set up test accounts before tests 3. **Use proper commitment levels**: `confirmed` or `finalized` for reliability 4. **Test error conditions**: Use `.simulate()` to test expected failures 5. **Clean up between tests**: Reset account state or use fresh keypairs 6. **Use `--skip-build` during iteration**: Speed up test runs 7. **Test with realistic data**: Don't just test happy paths --- ## Native Rust Testing ### Cargo Test Setup Native Rust programs use standard Rust testing with Mollusk: **Project structure:** ``` my-program/ ├── Cargo.toml ├── src/ │ ├── lib.rs │ ├── processor.rs │ └── instruction.rs └── tests/ ├── test_initialize.rs ├── test_update.rs └── test_close.rs ``` **Cargo.toml configuration:** ```toml [package] name = "my-program" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "lib"] [dependencies] solana-program = "2.1" [dev-dependencies] mollusk-svm = "0.9" mollusk-svm-programs-token = "0.9" solana-sdk = "2.1" [[bench]] name = "compute_units" harness = false [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ``` ### Mollusk with Native Programs **Basic test example:** ```rust // tests/test_initialize.rs use { mollusk_svm::Mollusk, my_program::{instruction::initialize, ID}, solana_sdk::{ account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, }, }; #[test] fn test_initialize() { let program_id = ID; let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let account = Pubkey::new_unique(); let instruction = Instruction { program_id, accounts: vec![ AccountMeta::new(user, true), AccountMeta::new(account, false), AccountMeta::new_readonly(solana_sdk::system_program::id(), false), ], data: initialize().data, }; let accounts = vec![ (user, Account { lamports: 10_000_000, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }), (account, Account { lamports: 0, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, }), ]; let result = mollusk.process_instruction(&instruction, &accounts); assert!(result.is_ok()); } ``` ### Manual Account Setup Native Rust tests require explicit account setup: ```rust use solana_sdk::account::Account; // Helper: Create system account fn system_account(lamports: u64) -> Account { Account { lamports, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, } } // Helper: Create program-owned account fn program_account(lamports: u64, data: Vec<u8>, owner: Pubkey) -> Account { Account { lamports, data, owner, executable: false, rent_epoch: 0, } } // Helper: Create rent-exempt account fn rent_exempt_account(data_len: usize, owner: Pubkey, mollusk: &Mollusk) -> Account { let lamports = mollusk.sysvars.rent.minimum_balance(data_len); Account { lamports, data: vec![0; data_len], owner, executable: false, rent_epoch: 0, } } // Usage #[test] fn test_with_helpers() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let data_account = Pubkey::new_unique(); let accounts = vec![ (user, system_account(10_000_000)), (data_account, rent_exempt_account(100, program_id, &mollusk)), ]; // Test // ... } ``` ### Testing CPIs Use `mollusk-svm-programs-token` for testing cross-program invocations: ```rust use { mollusk_svm::{result::Check, Mollusk}, mollusk_svm_programs_token::token, solana_sdk::{ account::Account, program_pack::Pack, pubkey::Pubkey, }, spl_token::state::{Account as TokenAccount, AccountState, Mint}, }; #[test] fn test_token_transfer_cpi() { // Initialize Mollusk with Token program let mut mollusk = Mollusk::default(); token::add_program(&mut mollusk); // Setup mint let mint = Pubkey::new_unique(); let decimals = 6; let mut mint_data = vec![0u8; Mint::LEN]; Mint::pack( Mint { mint_authority: Some(authority).into(), supply: 1_000_000, decimals, is_initialized: true, freeze_authority: None.into(), }, &mut mint_data, ).unwrap(); // Setup source token account let source = Pubkey::new_unique(); let mut source_data = vec![0u8; TokenAccount::LEN]; TokenAccount::pack( TokenAccount { mint, owner: authority, amount: 1_000_000, delegate: None.into(), state: AccountState::Initialized, is_native: None.into(), delegated_amount: 0, close_authority: None.into(), }, &mut source_data, ).unwrap(); // Setup destination token account let destination = Pubkey::new_unique(); let mut dest_data = vec![0u8; TokenAccount::LEN]; TokenAccount::pack( TokenAccount { mint, owner: recipient, amount: 0, delegate: None.into(), state: AccountState::Initialized, is_native: None.into(), delegated_amount: 0, close_authority: None.into(), }, &mut dest_data, ).unwrap(); let mint_rent = mollusk.sysvars.rent.minimum_balance(Mint::LEN); let account_rent = mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN); let accounts = vec![ (source, Account { lamports: account_rent, data: source_data, owner: token::ID, executable: false, rent_epoch: 0, }), (mint, Account { lamports: mint_rent, data: mint_data, owner: token::ID, executable: false, rent_epoch: 0, }), (destination, Account { lamports: account_rent, data: dest_data, owner: token::ID, executable: false, rent_epoch: 0, }), ]; // Create transfer instruction use spl_token::instruction::transfer_checked; let instruction = transfer_checked( &token::ID, &source, &mint, &destination, &authority, &[], 500_000, decimals, ).unwrap(); // Validate transfer let checks = vec![ Check::success(), Check::account(&source) .data_slice(64, &(500_000u64).to_le_bytes()) .build(), Check::account(&destination) .data_slice(64, &(500_000u64).to_le_bytes()) .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` ### Validation Patterns **Account state validation:** ```rust use mollusk_svm::result::Check; let checks = vec![ Check::success(), Check::account(&account_pubkey) .lamports(expected_lamports) .data(&expected_data) .owner(&expected_owner) .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); ``` **Error validation:** ```rust use solana_sdk::instruction::InstructionError; let checks = vec![ Check::instruction_err(InstructionError::InvalidAccountData), ]; mollusk.process_and_validate_instruction(&bad_instruction, &accounts, &checks); ``` **Compute unit validation:** ```rust let checks = vec![ Check::success(), Check::compute_units(5000), // Exactly 5000 CU ]; ``` **Data slice validation:** ```rust // Check specific bytes without loading full account data let checks = vec![ Check::account(&account) .data_slice(8, &[1, 2, 3, 4]) // Check bytes 8-11 .build(), ]; ``` --- ## Next Steps - For the testing strategy overview and pyramid structure, see **[Testing Overview](./testing-overview.md)** - For best practices, common patterns, and additional resources, see **[Testing Best Practices](./testing-practices.md)** -
testing-overview.md 12.5 KB
# Solana Program Testing Overview **High-level guide to testing Solana programs with the test pyramid structure** This file provides an overview of Solana program testing, the testing pyramid structure, and the types of tests you should write. For specific implementation details and framework-specific guidance, see the related files. --- ## Related Testing Documentation - **[Testing Frameworks](./testing-frameworks.md)** - Mollusk, LiteSVM, and Anchor testing implementations - **[Testing Best Practices](./testing-practices.md)** - Best practices, common patterns, and additional resources --- ## Table of Contents 1. [Why Testing Matters](#why-testing-matters) 2. [Types of Tests](#types-of-tests) 3. [Testing Frameworks Available](#testing-frameworks-available) 4. [Test Structure Pyramid](#test-structure-pyramid) --- ## Why Testing Matters for Solana Programs Solana programs are immutable after deployment and handle real financial assets. Comprehensive testing is critical to: - **Prevent loss of funds**: Bugs in deployed programs can lead to irreversible financial losses - **Ensure correctness**: Verify program logic works as intended under all conditions - **Optimize performance**: Monitor compute unit usage to stay within Solana's limits (1.4M CU cap) - **Build confidence**: Thorough testing enables safer deployments and upgrades - **Catch edge cases**: Test boundary conditions, error handling, and attack vectors --- ## Types of Tests **Unit Tests** - Test individual functions and instruction handlers in isolation - Fast, focused validation of specific logic - Run frequently during development **Integration Tests** - Test complete instruction flows with realistic account setups - Validate cross-program invocations (CPIs) - Ensure proper state transitions **Fuzz Tests** - Generate random inputs to find edge cases and vulnerabilities - Discover unexpected failure modes - Test input validation thoroughly **Compute Unit Benchmarks** - Monitor compute unit consumption for each instruction - Track performance regressions - Ensure programs stay within CU limits --- ## Testing Frameworks Available **Mollusk** (Recommended for both Anchor and Native Rust) - Lightweight SVM test harness - Exceptionally fast (no validator overhead) - Works with both Anchor and native Rust programs - Direct program execution via BPF loader - Requires explicit account setup (no AccountsDB) **LiteSVM** (Alternative for integration tests) - In-process Solana VM for testing - Available in Rust, TypeScript, and Python - Faster than solana-program-test - Supports RPC-like interactions - Good for complex integration scenarios **Anchor Test** (Anchor framework) - TypeScript-based testing using @coral-xyz/anchor - Integrates with local validator or LiteSVM - Natural for testing Anchor programs from client perspective - Slower but more realistic end-to-end tests **solana-program-test** (Legacy) - Full validator simulation - More realistic but much slower - Generally replaced by Mollusk and LiteSVM **Recommendation**: Use Mollusk for fast unit and integration tests. Use LiteSVM or Anchor tests for end-to-end validation when needed. --- ## Test Structure Pyramid ### Overview A production-grade Solana program should have a multi-level testing strategy. Each level serves a specific purpose and catches different types of bugs. ``` ┌─────────────────────┐ │ Devnet/Mainnet │ ← Smoke tests │ Smoke Tests │ (Manual, slow) └─────────────────────┘ ┌───────────────────────────┐ │ SDK Integration Tests │ ← Full transaction flow │ (LiteSVM/TypeScript) │ (Seconds per test) └───────────────────────────┘ ┌─────────────────────────────────────┐ │ Mollusk Program Tests │ ← Instruction-level │ (Unit + Integration in Rust) │ (~100ms per test) └─────────────────────────────────────┘ ┌───────────────────────────────────────────────┐ │ Inline Unit Tests (#[cfg(test)]) │ ← Pure functions │ (Math, validation, transformations) │ (Milliseconds) └───────────────────────────────────────────────┘ ``` ### Level 1: Inline Unit Tests **Purpose:** Test pure functions in isolation - math, validation logic, data transformations. **Location:** Inside your program code with `#[cfg(test)]` **Why needed:** - Instant feedback (milliseconds) - Runs with `cargo test` - no build artifacts needed - Catches arithmetic edge cases before they reach the SVM - Documents expected behavior inline with code **What belongs here:** - Share calculations: `1_000_000 * 5000 / 10000 = 500_000` - Overflow detection: `u64::MAX * 10000 = None` - Rounding behavior: `100 * 1 / 10000 = 0` (floors) - BPS (basis points) sum validation - Data serialization/deserialization helpers **What doesn't belong:** - Account validation (needs ownership checks) - CPI logic - Full instruction execution - State transitions **Example:** ```rust // In your program code (e.g., src/math.rs) pub fn calculate_fee(amount: u64, fee_bps: u16) -> Option<u64> { let fee = (amount as u128) .checked_mul(fee_bps as u128)? .checked_div(10_000)?; Some(fee as u64) } #[cfg(test)] mod tests { use super::*; #[test] fn test_calculate_fee_basic() { assert_eq!(calculate_fee(1_000_000, 250), Some(25_000)); // 2.5% assert_eq!(calculate_fee(1_000_000, 5000), Some(500_000)); // 50% } #[test] fn test_calculate_fee_rounding() { assert_eq!(calculate_fee(100, 1), Some(0)); // Rounds down assert_eq!(calculate_fee(10_000, 1), Some(1)); // 0.01% } #[test] fn test_calculate_fee_overflow() { assert_eq!(calculate_fee(u64::MAX, 10000), None); // Would overflow } } ``` ### Level 2: Mollusk Program Tests **Purpose:** Test individual instructions with full account setup but without validator overhead. **Location:** `tests/` directory or `#[cfg(test)]` modules **Why needed:** - Tests actual program binary execution - Validates account constraints, signer checks, ownership - ~100ms per test vs ~1s for full validator - Catches instruction-level bugs - Compute unit benchmarking **What belongs here:** - Each instruction handler (initialize, create_split, execute_split, etc.) - Error conditions (wrong signer, invalid account owner) - Account state transitions - Cross-program invocations (CPIs) - PDA derivation and signing - Rent exemption validation **Example:** ```rust // tests/test_initialize.rs use { mollusk_svm::Mollusk, my_program::{instruction::initialize, ID}, solana_sdk::{ account::Account, instruction::Instruction, pubkey::Pubkey, }, }; #[test] fn test_initialize_success() { let mollusk = Mollusk::new(&ID, "target/deploy/my_program"); let user = Pubkey::new_unique(); let account = Pubkey::new_unique(); let instruction = initialize(&user, &account); let accounts = vec![ (user, system_account(10_000_000)), (account, Account::default()), ]; let result = mollusk.process_instruction(&instruction, &accounts); assert!(result.is_ok()); } #[test] fn test_initialize_wrong_signer_fails() { let mollusk = Mollusk::new(&ID, "target/deploy/my_program"); let user = Pubkey::new_unique(); let wrong_signer = Pubkey::new_unique(); let mut instruction = initialize(&user, &Pubkey::new_unique()); instruction.accounts[0].is_signer = false; // Missing signature let accounts = vec![(user, system_account(10_000_000))]; let checks = vec![Check::instruction_err( InstructionError::MissingRequiredSignature )]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` ### Level 3: SDK Integration Tests **Purpose:** Test that SDK produces correct instructions that work end-to-end. **Location:** Separate SDK package (`sdk/tests/`) or TypeScript tests **Why needed:** - Validates serialization matches program expectations - Tests full transaction flow (multiple instructions) - Catches SDK bugs before users hit them - Client-perspective testing - Ensures TypeScript/Rust SDK matches program **What belongs here:** - SDK instruction builders produce valid transactions - Full flows: create → deposit → execute - Multiple instructions in one transaction - Account resolution (finding PDAs from SDK) - Error handling from client side - Event parsing and decoding **Example (LiteSVM):** ```rust // sdk/tests/integration_test.rs use { litesvm::LiteSVM, my_program_sdk::{instructions, MyProgramClient}, solana_sdk::{ signature::Keypair, signer::Signer, }, }; #[test] fn test_full_flow_create_and_execute() { let mut svm = LiteSVM::new(); // Add program let program_bytes = include_bytes!("../../target/deploy/my_program.so"); svm.add_program(MY_PROGRAM_ID, program_bytes); // Create client let payer = Keypair::new(); svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap(); let client = MyProgramClient::new(&svm, &payer); // Step 1: Initialize let tx1 = client.initialize().unwrap(); svm.send_transaction(tx1).unwrap(); // Step 2: Deposit let tx2 = client.deposit(1_000_000).unwrap(); svm.send_transaction(tx2).unwrap(); // Step 3: Execute let tx3 = client.execute().unwrap(); let result = svm.send_transaction(tx3).unwrap(); // Verify final state let account = client.get_account().unwrap(); assert_eq!(account.balance, 1_000_000); } ``` ### Level 4: Devnet/Mainnet Smoke Tests **Purpose:** Final validation in real environment. **Location:** Manual testing or automated CI scripts **Why needed:** - Real RPC, real fees, real constraints - Validates deployment configuration - Tests against actual on-chain state - Catches environment-specific issues - Verifies upgrades work correctly **What belongs here:** - Post-deployment smoke tests (critical paths only) - Upgrade validation (new version works) - Integration with other mainnet programs - Performance under real network conditions **Example (Manual script):** ```bash #!/bin/bash # scripts/smoke-test-devnet.sh echo "Running devnet smoke tests..." # Test 1: Initialize solana-keygen new --no-bpf-loader-deprecated --force -o /tmp/test-user.json solana airdrop 2 /tmp/test-user.json --url devnet my-program-cli initialize \ --program-id $PROGRAM_ID \ --payer /tmp/test-user.json \ --url devnet # Test 2: Execute main flow my-program-cli execute \ --amount 1000000 \ --payer /tmp/test-user.json \ --url devnet echo "✅ Smoke tests passed" ``` ### How to Use This Pyramid **During development:** 1. Write inline tests as you implement math/validation 2. Write Mollusk tests for each instruction 3. Run frequently: `cargo test` **Before PR/merge:** 1. Ensure all inline + Mollusk tests pass 2. Add SDK integration tests if SDK changed 3. Run compute unit benchmarks **Before deployment:** 1. All tests pass on devnet-compatible build 2. Deploy to devnet 3. Run manual smoke tests on devnet 4. If pass, proceed to mainnet **After deployment:** 1. Run smoke tests on mainnet 2. Monitor for errors 3. Keep tests updated as program evolves ### Benefits of This Structure **Fast feedback loop:** - Level 1 tests run in milliseconds - Catch bugs early without slow iteration **Comprehensive coverage:** - Pure logic (Level 1) - Program execution (Level 2) - Client integration (Level 3) - Real environment (Level 4) **Efficient CI/CD:** - Level 1-2 in every PR (fast) - Level 3 on merge to main - Level 4 post-deployment **Clear responsibilities:** - Each level tests different concerns - No redundant tests - Easier to maintain --- ## Next Steps - For implementation details on Mollusk, LiteSVM, and Anchor testing, see **[Testing Frameworks](./testing-frameworks.md)** - For best practices, common patterns, and additional resources, see **[Testing Best Practices](./testing-practices.md)** -
testing-practices.md 12.6 KB
# Solana Program Testing Best Practices **Common patterns, best practices, and additional testing resources** This file provides best practices for organizing tests, testing common scenarios, and efficiently running your test suite. For framework-specific details and the testing pyramid structure, see the related files. --- ## Related Testing Documentation - **[Testing Overview](./testing-overview.md)** - Testing pyramid structure and types of tests - **[Testing Frameworks](./testing-frameworks.md)** - Mollusk, LiteSVM, and Anchor testing implementations --- ## Table of Contents 1. [Testing Best Practices](#testing-best-practices) 2. [Common Testing Patterns](#common-testing-patterns) 3. [Additional Resources](#additional-resources) --- ## Testing Best Practices ### Test Organization **Organize by instruction:** ``` tests/ ├── test_initialize.rs ├── test_update.rs ├── test_transfer.rs ├── test_close.rs └── helpers/ ├── mod.rs ├── accounts.rs └── instructions.rs ``` **Use helper modules:** ```rust // tests/helpers/accounts.rs use solana_sdk::{account::Account, pubkey::Pubkey}; pub fn system_account(lamports: u64) -> Account { Account { lamports, data: vec![], owner: solana_sdk::system_program::id(), executable: false, rent_epoch: 0, } } pub fn token_account(/* ... */) -> Account { // ... } ``` ```rust // tests/test_initialize.rs mod helpers; use helpers::accounts::*; #[test] fn test_initialize() { let accounts = vec![ (user, system_account(10_000_000)), // ... ]; } ``` ### Edge Cases to Test **Account validation:** - Missing accounts - Wrong account owner - Account not writable when required - Account not signer when required - Uninitialized accounts - Already initialized accounts **Numeric boundaries:** - Zero values - Maximum values (u64::MAX) - Overflow conditions - Underflow conditions - Negative results (when using signed integers) **Authorization:** - Missing signer - Wrong signer - Multiple signers - PDA signer validation **State transitions:** - Invalid state transitions - Idempotent operations - Concurrent operations - State rollback on error **Resource limits:** - Rent exemption - Maximum account size - Compute unit limits - Stack depth limits (CPI) ### Error Condition Testing **Test expected failures:** ```rust #[test] fn test_insufficient_funds_fails() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let accounts = vec![ (user, system_account(100)), // Not enough lamports ]; let instruction = /* create transfer instruction for 1000 lamports */; let checks = vec![ Check::instruction_err(InstructionError::InsufficientFunds), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` **Test invalid data:** ```rust #[test] fn test_invalid_instruction_data() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let instruction = Instruction { program_id, accounts: /* ... */, data: vec![255, 255, 255], // Invalid instruction data }; let checks = vec![ Check::instruction_err(InstructionError::InvalidInstructionData), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` ### Compute Unit Monitoring **Set up continuous monitoring:** ```rust // benches/compute_units.rs use mollusk_svm_bencher::MolluskComputeUnitBencher; fn main() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let bencher = MolluskComputeUnitBencher::new(mollusk); // Benchmark each instruction bencher.bench(("initialize", &init_ix, &init_accounts)); bencher.bench(("update", &update_ix, &update_accounts)); bencher.bench(("close", &close_ix, &close_accounts)); bencher .must_pass(true) .out_dir("./target/benches") .execute(); } ``` **Add to CI/CD:** ```yaml # .github/workflows/test.yml - name: Run compute unit benchmarks run: cargo bench - name: Check for CU regressions run: | if git diff --exit-code target/benches/; then echo "No compute unit changes" else echo "Compute unit usage changed - review carefully" git diff target/benches/ fi ``` ### Running Tests Efficiently **Build before testing:** ```bash # Native Rust cargo build-sbf && cargo test # Anchor anchor build && anchor test ``` **Run specific tests:** ```bash # Native Rust cargo test test_initialize # Anchor anchor test -- --test test_initialize ``` **Show program output:** ```bash # Native Rust cargo test -- --nocapture # Anchor anchor test -- --nocapture ``` **Run tests in parallel (be careful with shared state):** ```bash cargo test -- --test-threads=4 ``` --- ## Common Testing Patterns ### Testing PDAs **Anchor approach:** ```typescript it("derives PDA correctly", async () => { const [pda, bump] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("seed"), user.publicKey.toBuffer()], program.programId ); await program.methods .initialize(bump) .accounts({ pda: pda, user: user.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([user]) .rpc(); const accountData = await program.account.myAccount.fetch(pda); expect(accountData.bump).to.equal(bump); }); ``` **Native Rust approach:** ```rust #[test] fn test_pda_derivation() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let seeds = &[b"seed", user.as_ref()]; let (pda, bump) = Pubkey::find_program_address(seeds, &program_id); let instruction = Instruction { program_id, accounts: vec![ AccountMeta::new(user, true), AccountMeta::new(pda, false), AccountMeta::new_readonly(system_program::id(), false), ], data: vec![0, bump], // Initialize instruction with bump }; let accounts = vec![ (user, system_account(10_000_000)), (pda, Account::default()), ]; let checks = vec![ Check::success(), Check::account(&pda) .owner(&program_id) .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` ### Testing Token Operations **Anchor with SPL Token:** ```typescript import { TOKEN_PROGRAM_ID, createMint, createAccount, mintTo } from "@solana/spl-token"; it("transfers tokens", async () => { // Create mint const mint = await createMint( provider.connection, wallet.payer, wallet.publicKey, null, 6 ); // Create token accounts const sourceAccount = await createAccount( provider.connection, wallet.payer, mint, user.publicKey ); const destAccount = await createAccount( provider.connection, wallet.payer, mint, recipient.publicKey ); // Mint tokens await mintTo( provider.connection, wallet.payer, mint, sourceAccount, wallet.publicKey, 1_000_000 ); // Transfer via program await program.methods .transferTokens(new anchor.BN(500_000)) .accounts({ source: sourceAccount, destination: destAccount, authority: user.publicKey, tokenProgram: TOKEN_PROGRAM_ID, }) .signers([user]) .rpc(); // Verify balances const sourceData = await getAccount(provider.connection, sourceAccount); const destData = await getAccount(provider.connection, destAccount); expect(sourceData.amount).to.equal(500_000n); expect(destData.amount).to.equal(500_000n); }); ``` **Native Rust with Mollusk:** See the [Testing CPIs](./testing-frameworks.md#testing-cpis) section in Testing Frameworks for a complete token transfer example. ### Testing Associated Token Accounts **Create ATA:** ```typescript import { getAssociatedTokenAddress } from "@solana/spl-token"; it("creates associated token account", async () => { const ata = await getAssociatedTokenAddress( mint, user.publicKey ); await program.methods .createAta() .accounts({ ata: ata, mint: mint, owner: user.publicKey, payer: wallet.publicKey, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .rpc(); const account = await getAccount(provider.connection, ata); expect(account.owner.toString()).to.equal(user.publicKey.toString()); expect(account.mint.toString()).to.equal(mint.toString()); }); ``` ### Testing Account Validation **Validate account owner:** ```rust #[test] fn test_wrong_owner_fails() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let account = Pubkey::new_unique(); let wrong_owner = Pubkey::new_unique(); let accounts = vec![ (account, Account { lamports: 1_000_000, data: vec![0; 100], owner: wrong_owner, // Wrong owner! executable: false, rent_epoch: 0, }), ]; let instruction = /* create instruction */; let checks = vec![ Check::instruction_err(InstructionError::InvalidAccountOwner), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` **Validate signer:** ```rust #[test] fn test_missing_signer_fails() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let user = Pubkey::new_unique(); let instruction = Instruction { program_id, accounts: vec![ AccountMeta::new(user, false), // Should be signer! ], data: vec![], }; let accounts = vec![ (user, system_account(1_000_000)), ]; let checks = vec![ Check::instruction_err(InstructionError::MissingRequiredSignature), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` ### Testing Rent Exemption ```rust #[test] fn test_account_is_rent_exempt() { let mollusk = Mollusk::new(&program_id, "target/deploy/my_program"); let account = Pubkey::new_unique(); let data_len = 100; let rent = mollusk.sysvars.rent; let rent_exempt_lamports = rent.minimum_balance(data_len); let accounts = vec![ (account, Account { lamports: rent_exempt_lamports, data: vec![0; data_len], owner: program_id, executable: false, rent_epoch: 0, }), ]; let instruction = /* create instruction */; let checks = vec![ Check::success(), Check::account(&account) .rent_exempt() .build(), ]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } ``` --- ## Additional Resources ### Documentation - **Mollusk GitHub**: https://github.com/anza-xyz/mollusk - **Mollusk Examples**: https://github.com/anza-xyz/mollusk/tree/main/harness/tests - **Mollusk API Docs**: https://docs.rs/mollusk-svm/latest/mollusk_svm/ - **Anchor Testing Guide**: https://www.anchor-lang.com/docs/testing - **LiteSVM**: https://github.com/amilz/litesvm - **Solana Testing Docs**: https://solana.com/docs/programs/testing ### Key Takeaways 1. **Use Mollusk for fast, focused tests** - It's the recommended approach for both Anchor and native Rust programs 2. **Test early and often** - Catching bugs before deployment saves time and money 3. **Test error conditions** - Don't just test happy paths 4. **Monitor compute units** - Use benchmarking to catch performance regressions 5. **Organize tests logically** - Group by instruction, use helper modules 6. **Build before testing** - Always run `cargo build-sbf` or `anchor build` before tests 7. **Use validation checks** - Leverage the `Check` API for comprehensive validation 8. **Test with realistic data** - Use proper rent-exempt balances and realistic account states ### Quick Reference Commands ```bash # Native Rust cargo build-sbf # Build program cargo test # Run tests cargo test -- --nocapture # Run tests with output cargo test test_name # Run specific test cargo bench # Run compute unit benchmarks # Anchor anchor build # Build program anchor test # Build, deploy, and test anchor test --skip-build # Test without rebuilding anchor test -- --nocapture # Test with logs anchor test -- --test test_name # Run specific test ``` --- ## Next Steps - For the testing strategy overview and pyramid structure, see **[Testing Overview](./testing-overview.md)** - For framework-specific implementation details, see **[Testing Frameworks](./testing-frameworks.md)** -
tokens-2022.md 5.2 KB
# SPL Token-2022 (Token Extensions Program) Token Extensions Program (Token-2022) guide covering extension types, setup for both Anchor and Native Rust, and practical examples including transfer hooks. Includes extension configuration, space calculation, and initialization patterns. **For related topics, see:** - **[tokens-overview.md](tokens-overview.md)** - Token fundamentals and account structures - **[tokens-operations.md](tokens-operations.md)** - Create, mint, transfer, burn, close operations - **[tokens-validation.md](tokens-validation.md)** - Account validation patterns - **[tokens-patterns.md](tokens-patterns.md)** - Common patterns and security ## Table of Contents 1. [What are Token Extensions?](#what-are-token-extensions) 2. [Available Extensions](#available-extensions) 3. [Using Token-2022 in Anchor](#using-token-2022-in-anchor) 4. [Using Token-2022 in Native Rust](#using-token-2022-in-native-rust) 5. [Transfer Hook Extension Example](#transfer-hook-extension-example-anchor) --- ## What are Token Extensions? The Token Extensions Program (Token-2022) provides additional features through extensions. Extensions are optional functionality that can be added to a token mint or token account. **Key Points:** - Extensions must be enabled during account creation - Cannot add extensions after creation - Some extensions are incompatible with each other - Extensions add state to the `tlv_data` field --- ## Available Extensions ```rust pub enum ExtensionType { TransferFeeConfig, // Transfer fees TransferFeeAmount, // Withheld fees MintCloseAuthority, // Close mint accounts ConfidentialTransferMint, // Confidential transfers DefaultAccountState, // Default state for new accounts ImmutableOwner, // Cannot change owner MemoTransfer, // Require memos NonTransferable, // Cannot transfer tokens InterestBearingConfig, // Tokens accrue interest PermanentDelegate, // Permanent delegate authority TransferHook, // Custom transfer logic MetadataPointer, // Point to metadata TokenMetadata, // On-chain metadata GroupPointer, // Token groups TokenGroup, // Group config GroupMemberPointer, // Group membership TokenGroupMember, // Member config // ... and more } ``` --- ## Using Token-2022 in Anchor ```rust use anchor_spl::token_2022::{self, Token2022}; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct CreateToken2022Mint<'info> { #[account( init, payer = payer, mint::decimals = 9, mint::authority = mint_authority, mint::token_program = token_program, )] pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Mint authority pub mint_authority: UncheckedAccount<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Program<'info, Token2022>, pub system_program: Program<'info, System>, } ``` **Note:** The `anchor-spl` crate includes the `token_2022_extensions` module for working with extensions, but not all extension instructions are fully implemented yet. You may need to manually implement CPI calls for some extensions. --- ## Using Token-2022 in Native Rust ```rust use spl_token_2022::{ extension::ExtensionType, instruction::initialize_mint2, }; pub fn create_token_2022_mint( payer: &AccountInfo, mint: &AccountInfo, mint_authority: &Pubkey, decimals: u8, extensions: &[ExtensionType], ) -> ProgramResult { // Calculate space needed for extensions let mut space = 82; // Base mint size for extension in extensions { space += extension.get_account_len(); } // Create account with proper size // ... (similar to regular mint creation) // Initialize extensions // Each extension has its own initialization instruction // Finally initialize mint invoke( &initialize_mint2( &spl_token_2022::ID, mint.key, mint_authority, None, decimals, )?, &[mint.clone()], )?; Ok(()) } ``` --- ## Transfer Hook Extension Example (Anchor) ```rust use anchor_lang::prelude::*; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; #[program] pub mod transfer_hook { use super::*; #[interface(spl_transfer_hook_interface::execute)] pub fn execute_transfer_hook( ctx: Context<TransferHook>, amount: u64, ) -> Result<()> { msg!("Transfer hook called! Amount: {}", amount); // Custom transfer logic here Ok(()) } } #[derive(Accounts)] pub struct TransferHook<'info> { pub source: InterfaceAccount<'info, TokenAccount>, pub destination: InterfaceAccount<'info, TokenAccount>, /// CHECK: authority pub authority: UncheckedAccount<'info>, } ``` --- ## Next Steps - **Common Patterns**: See [tokens-patterns.md](tokens-patterns.md) for escrow, staking, NFT creation patterns - **Security**: See [tokens-patterns.md](tokens-patterns.md) for comprehensive security best practices -
tokens-operations.md 22.6 KB
# SPL Token Program - Operations Complete guide to SPL Token operations including creating mints, minting tokens, transferring (with transfer_checked), burning, and closing token accounts. Shows both Anchor and Native Rust implementations side-by-side. **For related topics, see:** - **[tokens-overview.md](tokens-overview.md)** - Token fundamentals and account structures - **[tokens-validation.md](tokens-validation.md)** - Account validation patterns - **[tokens-2022.md](tokens-2022.md)** - Token Extensions Program features - **[tokens-patterns.md](tokens-patterns.md)** - Common patterns and security ## Table of Contents 1. [Creating Tokens](#creating-tokens) 2. [Minting Tokens](#minting-tokens) 3. [Transferring Tokens](#transferring-tokens) 4. [Burning Tokens](#burning-tokens) 5. [Closing Token Accounts](#closing-token-accounts) --- ## Creating Tokens ### Initialize a New Mint #### Using Anchor ```rust use anchor_spl::token_interface::{Mint, TokenInterface}; #[derive(Accounts)] pub struct CreateMint<'info> { #[account( init, payer = payer, mint::decimals = 9, mint::authority = mint_authority, mint::freeze_authority = freeze_authority, mint::token_program = token_program, )] pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Can be any account pub mint_authority: UncheckedAccount<'info>, /// CHECK: Can be any account (optional) pub freeze_authority: UncheckedAccount<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } pub fn create_mint(ctx: Context<CreateMint>) -> Result<()> { // Mint is automatically created and initialized by Anchor constraints msg!("Mint created: {}", ctx.accounts.mint.key()); Ok(()) } ``` **Key Anchor Constraints:** - `init` - Creates and initializes the account - `mint::decimals` - Number of decimal places - `mint::authority` - Who can mint tokens - `mint::freeze_authority` - Who can freeze token accounts (optional) - `mint::token_program` - Which token program to use #### Using Native Rust ```rust use spl_token::instruction::initialize_mint; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, rent::Rent, system_instruction, sysvar::Sysvar, }; pub fn create_mint( payer: &AccountInfo, mint_account: &AccountInfo, mint_authority: &Pubkey, freeze_authority: Option<&Pubkey>, decimals: u8, system_program: &AccountInfo, token_program: &AccountInfo, rent_sysvar: &AccountInfo, ) -> ProgramResult { // Mint account size let mint_size = 82; // Calculate rent let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(mint_size); // Create mint account via System Program invoke( &system_instruction::create_account( payer.key, mint_account.key, rent_lamports, mint_size as u64, &spl_token::ID, ), &[payer.clone(), mint_account.clone(), system_program.clone()], )?; // Initialize mint invoke( &initialize_mint( token_program.key, mint_account.key, mint_authority, freeze_authority, decimals, )?, &[ mint_account.clone(), rent_sysvar.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Initialize a Token Account (Non-ATA) #### Using Anchor ```rust use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct CreateTokenAccount<'info> { #[account( init, payer = payer, token::mint = mint, token::authority = owner, token::token_program = token_program, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Can be any account pub owner: UncheckedAccount<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } pub fn create_token_account(ctx: Context<CreateTokenAccount>) -> Result<()> { // Token account is automatically created and initialized Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::initialize_account3; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, rent::Rent, system_instruction, sysvar::Sysvar, }; pub fn create_token_account( payer: &AccountInfo, token_account: &AccountInfo, mint: &AccountInfo, owner: &Pubkey, system_program: &AccountInfo, token_program: &AccountInfo, ) -> ProgramResult { // Token account size let token_account_size = 165; // Calculate rent let rent = Rent::get()?; let rent_lamports = rent.minimum_balance(token_account_size); // Create token account invoke( &system_instruction::create_account( payer.key, token_account.key, rent_lamports, token_account_size as u64, &spl_token::ID, ), &[payer.clone(), token_account.clone(), system_program.clone()], )?; // Initialize token account invoke( &initialize_account3( token_program.key, token_account.key, mint.key, owner, )?, &[token_account.clone(), mint.clone(), token_program.clone()], )?; Ok(()) } ``` --- ## Minting Tokens ### Basic Minting (User Authority) #### Using Anchor ```rust use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct MintTokens<'info> { #[account(mut)] pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint_authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> { let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.mint_authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_context = CpiContext::new(cpi_program, cpi_accounts); token_interface::mint_to(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::mint_to; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, }; pub fn mint_tokens( mint: &AccountInfo, destination: &AccountInfo, mint_authority: &AccountInfo, amount: u64, token_program: &AccountInfo, ) -> ProgramResult { // Mint authority must be a signer if !mint_authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } invoke( &mint_to( token_program.key, mint.key, destination.key, mint_authority.key, &[], // No multisig signers amount, )?, &[ mint.clone(), destination.clone(), mint_authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Minting with PDA Authority #### Using Anchor ```rust use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct MintWithPDA<'info> { #[account( mut, mint::authority = mint_authority, )] pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub token_account: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [b"mint-authority"], bump, )] /// CHECK: PDA signer pub mint_authority: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn mint_with_pda(ctx: Context<MintWithPDA>, amount: u64) -> Result<()> { let seeds = &[ b"mint-authority", &[ctx.bumps.mint_authority], ]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.mint_authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ).with_signer(signer_seeds); token_interface::mint_to(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::mint_to; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey, }; pub fn mint_tokens_from_pda( program_id: &Pubkey, mint: &AccountInfo, destination: &AccountInfo, mint_authority_pda: &AccountInfo, token_program: &AccountInfo, amount: u64, pda_seeds: &[&[u8]], bump: u8, ) -> ProgramResult { // Validate PDA let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id); if expected_pda != *mint_authority_pda.key { return Err(ProgramError::InvalidSeeds); } // Prepare signer seeds let mut full_seeds = pda_seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; invoke_signed( &mint_to( token_program.key, mint.key, destination.key, mint_authority_pda.key, &[], amount, )?, &[ mint.clone(), destination.clone(), mint_authority_pda.clone(), token_program.clone(), ], signer_seeds, )?; Ok(()) } ``` --- ## Transferring Tokens ### Basic Transfer #### Using Anchor ```rust use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer}; #[derive(Accounts)] pub struct TransferTokens<'info> { #[account(mut)] pub from: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub to: InterfaceAccount<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ); token_interface::transfer(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::transfer; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, }; pub fn transfer_tokens( source: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, amount: u64, token_program: &AccountInfo, ) -> ProgramResult { // Authority must be a signer if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } invoke( &transfer( token_program.key, source.key, destination.key, authority.key, &[], // No multisig signers amount, )?, &[ source.clone(), destination.clone(), authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Transfer with Checks (Recommended) #### Using Anchor ```rust use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked}; #[derive(Accounts)] pub struct TransferTokensChecked<'info> { #[account(mut)] pub from: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub to: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn transfer_tokens_checked( ctx: Context<TransferTokensChecked>, amount: u64 ) -> Result<()> { token_interface::transfer_checked( CpiContext::new( ctx.accounts.token_program.to_account_info(), TransferChecked { from: ctx.accounts.from.to_account_info(), mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, ctx.accounts.mint.decimals, )?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::transfer_checked; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, }; pub fn transfer_tokens_checked( source: &AccountInfo, mint: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, amount: u64, decimals: u8, token_program: &AccountInfo, ) -> ProgramResult { if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } invoke( &transfer_checked( token_program.key, source.key, mint.key, destination.key, authority.key, &[], amount, decimals, )?, &[ source.clone(), mint.clone(), destination.clone(), authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Transfer with PDA Signer #### Using Anchor ```rust use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer}; #[derive(Accounts)] pub struct TransferWithPDA<'info> { #[account( mut, token::authority = authority, )] pub from: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub to: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [b"authority"], bump, )] /// CHECK: PDA signer pub authority: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn transfer_with_pda(ctx: Context<TransferWithPDA>, amount: u64) -> Result<()> { let seeds = &[ b"authority", &[ctx.bumps.authority], ]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ).with_signer(signer_seeds); token_interface::transfer(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::transfer; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey, }; pub fn transfer_tokens_from_pda( program_id: &Pubkey, source: &AccountInfo, destination: &AccountInfo, authority_pda: &AccountInfo, token_program: &AccountInfo, amount: u64, pda_seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id); if expected_pda != *authority_pda.key { return Err(ProgramError::InvalidSeeds); } let mut full_seeds = pda_seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; invoke_signed( &transfer( token_program.key, source.key, destination.key, authority_pda.key, &[], amount, )?, &[ source.clone(), destination.clone(), authority_pda.clone(), token_program.clone(), ], signer_seeds, )?; Ok(()) } ``` --- ## Burning Tokens ### Basic Burn #### Using Anchor ```rust use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct BurnTokens<'info> { #[account(mut)] pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub token_account: InterfaceAccount<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn burn_tokens(ctx: Context<BurnTokens>, amount: u64) -> Result<()> { let cpi_accounts = Burn { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ); token_interface::burn(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::burn; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, }; pub fn burn_tokens( token_account: &AccountInfo, mint: &AccountInfo, authority: &AccountInfo, amount: u64, token_program: &AccountInfo, ) -> ProgramResult { if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } invoke( &burn( token_program.key, token_account.key, mint.key, authority.key, &[], amount, )?, &[ token_account.clone(), mint.clone(), authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` ### Burn with PDA Authority #### Using Anchor ```rust use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct BurnWithPDA<'info> { #[account(mut)] pub mint: InterfaceAccount<'info, Mint>, #[account( mut, token::authority = authority, )] pub token_account: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [b"burn-authority"], bump, )] /// CHECK: PDA signer pub authority: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn burn_with_pda(ctx: Context<BurnWithPDA>, amount: u64) -> Result<()> { let seeds = &[ b"burn-authority", &[ctx.bumps.authority], ]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = Burn { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ).with_signer(signer_seeds); token_interface::burn(cpi_context, amount)?; Ok(()) } ``` #### Using Native Rust ```rust pub fn burn_tokens_from_pda( program_id: &Pubkey, token_account: &AccountInfo, mint: &AccountInfo, authority_pda: &AccountInfo, token_program: &AccountInfo, amount: u64, pda_seeds: &[&[u8]], bump: u8, ) -> ProgramResult { let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id); if expected_pda != *authority_pda.key { return Err(ProgramError::InvalidSeeds); } let mut full_seeds = pda_seeds.to_vec(); full_seeds.push(&[bump]); let signer_seeds: &[&[&[u8]]] = &[&full_seeds]; invoke_signed( &burn( token_program.key, token_account.key, mint.key, authority_pda.key, &[], amount, )?, &[ token_account.clone(), mint.clone(), authority_pda.clone(), token_program.clone(), ], signer_seeds, )?; Ok(()) } ``` --- ## Closing Token Accounts ### Close Token Account #### Using Anchor ```rust use anchor_spl::token_interface::{self, CloseAccount, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct CloseTokenAccount<'info> { #[account(mut)] pub token_account: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub destination: SystemAccount<'info>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> { let cpi_accounts = CloseAccount { account: ctx.accounts.token_account.to_account_info(), destination: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_context = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts ); token_interface::close_account(cpi_context)?; Ok(()) } ``` **Using Anchor Constraints (Simplified):** ```rust #[derive(Accounts)] pub struct CloseTokenAccount<'info> { #[account( mut, close = destination, token::authority = authority, )] pub token_account: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub destination: SystemAccount<'info>, pub authority: Signer<'info>, } pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> { // Account is automatically closed by Anchor constraints Ok(()) } ``` #### Using Native Rust ```rust use spl_token::instruction::close_account; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_error::ProgramError, }; pub fn close_token_account( token_account: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, token_program: &AccountInfo, ) -> ProgramResult { if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } invoke( &close_account( token_program.key, token_account.key, destination.key, authority.key, &[], )?, &[ token_account.clone(), destination.clone(), authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` --- ## Next Steps - **Validation**: See [tokens-validation.md](tokens-validation.md) for account validation patterns - **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features - **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and security best practices -
tokens-overview.md 8.2 KB
# SPL Token Program - Overview and Fundamentals Overview of SPL Token Program fundamentals including program types, account structures (Mint and Token accounts), and Associated Token Accounts (ATAs) with derivation and creation patterns. **For additional token topics, see:** - **[tokens-operations.md](tokens-operations.md)** - Create, mint, transfer, burn, close operations - **[tokens-validation.md](tokens-validation.md)** - Account validation patterns - **[tokens-2022.md](tokens-2022.md)** - Token Extensions Program features - **[tokens-patterns.md](tokens-patterns.md)** - Common patterns and security ## Table of Contents 1. [Token Program Overview](#token-program-overview) 2. [Token Account Structures](#token-account-structures) 3. [Associated Token Accounts](#associated-token-accounts) --- ## Token Program Overview ### SPL Token vs Token-2022 **SPL Token (Original):** - Program ID: `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` - Production-ready, stable, widely adopted - No new features planned - Use for standard fungible tokens **Token-2022 (Token Extensions Program):** - Program ID: `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` - Backwards-compatible with SPL Token - Supports extensions (transfer fees, confidential transfers, metadata pointers, etc.) - Use for advanced token features ### Key Concepts ``` ┌─────────────────────────────────────────┐ │ Mint Account │ ├─────────────────────────────────────────┤ │ - Defines a token type │ │ - Controls supply │ │ - Has mint authority (can create tokens)│ │ - Has freeze authority (can freeze accts)│ └─────────────────────────────────────────┘ │ │ Creates ▼ ┌─────────────────────────────────────────┐ │ Token Account │ ├─────────────────────────────────────────┤ │ - Holds token balance │ │ - Owned by a wallet or program │ │ - Associated with specific Mint │ │ - Can be frozen/delegated │ └─────────────────────────────────────────┘ ``` ### Required Dependencies **For Anchor:** ```toml [dependencies] anchor-lang = "0.32.1" anchor-spl = "0.32.1" [features] idl-build = [ "anchor-lang/idl-build", "anchor-spl/idl-build", ] ``` **For Native Rust:** ```toml [dependencies] spl-token = "6.0" spl-associated-token-account = "6.0" solana-program = "2.1" ``` --- ## Token Account Structures ### Mint Account **Size:** 82 bytes ```rust pub struct Mint { /// Optional authority to mint new tokens (Pubkey or None) pub mint_authority: COption<Pubkey>, // 36 bytes /// Total supply of tokens pub supply: u64, // 8 bytes /// Number of decimals (0 for NFTs, typically 6-9 for fungible) pub decimals: u8, // 1 byte /// Is initialized? pub is_initialized: bool, // 1 byte /// Optional authority to freeze token accounts pub freeze_authority: COption<Pubkey>, // 36 bytes } ``` **COption Format:** ```rust pub enum COption<T> { None, // Represented as [0, 0, 0, 0, ...] Some(T), // Represented as [1, followed by T bytes] } ``` ### Token Account **Size:** 165 bytes ```rust pub struct Account { /// The mint associated with this account pub mint: Pubkey, // 32 bytes /// The owner of this account pub owner: Pubkey, // 32 bytes /// The amount of tokens this account holds pub amount: u64, // 8 bytes /// If `delegate` is `Some` then `delegated_amount` represents /// the amount authorized by the delegate pub delegate: COption<Pubkey>, // 36 bytes /// The account's state pub state: AccountState, // 1 byte /// If is_native.is_some, this is a native token, and the value logs the /// rent-exempt reserve pub is_native: COption<u64>, // 12 bytes /// The amount delegated pub delegated_amount: u64, // 8 bytes /// Optional authority to close the account pub close_authority: COption<Pubkey>, // 36 bytes } pub enum AccountState { Uninitialized, Initialized, Frozen, } ``` --- ## Associated Token Accounts ### What are ATAs? **Associated Token Accounts (ATAs)** are PDAs that map a wallet address to a token account for a specific mint. **Derivation:** ```rust ATA = PDA( seeds: [wallet_address, TOKEN_PROGRAM_ID, mint_address], program: ASSOCIATED_TOKEN_PROGRAM_ID ) ``` **Benefits:** - **Deterministic**: Same wallet + mint always produces same ATA - **Discoverable**: Easy to find a user's token accounts - **Standard**: All wallets use this convention **Constants:** ```rust // Token Program ID pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // Associated Token Program ID pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); ``` ### Finding ATA Address #### Using Anchor ```rust use anchor_spl::associated_token::get_associated_token_address; // In client code or tests let ata_address = get_associated_token_address( &wallet_address, &mint_address, ); ``` #### Using Native Rust ```rust use spl_associated_token_account::get_associated_token_address; // Derive ATA address let ata_address = get_associated_token_address( &wallet_address, &mint_address, ); ``` ### Creating Associated Token Accounts #### Using Anchor ```rust use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct CreateTokenAccount<'info> { #[account( init, payer = payer, associated_token::mint = mint, associated_token::authority = owner, associated_token::token_program = token_program, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Can be any account pub owner: UncheckedAccount<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } pub fn create_ata(ctx: Context<CreateTokenAccount>) -> Result<()> { // ATA is automatically created by Anchor constraints Ok(()) } ``` #### Using Native Rust ```rust use spl_associated_token_account::instruction::create_associated_token_account; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, }; pub fn create_ata( payer: &AccountInfo, wallet: &AccountInfo, mint: &AccountInfo, ata: &AccountInfo, system_program: &AccountInfo, token_program: &AccountInfo, associated_token_program: &AccountInfo, ) -> ProgramResult { invoke( &create_associated_token_account( payer.key, wallet.key, mint.key, token_program.key, ), &[ payer.clone(), ata.clone(), wallet.clone(), mint.clone(), system_program.clone(), token_program.clone(), associated_token_program.clone(), ], )? ; Ok(()) } ``` --- ## Next Steps - **Token Operations**: See [tokens-operations.md](tokens-operations.md) for creating mints, minting, transferring, burning, and closing accounts - **Validation**: See [tokens-validation.md](tokens-validation.md) for account validation patterns - **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features - **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and security best practices -
tokens-patterns.md 21.7 KB
# SPL Token Program - Common Patterns and Security Common SPL Token patterns including escrow, staking, NFT creation, and account freezing. Comprehensive security considerations covering validation, authority checks, and defensive programming. Includes quick reference tables and security checklist. **For related topics, see:** - **[tokens-overview.md](tokens-overview.md)** - Token fundamentals and account structures - **[tokens-operations.md](tokens-operations.md)** - Create, mint, transfer, burn, close operations - **[tokens-validation.md](tokens-validation.md)** - Account validation patterns - **[tokens-2022.md](tokens-2022.md)** - Token Extensions Program features ## Table of Contents 1. [Pattern 1: Token Escrow](#pattern-1-token-escrow) 2. [Pattern 2: Token Staking](#pattern-2-token-staking) 3. [Pattern 3: NFT Creation](#pattern-3-nft-creation) 4. [Pattern 4: Freezing and Thawing Accounts](#pattern-4-freezing-and-thawing-accounts) 5. [Security Considerations](#security-considerations) 6. [Summary](#summary) --- ## Pattern 1: Token Escrow Program holds tokens temporarily on behalf of users. ### Using Anchor ```rust use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer}; #[derive(Accounts)] pub struct InitializeEscrow<'info> { #[account( init, payer = user, space = 8 + 32 + 8 + 1, seeds = [b"escrow", user.key().as_ref()], bump, )] pub escrow_state: Account<'info, EscrowState>, #[account( init, payer = user, token::mint = mint, token::authority = escrow_state, token::token_program = token_program, )] pub escrow_token_account: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub user_token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub user: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } #[account] pub struct EscrowState { pub user: Pubkey, pub amount: u64, pub bump: u8, } pub fn initialize_escrow(ctx: Context<InitializeEscrow>, amount: u64) -> Result<()> { // Transfer tokens to escrow token_interface::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token_account.to_account_info(), to: ctx.accounts.escrow_token_account.to_account_info(), authority: ctx.accounts.user.to_account_info(), }, ), amount, )?; // Save state ctx.accounts.escrow_state.user = ctx.accounts.user.key(); ctx.accounts.escrow_state.amount = amount; ctx.accounts.escrow_state.bump = ctx.bumps.escrow_state; Ok(()) } #[derive(Accounts)] pub struct ReleaseEscrow<'info> { #[account( mut, seeds = [b"escrow", escrow_state.user.as_ref()], bump = escrow_state.bump, has_one = user, close = user, )] pub escrow_state: Account<'info, EscrowState>, #[account(mut)] pub escrow_token_account: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub recipient_token_account: InterfaceAccount<'info, TokenAccount>, pub user: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn release_escrow(ctx: Context<ReleaseEscrow>) -> Result<()> { let seeds = &[ b"escrow", ctx.accounts.user.key().as_ref(), &[ctx.accounts.escrow_state.bump], ]; let signer_seeds = &[&seeds[..]]; token_interface::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.escrow_token_account.to_account_info(), to: ctx.accounts.recipient_token_account.to_account_info(), authority: ctx.accounts.escrow_state.to_account_info(), }, ).with_signer(signer_seeds), ctx.accounts.escrow_state.amount, )?; Ok(()) } ``` ### Using Native Rust ```rust use borsh::{BorshDeserialize, BorshSerialize}; use spl_token::instruction::transfer; #[derive(BorshSerialize, BorshDeserialize)] pub struct EscrowState { pub user: Pubkey, pub amount: u64, pub bump: u8, } pub fn initialize_escrow( program_id: &Pubkey, user: &AccountInfo, user_token_account: &AccountInfo, escrow_token_account: &AccountInfo, escrow_state: &AccountInfo, amount: u64, token_program: &AccountInfo, ) -> ProgramResult { // Transfer tokens to escrow invoke( &transfer( &spl_token::ID, user_token_account.key, escrow_token_account.key, user.key, &[], amount, )?, &[user_token_account.clone(), escrow_token_account.clone(), user.clone()], )?; // Save escrow state let (pda, bump) = Pubkey::find_program_address(&[b"escrow", user.key.as_ref()], program_id); let escrow = EscrowState { user: *user.key, amount, bump, }; escrow.serialize(&mut &mut escrow_state.data.borrow_mut()[..])?; Ok(()) } pub fn release_escrow( program_id: &Pubkey, escrow_state: &AccountInfo, escrow_token_account: &AccountInfo, recipient_token_account: &AccountInfo, escrow_pda: &AccountInfo, amount: u64, bump: u8, user: &Pubkey, ) -> ProgramResult { let signer_seeds: &[&[&[u8]]] = &[&[b"escrow", user.as_ref(), &[bump]]]; invoke_signed( &transfer( &spl_token::ID, escrow_token_account.key, recipient_token_account.key, escrow_pda.key, &[], amount, )?, &[escrow_token_account.clone(), recipient_token_account.clone(), escrow_pda.clone()], signer_seeds, )?; Ok(()) } ``` --- ## Pattern 2: Token Staking Users lock tokens to earn rewards. ### Using Anchor ```rust use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, Transfer}; #[derive(Accounts)] pub struct StakeTokens<'info> { #[account( init_if_needed, payer = user, space = 8 + 32 + 8 + 8 + 1, seeds = [b"stake", user.key().as_ref()], bump, )] pub stake_account: Account<'info, StakeAccount>, #[account(mut)] pub user_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, seeds = [b"vault"], bump, )] pub vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account(mut)] pub user: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } #[account] pub struct StakeAccount { pub user: Pubkey, pub amount_staked: u64, pub stake_timestamp: i64, pub bump: u8, } pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> Result<()> { // Transfer tokens to vault token_interface::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token_account.to_account_info(), to: ctx.accounts.vault_token_account.to_account_info(), authority: ctx.accounts.user.to_account_info(), }, ), amount, )?; // Update stake account let clock = Clock::get()?; ctx.accounts.stake_account.user = ctx.accounts.user.key(); ctx.accounts.stake_account.amount_staked += amount; ctx.accounts.stake_account.stake_timestamp = clock.unix_timestamp; ctx.accounts.stake_account.bump = ctx.bumps.stake_account; Ok(()) } #[derive(Accounts)] pub struct UnstakeTokens<'info> { #[account( mut, seeds = [b"stake", user.key().as_ref()], bump = stake_account.bump, has_one = user, )] pub stake_account: Account<'info, StakeAccount>, #[account(mut)] pub user_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, seeds = [b"vault"], bump, )] pub vault_token_account: InterfaceAccount<'info, TokenAccount>, /// CHECK: Vault authority PDA #[account( seeds = [b"vault-authority"], bump, )] pub vault_authority: UncheckedAccount<'info>, pub user: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn unstake_tokens(ctx: Context<UnstakeTokens>, amount: u64) -> Result<()> { require!( ctx.accounts.stake_account.amount_staked >= amount, ErrorCode::InsufficientStake ); let seeds = &[ b"vault-authority", &[ctx.bumps.vault_authority], ]; let signer_seeds = &[&seeds[..]]; // Transfer tokens back to user token_interface::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.vault_token_account.to_account_info(), to: ctx.accounts.user_token_account.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), }, ).with_signer(signer_seeds), amount, )?; // Update stake account ctx.accounts.stake_account.amount_staked -= amount; Ok(()) } ``` --- ## Pattern 3: NFT Creation Minting a non-fungible token (supply = 1, decimals = 0). ### Using Anchor ```rust use anchor_spl::token_interface::{self, Mint, MintTo, SetAuthority, TokenAccount, TokenInterface}; use anchor_spl::token_interface::spl_token_2022::instruction::AuthorityType; #[derive(Accounts)] pub struct CreateNFT<'info> { #[account( init, payer = payer, mint::decimals = 0, mint::authority = mint_authority, mint::token_program = token_program, )] pub mint: InterfaceAccount<'info, Mint>, #[account( init, payer = payer, associated_token::mint = mint, associated_token::authority = owner, associated_token::token_program = token_program, )] pub token_account: InterfaceAccount<'info, TokenAccount>, /// CHECK: Owner of the NFT pub owner: UncheckedAccount<'info>, pub mint_authority: Signer<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } pub fn create_nft(ctx: Context<CreateNFT>) -> Result<()> { // Mint exactly 1 token token_interface::mint_to( CpiContext::new( ctx.accounts.token_program.to_account_info(), MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.mint_authority.to_account_info(), }, ), 1, )?; // Remove mint authority to freeze supply token_interface::set_authority( CpiContext::new( ctx.accounts.token_program.to_account_info(), SetAuthority { account_or_mint: ctx.accounts.mint.to_account_info(), current_authority: ctx.accounts.mint_authority.to_account_info(), }, ), AuthorityType::MintTokens, None, )?; msg!("NFT created: {}", ctx.accounts.mint.key()); Ok(()) } ``` ### Using Native Rust ```rust use spl_token::instruction::{mint_to, set_authority, AuthorityType}; pub fn create_nft( mint: &AccountInfo, token_account: &AccountInfo, mint_authority: &AccountInfo, token_program: &AccountInfo, ) -> ProgramResult { // 1. Mint exactly 1 token invoke( &mint_to( &spl_token::ID, mint.key, token_account.key, mint_authority.key, &[], 1, // Exactly 1 token )?, &[mint.clone(), token_account.clone(), mint_authority.clone()], )?; // 2. Remove mint authority (make supply fixed) invoke( &set_authority( &spl_token::ID, mint.key, None, // Set to None AuthorityType::MintTokens, mint_authority.key, &[], )?, &[mint.clone(), mint_authority.clone()], )?; Ok(()) } ``` --- ## Pattern 4: Freezing and Thawing Accounts ### Using Anchor ```rust use anchor_spl::token_interface::{self, FreezeAccount, Mint, ThawAccount, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct FreezeTokenAccount<'info> { #[account( mint::freeze_authority = freeze_authority, )] pub mint: InterfaceAccount<'info, Mint>, #[account(mut)] pub token_account: InterfaceAccount<'info, TokenAccount>, pub freeze_authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn freeze_account(ctx: Context<FreezeTokenAccount>) -> Result<()> { token_interface::freeze_account( CpiContext::new( ctx.accounts.token_program.to_account_info(), FreezeAccount { account: ctx.accounts.token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), authority: ctx.accounts.freeze_authority.to_account_info(), }, ), )?; Ok(()) } pub fn thaw_account(ctx: Context<FreezeTokenAccount>) -> Result<()> { token_interface::thaw_account( CpiContext::new( ctx.accounts.token_program.to_account_info(), ThawAccount { account: ctx.accounts.token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), authority: ctx.accounts.freeze_authority.to_account_info(), }, ), )?; Ok(()) } ``` ### Using Native Rust ```rust use spl_token::instruction::{freeze_account, thaw_account}; pub fn freeze_token_account( token_account: &AccountInfo, mint: &AccountInfo, freeze_authority: &AccountInfo, token_program: &AccountInfo, ) -> ProgramResult { invoke( &freeze_account( token_program.key, token_account.key, mint.key, freeze_authority.key, &[], )?, &[ token_account.clone(), mint.clone(), freeze_authority.clone(), token_program.clone(), ], )?; Ok(()) } pub fn thaw_token_account( token_account: &AccountInfo, mint: &AccountInfo, freeze_authority: &AccountInfo, token_program: &AccountInfo, ) -> ProgramResult { invoke( &thaw_account( token_program.key, token_account.key, mint.key, freeze_authority.key, &[], )?, &[ token_account.clone(), mint.clone(), freeze_authority.clone(), token_program.clone(), ], )?; Ok(()) } ``` --- ## Security Considerations ### 1. Always Validate Token Accounts #### Anchor Approach ```rust #[derive(Accounts)] pub struct SafeTransfer<'info> { #[account( mut, constraint = source.mint == mint.key() @ ErrorCode::InvalidMint, constraint = source.owner == authority.key() @ ErrorCode::InvalidOwner, )] pub source: InterfaceAccount<'info, TokenAccount>, #[account( mut, constraint = destination.mint == mint.key() @ ErrorCode::InvalidMint, )] pub destination: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, pub authority: Signer<'info>, pub token_program: Interface<'info, TokenInterface>, } ``` #### Native Rust Approach ```rust // ❌ Dangerous - no validation pub fn unsafe_transfer( source: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, ) -> ProgramResult { // No checks! Attacker can pass any accounts invoke(&transfer_instruction, &accounts)?; Ok(()) } // ✅ Safe - validates everything pub fn safe_transfer( source: &AccountInfo, destination: &AccountInfo, authority: &AccountInfo, expected_mint: &Pubkey, ) -> ProgramResult { // Validate source validate_token_account(source, authority.key, expected_mint)?; // Validate destination let dest_token = TokenAccount::unpack(&destination.data.borrow())?; if dest_token.mint != *expected_mint { return Err(ProgramError::InvalidAccountData); } invoke(&transfer_instruction, &accounts)?; Ok(()) } ``` ### 2. Check Token Program ID #### Anchor Approach ```rust // Anchor automatically validates via Interface type pub token_program: Interface<'info, TokenInterface>, ``` #### Native Rust Approach ```rust pub fn validate_token_program(token_program: &AccountInfo) -> ProgramResult { if token_program.key != &spl_token::ID && token_program.key != &spl_token_2022::ID { msg!("Invalid Token Program"); return Err(ProgramError::IncorrectProgramId); } Ok(()) } ``` ### 3. Verify Mint Matches **Attack scenario:** Attacker passes token account for wrong mint. #### Anchor Approach ```rust #[account( constraint = token_account.mint == expected_mint.key() @ ErrorCode::InvalidMint, )] pub token_account: InterfaceAccount<'info, TokenAccount>, ``` #### Native Rust Approach ```rust // Always verify mint let source_token = TokenAccount::unpack(&source.data.borrow())?; let dest_token = TokenAccount::unpack(&dest.data.borrow())?; if source_token.mint != dest_token.mint { msg!("Mint mismatch between source and destination"); return Err(ProgramError::InvalidAccountData); } ``` ### 4. Authority Checks #### Anchor Approach ```rust #[account( constraint = token_account.owner == authority.key() @ ErrorCode::Unauthorized, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub authority: Signer<'info>, // Automatically validates is_signer ``` #### Native Rust Approach ```rust // Verify authority matches token account owner let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?; if token_account.owner != *authority.key { msg!("Authority doesn't own token account"); return Err(ProgramError::IllegalOwner); } // Verify authority signed if !authority.is_signer { msg!("Authority must sign"); return Err(ProgramError::MissingRequiredSignature); } ``` ### 5. Account State Checks #### Anchor Approach ```rust use spl_token::state::AccountState; pub fn check_not_frozen(ctx: Context<SomeContext>) -> Result<()> { let token_account = &ctx.accounts.token_account; require!( token_account.state == AccountState::Initialized, ErrorCode::AccountFrozen ); Ok(()) } ``` #### Native Rust Approach ```rust let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?; // Check not frozen if token_account.state == spl_token::state::AccountState::Frozen { msg!("Token account is frozen"); return Err(ProgramError::InvalidAccountData); } // Check initialized if token_account.state == spl_token::state::AccountState::Uninitialized { msg!("Token account not initialized"); return Err(ProgramError::UninitializedAccount); } ``` ### 6. Use TransferChecked Over Transfer **Why:** `transfer_checked` validates the mint and decimals, preventing certain attack vectors. #### Anchor Approach ```rust // ✅ Preferred - validates mint and decimals token_interface::transfer_checked( cpi_context, amount, decimals, )?; // ❌ Less secure - no mint/decimal validation token_interface::transfer( cpi_context, amount, )?; ``` #### Native Rust Approach ```rust // ✅ Preferred invoke( &transfer_checked( token_program.key, source.key, mint.key, destination.key, authority.key, &[], amount, decimals, )?, &accounts, )?; // ❌ Less secure invoke( &transfer( token_program.key, source.key, destination.key, authority.key, &[], amount, )?, &accounts, )?; ``` --- ## Summary ### Key Takeaways **Anchor Advantages:** - Automatic account validation through constraints - Cleaner, more concise code - Built-in safety checks - Type-safe account structures - Simplified CPI with `CpiContext` **Native Rust Advantages:** - Full control over all operations - No framework overhead - Explicit validation (can be more transparent) - Useful for understanding low-level mechanics ### Common Operations Quick Reference | Operation | Anchor Module | Native Rust Crate | |-----------|---------------|-------------------| | Mint tokens | `token_interface::mint_to` | `spl_token::instruction::mint_to` | | Transfer tokens | `token_interface::transfer` | `spl_token::instruction::transfer` | | Transfer checked | `token_interface::transfer_checked` | `spl_token::instruction::transfer_checked` | | Burn tokens | `token_interface::burn` | `spl_token::instruction::burn` | | Create ATA | `associated_token` constraint | `spl_associated_token_account` | | Close account | `token_interface::close_account` | `spl_token::instruction::close_account` | | Freeze account | `token_interface::freeze_account` | `spl_token::instruction::freeze_account` | ### Security Checklist - ✅ Validate token program ID - ✅ Verify token account ownership - ✅ Check mint matches expected - ✅ Confirm authority is signer - ✅ Ensure account not frozen - ✅ Validate ATA derivation if applicable - ✅ Use `transfer_checked` instead of `transfer` - ✅ Validate account state (initialized/frozen) - ✅ Check sufficient balance before operations ### Token Account Sizes - **Mint account:** 82 bytes - **Token account:** 165 bytes - **Token-2022 with extensions:** 82/165 + extension sizes Token integration is fundamental for DeFi, NFT, and gaming programs on Solana. Whether using Anchor or native Rust, understanding both approaches provides the flexibility to choose the right tool for your use case. -
tokens-validation.md 5.6 KB
# SPL Token Program - Validation Patterns Validation patterns for SPL Token accounts including ownership verification, mint validation, ATA address derivation checks, and balance verification. Covers both Anchor constraint-based and Native Rust manual validation approaches. **For related topics, see:** - **[tokens-overview.md](tokens-overview.md)** - Token fundamentals and account structures - **[tokens-operations.md](tokens-operations.md)** - Create, mint, transfer, burn, close operations - **[tokens-2022.md](tokens-2022.md)** - Token Extensions Program features - **[tokens-patterns.md](tokens-patterns.md)** - Common patterns and security ## Table of Contents 1. [Validate Token Account Ownership and Mint](#validate-token-account-ownership-and-mint) 2. [Validate ATA Address](#validate-ata-address) 3. [Check Token Balance](#check-token-balance) --- ## Validate Token Account Ownership and Mint ### Using Anchor ```rust use anchor_spl::token_interface::{TokenAccount, Mint}; #[derive(Accounts)] pub struct ValidateTokenAccount<'info> { #[account( constraint = token_account.owner == owner.key() @ ErrorCode::InvalidOwner, constraint = token_account.mint == mint.key() @ ErrorCode::InvalidMint, )] pub token_account: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Any account pub owner: UncheckedAccount<'info>, } pub fn validate_token_account(ctx: Context<ValidateTokenAccount>) -> Result<()> { // Validation is automatic via constraints // Additional checks if needed require!( ctx.accounts.token_account.amount >= 100, ErrorCode::InsufficientBalance ); Ok(()) } ``` ### Using Native Rust ```rust use spl_token::state::Account as TokenAccount; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, }; pub fn validate_token_account( token_account_info: &AccountInfo, expected_owner: &Pubkey, expected_mint: &Pubkey, ) -> ProgramResult { // 1. Verify owned by Token Program if token_account_info.owner != &spl_token::ID { msg!("Account not owned by Token Program"); return Err(ProgramError::IllegalOwner); } // 2. Deserialize token account let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?; // 3. Verify owner if token_account.owner != *expected_owner { msg!("Token account owner mismatch"); return Err(ProgramError::IllegalOwner); } // 4. Verify mint if token_account.mint != *expected_mint { msg!("Token account mint mismatch"); return Err(ProgramError::InvalidAccountData); } // 5. Verify not frozen if token_account.state != spl_token::state::AccountState::Initialized { msg!("Token account is frozen or uninitialized"); return Err(ProgramError::InvalidAccountData); } Ok(()) } ``` --- ## Validate ATA Address ### Using Anchor ```rust use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(Accounts)] pub struct ValidateATA<'info> { #[account( associated_token::mint = mint, associated_token::authority = owner, associated_token::token_program = token_program, )] pub ata: InterfaceAccount<'info, TokenAccount>, pub mint: InterfaceAccount<'info, Mint>, /// CHECK: Any account pub owner: UncheckedAccount<'info>, pub token_program: Interface<'info, TokenInterface>, } pub fn validate_ata(ctx: Context<ValidateATA>) -> Result<()> { // ATA address is automatically validated by Anchor constraints Ok(()) } ``` ### Using Native Rust ```rust use spl_associated_token_account::get_associated_token_address; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, }; pub fn validate_ata( ata_info: &AccountInfo, wallet: &Pubkey, mint: &Pubkey, ) -> ProgramResult { // Derive expected ATA address let expected_ata = get_associated_token_address(wallet, mint); // Validate match if expected_ata != *ata_info.key { msg!("Invalid ATA address"); return Err(ProgramError::InvalidAccountData); } Ok(()) } ``` --- ## Check Token Balance ### Using Anchor ```rust use anchor_spl::token_interface::TokenAccount; pub fn check_balance( ctx: Context<SomeContext>, minimum_amount: u64 ) -> Result<()> { let token_account = &ctx.accounts.token_account; require!( token_account.amount >= minimum_amount, ErrorCode::InsufficientBalance ); Ok(()) } ``` ### Using Native Rust ```rust use spl_token::state::Account as TokenAccount; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, program_pack::Pack, }; pub fn check_token_balance( token_account_info: &AccountInfo, minimum_amount: u64, ) -> ProgramResult { let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?; if token_account.amount < minimum_amount { msg!("Insufficient token balance: {} < {}", token_account.amount, minimum_amount); return Err(ProgramError::InsufficientFunds); } Ok(()) } ``` --- ## Next Steps - **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features - **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and comprehensive security best practices -
transaction-lifecycle.md 25.7 KB
# Transaction Lifecycle: Submission, Retry, and Confirmation This guide covers the complete lifecycle of Solana transactions from submission to confirmation, including why transactions get dropped, retry strategies, commitment levels, and monitoring patterns for production systems. ## Transaction Journey Overview ### The Full Path ``` [1] Client Creates and signs transaction ↓ [2] RPC Node Validates and forwards ↓ [3] Leader's TPU Transaction Processing Unit pipeline ├─ Fetch Stage Receives from network ├─ SigVerify Stage Verifies signatures ├─ Banking Stage Executes transactions ├─ PoH Service Records in Proof of History └─ Broadcast Stage Shares with cluster ↓ [4] Cluster Validation Validators vote on blocks ↓ [5] Confirmation Levels ├─ Processed Included in block by leader ├─ Confirmed Supermajority voted (~66% stake) └─ Finalized 32+ confirmed blocks after (~13 seconds) ``` ### Time line **Normal flow:** - Client → RPC: Instant (local network) - RPC → Leader: 100-400ms (network latency) - Leader processing: 400-600ms (slot time) - Confirmed: ~1-2 slots (~800-1200ms) - Finalized: ~32 slots (~13+ seconds) **Total time (happy path):** ~1-15 seconds ## Blockhash Expiration ### How Blockhashes Work Solana transactions include a `recent_blockhash` field for two purposes: 1. **Uniqueness**: Ensures each transaction is unique (prevents duplicates) 2. **Freshness**: Limits transaction validity to prevent spam **Critical constraint:** ```rust // Solana runtime maintains BlockhashQueue struct BlockhashQueue { last_hash: Hash, ages: HashMap<Hash, HashAge>, max_age: usize, // Currently 151 } // Transaction validation: fn is_valid_blockhash(blockhash: &Hash, queue: &BlockhashQueue) -> bool { queue.ages.contains_key(blockhash) // Must be in last 151 blockhashes } ``` ### The 151-Block Window **How it works:** 1. Each slot produces a new blockhash (~400-600ms per slot) 2. Runtime keeps last 151 blockhashes in `BlockhashQueue` 3. Transactions checked against this queue 4. If blockhash older than 150 blocks → **REJECTED** **Calculation:** ``` 151 blockhashes × ~600ms average slot time = ~90 seconds maximum 151 blockhashes × ~400ms minimum slot time = ~60 seconds minimum Effective window: 60-90 seconds ``` **Critical**: Once a blockhash exits the queue (>150 blocks old), transactions using it can **never** be processed. They're permanently invalid. ### Detecting Expiration **Using `lastValidBlockHeight`:** ```rust use solana_client::rpc_client::RpcClient; use solana_sdk::commitment_config::CommitmentConfig; async fn check_transaction_expiration( rpc_client: &RpcClient, last_valid_block_height: u64, ) -> bool { // Get current block height let current_block_height = rpc_client .get_block_height() .unwrap_or(0); // Transaction expired if current height > last valid height current_block_height > last_valid_block_height } ``` **Getting `lastValidBlockHeight`:** ```rust let blockhash_response = rpc_client.get_latest_blockhash()?; let blockhash = blockhash_response.value.0; let last_valid_block_height = blockhash_response.value.1; // Blocks until expiration println!("Blockhash: {}", blockhash); println!("Valid until block: {}", last_valid_block_height); ``` ### Why Transactions Expire **Design rationale:** 1. **Prevents replay attacks**: Old transactions can't be resubmitted years later 2. **Manages state bloat**: Runtime doesn't need infinite blockhash history 3. **Network spam protection**: Attackers can't flood network with ancient transactions 4. **Simplifies fee markets**: Recent activity determines current conditions **Trade-off**: 60-90 second window requires responsive clients and reliable networking. ## How Transactions Get Dropped ### Before Processing **1. UDP Packet Loss** Solana uses UDP for transaction forwarding (performance over reliability): ``` Client → RPC: UDP packet RPC → Leader: UDP packet Packet loss rate: 0.1-5% depending on network conditions ``` **Impact**: Transaction silently dropped, never reaches leader. **Detection**: No error, no confirmation - transaction just disappears. **Solution**: Retry mechanism (RPC default behavior). **2. RPC Node Congestion** RPC nodes maintain transaction queues: ```rust // RPC node queue limits const MAX_TRANSACTIONS_QUEUE: usize = 10_000; // When queue full: if queue.len() >= MAX_TRANSACTIONS_QUEUE { return Err("Transaction queue full, try again"); } ``` **Impact**: New transactions rejected when queue full. **Detection**: RPC returns error immediately. **Solution**: Back off and retry, or use different RPC endpoint. **3. RPC Node Lag** RPC nodes can fall behind cluster: ```rust // Check RPC health let processed_slot = rpc_client.get_slot()?; let max_shred_insert_slot = rpc_client.get_max_shred_insert_slot()?; let lag = max_shred_insert_slot.saturating_sub(processed_slot); if lag > 50 { println!("WARNING: RPC is {} slots behind", lag); // Consider using different RPC node } ``` **Impact**: Fetches stale blockhashes that expire quickly. **Solution**: Monitor RPC health, use multiple RPC providers. **4. Blockhash from Minority Fork** Clusters occasionally fork temporarily (~5% of slots): ``` Majority fork: Block A → Block B → Block C Minority fork: Block A → Block X (abandoned) ``` If you fetch blockhash from minority fork: - Blockhash is valid on minority fork - Majority fork has different blockhash - Transaction **never** valid on majority fork **Impact**: Transaction permanently invalid (never in BlockhashQueue of majority fork). **Detection**: Transaction never confirms, blockhash never appears in majority chain. **Solution**: Use `confirmed` commitment level when fetching blockhashes (not `processed`). ### After Processing But Before Finalization **5. Leader on Minority Fork** Transaction processed by leader, but leader's block abandoned by cluster: ``` 1. Leader processes transaction in slot 1000 2. Cluster votes on slot 1000 3. Supermajority votes for different fork 4. Leader's block (and transaction) discarded ``` **Impact**: Transaction processed but not confirmed. Must resubmit. **Detection**: Transaction shows as processed but never confirmed. **Solution**: Wait for `confirmed` level before assuming success. **6. Transaction Expiration During Retry** Default RPC retry behavior has limitations: ```rust // RPC retry logic (simplified): while !finalized && !expired { forward_to_leader(); sleep(2_seconds); } // Problem: What if we can't determine expiration? // RPC may stop retrying early! ``` **Impact**: RPC stops retrying before transaction actually expires. **Solution**: Implement custom retry logic with explicit expiration tracking. ## Commitment Levels ### Understanding Commitment Solana has three commitment levels representing stages of finality: ``` Processed ↓ (1-2 slots later) Confirmed ↓ (32+ slots later, ~13 seconds) Finalized ``` ### Processed **Definition**: Transaction processed by leader and included in a block. **Characteristics:** - Fastest (most recent) - Least safe (~5% chance of being on abandoned fork) - Can be rolled back if fork abandoned **When to use:** - Real-time UX updates (show pending state) - Price feeds where staleness is worse than occasional rollback - **NOT for blockhash fetching** (risk of minority fork blockhash) **Example:** ```rust use solana_client::rpc_config::RpcSendTransactionConfig; use solana_sdk::commitment_config::CommitmentLevel; let config = RpcSendTransactionConfig { skip_preflight: false, preflight_commitment: Some(CommitmentLevel::Processed), ..Default::default() }; // Risky! Blockhash might be from minority fork let signature = rpc_client.send_transaction_with_config(&transaction, config)?; ``` ### Confirmed **Definition**: Supermajority of validators voted for the block containing the transaction. **Characteristics:** - Fast (~1-2 slots, ~600-1200ms) - Safe (~<0.1% chance of rollback in normal conditions) - **RECOMMENDED for blockhash fetching** **When to use:** - **Default choice** for most operations - Blockhash fetching (balance of speed and safety) - Transaction submission (preflight commitment) - Confirmation monitoring **Example:** ```rust let commitment = CommitmentConfig::confirmed(); // Fetch blockhash at confirmed level let recent_blockhash = rpc_client.get_latest_blockhash_with_commitment(commitment)?; // Set preflight commitment to match let config = RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Confirmed), ..Default::default() }; ``` ### Finalized **Definition**: 32+ confirmed blocks have been built on top (mathematically impossible to rollback). **Characteristics:** - Slowest (~13+ seconds) - 100% safe (impossible to rollback) - Guaranteed by consensus algorithm **When to use:** - Financial settlement - Legal/compliance requirements - Cross-chain bridges - Critical state changes **Example:** ```rust let commitment = CommitmentConfig::finalized(); // Wait for finalization rpc_client.confirm_transaction_with_spinner( &signature, &recent_blockhash, commitment, )?; ``` ### Preflight Commitment Matching **Critical rule**: Preflight commitment MUST match blockhash fetch commitment. **Why:** ```rust // Scenario: Mismatch let blockhash = rpc.get_latest_blockhash_with_commitment(confirmed)?; // confirmed let config = RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Processed), // processed (WRONG!) ..Default::default() }; // RPC tries to simulate at processed level // But blockhash only exists at confirmed level // Result: "Blockhash not found" error ``` **Correct approach:** ```rust let commitment = CommitmentConfig::confirmed(); // Fetch blockhash let blockhash_response = rpc.get_latest_blockhash_with_commitment(commitment)?; let blockhash = blockhash_response.0; // Match preflight commitment let config = RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Confirmed), ..Default::default() }; let signature = rpc.send_transaction_with_config(&transaction, config)?; ``` ## RPC Retry Behavior ### Default Retry Logic RPC nodes automatically retry transactions: ```rust // Simplified RPC retry algorithm: const RETRY_INTERVAL: Duration = Duration::from_secs(2); const MAX_QUEUE_SIZE: usize = 10_000; loop { if transaction.is_finalized() { return Ok(signature); } if queue.len() >= MAX_QUEUE_SIZE { return Err("Queue full"); } if can_determine_expiration() { if transaction.is_expired() { return Err("Blockhash expired"); } } else { // Conservative: retry only once if can't determine expiration if retry_count > 1 { return Ok(signature); // Might not actually be finalized! } } forward_to_current_leader(); forward_to_next_leader(); sleep(RETRY_INTERVAL); retry_count += 1; } ``` ### Leader Forwarding RPC forwards transactions to: 1. **Current leader**: For immediate processing 2. **Next leader**: In case current leader rotation happens **Why both?** - Leader rotation happens every 4 slots (~1.6-2.4 seconds) - Transaction might arrive during rotation - Next leader can process in upcoming slots ### Queue Pressure During congestion: ``` Queue size: 10,000 transactions New transaction arrives: if queue.is_full(): reject("Transaction queue full") else: queue.push(transaction) retry_until_finalized() ``` **User experience:** - Fresh transactions rejected when queue full - Older transactions keep retrying - Can create priority inversion (old low-priority tx blocks new high-priority tx) **Solution**: Use `maxRetries: 0` to take manual control during congestion. ## Custom Retry Strategies ### Manual Retry Loop Taking full control: ```rust use solana_client::rpc_client::RpcClient; use solana_sdk::signature::Signature; use std::time::Duration; use tokio::time::sleep; async fn send_transaction_with_retry( rpc_client: &RpcClient, transaction: &Transaction, last_valid_block_height: u64, ) -> Result<Signature, Box<dyn std::error::Error>> { let config = RpcSendTransactionConfig { skip_preflight: true, // Already validated max_retries: Some(0), // Manual retry control ..Default::default() }; let signature = rpc_client.send_transaction_with_config( transaction, config, )?; // Manual retry loop loop { // Check if transaction confirmed match rpc_client.get_signature_status(&signature)? { Some(Ok(_)) => { println!("Transaction confirmed!"); return Ok(signature); } Some(Err(e)) => { return Err(format!("Transaction failed: {:?}", e).into()); } None => { // Not processed yet, continue } } // Check expiration let current_block_height = rpc_client.get_block_height()?; if current_block_height > last_valid_block_height { return Err("Transaction expired".into()); } // Resubmit rpc_client.send_transaction_with_config(transaction, config)?; // Wait before next retry sleep(Duration::from_millis(500)).await; } } ``` ### Exponential Backoff Reduce network load during congestion: ```rust async fn retry_with_exponential_backoff( rpc_client: &RpcClient, transaction: &Transaction, last_valid_block_height: u64, ) -> Result<Signature, Box<dyn std::error::Error>> { let signature = rpc_client.send_transaction(transaction)?; let mut retry_delay = Duration::from_millis(500); const MAX_DELAY: Duration = Duration::from_secs(8); loop { match rpc_client.get_signature_status(&signature)? { Some(Ok(_)) => return Ok(signature), Some(Err(e)) => return Err(e.into()), None => { // Check expiration if rpc_client.get_block_height()? > last_valid_block_height { return Err("Expired".into()); } // Resubmit rpc_client.send_transaction(transaction)?; // Exponential backoff sleep(retry_delay).await; retry_delay = std::cmp::min(retry_delay * 2, MAX_DELAY); } } } } ``` ### Constant Interval (Mango Approach) Aggressive resubmission: ```rust async fn retry_constant_interval( rpc_client: &RpcClient, transaction: &Transaction, last_valid_block_height: u64, ) -> Result<Signature, Box<dyn std::error::Error>> { let signature = rpc_client.send_transaction(transaction)?; const RETRY_INTERVAL: Duration = Duration::from_millis(500); loop { match rpc_client.get_signature_status(&signature)? { Some(Ok(_)) => return Ok(signature), Some(Err(e)) => return Err(e.into()), None => { if rpc_client.get_block_height()? > last_valid_block_height { return Err("Expired".into()); } // Constant interval resubmission rpc_client.send_transaction(transaction)?; sleep(RETRY_INTERVAL).await; } } } } ``` **Trade-offs:** - **Exponential backoff**: Network-friendly, slower confirmation - **Constant interval**: Faster confirmation, more network load - **Choice depends on**: Application needs, RPC provider limits, congestion levels ## Confirmation Monitoring ### Polling for Confirmation **Basic polling:** ```rust use solana_sdk::signature::Signature; fn wait_for_confirmation( rpc_client: &RpcClient, signature: &Signature, commitment: CommitmentConfig, ) -> Result<(), Box<dyn std::error::Error>> { loop { match rpc_client.get_signature_status_with_commitment( signature, commitment, )? { Some(Ok(_)) => { println!("Transaction confirmed at {:?}", commitment); return Ok(()); } Some(Err(e)) => { return Err(format!("Transaction failed: {:?}", e).into()); } None => { std::thread::sleep(Duration::from_millis(500)); } } } } ``` **With timeout:** ```rust use std::time::{Duration, Instant}; fn wait_for_confirmation_with_timeout( rpc_client: &RpcClient, signature: &Signature, timeout: Duration, ) -> Result<bool, Box<dyn std::error::Error>> { let start = Instant::now(); while start.elapsed() < timeout { match rpc_client.get_signature_status(signature)? { Some(Ok(_)) => return Ok(true), Some(Err(e)) => return Err(e.into()), None => std::thread::sleep(Duration::from_millis(500)), } } Ok(false) // Timed out } ``` ### Using `confirm_transaction` Built-in helper with expiration tracking: ```rust let commitment = CommitmentConfig::confirmed(); // Method 1: With blockhash context rpc_client.confirm_transaction_with_spinner( &signature, &recent_blockhash, commitment, )?; // Method 2: With last valid block height (recommended) let result = rpc_client.confirm_transaction_with_commitment( &signature, commitment, )?; if result.value { println!("Transaction confirmed!"); } else { println!("Transaction not confirmed (might have expired)"); } ``` ### WebSocket Subscriptions (Real-Time) For real-time updates without polling: ```rust use solana_client::pubsub_client::PubsubClient; use solana_sdk::commitment_config::CommitmentConfig; async fn subscribe_to_signature( ws_url: &str, signature: &Signature, ) -> Result<(), Box<dyn std::error::Error>> { let pubsub_client = PubsubClient::new(ws_url).await?; let (mut stream, unsubscribe) = pubsub_client .signature_subscribe(signature, Some(CommitmentConfig::confirmed())) .await?; // Wait for notification while let Some(response) = stream.next().await { match response.value { solana_client::rpc_response::RpcSignatureResult::ProcessedSignature(_) => { println!("Transaction confirmed!"); break; } } } unsubscribe().await; Ok(()) } ``` **Advantages:** - Real-time notification (no polling delay) - Lower RPC load - Immediate feedback **Disadvantages:** - WebSocket connection overhead - Need to handle disconnections - Not all RPC providers support WebSockets ## Best Practices ### 1. Fetch Fresh Blockhashes ```rust // BAD: Fetch once and reuse let blockhash = rpc.get_latest_blockhash()?; for tx in transactions { // All use same blockhash (increases expiration risk) send_transaction(tx, &blockhash)?; } // GOOD: Fetch fresh blockhash for each transaction for tx in transactions { let blockhash = rpc.get_latest_blockhash()?; send_transaction(tx, &blockhash)?; } // BETTER: Fetch fresh blockhash right before signing fn prepare_and_send(user_action: Action) { // User initiates action let blockhash = rpc.get_latest_blockhash()?; // Fetch now! // Build and sign (fast) let tx = build_transaction(user_action, &blockhash); sign_transaction(&tx); // Submit immediately send_transaction(&tx)?; } ``` ### 2. Use Confirmed Commitment ```rust // RECOMMENDED: Confirmed commitment let commitment = CommitmentConfig::confirmed(); let blockhash = rpc.get_latest_blockhash_with_commitment(commitment)?; // Risks minority fork let blockhash = rpc.get_latest_blockhash_with_commitment( CommitmentConfig::processed() )?; // Avoid! ``` ### 3. Match Preflight Commitment ```rust let commitment = CommitmentConfig::confirmed(); // Fetch blockhash let (blockhash, last_valid_block_height) = rpc .get_latest_blockhash_with_commitment(commitment)?; // Match preflight commitment let config = RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Confirmed), // MATCH! ..Default::default() }; ``` ### 4. Track Expiration Explicitly ```rust // Get expiration info let (blockhash, last_valid_block_height) = rpc.get_latest_blockhash()?; // Check before retry fn should_retry(rpc: &RpcClient, last_valid: u64) -> bool { rpc.get_block_height().unwrap_or(0) <= last_valid } ``` ### 5. Monitor RPC Health ```rust async fn check_rpc_health(rpc: &RpcClient) -> bool { let processed = rpc.get_slot().unwrap_or(0); let max_shred = rpc.get_max_shred_insert_slot().unwrap_or(0); let lag = max_shred.saturating_sub(processed); if lag > 50 { eprintln!("RPC lagging by {} slots", lag); return false; } true } ``` ### 6. Implement Proper Error Handling ```rust match rpc.send_transaction(&tx) { Ok(signature) => { println!("Submitted: {}", signature); // Wait for confirmation } Err(e) => { if e.to_string().contains("BlockhashNotFound") { // Blockhash expired, fetch fresh one let new_blockhash = rpc.get_latest_blockhash()?; // Re-sign transaction with new blockhash } else if e.to_string().contains("AlreadyProcessed") { // Transaction already submitted (safe to ignore) } else { // Other error, handle appropriately return Err(e.into()); } } } ``` ### 7. Use Skip Preflight Judiciously ```rust // When to skip preflight: // - During congestion (preflight adds latency) // - When retrying (already validated once) // - When you're confident about transaction validity let config = RpcSendTransactionConfig { skip_preflight: true, // Skip simulation preflight_commitment: Some(CommitmentLevel::Confirmed), max_retries: Some(0), ..Default::default() }; // Still recommended: Simulate ONCE before skip_preflight rpc.simulate_transaction(&tx)?; // Catch errors // Then submit with skip_preflight for speed ``` ## Production Patterns ### High-Throughput System ```rust struct TransactionSubmitter { rpc_client: Arc<RpcClient>, retry_queue: Arc<Mutex<VecDeque<RetryableTransaction>>>, } struct RetryableTransaction { transaction: Transaction, signature: Signature, last_valid_block_height: u64, submitted_at: Instant, retry_count: usize, } impl TransactionSubmitter { async fn submit_transaction(&self, tx: Transaction) -> Result<Signature, Error> { let (blockhash, last_valid) = self.rpc_client.get_latest_blockhash()?; // Submit initial let signature = self.rpc_client.send_transaction(&tx)?; // Add to retry queue let retryable = RetryableTransaction { transaction: tx, signature, last_valid_block_height: last_valid, submitted_at: Instant::now(), retry_count: 0, }; self.retry_queue.lock().unwrap().push_back(retryable); Ok(signature) } async fn retry_worker(&self) { loop { sleep(Duration::from_millis(500)).await; let mut queue = self.retry_queue.lock().unwrap(); for tx in queue.iter_mut() { // Check if confirmed match self.rpc_client.get_signature_status(&tx.signature) { Ok(Some(Ok(_))) => { // Confirmed, remove from queue (handle in cleanup pass) continue; } Ok(Some(Err(_))) => { // Failed, remove from queue continue; } _ => { // Not confirmed, check expiration let current_height = self.rpc_client.get_block_height().unwrap_or(0); if current_height > tx.last_valid_block_height { // Expired, remove from queue continue; } // Retry let _ = self.rpc_client.send_transaction(&tx.transaction); tx.retry_count += 1; } } } // Cleanup confirmed/failed/expired queue.retain(|tx| { matches!( self.rpc_client.get_signature_status(&tx.signature), Ok(None) // Still pending ) }); } } } ``` ### Wallet Integration ```rust async fn wallet_send_transaction( rpc: &RpcClient, unsigned_tx: Transaction, signer: &dyn Signer, ) -> Result<Signature, Error> { // Fetch blockhash immediately before signing let (blockhash, last_valid) = rpc.get_latest_blockhash()?; // Update transaction with fresh blockhash let mut tx = unsigned_tx.clone(); tx.message.recent_blockhash = blockhash; // Sign tx.sign(&[signer], blockhash); // Simulate first rpc.simulate_transaction(&tx)?; // Submit with retry let signature = tx.signatures[0]; send_with_retry(rpc, &tx, last_valid).await?; Ok(signature) } ``` ## Resources ### Official Documentation - [Transaction Retry Guide](https://solana.com/developers/guides/advanced/retry) - [Transaction Confirmation Guide](https://solana.com/developers/guides/advanced/confirmation) ### Technical References - [RpcClient Source](https://github.com/solana-labs/solana/blob/master/client/src/rpc_client.rs) - [Transaction Source](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction/mod.rs) - [BlockhashQueue Source](https://github.com/solana-labs/solana/blob/master/runtime/src/blockhash_queue.rs) ### Community Resources - [Solana Cookbook - Transactions](https://solanacookbook.com/references/basic-transactions.html) - [Solana Stack Exchange - Transaction Questions](https://solana.stackexchange.com/questions/tagged/transaction) -
versioned-transactions.md 28.1 KB
# Versioned Transactions and Address Lookup Tables This guide covers Solana's versioned transaction format and Address Lookup Tables (ALTs), which enable programs to work with more accounts per transaction by compressing account references. ## Introduction ### The Account Limit Problem Solana transactions are transmitted over UDP and must fit within the IPv6 MTU size of 1280 bytes. After accounting for headers, this leaves approximately 1232 bytes for the transaction packet data. **Legacy transaction constraints:** - Each account address: 32 bytes - Signatures and metadata: ~300-400 bytes overhead - **Result**: Maximum ~35 accounts per transaction This limitation became problematic as developers needed to compose multiple on-chain programs atomically, especially for complex DeFi operations like multi-hop swaps or protocol interactions. ### The Solution: Versioned Transactions Versioned transactions introduce a new transaction format that supports **Address Lookup Tables (ALTs)**, allowing accounts to be referenced by 1-byte indices instead of full 32-byte addresses. **Impact:** - Legacy (v0 without ALTs): ~35 accounts maximum - Versioned (v0 with ALTs): **64+ accounts** per transaction - 31-byte savings per account referenced from an ALT ## Transaction Versions ### Version Format Solana uses the high bit of the first byte to determine transaction version: ```rust // Version detection (first byte of transaction) if first_byte & 0x80 == 0 { // Legacy transaction (bit pattern: 0xxxxxxx) version = "legacy" } else { // Versioned transaction (bit pattern: 1xxxxxxx) // Remove version bit to get actual version number version = first_byte & 0x7F // Currently only version 0 exists } ``` ### Legacy Transactions **Structure:** ```rust pub struct LegacyMessage { pub header: MessageHeader, pub account_keys: Vec<Pubkey>, // All 32-byte addresses pub recent_blockhash: Hash, pub instructions: Vec<CompiledInstruction>, } ``` **Characteristics:** - No version byte (implicitly version "legacy") - All accounts must be fully specified (32 bytes each) - Maximum ~35 accounts due to packet size limits - Still supported and widely used for simple transactions ### Version 0 Transactions **Structure:** ```rust pub struct MessageV0 { pub header: MessageHeader, pub account_keys: Vec<Pubkey>, // Directly specified accounts pub recent_blockhash: Hash, pub instructions: Vec<CompiledInstruction>, pub address_table_lookups: Vec<MessageAddressTableLookup>, // NEW! } pub struct MessageAddressTableLookup { pub account_key: Pubkey, // ALT address (32 bytes) pub writable_indexes: Vec<u8>, // Writable account indices pub readonly_indexes: Vec<u8>, // Readonly account indices } ``` **Characteristics:** - Starts with version byte: `0x80` (128 in decimal, version 0) - Includes `address_table_lookups` field - Can reference accounts from ALTs using 1-byte indices - Enables 64+ accounts per transaction **Transaction size calculation:** ``` Version 0 overhead: + 1 byte (version) + 1 byte (number of lookup tables) + 34 bytes per lookup table (32-byte address + 2 length bytes) + 1 byte per account index referenced Example with 1 ALT referencing 30 accounts: 1 (version) + 1 (table count) + 34 (table) + 30 (indices) = 66 bytes Equivalent legacy transaction: 30 accounts × 32 bytes = 960 bytes Savings: 960 - 66 = 894 bytes! ``` ## Address Lookup Tables (ALTs) ### What Are ALTs? Address Lookup Tables are **on-chain accounts** that store collections of related addresses. They act as a lookup mechanism to compress account references in transactions. **Key properties:** - Managed by the Address Lookup Table Program (`AddressLookupTableProgram`) - Store up to **256 addresses** (indexed by u8: 0-255) - Can be created, extended, deactivated, and closed - Addresses are append-only for security ### ALT Account Structure ```rust pub struct AddressLookupTable<'a> { pub meta: LookupTableMeta, pub addresses: Cow<'a, [Pubkey]>, } pub struct LookupTableMeta { pub deactivation_slot: Slot, // Slot when deactivated (u64::MAX if active) pub last_extended_slot: Slot, // Last slot when addresses were added pub last_extended_slot_start_index: u8, // Index where last extension started pub authority: Option<Pubkey>, // Can add/deactivate (None = immutable) } ``` **On-chain layout:** ``` Bytes 0-55: LookupTableMeta (56 bytes) Bytes 56+: Raw list of Pubkey addresses (32 bytes each) ``` ### Creating Address Lookup Tables **Step 1: Create the table** ```rust use solana_sdk::{ address_lookup_table_account::instruction as alt_instruction, instruction::Instruction, pubkey::Pubkey, signer::Signer, }; // Get recent slot for table derivation let recent_slot = rpc_client.get_slot()?; // Create lookup table instruction let (create_ix, lookup_table_address) = alt_instruction::create_lookup_table( payer.pubkey(), // Authority payer.pubkey(), // Payer recent_slot, // Recent slot for PDA derivation ); // The lookup table address is derived deterministically: // PDA(seeds=[authority, recent_slot], program=AddressLookupTableProgram) ``` **Transaction to create:** ```rust let create_tx = Transaction::new_signed_with_payer( &[create_ix], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&create_tx)?; ``` **Important**: Wait for the transaction to be **finalized** before extending or using the table. **Step 2: Extend the table with addresses** ```rust // Addresses to add to the lookup table let addresses_to_add = vec![ pubkey1, pubkey2, pubkey3, // ... up to ~20 addresses per transaction ]; let extend_ix = alt_instruction::extend_lookup_table( lookup_table_address, payer.pubkey(), // Authority Some(payer.pubkey()), // Payer (optional) addresses_to_add, ); let extend_tx = Transaction::new_signed_with_payer( &[extend_ix], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); rpc_client.send_and_confirm_transaction(&extend_tx)?; ``` **Batching strategy:** - Each extend operation can add approximately **20 addresses** before hitting transaction size limits - For more addresses, send multiple extend transactions - Example from TeamRaccoons repo: Batch in chunks of 20 ```rust // Batch extend for large address sets let batch_size = 20; for chunk in addresses.chunks(batch_size) { let extend_ix = alt_instruction::extend_lookup_table( lookup_table_address, authority.pubkey(), Some(payer.pubkey()), chunk.to_vec(), ); // Send transaction... rpc_client.send_and_confirm_transaction(&tx)?; } ``` **Warmup period:** - Newly added addresses require **1 slot** before they can be used - Must wait for finalization before using in v0 transactions - Check `last_extended_slot` to ensure addresses are ready **Step 3: Fetch the lookup table** ```rust use solana_client::rpc_client::RpcClient; use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; let lookup_table_account = rpc_client .get_account(&lookup_table_address)?; let lookup_table = AddressLookupTableAccount::deserialize(&lookup_table_account.data)?; // Access addresses println!("Table contains {} addresses", lookup_table.addresses.len()); for (index, address) in lookup_table.addresses.iter().enumerate() { println!("Index {}: {}", index, address); } ``` ### Using ALTs in V0 Transactions **Build a v0 transaction with ALT:** ```rust use solana_sdk::{ message::{v0, VersionedMessage}, transaction::VersionedTransaction, address_lookup_table_account::AddressLookupTableAccount, }; // 1. Create your instructions (can reference >35 accounts) let instructions = vec![ // Your program instructions ]; // 2. Fetch lookup table accounts let lookup_table_account = rpc_client.get_account(&lookup_table_address)?; let lookup_table = AddressLookupTableAccount::deserialize(&lookup_table_account.data)?; // 3. Build v0 message let v0_message = v0::Message::try_compile( &payer.pubkey(), &instructions, &[lookup_table], // Pass lookup tables here recent_blockhash, )?; // 4. Create versioned transaction let versioned_tx = VersionedTransaction::try_new( VersionedMessage::V0(v0_message), &[&payer], // Signers )?; // 5. Send transaction let signature = rpc_client.send_and_confirm_transaction(&versioned_tx)?; ``` **How accounts are referenced:** When you create an instruction with accounts that exist in the ALT: ```rust use solana_sdk::instruction::{AccountMeta, Instruction}; // These accounts are in the lookup table at indices 0, 1, 2 let account_in_alt_0 = Pubkey::new_unique(); let account_in_alt_1 = Pubkey::new_unique(); let account_in_alt_2 = Pubkey::new_unique(); let ix = Instruction::new_with_bytes( program_id, &instruction_data, vec![ AccountMeta::new(account_in_alt_0, false), // Index 0 in ALT AccountMeta::new_readonly(account_in_alt_1, false), // Index 1 AccountMeta::new(account_in_alt_2, false), // Index 2 ], ); // When compiled with ALT, these become 1-byte indices instead of 32-byte addresses ``` ### Deactivating and Closing ALTs **Deactivation:** ```rust let deactivate_ix = alt_instruction::deactivate_lookup_table( lookup_table_address, authority.pubkey(), ); rpc_client.send_and_confirm_transaction(&tx)?; ``` **Why deactivate?** - Prevents the table from being used in new transactions - Required before closing - Creates a safety cooldown period **Cooldown period:** - Must wait until the deactivation slot exits the slot hashes sysvar (~2.5 days on mainnet) - Prevents same-slot recreation attacks - Ensures no in-flight transactions reference the table **Closing:** ```rust let close_ix = alt_instruction::close_lookup_table( lookup_table_address, authority.pubkey(), recipient.pubkey(), // Receives reclaimed rent ); rpc_client.send_and_confirm_transaction(&tx)?; ``` **Requirements:** - Table must be deactivated first - Deactivation slot must have exited slot hashes sysvar - Only authority can close - Rent is returned to specified recipient ### Freezing ALTs (Making Immutable) ```rust let freeze_ix = alt_instruction::freeze_lookup_table( lookup_table_address, authority.pubkey(), ); rpc_client.send_and_confirm_transaction(&tx)?; ``` **Effect:** - Sets authority to `None` - Table becomes **permanently immutable** - Cannot add more addresses - Cannot deactivate or close - Useful for protocol-level tables that should never change ## RPC Configuration for V0 Transactions **Critical requirement**: When fetching transactions, you must specify support for versioned transactions: ```rust use solana_client::rpc_config::RpcTransactionConfig; use solana_transaction_status::UiTransactionEncoding; let config = RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Json), commitment: Some(CommitmentConfig::confirmed()), max_supported_transaction_version: Some(0), // REQUIRED! }; let tx = rpc_client.get_transaction_with_config(&signature, config)?; ``` **Without `max_supported_transaction_version: Some(0)`:** - RPC calls will **fail** if they encounter a v0 transaction - Error: "Transaction version is not supported" - This affects: `getTransaction`, `getBlock`, `getSignaturesForAddress`, etc. **For account subscriptions:** ```rust use solana_client::rpc_config::RpcAccountInfoConfig; let config = RpcAccountInfoConfig { encoding: Some(UiAccountEncoding::JsonParsed), commitment: Some(CommitmentConfig::confirmed()), // No max_supported_transaction_version needed for account queries }; ``` ## Limitations and Constraints ### Hard Limits 1. **256 addresses per table** (u8 index limit) - Tables use 1-byte indices - Cannot store more than 256 addresses - Create multiple tables if needed 2. **256 unique accounts total per transaction** - Solana runtime limit - Includes both direct accounts and ALT references - Accounts can appear multiple times in instructions 3. **~20 addresses per extend operation** - Limited by transaction size - Must batch large address sets 4. **Transaction signers cannot be in ALTs** - All signers must be explicitly listed in the transaction - Cannot reference signer accounts from lookup tables - This is a security feature 5. **No recursive lookups** - Cannot reference another ALT from within an ALT - Cannot store ALT addresses in an ALT ### Security Constraints 1. **Append-only design** - Addresses cannot be removed or modified - Prevents front-running attacks - Once added, addresses are permanent (until table is closed) 2. **Warmup requirement** - New addresses need 1 slot before use - Prevents same-slot manipulation - Must wait for finalization 3. **Deactivation cooldown** - Tables cannot be closed immediately after deactivation - Must wait for slot to exit slot hashes sysvar - Protects in-flight transactions 4. **Authority control** - Only authority can extend or deactivate - Set to `None` to make immutable - Cannot change authority after freezing ### Hardware Wallet Limitations **Issue**: Hardware wallets cannot verify accounts referenced from ALTs **Why:** - Hardware wallets display all transaction accounts for user verification - They don't have access to fetch lookup table data on-chain - Cannot show which addresses the indices reference **Implications:** - Users must trust that the correct lookup table is being used - Phishing risk: Malicious apps could use attacker-controlled ALTs - Hardware wallet UX shows: "This transaction uses address lookup tables" **Mitigations:** - Use well-known, immutable (frozen) ALTs when possible - Publish ALT addresses in protocol documentation - Verify ALT contents before use in client code - Consider adding integrity check instructions ## Security Best Practices ### 1. Wait for Finalization ```rust // BAD: Using immediately after creation let (create_ix, alt_address) = alt_instruction::create_lookup_table(...); rpc_client.send_transaction(&create_tx)?; // Not confirmed! let extend_ix = alt_instruction::extend_lookup_table(alt_address, ...); // FAILS! // GOOD: Wait for finalization rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?; // Now safe to extend rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?; // Now safe to use in v0 transactions ``` ### 2. Verify Lookup Table Contents ```rust // Fetch and verify before use let lookup_table = rpc_client.get_account(&alt_address)?; let alt = AddressLookupTableAccount::deserialize(&lookup_table.data)?; // Verify expected addresses assert_eq!(alt.addresses.len(), expected_count); assert_eq!(alt.addresses[0], expected_address_0); // Check authority if relevant if let Some(authority) = alt.meta.authority { assert_eq!(authority, expected_authority); } ``` ### 3. Add Integrity Check Instructions For critical operations, add an instruction that verifies the lookup table contents: ```rust // Your program instruction pub fn verify_lookup_table( ctx: Context<VerifyLookupTable>, expected_addresses: Vec<Pubkey>, ) -> Result<()> { let lookup_table = &ctx.accounts.lookup_table; // Verify table contains expected addresses for (i, expected) in expected_addresses.iter().enumerate() { require_keys_eq!( lookup_table.addresses[i], *expected, ErrorCode::InvalidLookupTable ); } Ok(()) } ``` ### 4. Use Immutable Tables for Protocols ```rust // After fully populating a protocol-level table let freeze_ix = alt_instruction::freeze_lookup_table( protocol_alt_address, authority.pubkey(), ); rpc_client.send_and_confirm_transaction(&freeze_tx)?; // Now the table is permanently immutable // Users can trust it won't change ``` ### 5. Front-Running Prevention **Why ALTs are append-only:** ```rust // If removal were allowed, this attack would be possible: // 1. User submits swap transaction using ALT at index 5 // 2. Attacker sees pending transaction // 3. Attacker removes legitimate address, adds malicious address at index 5 // 4. User's transaction executes with malicious address // Append-only design prevents this: // - Addresses cannot be removed // - Indices remain stable // - Order cannot change ``` ## Code Examples ### Complete Example: Multi-Swap with ALT Based on the TeamRaccoons address-lookup-table-multi-swap example: ```rust use solana_client::rpc_client::RpcClient; use solana_sdk::{ address_lookup_table_account::instruction as alt_instruction, address_lookup_table_account::AddressLookupTableAccount, commitment_config::CommitmentConfig, instruction::Instruction, message::{v0, VersionedMessage}, pubkey::Pubkey, signature::{Keypair, Signer}, transaction::{Transaction, VersionedTransaction}, }; fn create_and_use_alt_for_swaps() -> Result<()> { let rpc_client = RpcClient::new_with_commitment( "https://api.devnet.solana.com".to_string(), CommitmentConfig::confirmed(), ); let payer = Keypair::new(); // Fund payer... // Step 1: Collect all accounts needed for swap chain let swap_accounts = vec![ token_program_id, associated_token_program_id, swap_program_1, pool_1_address, pool_1_authority, pool_1_token_a, pool_1_token_b, swap_program_2, pool_2_address, pool_2_authority, pool_2_token_a, pool_2_token_b, // ... many more accounts ]; // Step 2: Create lookup table let recent_slot = rpc_client.get_slot()?; let (create_ix, alt_address) = alt_instruction::create_lookup_table( payer.pubkey(), payer.pubkey(), recent_slot, ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let create_tx = Transaction::new_signed_with_payer( &[create_ix], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?; println!("Created ALT at {}", alt_address); // Step 3: Extend in batches of 20 for (batch_num, chunk) in swap_accounts.chunks(20).enumerate() { let extend_ix = alt_instruction::extend_lookup_table( alt_address, payer.pubkey(), Some(payer.pubkey()), chunk.to_vec(), ); let recent_blockhash = rpc_client.get_latest_blockhash()?; let extend_tx = Transaction::new_signed_with_payer( &[extend_ix], Some(&payer.pubkey()), &[&payer], recent_blockhash, ); rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?; println!("Extended ALT batch {}", batch_num); } // Step 4: Fetch the populated lookup table let alt_account = rpc_client.get_account(&alt_address)?; let lookup_table = AddressLookupTableAccount::deserialize(&alt_account.data)?; println!("ALT contains {} addresses", lookup_table.addresses.len()); // Step 5: Build multi-swap transaction using ALT let swap_instructions = vec![ create_swap_instruction(0, 1, 2, 3, 4, 5, 6), // Indices into ALT create_swap_instruction(7, 8, 9, 10, 11, 12, 13), create_swap_instruction(14, 15, 16, 17, 18, 19, 20), // Many more swaps... ]; let recent_blockhash = rpc_client.get_latest_blockhash()?; let v0_message = v0::Message::try_compile( &payer.pubkey(), &swap_instructions, &[lookup_table], recent_blockhash, )?; let versioned_tx = VersionedTransaction::try_new( VersionedMessage::V0(v0_message), &[&payer], )?; // Step 6: Send v0 transaction let signature = rpc_client.send_and_confirm_transaction(&versioned_tx)?; println!("Multi-swap completed: {}", signature); Ok(()) } fn create_swap_instruction( swap_program: u8, pool: u8, authority: u8, source: u8, dest: u8, pool_token_a: u8, pool_token_b: u8, ) -> Instruction { // Create instruction with account indices // These will be resolved from the ALT Instruction { program_id: /* from ALT index swap_program */, accounts: vec![ AccountMeta::new(/* ALT index pool */, false), AccountMeta::new_readonly(/* ALT index authority */, false), // ... etc ], data: /* swap instruction data */, } } ``` ### Example: Protocol-Level Immutable ALT ```rust // Create a permanent lookup table for protocol accounts fn create_protocol_alt( authority: &Keypair, protocol_accounts: Vec<Pubkey>, ) -> Result<Pubkey> { let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com"); // Create table let recent_slot = rpc_client.get_slot()?; let (create_ix, alt_address) = alt_instruction::create_lookup_table( authority.pubkey(), authority.pubkey(), recent_slot, ); let create_tx = /* ... */; rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?; // Extend with all protocol accounts for chunk in protocol_accounts.chunks(20) { let extend_ix = alt_instruction::extend_lookup_table( alt_address, authority.pubkey(), Some(authority.pubkey()), chunk.to_vec(), ); let extend_tx = /* ... */; rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?; } // Freeze the table (make immutable) let freeze_ix = alt_instruction::freeze_lookup_table( alt_address, authority.pubkey(), ); let freeze_tx = /* ... */; rpc_client.send_and_confirm_transaction_with_spinner(&freeze_tx)?; println!("Created immutable protocol ALT at {}", alt_address); // Publish this address in documentation // Users can trust it won't change Ok(alt_address) } ``` ## Troubleshooting ### Common Errors and Solutions **Error: "Transaction version is not supported"** ```rust // Problem: RPC not configured for v0 transactions let tx = rpc_client.get_transaction(&signature)?; // FAILS // Solution: Set max_supported_transaction_version let config = RpcTransactionConfig { max_supported_transaction_version: Some(0), ..Default::default() }; let tx = rpc_client.get_transaction_with_config(&signature, config)?; // Works ``` **Error: "Address lookup table not found"** ```rust // Problem: Using table before creation is finalized let (create_ix, alt_address) = alt_instruction::create_lookup_table(...); rpc_client.send_transaction(&tx)?; // Sent but not confirmed let extend_ix = alt_instruction::extend_lookup_table(alt_address, ...); // FAILS // Solution: Wait for confirmation rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?; // Now table exists ``` **Error: "Invalid lookup table index"** ```rust // Problem: Referencing index beyond table size let lookup_table = /* has 10 addresses */; let ix = Instruction { accounts: vec![ AccountMeta::new(/* index 15 */, false), // FAILS - index out of bounds ], // ... }; // Solution: Verify table contents and use valid indices assert!(index < lookup_table.addresses.len()); ``` **Error: "Cannot deactivate lookup table"** ```rust // Problem: Not the authority let deactivate_ix = alt_instruction::deactivate_lookup_table( alt_address, wrong_authority.pubkey(), // Not the actual authority ); // Solution: Use the correct authority let alt = AddressLookupTableAccount::deserialize(&account.data)?; let correct_authority = alt.meta.authority.expect("Table has no authority"); let deactivate_ix = alt_instruction::deactivate_lookup_table( alt_address, correct_authority, ); ``` **Error: "Cannot close lookup table"** ```rust // Problem 1: Table not deactivated let close_ix = alt_instruction::close_lookup_table(...); // FAILS // Solution: Deactivate first, then wait let deactivate_ix = alt_instruction::deactivate_lookup_table(...); // ... send deactivate transaction ... // ... wait for cooldown period (~2.5 days mainnet) ... let close_ix = alt_instruction::close_lookup_table(...); // Problem 2: Cooldown period not complete // Solution: Check if deactivation slot has exited slot hashes let slot_hashes = rpc_client.get_slot_hashes()?; let oldest_slot = slot_hashes.last().unwrap().0; if alt.meta.deactivation_slot < oldest_slot { // Safe to close } ``` ## Use Cases and Patterns ### 1. DEX Aggregators **Problem**: Multi-hop swaps require many accounts (pools, authorities, token accounts) **Solution**: Create ALT with all pool accounts ```rust // ALT contains: // [0-19]: Pool 1 accounts (program, pool, authority, tokens, mint, etc.) // [20-39]: Pool 2 accounts // [40-59]: Pool 3 accounts // [60-79]: Common accounts (token program, associated token program, etc.) // Transaction can now execute 3+ swaps atomically ``` ### 2. Complex Protocol Interactions **Problem**: DeFi protocols compose multiple programs (lending, swapping, staking) **Solution**: Protocol-specific ALT with all contract addresses ```rust // Protocol ALT: // [0]: Program ID // [1]: Global config account // [2-10]: Pool addresses // [11-20]: Oracle addresses // [21-30]: Treasury accounts // etc. ``` ### 3. NFT Minting/Trading **Problem**: Minting or trading multiple NFTs requires many metadata accounts **Solution**: Collection-specific ALT with all related accounts ```rust // Collection ALT: // [0]: Candy machine // [1]: Collection mint // [2]: Collection metadata // [3]: Collection master edition // [4-100]: Individual NFT addresses ``` ### 4. Transaction Builder Programs **Problem**: Building very large transactions (>64 accounts) **Solution**: Multi-transaction pattern with ALTs ```rust // Transaction 1: Create and populate ALT // Transaction 2: Execute main operation using ALT // Transaction 3: Clean up and close ALT ``` ## Best Practices Summary 1. **Always wait for finalization** before using newly created or extended tables 2. **Batch extend operations** in chunks of ~20 addresses 3. **Verify table contents** before use in production 4. **Use immutable tables** for protocol-level accounts 5. **Set max_supported_transaction_version** in all RPC calls 6. **Document ALT addresses** for protocol integrators 7. **Consider hardware wallet UX** - frozen tables are more trustworthy 8. **Add integrity checks** for critical operations 9. **Plan for cooldown** when closing tables 10. **Keep signers explicit** - never try to put signers in ALTs ## Program Compatibility **Important**: Programs are **completely unaware** of whether they were called via legacy or v0 transactions. From the program's perspective: - Account references work identically - No code changes needed - Same `AccountInfo` structures - Same validation logic The transaction version only affects: - How accounts are referenced in the transaction - Transaction size limits - Client-side transaction construction **This means:** - Existing programs work with v0 transactions without modification - New programs don't need version-specific logic - ALTs are purely a client-side optimization ## Resources ### Official Documentation - [Versioned Transactions Guide](https://solana.com/developers/guides/advanced/versions) - [Address Lookup Tables Guide](https://solana.com/developers/guides/advanced/lookup-tables) - [Versioned Transactions Proposal](https://docs.anza.xyz/proposals/versioned-transactions) ### Code Examples - [TeamRaccoons Multi-Swap Example](https://github.com/TeamRaccoons/address-lookup-table-multi-swap) - [Solana Program Library - Address Lookup Table](https://github.com/solana-labs/solana-program-library/tree/master/address-lookup-table) ### Technical References - [AddressLookupTableProgram Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/address_lookup_table/instruction.rs) - [solana-sdk VersionedTransaction](https://docs.rs/solana-sdk/latest/solana_sdk/transaction/struct.VersionedTransaction.html) - [solana-sdk Message v0](https://docs.rs/solana-sdk/latest/solana_sdk/message/v0/struct.Message.html) ### Community Resources - [Solana Cookbook - Versioned Transactions](https://solanacookbook.com/references/basic-transactions.html#versioned-transactions) - [Solana Stack Exchange - ALT Questions](https://solana.stackexchange.com/questions/tagged/address-lookup-table) -
vulnerability-patterns.md 12.5 KB
# 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) } ```
-
-
LICENSE.txt 8.9 KB
Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS -
SKILL.md 17.1 KB
--- name: solana-development description: Build, test, deploy, and audit Solana programs with Anchor or native Rust, plus ZK Compression (Light Protocol). Use for Solana contracts, token operations, compute optimization, deployment, program audits, or compressed tokens and PDAs. metadata: version: "0.7.3" categories: "development" topics: "solana, anchor, rust, zk-compression, smart-contracts" openclaw: homepage: https://github.com/tenequm/skills/tree/main/skills/solana-development emoji: "☀️" envVars: - name: SURFPOOL_DATASOURCE_RPC_URL required: false description: Dedicated mainnet RPC endpoint for surfpool's datasource (avoids public RPC rate limits when forking mainnet locally). --- # Solana Everything for building on Solana: developing programs (Anchor or native Rust), auditing them for security, and building with ZK Compression. All three share the same core model - accounts, PDAs, CPIs, tokens - and differ only in abstraction level and goal. ## What this skill covers | Area | Use when | Jump to | |------|----------|---------| | **Development** | Writing programs, tokens, tests, deployments | [Development](#development) | | **Security & Auditing** | Reviewing for vulnerabilities, writing exploits, audit reports | [Security and Auditing](#security-and-auditing) | | **ZK Compression** | Rent-free tokens/PDAs at scale via Light Protocol | [ZK Compression](#zk-compression) | --- # Development Build Solana programs with Anchor (recommended) or native Rust. Both share accounts, PDAs, CPIs, and tokens; they differ in syntax and abstraction. ## Quick Start ### Recommended: Anchor Framework Macros and tooling that cut boilerplate and generate TypeScript clients: ```rust use anchor_lang::prelude::*; declare_id!("YourProgramID"); #[program] pub mod my_program { use super::*; pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> { ctx.accounts.account.data = data; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 8)] pub account: Account<'info, MyAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct MyAccount { pub data: u64, } ``` ```bash cargo install --git https://github.com/coral-xyz/anchor avm --locked --force avm install latest && avm use latest anchor init my_project && cd my_project && anchor build && anchor test ``` **→ See [references/anchor.md](references/anchor.md) for the complete Anchor guide** ### Advanced: Native Rust Maximum control, optimization potential, and deeper runtime understanding: ```rust use solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, msg, }; entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { msg!("Processing instruction"); // Manual account parsing, validation, and instruction routing Ok(()) } ``` ```bash cargo new my_program --lib cd my_program # configure Cargo.toml (see native-rust.md) cargo build-sbf ``` **→ See [references/native-rust.md](references/native-rust.md) for the complete native Rust guide** ## When to use which | Your need | Approach | Reason | |-----------|----------|--------| | Standard DeFi/NFT program | Anchor | Faster, proven patterns | | TypeScript client needed | Anchor | Auto-generates IDL + types | | New to Solana | Anchor | Gentler learning curve | | Compute optimization critical | Native Rust | Direct control, no overhead | | Smallest program size | Native Rust | No abstraction layer | | Learning fundamentals | Native Rust | Understand the platform deeply | You can also start with Anchor for speed, then optimize hot paths with native patterns. Both can coexist in one workspace. ## Reference map **Foundations** - [accounts.md](references/accounts.md) - Account model, ownership, rent, validation - [pda.md](references/pda.md) - Program Derived Addresses: derivation, canonical bumps, signing - [cpi.md](references/cpi.md) - Cross-Program Invocations safely **Tokens** - [tokens-overview.md](references/tokens-overview.md) - Token accounts and ATAs - [tokens-operations.md](references/tokens-operations.md) - Create, mint, transfer, burn, close - [tokens-validation.md](references/tokens-validation.md) - Account validation patterns - [tokens-2022.md](references/tokens-2022.md) - Token Extensions Program - [tokens-patterns.md](references/tokens-patterns.md) - Common patterns and security **Testing** - [testing-overview.md](references/testing-overview.md) - Test pyramid and strategy - [testing-frameworks.md](references/testing-frameworks.md) - Mollusk, Anchor test, native Rust - [testing-practices.md](references/testing-practices.md) - Best practices and patterns - [surfpool.md](references/surfpool.md) - Local dev with mainnet forking, cheatcodes, IaC **Deployment** - [deployment.md](references/deployment.md) - Deploy, upgrade, verify, manage programs - [production-deployment.md](references/production-deployment.md) - Verified builds (Anchor 0.32.1) **Client** - [client-development.md](references/client-development.md) - dApp client: wallet connections, React hooks, SOL/SPL transfers, transaction management (framework-kit and @solana/kit 6.x) **Implementation details** - [serialization.md](references/serialization.md) - Data layout, Borsh, zero-copy - [error-handling.md](references/error-handling.md) - Custom errors, propagation, client handling - [security.md](references/security.md) - Defensive programming patterns during development **Advanced** - [compute-optimization.md](references/compute-optimization.md) - CU optimization and benchmarking - [versioned-transactions.md](references/versioned-transactions.md) - Address Lookup Tables for 256+ accounts - [durable-nonces.md](references/durable-nonces.md) - Offline signing with durable nonces - [transaction-lifecycle.md](references/transaction-lifecycle.md) - Submission, retries, confirmations **Low-level** - [sysvars.md](references/sysvars.md) - Clock, Rent, EpochSchedule, SlotHashes - [builtin-programs.md](references/builtin-programs.md) - System Program, Compute Budget Program ## Common tasks | Task | Pointer | |------|---------| | New program | Anchor: `anchor init` / Native: `cargo new --lib` → [anchor.md](references/anchor.md), [native-rust.md](references/native-rust.md) | | Initialize a PDA | [pda.md](references/pda.md) | | Transfer SPL tokens | [tokens-operations.md](references/tokens-operations.md) | | Fast unit tests | Mollusk → [testing-frameworks.md](references/testing-frameworks.md) | | Local mainnet fork | `surfpool start` → [surfpool.md](references/surfpool.md) | | Deploy to devnet | [deployment.md](references/deployment.md) | | Production verified build | `solana-verify build` → [production-deployment.md](references/production-deployment.md) | | Optimize compute | [compute-optimization.md](references/compute-optimization.md) | | Handle 40+ accounts | Address Lookup Tables → [versioned-transactions.md](references/versioned-transactions.md) | | Offline signing | Durable nonces → [durable-nonces.md](references/durable-nonces.md) | --- # Security and Auditing Systematic security review for Solana programs (Anchor or native Rust). The core principle: **attackers can pass arbitrary accounts to any instruction**, so there are no implicit guarantees - validate everything, trust nothing. ## Review process 1. **Initial assessment** - Framework (Anchor vs native), Anchor version, dependencies (oracles, external programs), instruction count, account types, program purpose. 2. **Systematic review** - For each instruction, check in order: account validation (signer/owner/writable/init), arithmetic safety (`checked_*`), PDA security (canonical bumps, seed uniqueness), CPI security (validated targets), oracle/external data (staleness, status). → [security-checklists.md](references/security-checklists.md) 3. **Vulnerability pattern detection** - Type cosplay, account reloading, improper closing, missing lamports/ownership checks, PDA substitution, arbitrary CPI, overflow/underflow. → [vulnerability-patterns.md](references/vulnerability-patterns.md) 4. **Architecture and testing review** - PDA design, space/rent, error handling, event emission, compute budget, test coverage (unit/integration/fuzz), upgrade and authority management. 5. **Generate report** - Findings by severity, critical first, quick wins, testing recommendations. ## Essential checks (every instruction) **Anchor:** ```rust #[derive(Accounts)] pub struct SecureInstruction<'info> { #[account( mut, has_one = authority, // relationship check seeds = [b"vault", user.key().as_ref()], bump // canonical bump )] pub vault: Account<'info, Vault>, pub authority: Signer<'info>, // signer required pub token_program: Program<'info, Token>, // program validation } let total = balance.checked_add(amount).ok_or(ErrorCode::Overflow)?; ``` **Native Rust:** ```rust if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); } if vault.owner != program_id { return Err(ProgramError::IllegalOwner); } let total = balance.checked_add(amount).ok_or(ProgramError::ArithmeticOverflow)?; ``` ## Critical anti-patterns ❌ **Never:** `saturating_*` arithmetic (hides errors), `unwrap()`/`expect()` in production, `init_if_needed` without extra checks, skipped signer validation, unchecked arithmetic, arbitrary CPI targets, forgetting to reload accounts after mutations. ✅ **Always:** `checked_*` arithmetic, `ok_or(error)?` on Options, explicit `init` with validation, `Signer`/`is_signer` checks, `Program<'info, T>` for CPI targets, reload after external mutations, validate ownership + discriminator + relationships. ## Severity and finding format - 🔴 **Critical** - Funds stolen/lost, protocol broken - 🟠 **High** - Disruption, partial fund loss possible - 🟡 **Medium** - Edge cases, griefing, suboptimal behavior - 🔵 **Low** - Code quality, gas, best practices - 💡 **Informational** - Recommendations, documentation ```markdown ## 🔴 [CRITICAL] Title **Location:** `programs/vault/src/lib.rs:45-52` **Issue:** Brief description. **Vulnerable Code:** ```rust ... ``` **Exploit Scenario:** How it's exploited, step by step. **Recommendation:** ```rust ... ``` **References:** Links to docs or similar exploits. ``` ## Reference map - [security-fundamentals.md](references/security-fundamentals.md) - Security mindset, threat modeling, core validation, input/state/arithmetic safety, re-entrancy - [vulnerability-patterns.md](references/vulnerability-patterns.md) - Each vuln with vulnerable code, exploit scenario, secure fix, references - [security-checklists.md](references/security-checklists.md) - Account, arithmetic, PDA, CPI, oracle, token checklists - [anchor-security.md](references/anchor-security.md) - Anchor constraints, CpiContext, events, custom errors - [native-security.md](references/native-security.md) - Manual validation, secure PDA signing, discriminators, rent exemption - [caveats.md](references/caveats.md) - Solana quirks, Anchor limitations, testing blind spots, version issues ## Key questions for every audit 1. Can an attacker substitute accounts? (PDA validation, program IDs, `has_one`) 2. Can arithmetic overflow/underflow? (checked ops, division by zero) 3. Are all accounts validated? (owner, signer, writable, initialized) 4. Can the program be drained? (authorization, reentrancy, account confusion) 5. What happens in edge cases? (zero amounts, max values, closed accounts, expired data) 6. Are external dependencies safe? (oracle staleness/status, CPI targets, token program) ## Modern practices (2025) Anchor 0.30+ • Token-2022 with proper extension handling • `InitSpace` derive • emit events for critical state changes • fuzz tests with Trident • document invariants • roadmap: Dev → Audit → Testnet → Audit → Mainnet. --- # ZK Compression ZK Compression enables rent-free tokens and PDAs by storing state on the ledger (not in accounts), using zero-knowledge proofs to validate state transitions. Built by Light Protocol, indexed by Helius Photon. ## When to use **Use when:** creating millions of token accounts (~5000x cheaper), airdrops/loyalty/gaming mints to many recipients, many infrequently-updated user accounts, low-update-frequency PDAs. **Use regular accounts when:** updated frequently (>1000 lifetime writes), large data accessed on-chain, or compute budget is critical (compression adds ~100k CU). ## Quick Start ```bash # TypeScript client npm install @lightprotocol/stateless.js @lightprotocol/compressed-token # Rust SDK for programs cargo add light-sdk # CLI + local validator npm install -g @lightprotocol/zk-compression-cli light test-validator # local validator with compression light init my-program # new Anchor project with compression ``` ### Mint compressed tokens (TypeScript) ```typescript import { createRpc } from '@lightprotocol/stateless.js'; import { createMint, mintTo, transfer } from '@lightprotocol/compressed-token'; const rpc = createRpc(); // or createRpc('https://mainnet.helius-rpc.com?api-key=YOUR_KEY') const { mint } = await createMint(rpc, payer, payer.publicKey, 9); await mintTo(rpc, payer, mint, recipient, payer, 1_000_000_000); await transfer(rpc, payer, mint, 500_000_000, owner, recipient); const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint }); ``` **→ See [references/compressed-pdas.md](references/compressed-pdas.md) for the full Anchor program with compressed PDAs (LightAccount, validity proofs, CPI).** ## Core concepts **Compressed account model** - Like regular accounts but stored on the ledger instead of AccountsDB. No rent. Identified by content hash (changes on every write) or an optional persistent address (PDA-like). State validated by ZK proofs. **State trees** - Concurrent Merkle trees (Poseidon hashing); only the root is on-chain. **V2 batched trees** (mainnet, Jan 2026): ~250x cheaper state-root updates and ~70% less CU vs V1. New deployments use V2 automatically; V1 still supported. **Validity proofs** - Groth16 ZK proofs, constant 128 bytes regardless of account count, ~100k CU to verify. **LightAccount operations:** ```rust LightAccount::<T>::new_init(&program_id, Some(address), tree_index); // create LightAccount::<T>::new_mut(&program_id, &account_meta, current_state)?; // modify LightAccount::<T>::new_close(&program_id, &account_meta, current_state)?; // close ``` ## RPC and infrastructure Query compressed state via Helius RPC or self-hosted Photon. Key methods: `getCompressedAccount`, `getCompressedAccountsByOwner`, `getCompressedTokenAccountsByOwner`, `getCompressedTokenBalancesByOwner`, `getValidityProof`, `getMultipleCompressedAccounts`, `getCompressionSignaturesForAccount`. Node types: **Photon RPC** (indexes state, serves queries - Helius or self-host), **Prover** (generates proofs), **Forester** (maintains trees, empties nullifier queues). ```bash cargo install photon-indexer photon --rpc-url=https://api.devnet.solana.com ``` ## Trade-offs Larger transactions (+128 byte proof + account data) • higher CU (~100k proof + ~6k per account) • each write nullifies old state and appends new • requires a Photon indexer for queries. With V2 batched trees, break-even shifts well past V1's ~1000-write threshold. ## Reference map - [compressed-accounts.md](references/compressed-accounts.md) - Account model, hashing, addresses - [compressed-tokens.md](references/compressed-tokens.md) - Token operations, pools, batch operations - [compressed-pdas.md](references/compressed-pdas.md) - Building programs with compressed PDAs - [client-integration.md](references/client-integration.md) - TypeScript/Rust client setup, RPC methods External: [ZK Compression Docs](https://www.zkcompression.com/) • [Light Protocol](https://github.com/Lightprotocol/light-protocol) • [Helius SDK](https://github.com/helius-labs/helius-sdk) • [Photon Indexer](https://github.com/helius-labs/photon) • [Program Examples](https://github.com/Lightprotocol/program-examples) --- # Resources Official docs, tools, learning paths, security guides, audit reports, security firms, and community links: **→ See [references/resources.md](references/resources.md)** ## General best practices ✅ Validate every account (ownership, signers, mutability) • use `checked_*` arithmetic • test extensively (unit, integration, edge cases) • use PDAs for program-owned accounts • minimize and profile compute • add `security.txt` so researchers can reach you. **Anchor-specific:** `InitSpace` derive for space • `has_one` constraints • `Program<'info, T>` for CPI validation • `emit!` events • group related constraints. **Native-specific:** `next_account_info` for safe iteration • cache PDA bumps • zero-copy for large structs (50%+ CU savings) • minimize logging (pubkey formatting is expensive) • `solana-verify build` for production. ## Getting help - Anchor: [Discord](https://discord.gg/srmqvxf) • [Docs](https://www.anchor-lang.com/docs) - Solana: [Stack Exchange](https://solana.stackexchange.com/) • [Discord](https://discord.gg/solana)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.