commit d733741f8ae683de81ba5c49f240dd2748b3ecaf Author: Zhongwei Li Date: Sun Nov 30 09:01:25 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..5dae234 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "solana", + "description": "Comprehensive Solana development toolkit: build programs with Anchor/native Rust and audit for security vulnerabilities", + "version": "0.3.0", + "author": { + "name": "Misha Kolesnik", + "email": "misha@kolesnik.io" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b197eb --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# solana + +Comprehensive Solana development toolkit: build programs with Anchor/native Rust and audit for security vulnerabilities diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..cf12bc1 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,177 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:tenequm/claude-plugins:solana", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "8d6d0b42c3df4c8e1691d49535d8ef4e2f253ecb", + "treeHash": "3b2820eb7bb1a7702519fc61f932254d060916ec7f9973e9c392b2d938164ea3", + "generatedAt": "2025-11-28T10:28:38.311484Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "solana", + "description": "Comprehensive Solana development toolkit: build programs with Anchor/native Rust and audit for security vulnerabilities", + "version": "0.3.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "ea510a3b679258a665ddf73e48c2a41fb2a5f96cc1efa494c4dbb00d89f41dbb" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "38bd336664ff2ff44fdb4fc03c112b8aecef47de662d299e7583e21c67110a20" + }, + { + "path": "skills/solana-security/SKILL.md", + "sha256": "75d5381b6075a5f5020b75a9c62b3f1ade01946c7e6153871c6f3d4862e1ea78" + }, + { + "path": "skills/solana-security/references/resources.md", + "sha256": "ff1b54b8fc766fd581982233e0e61cad7216d96f387a1da65554443ba6d41775" + }, + { + "path": "skills/solana-security/references/native-security.md", + "sha256": "aeada39c6ac81ef2451e5427c6daadbc585ce92d0934bb09e935cb5455d506d2" + }, + { + "path": "skills/solana-security/references/security-checklists.md", + "sha256": "c6f3cd7cf458bc7e86bcbb3abe0c84c00eb770ac2de95f40b909fa5858b295ea" + }, + { + "path": "skills/solana-security/references/security-fundamentals.md", + "sha256": "5ff853592c44e1d5890195f06a3088419f6de87d2aa1e0f577535ad191263ac0" + }, + { + "path": "skills/solana-security/references/anchor-security.md", + "sha256": "d875af51dd1ca7ad10ae993a1645c2d66f7025e907b333c68f7058ebbd6d52f4" + }, + { + "path": "skills/solana-security/references/vulnerability-patterns.md", + "sha256": "a3f2372201995cb1ae43e4de01bac8c803c0f7c82ca43390bd02e656de08e9b3" + }, + { + "path": "skills/solana-security/references/caveats.md", + "sha256": "b1f58e6b8598b347cc4e6d5f61f8cf44c3e3f66a4a761aed6f09e7f7a8e78a0d" + }, + { + "path": "skills/solana-development/SKILL.md", + "sha256": "83c9f4fed6fe019f4c0c9e09b2021cbe9f66aabc6003221e276187f215b0d9df" + }, + { + "path": "skills/solana-development/references/tokens-patterns.md", + "sha256": "77bd5acc22783307a112d70ed53f0eee298c848b06339efa939a9716794ccc3a" + }, + { + "path": "skills/solana-development/references/production-deployment.md", + "sha256": "8780409d1a0024961b3d91c29face381c3530af70f3189618ad21c25640c3076" + }, + { + "path": "skills/solana-development/references/testing-overview.md", + "sha256": "c417d1822c2a5609ad1018147f698809406dd642f103aa89b80f1c35e5d15035" + }, + { + "path": "skills/solana-development/references/resources.md", + "sha256": "c843f1e808ea228fad7215777f2ab78e9d3f47ba9009f264088c5dd113e80d6d" + }, + { + "path": "skills/solana-development/references/transaction-lifecycle.md", + "sha256": "babed04e7fff1aa66bc5d235fe403b7833e19a6a871e932cc0498435cd1847f6" + }, + { + "path": "skills/solana-development/references/pda.md", + "sha256": "b8d218fb7e7d5073ca55c18f8d5ffc9657ca747d026167a1fd7dd2aa0062815c" + }, + { + "path": "skills/solana-development/references/serialization.md", + "sha256": "6ac1d563fe27f111a3f1f0911062746502c2acb4c0d02f35525eba222fde9a2d" + }, + { + "path": "skills/solana-development/references/testing-frameworks.md", + "sha256": "44e1a11978db3903462ad0bbdcc09a1d06d0da49ab647531733edc022d7f90d4" + }, + { + "path": "skills/solana-development/references/testing-practices.md", + "sha256": "e1ac330435d6189ce482f0d9c28df8a18b61199c4c39db8f3186de46b0253c1d" + }, + { + "path": "skills/solana-development/references/tokens-2022.md", + "sha256": "fc3618fd671402eb6455d1bc6ecaa42bcf853bea3eb2a8f68e7a40df971d95f7" + }, + { + "path": "skills/solana-development/references/native-rust.md", + "sha256": "d62adf628c656cd588882f230f2ebfa757895968e5f5db19152ed9dbb0d8c1ab" + }, + { + "path": "skills/solana-development/references/durable-nonces.md", + "sha256": "a7acc229a847e93613472a89602502b247ba641ad9cbd394fc2e1a3532203df5" + }, + { + "path": "skills/solana-development/references/versioned-transactions.md", + "sha256": "e46b2f87889542d13be9ce169446e125257746dc24bebe19c8b169deea76f91e" + }, + { + "path": "skills/solana-development/references/accounts.md", + "sha256": "278f56289d3de18cfc065cdc654dd4e497360f9d7ca7661b2dbebd185936a571" + }, + { + "path": "skills/solana-development/references/anchor.md", + "sha256": "8f304a21e8888a2cbb2a0419fa196c19997d616048bdbc63a816e18aaba8c050" + }, + { + "path": "skills/solana-development/references/deployment.md", + "sha256": "3bf2e05b645b03a2aedd06e033bc9e4a4519a91b1c33ae0fe312a677f08f3139" + }, + { + "path": "skills/solana-development/references/tokens-overview.md", + "sha256": "2810534a017a70917d61a3cd44ca624491d311d40c3bfd57aa82636b169df50b" + }, + { + "path": "skills/solana-development/references/tokens-validation.md", + "sha256": "e8cfddd0d9b64d157794fce40c27bff1481b757b90bdc2fdc8515ed21ce62e75" + }, + { + "path": "skills/solana-development/references/sysvars.md", + "sha256": "b9f8322981670aaf0a015785700816fff41cf73aa3fdb013340d8cf3078223fc" + }, + { + "path": "skills/solana-development/references/error-handling.md", + "sha256": "bbc1fcf9db104cbecdb7ed8deac73297f86b1b799ec69dd4e9edf4d8b05d28bf" + }, + { + "path": "skills/solana-development/references/tokens-operations.md", + "sha256": "93f5c53806860f5eaff5be916a846ae0897d5e64ce279753df625664c717082f" + }, + { + "path": "skills/solana-development/references/builtin-programs.md", + "sha256": "a1a827d54655bb92f106d4d5af3d7f68727842773445be66eec2fc92ab7b1edc" + }, + { + "path": "skills/solana-development/references/cpi.md", + "sha256": "573402dc99ca383e5be1a89d81ce291b0b95c8658238985859a674f6844e4ecb" + }, + { + "path": "skills/solana-development/references/security.md", + "sha256": "76597b6d5951252f71a8fd72309b87ecddced6f31b3bd4fd5117190eaa92b4b9" + }, + { + "path": "skills/solana-development/references/compute-optimization.md", + "sha256": "4954d2caac8bd22afe00454566a73a62973a73d4f0d1e7808f02b06bfe593d5a" + } + ], + "dirSha256": "3b2820eb7bb1a7702519fc61f932254d060916ec7f9973e9c392b2d938164ea3" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/solana-development/SKILL.md b/skills/solana-development/SKILL.md new file mode 100644 index 0000000..9073e9c --- /dev/null +++ b/skills/solana-development/SKILL.md @@ -0,0 +1,367 @@ +--- +name: solana-development +description: Build Solana programs with Anchor framework or native Rust. Use when developing Solana smart contracts, implementing token operations, testing programs, deploying to networks, or working with Solana development. Covers both high-level Anchor framework (recommended) and low-level native Rust for advanced use cases. +--- + +# Solana Development + +Build Solana programs using Anchor framework or native Rust. Both approaches share the same core concepts (accounts, PDAs, CPIs, tokens) but differ in syntax and abstraction level. + +## Quick Start + +### Recommended: Anchor Framework + +Anchor provides macros and tooling that reduce boilerplate and increase developer productivity: + +```rust +use anchor_lang::prelude::*; + +declare_id!("YourProgramID"); + +#[program] +pub mod my_program { + use super::*; + + pub fn initialize(ctx: Context, 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, +} +``` + +**When to use Anchor:** +- Building DeFi, NFT, or standard programs +- Need TypeScript client generation with IDL +- Want faster development with less boilerplate +- Following common Solana patterns +- New to Solana development + +**Installation:** +```bash +cargo install --git https://github.com/coral-xyz/anchor avm --locked --force +avm install latest +avm use latest +anchor --version +``` + +**Create project:** +```bash +anchor init my_project +cd my_project +anchor build +anchor test +``` + +**→ See [references/anchor.md](references/anchor.md) for complete Anchor guide** + +### Advanced: Native Rust + +Native Rust provides maximum control, optimization potential, and deeper understanding of Solana's runtime: + +```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 and validation + // Manual instruction routing + Ok(()) +} +``` + +**When to use Native Rust:** +- Need maximum compute efficiency (CU optimization critical) +- Require advanced features (versioned transactions, durable nonces, ALTs) +- Learning Solana fundamentals from first principles +- Building highly optimized or specialized programs +- Framework overhead is unacceptable + +**Setup:** +```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 complete native Rust guide** + +## Core Concepts + +Essential knowledge for all Solana developers, regardless of framework: + +### Foundational Concepts + +- **[accounts.md](references/accounts.md)** - Account model, ownership, rent, validation patterns +- **[pda.md](references/pda.md)** - Program Derived Addresses: derivation, canonical bumps, signing patterns +- **[cpi.md](references/cpi.md)** - Cross-Program Invocations: calling other programs safely + +### Program Integration + +- **[tokens-overview.md](references/tokens-overview.md)** - Token account structures and ATAs +- **[tokens-operations.md](references/tokens-operations.md)** - Create, mint, transfer, burn, close operations +- **[tokens-validation.md](references/tokens-validation.md)** - Account validation patterns +- **[tokens-2022.md](references/tokens-2022.md)** - Token Extensions Program features +- **[tokens-patterns.md](references/tokens-patterns.md)** - Common patterns and security +- **[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 +- **[deployment.md](references/deployment.md)** - Deploy, upgrade, verify, and manage programs +- **[production-deployment.md](references/production-deployment.md)** - Verified builds for production (Anchor 0.32.1 workflow) + +### Implementation Details + +- **[serialization.md](references/serialization.md)** - Account data layout, Borsh, zero-copy patterns +- **[error-handling.md](references/error-handling.md)** - Custom error types, propagation, client-side handling +- **[security.md](references/security.md)** - Security best practices and defensive programming patterns + +### Advanced Features + +- **[compute-optimization.md](references/compute-optimization.md)** - CU optimization techniques 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 transaction nonces +- **[transaction-lifecycle.md](references/transaction-lifecycle.md)** - Submission, retry patterns, confirmations + +### Low-Level Details + +- **[sysvars.md](references/sysvars.md)** - System variables (Clock, Rent, EpochSchedule, SlotHashes) +- **[builtin-programs.md](references/builtin-programs.md)** - System Program and Compute Budget Program + +### Resources + +- **[resources.md](references/resources.md)** - Official docs, tools, learning paths, community + +## Common Tasks Quick Reference + +**Create a new program:** +- Anchor: `anchor init my_project` → [anchor.md#getting-started](references/anchor.md) +- Native: `cargo new my_program --lib` → [native-rust.md#setup](references/native-rust.md) + +**Initialize a PDA account:** +- Anchor: Use `#[account(init, seeds = [...], bump)]` → [pda.md#anchor](references/pda.md) +- Native: Manual `invoke_signed` with System Program → [pda.md#native](references/pda.md) + +**Transfer SPL tokens:** +- Anchor: Use `anchor_spl::token::transfer` → [tokens-operations.md#transferring-tokens](references/tokens-operations.md) +- Native: CPI to Token Program → [tokens-operations.md#transferring-tokens](references/tokens-operations.md) + +**Test your program:** +- Both: Mollusk for fast unit tests → [testing-frameworks.md#mollusk-testing](references/testing-frameworks.md) +- Anchor: `anchor test` for integration tests → [testing-frameworks.md#anchor-specific-testing](references/testing-frameworks.md) + +**Deploy to devnet:** +- Anchor: `anchor deploy` → [deployment.md#anchor](references/deployment.md) +- Native: `solana program deploy` → [deployment.md#native](references/deployment.md) + +**Deploy to production (verified builds):** +- Both: `solana-verify build` + `solana program deploy` → [production-deployment.md](references/production-deployment.md) + +**Optimize compute units:** +- Both: Profile with Mollusk bencher → [compute-optimization.md](references/compute-optimization.md) + +**Handle 40+ accounts:** +- Both: Use Address Lookup Tables → [versioned-transactions.md](references/versioned-transactions.md) + +**Offline transaction signing:** +- Both: Use durable nonces → [durable-nonces.md](references/durable-nonces.md) + +## Decision Guide + +| Your Need | Recommended Approach | Reason | +|-----------|---------------------|---------| +| Standard DeFi/NFT program | Anchor | Faster development, proven patterns | +| TypeScript client needed | Anchor | Auto-generates IDL and client types | +| Learning Solana fundamentals | Native Rust first | Understand the platform deeply | +| Compute optimization critical | Native Rust | Direct control, minimal overhead | +| Advanced tx features (ALTs, nonces) | Either (slight edge to Native) | Framework-agnostic features | +| Fast prototyping | Anchor | Less boilerplate, faster iteration | +| Maximum control over every detail | Native Rust | No abstraction layer | +| Team familiar with frameworks | Anchor | Lower learning curve | +| Program size matters | Native Rust | Smaller compiled programs | + +**Note:** You can also start with Anchor for rapid development, then optimize critical paths with native Rust patterns if needed. + +## Framework Comparison + +| Aspect | Anchor | Native Rust | +|--------|--------|-------------| +| **Setup complexity** | Simple (`anchor init`) | Manual (Cargo.toml, entrypoint) | +| **Boilerplate** | Minimal (macros handle it) | Significant (manual everything) | +| **Account validation** | Declarative (`#[account(...)]`) | Manual (explicit checks) | +| **Serialization** | Automatic (Borsh via macros) | Manual (Borsh or custom) | +| **Type safety** | High (compile-time checks) | High (but more verbose) | +| **IDL generation** | Automatic | Manual or tools | +| **Client library** | TypeScript + Rust auto-gen | Manual client code | +| **Testing** | `anchor test`, Mollusk | Mollusk, cargo test | +| **Deployment** | `anchor deploy` | `solana program deploy` | +| **Compute overhead** | Small (~1-3% typical) | None (direct) | +| **Program size** | Slightly larger | Smaller | +| **Learning curve** | Gentler (abstractions help) | Steeper (need SVM knowledge) | +| **Debugging** | Good (clear macro errors) | More complex (lower level) | +| **Community** | Large (most use Anchor) | Growing (optimization focus) | + +## Typical Development Workflow + +### Anchor Workflow + +1. **Init**: `anchor init my_project` +2. **Define accounts**: Use `#[derive(Accounts)]` with constraints +3. **Implement instructions**: Write functions in `#[program]` module +4. **Define state**: Use `#[account]` macro for account structures +5. **Test**: Write tests in `tests/`, run `anchor test` +6. **Deploy**: `anchor deploy` to configured network +7. **Client**: Import generated IDL and types in TypeScript/Rust + +### Native Rust Workflow + +1. **Setup**: `cargo new my_program --lib`, configure Cargo.toml +2. **Define entrypoint**: Implement `process_instruction` function +3. **Define state**: Manual Borsh serialization structs +4. **Implement instructions**: Manual routing and account parsing +5. **Validate accounts**: Explicit ownership, signer, writable checks +6. **Test**: Write Mollusk tests, run `cargo test` +7. **Build**: `cargo build-sbf` +8. **Deploy**: `solana program deploy target/deploy/program.so` +9. **Client**: Build client manually or use web3.js/rs + +## Best Practices + +### General (Both Approaches) + +✅ **Always validate accounts**: Check ownership, signers, mutability +✅ **Use checked arithmetic**: `.checked_add()`, `.checked_sub()`, etc. +✅ **Test extensively**: Unit tests, integration tests, edge cases +✅ **Handle errors gracefully**: Return descriptive errors +✅ **Document your code**: Explain account requirements and constraints +✅ **Version your programs**: Plan for upgrades and migrations +✅ **Use PDAs for program-owned accounts**: Don't pass private keys +✅ **Minimize compute units**: Profile and optimize hot paths +✅ **Add security.txt**: Make it easy for researchers to contact you + +### Anchor-Specific + +✅ **Use `InitSpace` derive**: Auto-calculate account space +✅ **Prefer `has_one` constraints**: Clearer than custom constraints +✅ **Use `Program<'info, T>`**: Validate program accounts in CPIs +✅ **Emit events**: Use `emit!` for important state changes +✅ **Group related constraints**: Keep account validation readable + +### Native Rust-Specific + +✅ **Use `next_account_info`**: Safe account iteration +✅ **Cache PDA bumps**: Store bump in account, use `create_program_address` +✅ **Zero-copy when possible**: 50%+ CU savings for large structs +✅ **Minimize logging**: Especially avoid pubkey formatting (expensive) +✅ **Build verifiable**: Use `solana-verify build` for production + +## Security Considerations + +**Both frameworks require security vigilance:** + +⚠️ **Common vulnerabilities:** +- Missing signer checks +- Integer overflow/underflow +- Account confusion attacks +- PDA substitution +- Arbitrary CPI targets +- Missing account ownership checks +- Insufficient rent exemption +- Account closing without zeroing + +**→ For defensive programming patterns and secure coding practices, see [security.md](references/security.md)** + +That guide provides: +- Core security rules and principles +- Account validation patterns +- Arithmetic safety guidelines +- Pre-deployment security checklist + +**→ For comprehensive security audits, use the `solana-security` skill** + +That skill provides: +- Systematic vulnerability analysis +- Attack scenarios and exploit POCs +- Framework-specific security reviews +- Professional audit workflows + +## When to Switch or Combine + +**Start with Anchor, optimize later:** +- Build MVP with Anchor for speed +- Profile to find CU bottlenecks +- Optimize critical paths with native patterns +- Keep Anchor for non-critical code + +**Start with Native, add Anchor features:** +- Build core program logic in native Rust +- Use Anchor's client generation separately +- Leverage anchor-spl for common patterns +- Maintain control where it matters + +**Use both in a workspace:** +```toml +[workspace] +members = [ + "programs/core", # Native Rust + "programs/wrapper", # Anchor facade +] +``` + +## 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) +- **General**: See [resources.md](references/resources.md) for comprehensive links + +## Next Steps + +**New to Solana?** +1. Read [accounts.md](references/accounts.md) - Understand the account model +2. Read [anchor.md](references/anchor.md) - Start with Anchor framework +3. Read [security.md](references/security.md) - Learn secure coding from the start +4. Build a simple program following [testing-overview.md](references/testing-overview.md) +5. Deploy to devnet using [deployment.md](references/deployment.md) + +**Coming from another blockchain?** +1. Read [accounts.md](references/accounts.md) - Solana's model is different +2. Read [pda.md](references/pda.md) - Unique to Solana +3. Choose Anchor for familiar framework experience +4. Explore [resources.md](references/resources.md) for migration guides + +**Want to optimize?** +1. Start with working Anchor program +2. Profile with [compute-optimization.md](references/compute-optimization.md) +3. Learn native patterns from [native-rust.md](references/native-rust.md) +4. Refactor bottlenecks selectively + +**Building production apps?** +1. Master [security considerations](references/pda.md#security) +2. Use [testing-practices.md](references/testing-practices.md) for comprehensive best practices +3. Follow [production-deployment.md](references/production-deployment.md) for verified builds +4. Get security audit with `solana-security` skill diff --git a/skills/solana-development/references/accounts.md b/skills/solana-development/references/accounts.md new file mode 100644 index 0000000..79bcd03 --- /dev/null +++ b/skills/solana-development/references/accounts.md @@ -0,0 +1,892 @@ +# 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, + /// 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) +- 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::(&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>, // Mutable lamport balance + pub data: Rc>, // 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 { + // 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. diff --git a/skills/solana-development/references/anchor.md b/skills/solana-development/references/anchor.md new file mode 100644 index 0000000..ee47f4a --- /dev/null +++ b/skills/solana-development/references/anchor.md @@ -0,0 +1,1647 @@ +# 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, data: u64) -> Result<()> { + ctx.accounts.new_account.data = data; + msg!("Data set to: {}!", data); + Ok(()) + } + + pub fn update(ctx: Context, new_data: u64) -> Result<()> { + ctx.accounts.account.data = new_data; + Ok(()) + } +} +``` + +**Context 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, 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, data: u64) -> Result<()> { + ctx.accounts.new_account.data = data; + ctx.accounts.new_account.authority = ctx.accounts.authority.key(); + Ok(()) + } + + pub fn update(ctx: Context, 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) -> 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/.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 -f target/idl/program.json +``` + +Fetch IDL from chain: + +```bash +anchor idl fetch +``` + +--- + +## TypeScript Client + +### Installation + +```bash +npm install @coral-xyz/anchor @solana/web3.js +# or +yarn add @coral-xyz/anchor @solana/web3.js +``` + +**Note:** Only compatible with `@solana/web3.js` v1, not v2. + +### Setup Program Instance + +#### With Wallet (Frontend) + +```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.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 -f target/idl/program.json + +# Fetch IDL from chain +anchor idl fetch + +# Upgrade on-chain IDL +anchor idl upgrade -f target/idl/program.json + +# Get IDL authority +anchor idl authority + +# Set new IDL authority +anchor idl set-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 + +# Run migration script +anchor migrate + +# Close deployed program (reclaim rent) +solana program close +``` + +### Local Validator + +```bash +# Start local validator +solana-test-validator + +# Start with program loaded +solana-test-validator --bpf-program 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, 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, 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, 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; + + 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, 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, 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::() + )] + pub large_account: AccountLoader<'info, LargeData>, + + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn initialize(ctx: Context) -> Result<()> { + let mut large_account = ctx.accounts.large_account.load_init()?; + large_account.data[0] = 42; + Ok(()) +} + +pub fn update(ctx: Context) -> 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) -> 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) -> 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, 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) -> Result<()> { + for account_info in ctx.remaining_accounts.iter() { + let account = Account::::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, 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. diff --git a/skills/solana-development/references/builtin-programs.md b/skills/solana-development/references/builtin-programs.md new file mode 100644 index 0000000..d38228f --- /dev/null +++ b/skills/solana-development/references/builtin-programs.md @@ -0,0 +1,931 @@ +# 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::(); + 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, + 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. diff --git a/skills/solana-development/references/compute-optimization.md b/skills/solana-development/references/compute-optimization.md new file mode 100644 index 0000000..ff74c4c --- /dev/null +++ b/skills/solana-development/references/compute-optimization.md @@ -0,0 +1,680 @@ +# 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 = 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 = Vec::new(); +for _ in 0..6 { + a.push(1); +} +``` + +**Initialization vs pushing:** + +```rust +// 357 CU - Pushing elements one by one +let mut a: Vec = Vec::new(); +for _ in 0..6 { + a.push(1); +} + +// 125 CU - Direct initialization (65% savings!) +let _a: Vec = 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) -> Result<()> { + Ok(()) +} + +// 2,600 CU total for increment (including serialization overhead) +pub fn increment(ctx: Context) -> 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) -> Result<()> { + Ok(()) +} + +// 1,254 CU total for increment (52% savings!) +pub fn increment_zero_copy(ctx: Context) -> 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 { + 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 { + 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) -> 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) -> 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 diff --git a/skills/solana-development/references/cpi.md b/skills/solana-development/references/cpi.md new file mode 100644 index 0000000..0bef921 --- /dev/null +++ b/skills/solana-development/references/cpi.md @@ -0,0 +1,824 @@ +# 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, + + /// Serialized instruction data + pub data: Vec, +} + +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. diff --git a/skills/solana-development/references/deployment.md b/skills/solana-development/references/deployment.md new file mode 100644 index 0000000..d0adf86 --- /dev/null +++ b/skills/solana-development/references/deployment.md @@ -0,0 +1,1828 @@ +# 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 \ + --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 ` - 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 + +# Output shows: +# Program Id: +# Owner: BPFLoaderUpgradeable1111111111111111111111111 +# ProgramData Address: +# Authority: # If upgradeable +# Last Deployed In Slot: ... +# Data Length: ... +``` + +### Anchor Upgrades + +**Upgrade deployed program:** +```bash +anchor upgrade target/deploy/my_program.so \ + --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 \ + --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 \ + --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 +# 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 $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 +``` + +**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, +} + +// 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) -> 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 \ + -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 + +# 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 \ + https://github.com/your-org/your-repo \ + --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 \ + https://github.com/your-org/your-repo +``` + +**Manual job submission:** +```bash +solana-verify remote submit-job \ + --program-id \ + --uploader +``` + +**Check verification status:** +```bash +solana-verify remote get-job-status --job-id +``` + +**Verification API endpoint:** +``` +https://verify.osec.io/status/ +``` + +**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 + +# Output: +# Program Id: YourProgramId +# Owner: BPFLoaderUpgradeable1111111111111111111111111 +# ProgramData Address: +# 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 \ + --new-upgrade-authority +``` + +**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 \ + --new-upgrade-authority $SQUADS_VAULT +``` + +### Making Programs Immutable + +**WARNING:** This is irreversible! + +```bash +solana program set-upgrade-authority --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 + +# 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 \ + -u mainnet-beta \ + --with-compute-unit-price 50000 +``` + +**3. Verify locally:** +```bash +solana-verify verify-from-repo \ + -u mainnet-beta \ + --program-id \ + https://github.com/your-org/repo \ + --commit-hash +``` + +**4. Transfer authority to Squads multisig:** + +Get Squads vault address from https://v4.squads.so/ + +```bash +SQUADS_VAULT="YourSquadsVaultAddress" + +solana program set-upgrade-authority \ + --new-upgrade-authority $SQUADS_VAULT +``` + +**5. Export verification PDA transaction:** + +```bash +solana-verify verify-from-repo \ + -u mainnet-beta \ + --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 \ + --uploader +``` + +**8. Monitor verification:** + +```bash +# Check job status +solana-verify remote get-job-status --job-id + +# Or visit +https://verify.osec.io/status/ +``` + +### 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: +``` + +**Transfer buffer authority to multisig:** +```bash +solana program set-buffer-authority \ + --new-buffer-authority +``` + +**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 "" \ + --buffer "" \ + --multisig "" \ + --member "" \ + --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 "" \ + --buffer "" \ + --program "" +``` + +--- + +## Network-Specific Considerations + +### Localhost Development + +**Start test validator:** +```bash +# Basic +solana-test-validator + +# With program pre-deployed +solana-test-validator --bpf-program target/deploy/my_program.so + +# With cloned mainnet accounts +solana-test-validator \ + --clone \ + --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 \ + -u mainnet-beta \ + --with-compute-unit-price 100000 \ + --max-sign-attempts 100 \ + --use-rpc +``` + +**Post-deployment verification:** +```bash +# Verify deployment +solana program show -u mainnet-beta + +# Verify hash matches +solana-verify get-program-hash -u mainnet-beta + +# Submit verification job +solana-verify verify-from-repo \ + --remote \ + -u mainnet-beta \ + --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 + +# 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 + +# 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:** +```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 --limit 10 + +# Or use explorers: +# https://explorer.solana.com/address/ +# https://solscan.io/account/ +``` + +**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 \ + --filepath target/idl/my_program.json \ + --provider.cluster devnet +``` + +**Upgrade IDL after program upgrade:** +```bash +anchor idl upgrade \ + --filepath target/idl/my_program.json \ + --provider.cluster devnet +``` + +**Fetch published IDL:** +```bash +anchor idl fetch \ + --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 + +# Reclaims rent SOL to wallet +``` + +**Close entire program (irreversible!):** +```bash +solana program close + +# 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 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 +# 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 $DIFF + +# Check rent for extension +solana rent $DIFF + +# Now deploy upgrade +solana program deploy target/deploy/my_program.so --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 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 +``` + +### 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 +``` + +**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 \ + ... +``` + +--- + +## 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:
" >> 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 target/deploy/program.so --provider.cluster devnet +# Verify upgrade worked + +# 5. Verified build test +solana-verify build +solana-verify verify-from-repo --program-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 \ + -u mainnet-beta \ + --with-compute-unit-price 100000 +``` + +## Verify + +```bash +solana-verify verify-from-repo \ + --remote \ + -u mainnet-beta \ + --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/ +``` + +--- + +## 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! diff --git a/skills/solana-development/references/durable-nonces.md b/skills/solana-development/references/durable-nonces.md new file mode 100644 index 0000000..ead1903 --- /dev/null +++ b/skills/solana-development/references/durable-nonces.md @@ -0,0 +1,961 @@ +# 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), + Current(Box), +} + +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> { + // 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> { + // 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::(&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> { + 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, + 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> { + // 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> { + 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> { + 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> { + 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 \ + --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 \ + --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, Box> { + 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> { + // 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> { + // 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 +``` + +**Get current nonce:** +```bash +solana nonce +``` + +**Manually advance nonce:** +```bash +solana new-nonce +``` + +**Get nonce account info:** +```bash +solana nonce-account +``` + +**Withdraw from nonce:** +```bash +solana withdraw-from-nonce-account +``` + +**Change nonce authority:** +```bash +solana authorize-nonce-account +``` + +**Sign transaction offline:** +```bash +solana \ + --sign-only \ + --nonce \ + --nonce-authority \ + --blockhash +``` + +**Submit pre-signed transaction:** +```bash +solana \ + --nonce \ + --nonce-authority \ + --blockhash \ + --signer +``` + +## 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/) diff --git a/skills/solana-development/references/error-handling.md b/skills/solana-development/references/error-handling.md new file mode 100644 index 0000000..c60082d --- /dev/null +++ b/skills/solana-development/references/error-handling.md @@ -0,0 +1,800 @@ +# 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 for ProgramError`: + +```rust +impl From 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 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. diff --git a/skills/solana-development/references/native-rust.md b/skills/solana-development/references/native-rust.md new file mode 100644 index 0000000..05cb2cb --- /dev/null +++ b/skills/solana-development/references/native-rust.md @@ -0,0 +1,2042 @@ +# 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>, // Account balance + pub data: Rc>, // 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(&self) -> Result; + + // Serialize data into account + pub fn serialize_data(&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::()]) + .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, +} +``` + +### 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(&self, writer: &mut W) -> BorshResult<()> { + self.flag.serialize(writer)?; + self.value.serialize(writer)?; + Ok(()) + } +} + +impl BorshDeserialize for CustomState { + fn deserialize_reader(reader: &mut R) -> BorshResult { + 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 { + 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::(); +} + +// 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::( + &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 { + 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::(); +} +``` + +**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, +) -> 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, +) -> 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 +``` + +**Calculate Deployment Cost:** + +Program cost formula: `rent_exemption(program_size)` + +```bash +# Get rent for specific size +solana rent + +# 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 \ + --remote https://github.com/user/repo \ + --commit-hash \ + --library-name program_name + +# Verify with mount path (for workspace) +solana-verify verify-from-repo \ + --program-id \ + --remote https://github.com/user/repo \ + --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: + +# Set buffer authority to multisig +solana program set-buffer-authority --new-buffer-authority + +# Later: Deploy from buffer (requires multisig) +solana program deploy --buffer --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 + +# 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 + +# Output: +# Program Id: +# Owner: BPFLoaderUpgradeab1e11111111111111111111111 +# ProgramData Address: +# Authority: +# Last Deployed In Slot: 123456789 +# Data Length: 204800 bytes +# Balance: 1.42607328 SOL +``` + +**Show Program Data:** + +```bash +# Get upgrade authority +solana program show | grep Authority + +# Get program size +solana program show | grep "Data Length" +``` + +### Authority Transfers + +**Transfer Upgrade Authority:** + +```bash +# Transfer to new authority +solana program set-upgrade-authority \ + \ + --new-upgrade-authority + +# Transfer to multisig +solana program set-upgrade-authority \ + \ + --new-upgrade-authority +``` + +### Making Programs Immutable + +Remove upgrade authority to make program immutable: + +```bash +# Make immutable (IRREVERSIBLE!) +solana program set-upgrade-authority --final + +# Verify immutability +solana program show +# 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 + +# Close and send rent to specific recipient +solana program close --recipient + +# 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 { + let rent = Rent::get()?; + Ok(rent.minimum_balance(data_len)) +} +``` + +**Check if Rent Exempt:** + +```rust +fn is_rent_exempt(account: &AccountInfo) -> Result { + 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 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 for ProgramError { + fn from(e: MyError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for MyError { + fn type_of() -> &'static str { + "MyError" + } +} + +impl PrintProgramError for MyError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + 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 { + let clock = Clock::get()?; + Ok(clock.unix_timestamp) +} + +fn get_current_slot() -> Result { + 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.* diff --git a/skills/solana-development/references/pda.md b/skills/solana-development/references/pda.md new file mode 100644 index 0000000..c59bbb4 --- /dev/null +++ b/skills/solana-development/references/pda.md @@ -0,0 +1,796 @@ +# 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. diff --git a/skills/solana-development/references/production-deployment.md b/skills/solana-development/references/production-deployment.md new file mode 100644 index 0000000..5ed5454 --- /dev/null +++ b/skills/solana-development/references/production-deployment.md @@ -0,0 +1,498 @@ +# 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 \ + 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 \ + https://github.com/your-org/your-repo \ + --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 + +# 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 ` + +### 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 | 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 $DIFF +fi + +# 5. Deploy upgrade +solana program deploy target/deploy/my_program.so \ + --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 \ + 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: + +# 3. Transfer buffer to multisig +solana program set-buffer-authority \ + --new-buffer-authority + +# 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 \ + 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 + +# 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 \ + 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 + +# 3. Verify +solana-verify verify-from-repo \ + -u \ + --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. diff --git a/skills/solana-development/references/resources.md b/skills/solana-development/references/resources.md new file mode 100644 index 0000000..b67a53d --- /dev/null +++ b/skills/solana-development/references/resources.md @@ -0,0 +1,187 @@ +# Development Resources + +Comprehensive collection of official documentation, development tools, learning paths, and community resources for Solana program development. + +## 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 +- [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) - Solana JavaScript SDK +- [@coral-xyz/anchor](https://www.npmjs.com/package/@coral-xyz/anchor) - Anchor TypeScript client +- [@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 + +### 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 + +## Version Information + +**Current versions (as of 2025):** +- Latest Anchor: 0.30+ +- Recommended Solana CLI: Latest stable (check with `solana --version`) +- Rust minimum: 1.70+ +- 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:** For security-specific resources, vulnerability databases, audit reports, and security tools, see the `solana-security` skill. diff --git a/skills/solana-development/references/security.md b/skills/solana-development/references/security.md new file mode 100644 index 0000000..2c857a9 --- /dev/null +++ b/skills/solana-development/references/security.md @@ -0,0 +1,613 @@ +# Security Best Practices for Solana Development + +Essential security principles and defensive programming patterns for building secure Solana programs with Anchor or native Rust. + +> **Note:** This guide focuses on defensive programming during development. For comprehensive security audits, vulnerability analysis, and attack vectors, use the **`solana-security` skill**. + +## Table of Contents + +1. [Security Mindset](#security-mindset) +2. [Core Security Rules](#core-security-rules) +3. [Account Validation](#account-validation) +4. [Arithmetic Safety](#arithmetic-safety) +5. [PDA Security](#pda-security) +6. [CPI Security](#cpi-security) +7. [Common Pitfalls](#common-pitfalls) +8. [Pre-Deployment Checklist](#pre-deployment-checklist) + +--- + +## Security Mindset + +### Think Like an Attacker + +**Fundamental principle:** Attackers control everything they send to your program. + +- ❌ Don't assume: "Users won't do that" +- ❌ Don't assume: "The client validates this" +- ❌ Don't assume: "This account must be correct" +- ✅ Do validate: Every account, every parameter, every assumption + +### You Control Nothing + +Once deployed, your program: +- Cannot control which accounts are passed in +- Cannot control instruction data +- Cannot control timing or ordering +- Cannot prevent malicious clients + +**Your only control:** How your program validates and handles inputs. + +--- + +## Core Security Rules + +### Rule 1: Validate Every Account + +**Always verify:** + +**Anchor:** +```rust +#[derive(Accounts)] +pub struct SecureInstruction<'info> { + // ✅ Signer required + pub authority: Signer<'info>, + + // ✅ Owner validation + relationship + #[account( + mut, + has_one = authority, // vault.authority == authority.key() + )] + pub vault: Account<'info, Vault>, + + // ✅ Program ID validation + pub token_program: Program<'info, Token>, +} +``` + +**Native Rust:** +```rust +// ✅ Signer check +if !authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); +} + +// ✅ Owner check +if vault.owner != program_id { + return Err(ProgramError::IllegalOwner); +} + +// ✅ Program ID check +if *token_program.key != spl_token::id() { + return Err(ProgramError::IncorrectProgramId); +} +``` + +### Rule 2: Use Checked Arithmetic + +**Never use:** +- `+`, `-`, `*`, `/` operators directly +- `saturating_*` methods (hide errors) +- `unwrap()` or `expect()` on arithmetic + +**Always use:** +```rust +// ✅ Checked operations +let total = balance + .checked_add(amount) + .ok_or(ErrorCode::Overflow)?; + +let remaining = total + .checked_sub(withdrawal) + .ok_or(ErrorCode::InsufficientFunds)?; + +let product = price + .checked_mul(quantity) + .ok_or(ErrorCode::Overflow)?; + +let share = total + .checked_div(parts) + .ok_or(ErrorCode::DivisionByZero)?; +``` + +### Rule 3: Validate PDAs Properly + +**Anchor:** +```rust +#[derive(Accounts)] +pub struct SecurePDA<'info> { + // ✅ Use canonical bump + #[account( + seeds = [b"vault", user.key().as_ref()], + bump, // Automatically validates canonical bump + )] + pub vault: Account<'info, Vault>, +} +``` + +**Native Rust:** +```rust +// ✅ Find canonical bump +let (expected_pda, bump) = Pubkey::find_program_address( + &[b"vault", user.key.as_ref()], + program_id, +); + +// ✅ Validate PDA matches +if expected_pda != *vault.key { + return Err(ProgramError::InvalidSeeds); +} + +// Store bump for future use with create_program_address +``` + +### Rule 4: Secure Cross-Program Invocations + +**Anchor:** +```rust +// ✅ Program type validation +pub token_program: Program<'info, Token>, + +// ✅ Use CpiContext +let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + transfer_accounts, +); + +token::transfer(cpi_ctx, amount)?; +``` + +**Native Rust:** +```rust +// ✅ Validate program ID before CPI +if *token_program.key != spl_token::id() { + return Err(ProgramError::IncorrectProgramId); +} + +// ✅ Build instruction safely +let ix = spl_token::instruction::transfer( + token_program.key, + source.key, + destination.key, + authority.key, + &[], + amount, +)?; + +invoke(&ix, &[source, destination, authority, token_program])?; +``` + +### Rule 5: Handle Errors Gracefully + +**Never:** +```rust +// ❌ Don't panic or unwrap +let value = some_operation().unwrap(); + +// ❌ Don't ignore errors +some_operation(); +``` + +**Always:** +```rust +// ✅ Propagate errors +let value = some_operation() + .ok_or(ErrorCode::OperationFailed)?; + +// ✅ Or handle explicitly +let value = match some_operation() { + Some(v) => v, + None => return Err(ErrorCode::OperationFailed.into()), +}; +``` + +--- + +## Account Validation + +### Essential Checks + +For every account, verify: + +1. **Signer** - Does this account need to sign? +2. **Owner** - Who owns this account? Is it our program? +3. **Writable** - Does this need `mut`? +4. **Type** - Is this the right account type? +5. **Relationships** - Do related accounts match? + +### Validation Pattern + +```rust +// Native Rust comprehensive validation +pub fn validate_account( + account: &AccountInfo, + expected_owner: &Pubkey, + must_be_signer: bool, + must_be_writable: bool, +) -> ProgramResult { + // Check signer + if must_be_signer && !account.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + // Check owner + if account.owner != expected_owner { + return Err(ProgramError::IllegalOwner); + } + + // Check writable + if must_be_writable && !account.is_writable { + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) +} +``` + +--- + +## Arithmetic Safety + +### Common Vulnerabilities + +**Overflow example:** +```rust +// ❌ VULNERABLE: Can overflow +pub fn deposit(ctx: Context, 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, 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) -> 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, 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) -> Result<()> { + let data = ctx.accounts.account.try_borrow_data()?; + // What if attacker passes wrong account type? +} + +// ✅ Use typed Account +pub fn good(ctx: Context) -> Result<()> { + // Anchor verifies discriminator automatically + let vault = &ctx.accounts.vault; +} +``` + +--- + +## Pre-Deployment Checklist + +Before deploying to mainnet: + +### Code Review + +- [ ] All accounts validated (signer, owner, writable) +- [ ] All arithmetic uses `checked_*` methods +- [ ] All PDAs use canonical bumps +- [ ] All CPIs validate target programs +- [ ] No `unwrap()` or `expect()` in production code +- [ ] No `init_if_needed` without additional checks +- [ ] All error cases handled gracefully + +### Testing + +- [ ] Unit tests cover all instructions +- [ ] Integration tests cover instruction interactions +- [ ] Edge cases tested (zero amounts, max values, overflow) +- [ ] Error conditions tested (invalid accounts, unauthorized access) +- [ ] Fuzz testing with Trident (if possible) + +### Security Audit + +- [ ] Internal code review completed +- [ ] External security audit (recommended for >$100k TVL) +- [ ] Use `solana-security` skill for systematic review +- [ ] All critical/high severity findings resolved +- [ ] Medium findings assessed and documented + +### Documentation + +- [ ] Account structures documented +- [ ] Instruction requirements documented +- [ ] Known limitations documented +- [ ] Upgrade strategy documented +- [ ] Emergency procedures documented + +### Deployment + +- [ ] Tested on devnet extensively +- [ ] Tested on mainnet-beta with small amounts +- [ ] Upgrade authority secured (multisig recommended) +- [ ] Monitoring and alerts configured +- [ ] Emergency pause mechanism (if applicable) + +--- + +## When to Use the Security Skill + +Use the **`solana-security` skill** for: + +- 🔍 **Comprehensive security audits** - Systematic review of entire codebase +- 🐛 **Vulnerability analysis** - Identifying exploit scenarios +- 📋 **Security checklists** - Category-by-category validation +- ⚠️ **Attack vectors** - Understanding how programs can be exploited +- 🛡️ **Framework-specific patterns** - Anchor vs native Rust security +- 📚 **Vulnerability databases** - Learning from past exploits + +Use **this skill (solana-development)** for: + +- 💻 **Building programs** - Implementation guidance +- ✅ **Defensive programming** - Secure coding patterns +- 🏗️ **Development workflows** - Testing, deployment, optimization +- 📖 **Framework learning** - Anchor and native Rust how-tos + +--- + +## Quick Security Reference + +### Anchor Security Checklist + +```rust +#[derive(Accounts)] +pub struct Secure<'info> { + // ✅ Signer + pub authority: Signer<'info>, + + // ✅ Validation + relationships + #[account( + mut, + has_one = authority, + seeds = [b"vault", user.key().as_ref()], + bump, + )] + pub vault: Account<'info, Vault>, + + // ✅ Program validation + pub token_program: Program<'info, Token>, +} + +pub fn secure_fn(ctx: Context, 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. diff --git a/skills/solana-development/references/serialization.md b/skills/solana-development/references/serialization.md new file mode 100644 index 0000000..d4fef20 --- /dev/null +++ b/skills/solana-development/references/serialization.md @@ -0,0 +1,620 @@ +# 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 +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, // 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 { + 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::(&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 = accounts + .iter() + .map(|acc| UserAccount::try_from_slice(&acc.data.borrow())) + .collect::, _>>()?; + + // 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::(); + + 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, +} + +#[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. diff --git a/skills/solana-development/references/sysvars.md b/skills/solana-development/references/sysvars.md new file mode 100644 index 0000000..734fab8 --- /dev/null +++ b/skills/solana-development/references/sysvars.md @@ -0,0 +1,992 @@ +# 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, +} + +pub fn create_event( + event_account: &AccountInfo, + data: Vec, +) -> 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 { + 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 { + 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 { + 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. diff --git a/skills/solana-development/references/testing-frameworks.md b/skills/solana-development/references/testing-frameworks.md new file mode 100644 index 0000000..fc7da09 --- /dev/null +++ b/skills/solana-development/references/testing-frameworks.md @@ -0,0 +1,1255 @@ +# 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; + + 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; + 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::(); + + // Set specific timestamp + clock.unix_timestamp = 1735689600; // January 1st 2025 + svm.set_sysvar::(&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, 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)** diff --git a/skills/solana-development/references/testing-overview.md b/skills/solana-development/references/testing-overview.md new file mode 100644 index 0000000..0f1597f --- /dev/null +++ b/skills/solana-development/references/testing-overview.md @@ -0,0 +1,406 @@ +# 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 { + 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)** diff --git a/skills/solana-development/references/testing-practices.md b/skills/solana-development/references/testing-practices.md new file mode 100644 index 0000000..0477417 --- /dev/null +++ b/skills/solana-development/references/testing-practices.md @@ -0,0 +1,528 @@ +# 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)** diff --git a/skills/solana-development/references/tokens-2022.md b/skills/solana-development/references/tokens-2022.md new file mode 100644 index 0000000..47e440d --- /dev/null +++ b/skills/solana-development/references/tokens-2022.md @@ -0,0 +1,172 @@ +# 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, + 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 diff --git a/skills/solana-development/references/tokens-operations.md b/skills/solana-development/references/tokens-operations.md new file mode 100644 index 0000000..5aeace8 --- /dev/null +++ b/skills/solana-development/references/tokens-operations.md @@ -0,0 +1,982 @@ +# 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) -> 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) -> 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, 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, 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, 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, + 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, 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, 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, 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) -> 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) -> 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 diff --git a/skills/solana-development/references/tokens-overview.md b/skills/solana-development/references/tokens-overview.md new file mode 100644 index 0000000..6ae4b92 --- /dev/null +++ b/skills/solana-development/references/tokens-overview.md @@ -0,0 +1,301 @@ +# 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, // 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, // 36 bytes +} +``` + +**COption Format:** +```rust +pub enum COption { + 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, // 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, // 12 bytes + + /// The amount delegated + pub delegated_amount: u64, // 8 bytes + + /// Optional authority to close the account + pub close_authority: COption, // 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) -> 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 diff --git a/skills/solana-development/references/tokens-patterns.md b/skills/solana-development/references/tokens-patterns.md new file mode 100644 index 0000000..50a5218 --- /dev/null +++ b/skills/solana-development/references/tokens-patterns.md @@ -0,0 +1,860 @@ +# 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, 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) -> 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, 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, 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) -> 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) -> 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) -> 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) -> 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. diff --git a/skills/solana-development/references/tokens-validation.md b/skills/solana-development/references/tokens-validation.md new file mode 100644 index 0000000..56701f1 --- /dev/null +++ b/skills/solana-development/references/tokens-validation.md @@ -0,0 +1,221 @@ +# 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) -> 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) -> 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, + 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 diff --git a/skills/solana-development/references/transaction-lifecycle.md b/skills/solana-development/references/transaction-lifecycle.md new file mode 100644 index 0000000..056f27b --- /dev/null +++ b/skills/solana-development/references/transaction-lifecycle.md @@ -0,0 +1,978 @@ +# 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, + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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, + retry_queue: Arc>>, +} + +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 { + 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 { + // 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) diff --git a/skills/solana-development/references/versioned-transactions.md b/skills/solana-development/references/versioned-transactions.md new file mode 100644 index 0000000..1de25c7 --- /dev/null +++ b/skills/solana-development/references/versioned-transactions.md @@ -0,0 +1,953 @@ +# 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, // All 32-byte addresses + pub recent_blockhash: Hash, + pub instructions: Vec, +} +``` + +**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, // Directly specified accounts + pub recent_blockhash: Hash, + pub instructions: Vec, + pub address_table_lookups: Vec, // NEW! +} + +pub struct MessageAddressTableLookup { + pub account_key: Pubkey, // ALT address (32 bytes) + pub writable_indexes: Vec, // Writable account indices + pub readonly_indexes: Vec, // 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, // 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, + expected_addresses: Vec, +) -> 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, +) -> Result { + 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) diff --git a/skills/solana-security/SKILL.md b/skills/solana-security/SKILL.md new file mode 100644 index 0000000..0167cd6 --- /dev/null +++ b/skills/solana-security/SKILL.md @@ -0,0 +1,325 @@ +--- +name: solana-security +description: Audit Solana programs (Anchor or native Rust) for security vulnerabilities. Use when reviewing smart contract security, finding exploits, analyzing attack vectors, performing security assessments, or when explicitly asked to audit, review security, check for bugs, or find vulnerabilities in Solana programs. +--- + +# Solana Security Auditing + +Systematic security review framework for Solana programs, supporting both Anchor and native Rust implementations. + +## Review Process + +Follow this systematic 5-step process for comprehensive security audits: + +### Step 1: Initial Assessment + +Understand the program's context and structure: + +- **Framework**: Anchor vs Native Rust (check for `use anchor_lang::prelude::*`) +- **Anchor version**: Check `Cargo.toml` for compatibility and known issues +- **Dependencies**: Oracles (Pyth, Switchboard), external programs, token programs +- **Program structure**: Count instructions, identify account types, analyze state management +- **Complexity**: Lines of code, instruction count, PDA patterns +- **Purpose**: DeFi, NFT, governance, gaming, etc. + +### Step 2: Systematic Security Review + +For each instruction, perform security checks in this order: + +1. **Account Validation** - Verify signer, owner, writable, and initialization checks +2. **Arithmetic Safety** - Check all math operations use `checked_*` methods +3. **PDA Security** - Validate canonical bumps and seed uniqueness +4. **CPI Security** - Ensure cross-program invocations validate target programs +5. **Oracle/External Data** - Verify price staleness and oracle status checks + +**→ See [references/security-checklists.md](references/security-checklists.md) for detailed checklists** + +### Step 3: Vulnerability Pattern Detection + +Scan for common vulnerability patterns: + +- Type cosplay attacks +- Account reloading issues +- Improper account closing +- Missing lamports checks +- PDA substitution attacks +- Arbitrary CPI vulnerabilities +- Missing ownership validation +- Integer overflow/underflow + +**→ See [references/vulnerability-patterns.md](references/vulnerability-patterns.md) for code examples and exploit scenarios** + +### Step 4: Architecture and Testing Review + +Evaluate overall design quality: + +- PDA design patterns and collision prevention +- Account space allocation and rent exemption +- Error handling approach and coverage +- Event emission for critical state changes +- Compute budget optimization +- Test coverage (unit, integration, fuzz) +- Upgrade strategy and authority management + +### Step 5: Generate Security Report + +Provide findings using this structure: + +**Severity Levels:** +- 🔴 **Critical**: Funds can be stolen/lost, protocol completely broken +- 🟠 **High**: Protocol can be disrupted, partial fund loss possible +- 🟡 **Medium**: Suboptimal behavior, edge cases, griefing attacks +- 🔵 **Low**: Code quality, gas optimization, best practices +- 💡 **Informational**: Recommendations, improvements, documentation + +**Finding Format:** +```markdown +## 🔴 [CRITICAL] Title + +**Location:** `programs/vault/src/lib.rs:45-52` + +**Issue:** +Brief description of the vulnerability + +**Vulnerable Code:** +```rust +// Show the problematic code +``` + +**Exploit Scenario:** +Step-by-step explanation of how this can be exploited + +**Recommendation:** +```rust +// Show the secure alternative +``` + +**References:** +- [Link to relevant documentation or similar exploits] +``` + +**Report Summary:** +- Total findings by severity +- Critical issues first (prioritize by risk) +- Quick wins (easy fixes with high impact) +- Recommendations for testing improvements + +## Quick Reference + +### Essential Checks (Every Instruction) + +**Anchor:** +```rust +// ✅ Account validation with constraints +#[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 +} + +// ✅ Checked arithmetic +let total = balance.checked_add(amount) + .ok_or(ErrorCode::Overflow)?; +``` + +**Native Rust:** +```rust +// ✅ Manual account validation +if !authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); +} + +if vault.owner != program_id { + return Err(ProgramError::IllegalOwner); +} + +// ✅ Checked arithmetic +let total = balance.checked_add(amount) + .ok_or(ProgramError::ArithmeticOverflow)?; +``` + +### Critical Anti-Patterns + +❌ **Never Do:** +- Use `saturating_*` arithmetic methods (hide errors) +- Use `unwrap()` or `expect()` in production code +- Use `init_if_needed` without additional checks +- Skip signer validation ("they wouldn't call this...") +- Use unchecked arithmetic operations +- Allow arbitrary CPI targets +- Forget to reload accounts after mutations + +✅ **Always Do:** +- Use `checked_*` arithmetic (`checked_add`, `checked_sub`, etc.) +- Use `ok_or(error)?` for Option unwrapping +- Use explicit `init` with proper validation +- Require `Signer<'info>` or `is_signer` checks +- Use `Program<'info, T>` for CPI program validation +- Reload accounts after external calls that mutate state +- Validate account ownership, discriminators, and relationships + +## Framework-Specific Patterns + +### Anchor Security Patterns + +**→ See [references/anchor-security.md](references/anchor-security.md) for:** +- Account constraint best practices +- Common Anchor-specific vulnerabilities +- Secure CPI patterns with `CpiContext` +- Event emission and monitoring +- Custom error handling + +### Native Rust Security Patterns + +**→ See [references/native-security.md](references/native-security.md) for:** +- Manual account validation patterns +- Secure PDA derivation and signing +- Low-level CPI security +- Account discriminator patterns +- Rent exemption validation + +## Modern Practices (2025) + +- **Use Anchor 0.30+** for latest security features +- **Implement Token-2022** with proper extension handling +- **Use `InitSpace` derive** for automatic space calculation +- **Emit events** for all critical state changes +- **Write fuzz tests** with Trident framework +- **Document invariants** in code comments +- **Follow progressive roadmap**: Dev → Audit → Testnet → Audit → Mainnet + +## Security Fundamentals + +**→ See [references/security-fundamentals.md](references/security-fundamentals.md) for:** +- Security mindset and threat modeling +- Core validation patterns (signers, owners, mutability) +- Input validation best practices +- State management security +- Arithmetic safety +- Re-entrancy considerations + +## Common Vulnerabilities + +**→ See [references/vulnerability-patterns.md](references/vulnerability-patterns.md) for:** +- Missing signer validation +- Integer overflow/underflow +- PDA substitution attacks +- Account confusion +- Arbitrary CPI +- Type cosplay +- Improper account closing +- Precision loss in calculations + +Each vulnerability includes: +- ❌ Vulnerable code example +- 💥 Exploit scenario +- ✅ Secure alternative +- 📚 References + +## Security Checklists + +**→ See [references/security-checklists.md](references/security-checklists.md) for:** +- Account validation checklist +- Arithmetic safety checklist +- PDA and account security checklist +- CPI security checklist +- Oracle and external data checklist +- Token integration checklist + +## Known Issues and Caveats + +**→ See [references/caveats.md](references/caveats.md) for:** +- Solana-specific quirks and gotchas +- Anchor framework limitations +- Testing blind spots +- Common misconceptions +- Version-specific issues + +## Security Resources + +**→ See [references/resources.md](references/resources.md) for:** +- Official security documentation +- Security courses and tutorials +- Vulnerability databases +- Audit report examples +- Security tools (Trident, fuzzers) +- Security firms and auditors + +## Key Questions for Every Audit + +Always verify these critical security properties: + +1. **Can an attacker substitute accounts?** + - PDA validation, program ID checks, has_one constraints + +2. **Can arithmetic overflow or underflow?** + - All math uses checked operations, division by zero protected + +3. **Are all accounts properly validated?** + - Owner, signer, writable, initialized checks present + +4. **Can the program be drained?** + - Authorization checks, reentrancy protection, account confusion prevention + +5. **What happens in edge cases?** + - Zero amounts, max values, closed accounts, expired data + +6. **Are external dependencies safe?** + - Oracle validation (staleness, status), CPI targets verified, token program checks + +## Audit Workflow + +### Before Starting + +1. Understand the protocol purpose and mechanics +2. Review documentation and specifications +3. Set up local development environment +4. Run existing tests and check coverage + +### During Audit + +1. Follow the 5-step review process systematically +2. Document findings with severity and remediation +3. Create proof-of-concept exploits for critical issues +4. Test fixes and verify they work + +### After Audit + +1. Present findings clearly prioritized by severity +2. Provide actionable remediation steps +3. Re-audit after fixes are implemented +4. Document lessons learned for the protocol + +## Testing for Security + +Beyond code review, validate security through testing: + +- **Unit tests**: Test each instruction's edge cases +- **Integration tests**: Test cross-instruction interactions +- **Fuzz testing**: Use Trident to discover unexpected behaviors +- **Exploit scenarios**: Write POCs for found vulnerabilities +- **Upgrade testing**: Verify migration paths are secure + +## Core Principle + +**In Solana's account model, attackers can pass arbitrary accounts to any instruction.** + +Security requires explicitly validating: +- ✅ Every account's ownership +- ✅ Every account's type (discriminator) +- ✅ Every account's relationships +- ✅ Every account's state +- ✅ Every signer requirement +- ✅ Every arithmetic operation +- ✅ Every external call + +There are no implicit guarantees. **Validate everything, trust nothing.** diff --git a/skills/solana-security/references/anchor-security.md b/skills/solana-security/references/anchor-security.md new file mode 100644 index 0000000..1bb42ff --- /dev/null +++ b/skills/solana-security/references/anchor-security.md @@ -0,0 +1,1150 @@ +# 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, 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, 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) -> 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) -> 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) -> 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) -> 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, 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, 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) -> 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:"`). + +**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) -> 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, 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) -> Result<()> { + let _ = dangerous_function(); // WRONG - error silenced! + Ok(()) +} +``` + +**CORRECT - Propagate errors:** +```rust +pub fn risky_operation(ctx: Context) -> Result<()> { + dangerous_function()?; // Propagate error + Ok(()) +} +``` + +### 5.3 Avoiding Silent Failures + +**VULNERABLE - No error on failure:** +```rust +pub fn transfer(ctx: Context, 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, 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, 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, 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, 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, + 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::::try_from(account_info)?; + + // Process account... + } + + Ok(()) +} +``` + +### 8.3 Instruction Data Validation + +**SECURE - Validate all inputs:** +```rust +pub fn create_proposal( + ctx: Context, + 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) -> 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. diff --git a/skills/solana-security/references/caveats.md b/skills/solana-security/references/caveats.md new file mode 100644 index 0000000..9da9a63 --- /dev/null +++ b/skills/solana-security/references/caveats.md @@ -0,0 +1,386 @@ +# Important Caveats + +Critical limitations, quirks, and gotchas in Solana and Anchor development that every security reviewer must know. + +## Anchor Framework Limitations + +### 1. `init_if_needed` Re-initialization Risk + +```rust +// Dangerous: Can bypass initialization logic +#[account(init_if_needed, payer = user, space = ...)] +pub user_account: Account<'info, UserAccount>, +``` + +**Issue:** If account already exists, initialization is skipped entirely. Existing malicious or inconsistent data is not validated. + +**When to use:** Only when you explicitly validate existing accounts in instruction logic. + +### 2. `AccountLoader` Missing Discriminator Check + +```rust +// Does NOT validate discriminator by default! +#[account(mut)] +pub user: AccountLoader<'info, User>, +``` + +**Issue:** `AccountLoader` is for zero-copy accounts and doesn't check the account discriminator automatically. Enables type cosplay attacks. + +**Solution:** Use `Account<'info, T>` when possible, or add manual discriminator check. + +### 3. `close` Constraint Ordering + +```rust +// ❌ Wrong: close must be last +#[account( + close = receiver, + mut, + has_one = authority +)] + +// ✅ Correct: close is last +#[account( + mut, + has_one = authority, + close = receiver +)] +``` + +**Issue:** Anchor processes constraints in order. If `close` isn't last, subsequent constraints may check zeroed account. + +### 4. Space Calculation Errors Are Permanent + +```rust +// If this space is wrong, account is unusable! +#[account( + init, + payer = user, + space = 8 + 32 // Too small = can't deserialize later! +)] +pub user_account: Account<'info, UserAccount>, +``` + +**Issue:** Once initialized, account size is fixed. Too small = deserialization fails. Too large = wasted rent. + +**Solution:** Always use `InitSpace` derive macro: +```rust +#[account] +#[derive(InitSpace)] +pub struct UserAccount { + pub authority: Pubkey, + #[max_len(100)] + pub name: String, +} + +// Then use: +space = 8 + UserAccount::INIT_SPACE +``` + +### 5. `constraint` Expression Limitations + +```rust +// constraint expressions can't call functions that return Results! +#[account( + constraint = some_validation(account.value)? @ ErrorCode::Invalid // Compile error! +)] +``` + +**Issue:** Constraint expressions must be simple boolean checks. Cannot use `?` operator. + +**Solution:** Validate in instruction body for complex checks. + +## Solana Runtime Quirks + +### 1. Account Data Persists After Zeroing Lamports + +```rust +// Within same transaction: +**account.lamports.borrow_mut() = 0; +let data = account.try_borrow_data()?; // Still readable! +``` + +**Issue:** Account data remains accessible within the transaction even after lamports are zeroed. Only garbage collected after transaction completes. + +**Implication:** Always check lamports before reading account data. + +### 2. Non-Canonical PDA Bumps + +```rust +// Multiple PDAs possible with different bumps! +let (pda_255, bump_255) = Pubkey::find_program_address(seeds, program_id); // bump = 255 +let (pda_254, bump_254) = Pubkey::create_program_address(&[seeds, &[254]], program_id); // Also valid! +``` + +**Issue:** Same seeds can derive multiple PDAs with different bumps. Creates confusion and potential exploits. + +**Solution:** Always use canonical bump (255 counting down to first valid). Anchor's `bump` constraint enforces this. + +### 3. Compute Budget Limits + +| Network | Base Compute Units | With Optimization | +|---------|-------------------|-------------------| +| Mainnet | 200,000 | Up to 1,400,000 (with request) | +| Devnet | 200,000 | Up to 1,400,000 | + +**Issue:** Complex programs can exceed compute budget, causing transaction failure. + +**Optimization strategies:** +- Minimize CPIs (each costs ~1000 CU) +- Use `AccountLoader` for large accounts +- Avoid loops with variable length +- Request higher compute budget: `ComputeBudgetProgram::set_compute_unit_limit()` + +### 4. Transaction Size Limit + +**Hard limit:** ~1232 bytes for transaction + +**Implications:** +- Limits number of accounts (~35-40 accounts typical max) +- Large instructions need Account Compression or chunking +- Can't pass large data directly in instruction + +**Solutions:** +- Use PDAs to store large data +- Break operations into multiple transactions +- Use lookup tables for frequent accounts + +### 5. Account Snapshot Loading + +```rust +let balance_before = ctx.accounts.vault.balance; +// CPI happens here +// balance_before is STALE - account was loaded before CPI +``` + +**Issue:** Accounts are loaded as snapshots at transaction start. Modifications during transaction (via CPIs) don't update the loaded data. + +**Solution:** Call `.reload()` after any CPI that might modify the account. + +## Token Program Gotchas + +### 1. ATA Addresses Are Deterministic But Not Guaranteed + +```rust +let ata = get_associated_token_address(&owner, &mint); +// ata address is deterministic but account might not exist! +``` + +**Issue:** ATA address can be calculated but account may not be initialized. + +**Solution:** Check account exists and is initialized before use, or use `init_if_needed` with proper validation. + +### 2. Delegates Don't Automatically Reset + +```rust +// After transfer of ownership: +token_account.owner = new_owner; +// BUT: delegate and delegated_amount are NOT reset! +``` + +**Issue:** Changing owner doesn't clear delegate/close authority. Old delegate can still spend. + +**Solution:** Explicitly reset authorities when changing ownership: +```rust +account.delegate = COption::None; +account.delegated_amount = 0; +if account.is_native() { + account.close_authority = COption::None; +} +``` + +### 3. Token-2022 Extension Rent + +**Issue:** Each extension adds rent cost. Account size varies by extensions enabled. + +**Extensions and their sizes:** +- Transfer Fee: ~83 bytes +- Transfer Hook: ~107 bytes +- Permanent Delegate: ~36 bytes +- Interest Bearing: ~40 bytes + +**Solution:** Calculate rent based on all enabled extensions. + +### 4. Token-2022 Transfer Hooks Can Be Malicious + +```rust +// Transfer hook can call arbitrary program! +pub struct TransferHookAccount { + pub program_id: Pubkey, // Could be malicious +} +``` + +**Issue:** Transfer hook extensions allow calling external program during transfers. Malicious hook can fail transaction or drain funds. + +**Solution:** +- Validate transfer hook program if accepting specific tokens +- Consider disallowing tokens with transfer hooks +- Use Anchor's `TransferChecked` instruction + +## Testing Blind Spots + +### 1. Concurrent Transaction Ordering + +**Issue:** Tests typically run transactions sequentially. In production, concurrent transactions can interleave in unexpected ways. + +**Vulnerability example:** +```rust +// Transaction 1: Check balance = 100 +// Transaction 2: Withdraw 80 (balance now 20) +// Transaction 1: Withdraw 80 (uses stale check, balance now -60!) +``` + +**Mitigation:** +- Use atomic operations +- Reload accounts before critical operations +- Design for idempotency + +### 2. Account Rent Reclaim Attacks + +**Issue:** When account rent falls below minimum, validator can reclaim the account. Tests don't simulate this. + +**Solution:** Ensure all accounts are rent-exempt (2+ years of rent). + +### 3. Sysvar Manipulation in Tests + +```rust +// In tests, you can set arbitrary clock values +ctx.accounts.clock = Clock { unix_timestamp: attacker_value, ... }; +``` + +**Issue:** Tests may not catch reliance on tamper-resistant sysvars. + +**Solution:** In production, always load sysvars from official sysvar accounts: +```rust +pub clock: Sysvar<'info, Clock>, // Validated address +``` + +### 4. Devnet vs Mainnet Differences + +| Aspect | Devnet | Mainnet | +|--------|--------|---------| +| Oracle prices | Often stale/fake | Real-time | +| Program versions | May differ | Stable versions | +| Compute limits | More lenient | Strict | +| Congestion | Minimal | Can be high | +| Token availability | Test tokens | Real value | + +**Issue:** Programs tested only on devnet may fail on mainnet. + +**Solution:** Test on mainnet-fork or mainnet with small amounts before full deployment. + +## Rust-Specific Gotchas + +### 1. `unwrap()` Panics + +```rust +// Panics kill the entire transaction! +let value = some_option.unwrap(); // ❌ Never do this +``` + +**Solution:** Always use proper error handling: +```rust +let value = some_option.ok_or(ErrorCode::MissingValue)?; +``` + +### 2. Integer Division Truncation + +```rust +let result = 5 / 2; // result = 2, not 2.5! +``` + +**Issue:** Integer division truncates, potentially causing precision loss in financial calculations. + +**Solution:** Use `Decimal` type for precise calculations, or multiply before divide: +```rust +let result = (5 * PRECISION) / 2 / PRECISION; +``` + +### 3. Overflow in Debug vs Release + +```rust +// Debug mode: panics on overflow +// Release mode: wraps silently! +let x: u8 = 255; +let y = x + 1; // Debug: panic, Release: y = 0 +``` + +**Solution:** Always use `checked_*` methods - they work same in debug and release. + +## Cross-Program Invocation (CPI) Gotchas + +### 1. CPI Success Doesn't Guarantee Correct State + +```rust +// CPI returns success but state may be unexpected +invoke(&transfer_instruction, &accounts)?; +// Transfer succeeded but amount might be different due to fees! +``` + +**Solution:** Reload and validate account state after CPI. + +### 2. Signer Seeds Must Be Exact + +```rust +// Seeds for signing must match PDA derivation exactly +let seeds = &[ + b"vault", + user.key().as_ref(), + &[bump], // Must be same bump used to derive PDA +]; + +invoke_signed(&instruction, &accounts, &[seeds])?; +``` + +**Issue:** Wrong seeds = "signature verification failed" error. + +### 3. CPI Depth Limit + +**Limit:** 4 levels of CPI depth + +**Issue:** Program A → Program B → Program C → Program D → Program E (fails!) + +**Solution:** Design programs to minimize CPI depth. + +## Common Misunderstandings + +### 1. "Anchor Prevents All Security Issues" + +**False:** Anchor prevents some common issues (missing discriminators, wrong account types) but doesn't validate business logic, arithmetic, or authorization. + +### 2. "Devnet Testing Is Sufficient" + +**False:** Mainnet has different compute limits, real oracle data, congestion, and MEV considerations. + +### 3. "One Audit Makes Code Secure" + +**False:** Audits find issues in a snapshot. Code changes after audit reintroduce risk. Need continuous security review. + +### 4. "`checked_*` Methods Are Slower" + +**False:** Rust compiler optimizes these similarly to unchecked arithmetic. Always use checked methods. + +### 5. "PDAs Can't Sign" + +**True for external transactions, false for CPIs:** PDAs can sign CPIs using `invoke_signed` but can't sign transactions directly. + +## Version-Specific Issues + +### Anchor Version Compatibility + +- **< 0.28**: No `InitSpace` derive, manual space calculation error-prone +- **< 0.29**: Different constraint syntax +- **0.30+**: Breaking changes in error handling and account initialization + +**Solution:** Check `Cargo.toml` for version and consult [Anchor Changelog](https://github.com/coral-xyz/anchor/blob/master/CHANGELOG.md). + +### Solana Version Differences + +- **Pre-1.14**: Different fee structure +- **Pre-1.16**: No Address Lookup Tables +- **Pre-1.17**: No Token-2022 + +**Solution:** Verify target Solana version matches deployment network. + +--- + +**Key Takeaway:** Many "obvious" assumptions about blockchain behavior don't hold in Solana. Always validate against actual runtime behavior, not assumptions from other chains. diff --git a/skills/solana-security/references/native-security.md b/skills/solana-security/references/native-security.md new file mode 100644 index 0000000..2417b9b --- /dev/null +++ b/skills/solana-security/references/native-security.md @@ -0,0 +1,1133 @@ +# 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 { + if self.account.owner != expected_owner { + return Err(ProgramError::IncorrectProgramId); + } + Ok(self) + } + + pub fn signer(self) -> Result { + if !self.account.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + Ok(self) + } + + pub fn writable(self) -> Result { + if !self.account.is_writable { + return Err(ProgramError::InvalidAccountData); + } + Ok(self) + } + + pub fn key(self, expected_key: &Pubkey) -> Result { + if self.account.key != expected_key { + return Err(ProgramError::InvalidAccountData); + } + Ok(self) + } + + pub fn initialized(self) -> Result { + 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 { + 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, // 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, +} + +impl Config { + pub fn from_account_info(account: &AccountInfo) -> Result { + 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 { + 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 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 { + // 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. diff --git a/skills/solana-security/references/resources.md b/skills/solana-security/references/resources.md new file mode 100644 index 0000000..66e3cbb --- /dev/null +++ b/skills/solana-security/references/resources.md @@ -0,0 +1,177 @@ +# Resources + +Comprehensive collection of official documentation, security guides, audit reports, and learning materials for Solana development and security. + +## Official Documentation + +### Solana Core +- [Solana Docs](https://solana.com/docs/) - Official Solana documentation +- [Solana Cookbook](https://solana.com/developers/cookbook) - Recipes for common Solana tasks +- [Solana Courses](https://solana.com/developers/courses/) - Official learning paths +- [Program Examples](https://github.com/solana-developers/program-examples) - Multi-framework examples +- [Developer Bootcamp 2024](https://github.com/solana-developers/developer-bootcamp-2024) + +### Anchor Framework +- [Anchor Docs](https://www.anchor-lang.com/docs) - Official Anchor documentation +- [Anchor Book](https://book.anchor-lang.com/) - Comprehensive Anchor guide +- [Anchor by Example](https://examples.anchor-lang.com/) - Example programs +- [Anchor Lang Docs](https://docs.rs/anchor-lang) - API documentation +- [Anchor SPL Docs](https://docs.rs/anchor-spl) - SPL integration helpers + +### SPL Programs +- [SPL Documentation](https://spl.solana.com/) - Solana Program Library docs +- [Token Program](https://github.com/solana-program/token) - SPL Token source +- [Token-2022](https://github.com/solana-program/token-2022) - Next-gen token program +- [Associated Token Account](https://github.com/solana-program/associated-token-account) +- [Token Metadata](https://github.com/solana-program/token-metadata) +- [Metaplex Token Metadata](https://github.com/metaplex-foundation/mpl-token-metadata) + +## Security Resources + +### Curated Security Lists +- [Awesome Solana Security (0xMacro)](https://github.com/0xMacro/awesome-solana-security) - **Actively maintained**, comprehensive resource list +- [Rektoff Security Roadmap](https://github.com/Rektoff/Security-Roadmap-for-Solana-applications) - Full lifecycle security strategy +- [SlowMist Best Practices](https://github.com/slowmist/solana-smart-contract-security-best-practices) - Common pitfalls with examples +- [Ackee Solana Handbook](https://ackee.xyz/solana/book/latest/) - Comprehensive development guide + +### Security Guides & Articles +- [Helius Security Guide](https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security) - Common vulnerabilities explained +- [Neodyme Breakpoint Workshop](https://github.com/neodyme-labs/neodyme-breakpoint-workshop) - Hands-on security training +- [Solana Security Course](https://solana.com/developers/courses/program-security) - Official security course +- [Asymmetric Research CPI Vulnerabilities](https://blog.asymmetric.re/invocation-security-navigating-vulnerabilities-in-solana-cpis/) +- [Ottersec Lamport Transfers](https://osec.io/blog/2025-05-14-king-of-the-sol) - SOL transfer vulnerabilities +- [Infect3d Auditing Essentials](https://www.infect3d.xyz/blog/solana-quick-start) + +### Vulnerability Collections +- [Urataps Audit Examples](https://github.com/urataps/solana-audit-examples) - Programs with vulnerabilities +- [ImmuneBytes Attack Vectors](https://github.com/ImmuneBytes-Security-Audit/Blockchain-Attack-Vectors/tree/main/Solana%20Attack%20Vectors) +- [Exvul Security Guide](https://exvul.com/rust-smart-contract-security-guide-in-solana/) +- [Nirlin Advanced Vulnerabilities](https://substack.com/inbox/post/164534668) + +### Video Tutorials +- [Zigtur Security Walkthrough](https://www.youtube.com/watch?v=xd6qfY-GDYY) +- [M4rio Security Walkthrough](https://www.youtube.com/watch?v=q4z8tIi43lg) + +### Token-2022 Security +- [Offside Token-2022 Part 1](https://blog.offside.io/p/token-2022-security-best-practices-part-1) +- [Offside Token-2022 Part 2](https://blog.offside.io/p/token-2022-security-best-practices-part-2) +- [Neodyme Token-2022 Security](https://neodyme.io/en/blog/token-2022) + +### Deep Dives & Research +- [r0bre's 100 Daily Solana Tips](https://accretionxyz.substack.com/p/r0bres-100-daily-solana-tips) +- [Accretion Hidden IDL Instructions](https://accretionxyz.substack.com/p/hidden-idl-instructions-and-how-to) +- [Farouk ELALEM Under the Hood](https://ubermensch.blog/under-the-hood-of-solana-program-execution-from-rust-code-to-sbf-bytecode) +- [Lucrative_Panda Security History](https://medium.com/@lucrativepanda/a-comprehensive-analysis-of-solanas-security-history-all-incidents-impacts-and-evolution-up-to-1b1564c7ddfe) + +## Essential Codebases to Study + +Study these production codebases to learn security patterns: + +### Framework & Core Programs +- [Anchor Framework](https://github.com/solana-foundation/anchor) - The framework itself +- [Solana System Program](https://github.com/solana-program/system) +- [SPL Token Program](https://github.com/solana-program/token) +- [Token-2022](https://github.com/solana-program/token-2022) + +### Production Protocols +- [Raydium AMM](https://github.com/raydium-io/raydium-cp-swap) - DEX protocol +- [Kamino Lending](https://github.com/Kamino-Finance/klend) - Lending protocol +- [Squads Multisig](https://github.com/Squads-Protocol/v4) - Multisig protocol + +## Audit Reports + +Study real security audits to learn from actual vulnerabilities: + +### Code4rena +- [Pump Science](https://code4rena.com/reports/2025-01-pump-science) - 2 High, 3 Medium + +### Sherlock +- [Orderly](https://audits.sherlock.xyz/contests/524/report) - 2 High, 1 Medium +- [WOOFi](https://audits.sherlock.xyz/contests/535/report) - 2 High, 3 Medium + +### Cantina +Contact `0xmorph` in Cantina Discord for read access: +- [Grass](https://cantina.xyz/competitions/3211ee0d-133f-43a0-837e-8dc1ecfaa424) - 13 High, 6 Medium +- [Olas](https://cantina.xyz/competitions/829164bf-7fba-4b84-a6b8-76652205bd97) - 2 High, 3 Medium +- [Tensor](https://cantina.xyz/competitions/21787352-de2c-4a77-af09-cc0a250d1f04) - 5 High, 10 Medium +- [ZetaChain](https://cantina.xyz/competitions/80a33cf0-ad69-4163-a269-d27756aacb5e) - 6 High, 27 Medium +- [Inclusive Finance](https://cantina.xyz/competitions/3eff5a8f-b73a-4cfe-8c54-546b475548f0) - 45 High, 25 Medium +- [Reserve Index](https://cantina.xyz/code/8b94becd-54e7-41cd-88e6-caae7becc76a) - 10 High, 11 Medium + +## Learning Paths + +### For EVM Developers +- [RareSkills Solana Course](https://www.rareskills.io/solana-tutorial) - Ethereum to Solana +- [0xkowloon Anchor for EVM](https://0xkowloon.gitbook.io/anchor-for-evm-developers) + +### For Rust Learners +- [Rust Book](https://doc.rust-lang.org/book/) +- [Rust by Example](https://doc.rust-lang.org/rust-by-example/index.html) + +### Native Rust (Non-Anchor) +- [Solana Native Rust Docs](https://solana.com/docs/programs/rust) +- [Native Development Course](https://solana.com/developers/courses/native-onchain-development) + +### Blueshift Challenges +- [Blueshift Courses](https://learn.blueshift.gg/) - Anchor and Pinocchio + +## Tools + +### Development +- [Solana Playground](https://beta.solpg.io/) - Browser-based IDE +- [Rust Playground](https://play.rust-lang.org/) - Test Rust snippets + +### Security & Analysis +- [Trident](https://github.com/Ackee-Blockchain/trident) - Fuzz testing framework +- [Certora Prover](https://docs.certora.com/en/latest/docs/solana/index.html) - Formal verification +- [Sec3 IDL Guesser](https://github.com/sec3-service/IDLGuesser) - Reverse engineer IDLs +- [Anchor X-ray](https://github.com/crytic/anchorx-ray) - Visualize accounts (Trail of Bits) +- [Anchor Version Detector](https://github.com/johnsaigle/anchor-version-detector) - Compatibility checker + +### Testing +- [Anchor Test Framework](https://book.anchor-lang.com/anchor_in_depth/testing.html) +- [Solana Test Validator](https://docs.solana.com/developing/test-validator) + +## CTFs & Practice + +### Capture The Flag +- [Ackee Solana CTF](https://github.com/Ackee-Blockchain/Solana-Auditors-Bootcamp/tree/master/Capture-the-Flag) + +### Bootcamps +- [Rektoff 6-Week Bootcamp](https://www.rektoff.xyz/bootcamp) - Free, Solana Foundation supported +- [Ackee Auditors Bootcamp](https://ackee.xyz/solana-auditors-bootcamp) + +## Community & Support + +### Q&A Platforms +- [Solana Stack Exchange](https://solana.stackexchange.com/) + +### Blogs & Newsletters +- [Helius Blog](https://www.helius.dev/blog) - Frequent Solana content +- [Pine Analytics Substack](https://substack.com/@pineanalytics1) - Protocol deep dives + +## Security Firms + +Top firms for Solana security audits: +- [Runtime Verification](https://runtimeverification.com/) +- [OtterSec](https://osec.io/) +- [Neodyme](https://neodyme.io/en/) +- [Sec3](https://www.sec3.dev/) +- [Zellic](https://www.zellic.io/) +- [Ackee Blockchain](https://ackee.xyz/) +- [Hexens](https://hexens.io/) +- [Trail of Bits](https://www.trailofbits.com/) +- [Kudelski Security](https://kudelskisecurity.com/) +- [Cantina](https://cantina.xyz/) +- [Certora](https://www.certora.com/) +- [Sherlock](https://www.sherlock.xyz/) + +## Version Information + +- Latest Anchor version (as of 2025): 0.30+ +- Recommended Solana CLI: Latest stable +- Rust minimum version: 1.70+ + +--- + +**Note:** This is a curated collection from the Awesome Solana Security repository and other trusted sources. Resources are selected for their quality, maintenance status, and relevance to modern Solana development practices. diff --git a/skills/solana-security/references/security-checklists.md b/skills/solana-security/references/security-checklists.md new file mode 100644 index 0000000..b1903f0 --- /dev/null +++ b/skills/solana-security/references/security-checklists.md @@ -0,0 +1,291 @@ +# Security Checklists + +Comprehensive validation checklists for Solana program security reviews. + +## Account Validation Checklist + +For every account in every instruction: + +- [ ] **Signer validation**: Uses `Signer<'info>` or `is_signer` check when needed +- [ ] **Owner validation**: Uses `#[account(owner = ...)]` or manual owner check +- [ ] **Writable checks**: Properly marked `mut` when account data will be modified +- [ ] **Account initialization**: Checks if account is initialized before use +- [ ] **PDA validation**: Validates seeds and uses canonical bump +- [ ] **Discriminator check**: For `AccountLoader`, validates account type +- [ ] **Account relationships**: Uses `has_one` for related accounts + +```rust +// Complete account validation example +#[derive(Accounts)] +pub struct SecureInstruction<'info> { + #[account( + mut, + has_one = authority, // Relationship validation + seeds = [b"vault", authority.key().as_ref()], + bump, // Canonical bump + )] + pub vault: Account<'info, Vault>, + + pub authority: Signer<'info>, // Signer required + + #[account( + mut, + constraint = token_account.owner == authority.key(), // Custom validation + )] + pub token_account: Account<'info, TokenAccount>, + + pub token_program: Program<'info, Token>, // Program validation +} +``` + +## Arithmetic Safety Checklist + +For all mathematical operations: + +- [ ] **Addition**: Uses `checked_add()` instead of `+` +- [ ] **Subtraction**: Uses `checked_sub()` instead of `-` +- [ ] **Multiplication**: Uses `checked_mul()` instead of `*` +- [ ] **Division**: Uses `checked_div()` instead of `/` +- [ ] **Division by zero**: Validates divisor is non-zero +- [ ] **Precision loss**: Uses `try_floor_u64()` instead of `try_round_u64()` to prevent arbitrage +- [ ] **Avoid saturating**: Does not use `saturating_*` methods (they hide errors) +- [ ] **Proper error handling**: All arithmetic wrapped in `ok_or(error)?` + +```rust +// Secure arithmetic examples +let total = balance + .checked_add(amount) + .ok_or(ErrorCode::Overflow)?; + +let share = total + .checked_div(denominator) + .ok_or(ErrorCode::DivisionByZero)?; + +// For Decimal types (token amounts) +let liquidity = Decimal::from(collateral_amount) + .try_div(rate)? + .try_floor_u64()?; // Not try_round_u64()! +``` + +## PDA and Account Security Checklist + +- [ ] **Canonical bump**: PDAs use `bump` in seeds constraint (not hardcoded) +- [ ] **Unique seeds**: Seeds include unique identifier (user pubkey, mint, etc.) +- [ ] **No duplicate accounts**: Same account not used twice as mutable +- [ ] **Init vs init_if_needed**: Uses `init` with proper validation, not `init_if_needed` +- [ ] **has_one constraints**: Related accounts validated with `has_one` +- [ ] **Custom constraints**: Complex validation uses `constraint` expression +- [ ] **Seed collision**: Seeds designed to prevent collisions + +```rust +// Secure PDA patterns +#[account( + init, + payer = authority, + space = 8 + UserAccount::INIT_SPACE, + seeds = [ + b"user", + authority.key().as_ref(), // Unique to user + mint.key().as_ref(), // Unique to mint + ], + bump +)] +pub user_account: Account<'info, UserAccount>, +``` + +## CPI Security Checklist + +For all Cross-Program Invocations: + +- [ ] **Program validation**: Target program is validated (uses `Program<'info, T>`) +- [ ] **Signer seeds**: PDA signers pass seeds correctly in `invoke_signed` +- [ ] **Return value checking**: CPI success doesn't guarantee correct state +- [ ] **Account reloading**: Reload accounts after CPI that may modify them +- [ ] **No arbitrary CPI**: Program account is not user-controlled +- [ ] **Privilege escalation**: CPI doesn't grant unexpected permissions + +```rust +// Secure CPI pattern +#[derive(Accounts)] +pub struct SecureCPI<'info> { + pub token_program: Program<'info, Token>, // Type-validated + // ... other accounts +} + +pub fn secure_cpi(ctx: Context) -> 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 { + // 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::::unpack(&mint_data)?; + + if let Ok(transfer_hook) = mint_with_extensions.get_extension::() { + // Handle transfer hook properly + // ... transfer hook logic + } + + // Check for transfer fee + if let Ok(transfer_fee_config) = mint_with_extensions.get_extension::() { + // 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 + } +} +``` diff --git a/skills/solana-security/references/security-fundamentals.md b/skills/solana-security/references/security-fundamentals.md new file mode 100644 index 0000000..d6d9ca6 --- /dev/null +++ b/skills/solana-security/references/security-fundamentals.md @@ -0,0 +1,1134 @@ +# 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 { + // 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. diff --git a/skills/solana-security/references/vulnerability-patterns.md b/skills/solana-security/references/vulnerability-patterns.md new file mode 100644 index 0000000..8005d11 --- /dev/null +++ b/skills/solana-security/references/vulnerability-patterns.md @@ -0,0 +1,525 @@ +# Common Vulnerability Patterns + +Detailed examples of common Solana smart contract vulnerabilities with exploit scenarios and secure alternatives. + +## 1. Missing Signer Validation + +### Vulnerability +```rust +// ❌ VULNERABLE +pub fn withdraw(ctx: Context, 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, 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, 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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, 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, 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) -> 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) -> 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 { + 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 { + 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) -> 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 { + 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 { + 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) +} +```