Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:01:25 +08:00
commit d733741f8a
37 changed files with 26647 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# solana
Comprehensive Solana development toolkit: build programs with Anchor/native Rust and audit for security vulnerabilities

177
plugin.lock.json Normal file
View File

@@ -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": []
}
}

View File

@@ -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<Initialize>, data: u64) -> Result<()> {
ctx.accounts.account.data = data;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyAccount {
pub data: u64,
}
```
**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

View File

@@ -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<u8>,
/// the program that owns this account
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent (DEPRECATED)
pub rent_epoch: Epoch,
}
```
### Field Details
#### `lamports` (u64)
- The account's balance in lamports (1 SOL = 1,000,000,000 lamports)
- Every account must maintain a minimum balance for rent exemption
- Rent works as a **refundable deposit** - recoverable when account is closed
- Only the account owner can deduct lamports
- Any program can **add** lamports to any account
#### `data` (Vec<u8>)
- Maximum size: **10 MiB** (10,485,760 bytes)
- Can contain any arbitrary sequence of bytes
- Structure defined by the owning program
- Common patterns:
- **Program accounts**: Executable code or pointer to program data account
- **Data accounts**: Serialized state (often using Borsh)
#### `owner` (Pubkey)
- The program ID that owns this account
- **Critical security property**: Only the owner can modify `data` or deduct `lamports`
- Cannot be changed after account creation (except by System Program for newly created accounts)
- Newly created accounts start owned by System Program
#### `executable` (bool)
- `true`: Account contains executable program code
- `false`: Account is a data account
- Cannot be changed after being set to `true`
#### `rent_epoch` (Epoch)
- **DEPRECATED** - no longer used
- Remains in struct for backward compatibility
- Rent is now a one-time refundable deposit, not periodic payment
---
## Account Types
### 1. Program Accounts (Executable)
Program accounts contain executable code and are owned by a [loader program](https://solana.com/docs/core/programs#loader-programs).
**Simple Program Account Structure:**
```
┌─────────────────────────────────────┐
│ Program Account │
├─────────────────────────────────────┤
│ lamports: 1000000 │
│ data: [executable bytecode] │
│ owner: BPFLoaderUpgradeab1e... │
│ executable: true │
└─────────────────────────────────────┘
```
**Loader-v3 Program Structure (Upgradeable):**
Programs deployed with loader-v3 use a **two-account model**:
```
┌─────────────────────────────────────┐
│ Program Account │
├─────────────────────────────────────┤
│ data: [pointer to program data] │ ──┐
│ executable: true │ │
└─────────────────────────────────────┘ │
┌─────────────────────────────────────┐
│ Program Data Account │
├─────────────────────────────────────┤
│ data: [actual executable bytecode] │
│ executable: false │
└─────────────────────────────────────┘
```
This separation enables:
- Program upgrades without changing the program address
- Buffer accounts for staging uploads
- Separate upgrade authority management
### 2. Data Accounts (Non-Executable)
Data accounts store program state and are owned by programs (or System Program).
#### a) Program State Accounts
Accounts created and owned by your program to store application state:
```rust
// Example: Note account owned by a note-taking program
pub struct NoteAccount {
pub is_initialized: bool,
pub author: Pubkey,
pub note_id: u64,
pub content: String,
}
```
**Creation Process:**
1. Invoke System Program to create account (allocate space, transfer lamports)
2. System Program transfers ownership to your program
3. Your program initializes the account data
```rust
// Step 1: Create account via System Program CPI
invoke_signed(
&system_instruction::create_account(
initializer.key,
pda_account.key,
rent_lamports,
account_len.try_into().unwrap(),
program_id, // Transfer ownership to our program
),
&[initializer.clone(), pda_account.clone(), system_program.clone()],
&[&[seeds, &[bump_seed]]],
)?;
// Step 2: Initialize the account data
let mut account_data = try_from_slice_unchecked::<NoteAccount>(&pda_account.data.borrow())?;
account_data.is_initialized = true;
account_data.author = *initializer.key;
account_data.note_id = note_id;
account_data.content = content;
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
```
#### b) System Accounts (Wallet Accounts)
Accounts owned by the System Program, typically used as user wallets:
```
┌─────────────────────────────────────┐
│ Wallet Account │
├─────────────────────────────────────┤
│ lamports: 1000000000 │
│ data: [] │
│ owner: 11111111111111111111... │ ← System Program
│ executable: false │
└─────────────────────────────────────┘
```
**Characteristics:**
- Can sign transactions (if you have the private key)
- Can pay transaction fees
- Can transfer SOL
- Created automatically when funded with SOL
#### c) Sysvar Accounts
Special accounts at predefined addresses that provide cluster state data:
| Sysvar | Address | Purpose |
|--------|---------|---------|
| Clock | `SysvarC1ock11111111111111111111111111111111` | Current slot, epoch, timestamp |
| Rent | `SysvarRent111111111111111111111111111111111` | Rent rate calculation |
| EpochSchedule | `SysvarEpochSchedu1e111111111111111111111111` | Epoch duration info |
| SlotHashes | `SysvarS1otHashes111111111111111111111111111` | Recent slot hashes |
**Access Pattern:**
```rust
use solana_program::sysvar::{clock::Clock, Sysvar};
let clock = Clock::get()?;
let current_timestamp = clock.unix_timestamp;
```
---
## Account Ownership
### Ownership Rules
**The Golden Rule:** Only the account owner can:
1. Modify the account's `data` field
2. Deduct lamports from the account
**Critical Security Implication:**
Programs must verify account ownership to prevent unauthorized state modifications.
### Ownership in Program Context
When a program receives accounts in an instruction:
```rust
pub fn process_instruction(
program_id: &Pubkey, // Your program's ID
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let data_account = next_account_info(account_info_iter)?;
// CRITICAL: Verify ownership before modifying
if data_account.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
// Safe to modify - we own this account
// ...
}
```
### AccountInfo Structure
Programs receive accounts as `AccountInfo` structs:
```rust
pub struct AccountInfo<'a> {
pub key: &'a Pubkey, // Account address
pub is_signer: bool, // Did this account sign the transaction?
pub is_writable: bool, // Is this account writable in this instruction?
pub lamports: Rc<RefCell<&'a mut u64>>, // Mutable lamport balance
pub data: Rc<RefCell<&'a mut [u8]>>, // Mutable data
pub owner: &'a Pubkey, // Owner program ID
pub executable: bool, // Is this executable?
pub rent_epoch: Epoch, // Deprecated
}
```
**Key Operations:**
```rust
// Read data
let data = data_account.data.borrow();
let account_state = MyState::try_from_slice(&data)?;
// Write data
let mut data = data_account.data.borrow_mut();
account_state.serialize(&mut *data)?;
// Modify lamports
**data_account.lamports.borrow_mut() += transfer_amount;
```
---
## Rent Mechanics
Rent is a **refundable security deposit** required to store data on-chain. Despite the name "rent", it's not a recurring fee—it's a one-time deposit fully recoverable when the account is closed.
### Rent Calculation
Rent is proportional to account size:
```rust
use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;
// Get current rent rates
let rent = Rent::get()?;
// Calculate minimum balance for rent exemption
let account_size: usize = 1000; // bytes
let rent_lamports = rent.minimum_balance(account_size);
```
**Formula:**
Based on [agave source](https://github.com/anza-xyz/agave/blob/v2.1.13/sdk/rent/src/lib.rs#L93-L97):
```rust
minimum_balance = (LAMPORTS_PER_BYTE_YEAR * account_size) * EXEMPTION_THRESHOLD / slots_per_year
```
**Constants:**
- `LAMPORTS_PER_BYTE_YEAR`: 3,480 lamports
- `EXEMPTION_THRESHOLD`: 2.0 (200% of annual rent)
- Typical cost: ~0.00139536 SOL per 100 bytes
### Rent Exemption
**All accounts must be rent-exempt.** This means:
- Account lamport balance ≥ `rent.minimum_balance(account.data.len())`
- The Solana runtime enforces this requirement
- Non-exempt accounts cannot be created
### Practical Example
```rust
pub fn create_data_account(
program_id: &Pubkey,
accounts: &[AccountInfo],
data_size: usize,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let new_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Calculate rent-exempt balance
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(data_size);
// Create account with rent-exempt balance
invoke(
&system_instruction::create_account(
payer.key,
new_account.key,
rent_lamports, // Must be rent-exempt
data_size as u64,
program_id,
),
&[payer.clone(), new_account.clone(), system_program.clone()],
)?;
Ok(())
}
```
### Closing Accounts (Recovering Rent)
To recover rent when an account is no longer needed:
```rust
pub fn close_account(
account_to_close: &AccountInfo,
destination: &AccountInfo,
) -> ProgramResult {
// Transfer all lamports to destination
let dest_lamports = destination.lamports();
**destination.lamports.borrow_mut() = dest_lamports
.checked_add(**account_to_close.lamports.borrow())
.ok_or(ProgramError::ArithmeticOverflow)?;
// Zero out lamports in closed account
**account_to_close.lamports.borrow_mut() = 0;
// Zero out data (security best practice)
let mut data = account_to_close.data.borrow_mut();
data.fill(0);
Ok(())
}
```
**Important:** The runtime will garbage-collect accounts with 0 lamports.
---
## Account Validation Patterns
Proper account validation is **critical for security**. Programs must verify accounts before using them.
### 1. Ownership Check
**Purpose:** Ensure an account is owned by the expected program.
**When to use:**
- Before reading/writing account data
- When validating PDAs
- When ensuring proper account initialization
```rust
// Basic ownership check
if account.owner != program_id {
msg!("Account not owned by this program");
return Err(ProgramError::IllegalOwner);
}
// PDA ownership check (essential for security)
if note_pda.owner != program_id {
msg!("Invalid note account - wrong owner");
return Err(ProgramError::IllegalOwner);
}
```
**Why it matters:**
Without ownership checks, malicious actors can pass arbitrary accounts that match the expected data format but are controlled by other programs or themselves.
### 2. Signer Check
**Purpose:** Verify that an account signed the transaction.
**When to use:**
- Before transferring funds from an account
- Before modifying user-specific data
- Before any privileged operation
```rust
if !initializer.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature);
}
// Practical example: Only allow note author to update
pub fn update_note(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_content: String,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let author = next_account_info(account_info_iter)?;
let note_pda = next_account_info(account_info_iter)?;
// Verify author signed the transaction
if !author.is_signer {
msg!("Author must sign to update note");
return Err(ProgramError::MissingRequiredSignature);
}
// Deserialize and verify author matches
let note_data = NoteAccount::try_from_slice(&note_pda.data.borrow())?;
if note_data.author != *author.key {
msg!("Author mismatch");
return Err(ProgramError::IllegalOwner);
}
// Safe to proceed with update
// ...
}
```
### 3. Writable Check
**Purpose:** Verify an account is marked as writable.
**When to use:**
- Before modifying account data
- Before changing lamport balances
- Enforced automatically by runtime, but explicit checks improve clarity
```rust
if !account.is_writable {
msg!("Account must be writable");
return Err(ProgramError::InvalidAccountData);
}
```
### 4. Initialization Check
**Purpose:** Prevent re-initialization or use of uninitialized accounts.
**Pattern: Flag-based initialization**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct DataAccount {
pub is_initialized: bool,
// ... other fields
}
impl DataAccount {
pub fn is_initialized(&self) -> bool {
self.is_initialized
}
}
// On creation - check NOT initialized
if account_data.is_initialized() {
msg!("Account already initialized");
return Err(ProgramError::AccountAlreadyInitialized);
}
// On update - check IS initialized
if !account_data.is_initialized() {
msg!("Account not initialized");
return Err(ProgramError::UninitializedAccount);
}
```
### 5. PDA Validation
**Purpose:** Verify a provided PDA matches expected derivation.
**Critical for security:** Always validate PDAs using canonical bump.
```rust
pub fn validate_pda(
program_id: &Pubkey,
accounts: &[AccountInfo],
note_id: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let author = next_account_info(account_info_iter)?;
let note_pda = next_account_info(account_info_iter)?;
// Derive expected PDA
let (expected_pda, _bump) = Pubkey::find_program_address(
&[
author.key.as_ref(),
note_id.to_le_bytes().as_ref(),
],
program_id,
);
// Validate match
if expected_pda != *note_pda.key {
msg!("Invalid PDA - seeds don't match");
return Err(ProgramError::InvalidSeeds);
}
Ok(())
}
```
**Why use `find_program_address` instead of accepting a bump?**
- Prevents bump seed manipulation attacks
- Ensures canonical bump is used
- Eliminates category of security vulnerabilities
### 6. Account Type Validation
**Purpose:** Ensure account contains expected data type.
**Pattern: Discriminator/Type Field**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub enum AccountType {
Uninitialized,
UserProfile,
GameState,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountData {
pub account_type: AccountType,
// ... other fields
}
// Validation
let account_data = AccountData::try_from_slice(&account.data.borrow())?;
if !matches!(account_data.account_type, AccountType::UserProfile) {
msg!("Wrong account type");
return Err(ProgramError::InvalidAccountData);
}
```
---
## Security Best Practices
### 1. Always Validate Before Trusting
**Never assume accounts are correct.** Always validate:
```rust
pub fn secure_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let user_data_pda = next_account_info(account_info_iter)?;
// ✅ Signer check
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// ✅ Ownership check
if user_data_pda.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
// ✅ PDA validation
let (expected_pda, _) = Pubkey::find_program_address(
&[b"user_data", user.key.as_ref()],
program_id,
);
if expected_pda != *user_data_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// ✅ Initialization check
let data = UserData::try_from_slice(&user_data_pda.data.borrow())?;
if !data.is_initialized {
return Err(ProgramError::UninitializedAccount);
}
// Now safe to proceed
// ...
}
```
### 2. Fail Fast with Meaningful Errors
Return errors immediately when validation fails:
```rust
// ✅ Good - fail fast
if !account.is_signer {
msg!("User must sign the transaction");
return Err(ProgramError::MissingRequiredSignature);
}
// ❌ Bad - continues with invalid state
if account.is_signer {
// process...
}
```
### 3. Use Type Safety
Leverage Rust's type system for compile-time guarantees:
```rust
// Define a validated account type
pub struct ValidatedUserAccount<'a> {
info: &'a AccountInfo<'a>,
data: UserAccountData,
}
impl<'a> ValidatedUserAccount<'a> {
pub fn validate(
account: &'a AccountInfo<'a>,
program_id: &Pubkey,
) -> Result<Self, ProgramError> {
// Ownership check
if account.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
// Deserialize and validate
let data = UserAccountData::try_from_slice(&account.data.borrow())?;
if !data.is_initialized {
return Err(ProgramError::UninitializedAccount);
}
Ok(Self { info: account, data })
}
}
// Usage guarantees validated account
pub fn process_with_validated_account(
validated: ValidatedUserAccount,
) -> ProgramResult {
// No need to re-validate!
// ...
}
```
### 4. Check Arithmetic Operations
Always use checked math to prevent overflow/underflow:
```rust
// ❌ Dangerous - can overflow
let total = amount1 + amount2;
// ✅ Safe - returns error on overflow
let total = amount1
.checked_add(amount2)
.ok_or(ProgramError::ArithmeticOverflow)?;
```
### 5. Validate Data Constraints
Check business logic constraints:
```rust
pub fn allocate_points(
character_account: &AccountInfo,
new_strength: u8,
) -> ProgramResult {
let mut character = Character::try_from_slice(&character_account.data.borrow())?;
// Validate attribute cap
if character.strength.checked_add(new_strength).ok_or(ProgramError::ArithmeticOverflow)? > 100 {
msg!("Attribute cannot exceed 100");
return Err(ProgramError::InvalidArgument);
}
// Validate allowance
if new_strength > character.available_points {
msg!("Insufficient available points");
return Err(ProgramError::InsufficientFunds);
}
character.strength += new_strength;
character.available_points -= new_strength;
character.serialize(&mut &mut character_account.data.borrow_mut()[..])?;
Ok(())
}
```
---
## Common Vulnerabilities
### 1. Missing Ownership Check
**Vulnerability:**
```rust
// ❌ No ownership validation
pub fn update_data(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_value: u64,
) -> ProgramResult {
let data_account = &accounts[0];
// Dangerous - could be any account!
let mut data = MyData::try_from_slice(&data_account.data.borrow())?;
data.value = new_value;
data.serialize(&mut &mut data_account.data.borrow_mut()[..])?;
Ok(())
}
```
**Exploit:**
Attacker passes an account they control that happens to deserialize correctly, modifying arbitrary data.
**Fix:**
```rust
// ✅ With ownership check
if data_account.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
```
### 2. Missing Signer Check
**Vulnerability:**
```rust
// ❌ No signer validation
pub fn withdraw(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let user_account = &accounts[0];
let vault = &accounts[1];
// Dangerous - anyone can drain anyone's funds!
**user_account.lamports.borrow_mut() += amount;
**vault.lamports.borrow_mut() -= amount;
Ok(())
}
```
**Exploit:**
Attacker calls instruction with victim's account, draining their funds without signature.
**Fix:**
```rust
// ✅ With signer check
if !user_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
```
### 3. PDA Substitution Attack
**Vulnerability:**
```rust
// ❌ Accepts PDA without validation
pub fn update_user_data(
program_id: &Pubkey,
accounts: &[AccountInfo],
user: &AccountInfo,
user_pda: &AccountInfo,
) -> ProgramResult {
// No PDA derivation check!
let mut data = UserData::try_from_slice(&user_pda.data.borrow())?;
data.balance += 100;
data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?;
Ok(())
}
```
**Exploit:**
Attacker passes a different user's PDA, crediting that user's balance instead.
**Fix:**
```rust
// ✅ Validate PDA derivation
let (expected_pda, _) = Pubkey::find_program_address(
&[b"user_data", user.key.as_ref()],
program_id,
);
if expected_pda != *user_pda.key {
return Err(ProgramError::InvalidSeeds);
}
```
### 4. Integer Overflow/Underflow
**Vulnerability:**
```rust
// ❌ Unchecked arithmetic
pub fn add_rewards(
account: &AccountInfo,
reward: u64,
) -> ProgramResult {
let mut user = UserData::try_from_slice(&account.data.borrow())?;
user.total_rewards = user.total_rewards + reward; // Can overflow!
user.serialize(&mut &mut account.data.borrow_mut()[..])?;
Ok(())
}
```
**Exploit:**
Overflow wraps around: u64::MAX + 1 = 0, causing balance to reset.
**Fix:**
```rust
// ✅ Checked arithmetic
user.total_rewards = user.total_rewards
.checked_add(reward)
.ok_or(ProgramError::ArithmeticOverflow)?;
```
### 5. Unvalidated Account Reuse
**Vulnerability:**
```rust
// ❌ No initialization check
pub fn update_score(
accounts: &[AccountInfo],
score: u64,
) -> ProgramResult {
let score_account = &accounts[0];
let mut data = ScoreData::try_from_slice(&score_account.data.borrow())?;
// What if account was never initialized?
data.score = score;
data.serialize(&mut &mut score_account.data.borrow_mut()[..])?;
Ok(())
}
```
**Exploit:**
Reusing uninitialized memory can lead to undefined behavior or data corruption.
**Fix:**
```rust
// ✅ Check initialization
if !data.is_initialized {
return Err(ProgramError::UninitializedAccount);
}
```
---
## Summary
**Critical Account Validation Checklist:**
-**Ownership check**: Verify `account.owner == expected_program_id`
-**Signer check**: Verify `account.is_signer` for privileged operations
-**PDA validation**: Use `find_program_address` with expected seeds
-**Initialization check**: Verify account is initialized before use
-**Type validation**: Ensure account contains expected data structure
-**Rent exemption**: Calculate and enforce rent-exempt balances
-**Arithmetic safety**: Use `checked_add`, `checked_sub`, etc.
-**Data constraints**: Validate business logic rules
**Think Like an Attacker:**
For every account your program receives, ask:
- "What if this is the wrong account?"
- "What if this account isn't owned by my program?"
- "What if the user didn't sign for this?"
- "What if this account is uninitialized?"
- "What if these seeds derive a different PDA?"
Validate everything. Trust nothing.

File diff suppressed because it is too large Load Diff

View File

@@ -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::<UserData>();
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
// 3. Create account via System Program CPI
let create_ix = system_instruction::create_account(
payer.key,
user_account.key,
lamports,
space as u64,
program_id,
);
let signer_seeds: &[&[&[u8]]] = &[&[b"user", user_pubkey.as_ref(), &[bump]]];
invoke_signed(
&create_ix,
&[payer.clone(), user_account.clone(), system_program.clone()],
signer_seeds,
)?;
// 4. Initialize account data
let clock = Clock::get()?;
let user_data = UserData {
user: user_pubkey,
balance: 0,
created_at: clock.unix_timestamp,
};
user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
msg!("Created user account for {}", user_pubkey);
Ok(())
}
```
---
## Compute Budget Program
**Program ID:** `solana_program::compute_budget::ID` (`ComputeBudget111111111111111111111111111111`)
The Compute Budget Program allows transactions to request specific compute unit limits, heap sizes, and priority fees.
### Core Functionality
1. **Set compute unit limit** - Maximum CUs for transaction
2. **Set compute unit price** - Priority fee per CU
3. **Request heap size** - Heap memory allocation
### Compute Budget Instructions
```rust
use solana_program::compute_budget::ComputeBudgetInstruction;
pub enum ComputeBudgetInstruction {
RequestUnitsDeprecated, // Deprecated
RequestHeapFrame(u32), // Request heap frame (bytes)
SetComputeUnitLimit(u32), // Set max CUs
SetComputeUnitPrice(u64), // Set priority fee (microlamports per CU)
SetLoadedAccountsDataSizeLimit(u32), // Set loaded accounts data limit
}
```
---
### SetComputeUnitLimit
**Sets the maximum compute units available to the transaction.**
#### Function Signature
```rust
pub fn set_compute_unit_limit(units: u32) -> Instruction
```
#### Default Limits
- **Default per instruction:** 200,000 CUs
- **Default per transaction:** 1,400,000 CUs (with requested CU limit)
- **Maximum:** 1,400,000 CUs
#### Usage in Native Rust
**Important:** Compute Budget instructions are added to the transaction by the **client**, not inside the program.
**Client-side example (for reference):**
```rust
// This code runs CLIENT-SIDE, not in the program
use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
transaction::Transaction,
};
let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000);
let transaction = Transaction::new_signed_with_payer(
&[
compute_budget_ix, // Must be first
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
**⚠️ Note:** Programs cannot modify their own compute budget. These instructions must be added client-side before sending the transaction.
---
### SetComputeUnitPrice
**Sets the priority fee per compute unit (for transaction prioritization).**
#### Function Signature
```rust
pub fn set_compute_unit_price(microlamports: u64) -> Instruction
```
#### Priority Fee Calculation
```
Total Priority Fee = (CUs Used × microlamports) / 1,000,000
```
**Example:**
- CUs used: 50,000
- Price: 10,000 microlamports per CU
- Fee: (50,000 × 10,000) / 1,000,000 = 500 lamports
#### Usage (Client-side)
```rust
// Client-side code
let compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(20_000);
let transaction = Transaction::new_signed_with_payer(
&[
compute_unit_price_ix, // Set priority fee
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
**Use cases:**
- High-priority transactions (arbitrage, liquidations)
- Congested network periods
- Time-sensitive operations
---
### RequestHeapFrame
**Requests additional heap memory for the transaction.**
#### Function Signature
```rust
pub fn request_heap_frame(bytes: u32) -> Instruction
```
#### Default Heap
- **Default:** 32 KB
- **Maximum:** 256 KB
#### Usage (Client-side)
```rust
// Client-side code
let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); // 256 KB
let transaction = Transaction::new_signed_with_payer(
&[
heap_size_ix, // Request more heap
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
**When to use:**
- Large data structures
- Complex deserialization
- Temporary buffers
**⚠️ Cost:** Requesting heap increases CU consumption.
---
### SetLoadedAccountsDataSizeLimit
**Sets the maximum total size of loaded account data.**
#### Function Signature
```rust
pub fn set_loaded_accounts_data_size_limit(bytes: u32) -> Instruction
```
#### Default Limit
- **Default:** 64 MB per transaction
#### Usage (Client-side)
```rust
// Client-side code
let accounts_data_limit_ix =
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(128 * 1024 * 1024);
let transaction = Transaction::new_signed_with_payer(
&[
accounts_data_limit_ix,
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
```
**Use cases:**
- Transactions with many large accounts
- Bulk processing operations
---
### Complete Client-side Example
```rust
use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
transaction::Transaction,
signature::{Keypair, Signer},
pubkey::Pubkey,
};
pub fn build_optimized_transaction(
payer: &Keypair,
program_id: &Pubkey,
program_ix_data: &[u8],
accounts: Vec<AccountMeta>,
recent_blockhash: Hash,
) -> Transaction {
// 1. Set compute unit limit (if default 200k is insufficient)
let compute_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000);
// 2. Set priority fee (for faster processing)
let compute_price_ix = ComputeBudgetInstruction::set_compute_unit_price(10_000);
// 3. Request additional heap if needed
let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(128 * 1024); // 128 KB
// 4. Your program instruction
let program_ix = Instruction {
program_id: *program_id,
accounts,
data: program_ix_data.to_vec(),
};
// 5. Build transaction (compute budget instructions FIRST)
Transaction::new_signed_with_payer(
&[
compute_limit_ix,
compute_price_ix,
heap_size_ix,
program_ix,
],
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
)
}
```
---
## Other Built-in Programs
### BPF Loader
**Purpose:** Loads and executes Solana programs.
**Program IDs:**
- `BPFLoader1111111111111111111111111111111111` (deprecated)
- `BPFLoader2111111111111111111111111111111111` (upgradeable)
- `BPFLoaderUpgradeab1e11111111111111111111111` (current)
**Usage:** Primarily used by the runtime. Programs rarely interact with BPF Loader directly.
### Stake Program
**Program ID:** `Stake11111111111111111111111111111111111111`
**Purpose:** Staking SOL to validators.
**Common operations:**
- Create stake accounts
- Delegate stake
- Deactivate stake
- Withdraw stake
**Use case:** Staking pools, liquid staking protocols.
### Vote Program
**Program ID:** `Vote111111111111111111111111111111111111111`
**Purpose:** Validator voting and consensus.
**Use case:** Validator operations, rarely used by general programs.
---
## CPI Patterns
### System Program CPI Pattern
**Standard pattern for calling System Program:**
```rust
use solana_program::{
program::invoke,
system_instruction,
};
pub fn system_program_cpi(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
) -> ProgramResult {
// 1. Verify System Program
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
// 2. Create instruction
let ix = system_instruction::transfer(from.key, to.key, 1_000_000);
// 3. Invoke
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
Ok(())
}
```
### PDA Signing Pattern
**When PDAs need to sign:**
```rust
pub fn pda_system_cpi(
pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
program_id: &Pubkey,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// 1. Verify PDA
let (expected_pda, _) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda.key {
return Err(ProgramError::InvalidSeeds);
}
// 2. Create instruction
let ix = system_instruction::transfer(pda.key, to.key, 500_000);
// 3. Prepare signer seeds
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
// 4. Invoke with PDA signature
invoke_signed(
&ix,
&[pda.clone(), to.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
```
### Validation Pattern
**Always validate accounts before CPI:**
```rust
pub fn safe_system_cpi(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
// ✅ Validate System Program
if system_program.key != &solana_program::system_program::ID {
msg!("Invalid System Program");
return Err(ProgramError::IncorrectProgramId);
}
// ✅ Validate signer
if !from.is_signer {
msg!("From account must be signer");
return Err(ProgramError::MissingRequiredSignature);
}
// ✅ Validate sufficient balance
if from.lamports() < amount {
msg!("Insufficient balance");
return Err(ProgramError::InsufficientFunds);
}
// Execute CPI
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
Ok(())
}
```
---
## Best Practices
### 1. Always Validate Program IDs
```rust
// ✅ Validate before CPI
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
```
### 2. Use Rent Exemption
```rust
// ✅ Always create accounts with rent exemption
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
// ❌ Don't use arbitrary amounts
let lamports = 1_000_000; // May not be rent-exempt!
```
### 3. Verify PDA Before Creation
```rust
// ✅ Verify PDA derivation
let (expected_pda, bump) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
```
### 4. Use invoke_signed for PDAs
```rust
// ✅ PDAs sign with invoke_signed
invoke_signed(&ix, accounts, signer_seeds)?;
// ❌ Regular invoke won't work for PDA signers
invoke(&ix, accounts)?; // Fails if PDA needs to sign
```
### 5. Set Compute Budget Client-side
```rust
// ✅ Add compute budget instructions in client
let ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(400_000),
your_program_ix,
];
// ❌ Cannot set from within program
// Programs cannot modify their own compute budget
```
### 6. Order Compute Budget Instructions First
```rust
// ✅ Compute budget instructions FIRST
let ixs = vec![
compute_limit_ix,
compute_price_ix,
heap_size_ix,
program_ix,
];
// ❌ Wrong order - may not apply
let ixs = vec![
program_ix,
compute_limit_ix, // Too late!
];
```
### 7. Check Account Ownership Before Transfer
```rust
// ✅ Validate ownership for security
if from_account.owner != &solana_program::system_program::ID {
msg!("Can only transfer from System-owned accounts");
return Err(ProgramError::IllegalOwner);
}
```
---
## Summary
**Key Takeaways:**
1. **System Program** handles account creation, transfers, and allocation
2. **Compute Budget Program** instructions are added **client-side**, not in programs
3. **Always validate** program IDs before CPI
4. **Use rent exemption** when creating accounts
5. **PDAs require invoke_signed** for signing operations
**Most Common Operations:**
| Operation | Instruction | Use Case |
|-----------|------------|----------|
| Create account | `create_account` | New program accounts |
| Transfer lamports | `transfer` | SOL transfers |
| Set CU limit | `set_compute_unit_limit` | High-CU transactions |
| Set priority fee | `set_compute_unit_price` | Fast transaction processing |
| Request heap | `request_heap_frame` | Large data operations |
**System Program CPI Template:**
```rust
// Validate
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
// Create instruction
let ix = system_instruction::transfer(from.key, to.key, amount);
// Invoke (or invoke_signed for PDAs)
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
```
**Compute Budget Client Template:**
```rust
// Client-side
let ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(300_000),
ComputeBudgetInstruction::set_compute_unit_price(10_000),
your_program_ix,
];
```
Master these built-in programs for efficient account management and transaction optimization in production Solana programs.

View File

@@ -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<u64> = Vec::new();
for _ in 0..6 {
a.push(1);
}
// 600 CU - i32 (default integer type)
let mut a = Vec::new();
for _ in 0..6 {
a.push(1);
}
// 459 CU - u8 (best for small values)
let mut a: Vec<u8> = Vec::new();
for _ in 0..6 {
a.push(1);
}
```
**Initialization vs pushing:**
```rust
// 357 CU - Pushing elements one by one
let mut a: Vec<u64> = Vec::new();
for _ in 0..6 {
a.push(1);
}
// 125 CU - Direct initialization (65% savings!)
let _a: Vec<u64> = vec![1, 1, 1, 1, 1, 1];
```
**Best practice**: Use the smallest data type that fits your requirements (u8 > u16 > u32 > u64), and prefer `vec![]` initialization over repeated `push()` calls.
### 3. Serialization: Zero-Copy vs Borsh
Zero-copy deserialization can provide massive CU savings for account operations.
**Standard Borsh serialization:**
```rust
// 6,302 CU - Standard account initialization
pub fn initialize(_ctx: Context<InitializeCounter>) -> Result<()> {
Ok(())
}
// 2,600 CU total for increment (including serialization overhead)
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1).unwrap(); // 108 CU for operation
Ok(())
}
```
**Zero-copy optimization:**
```rust
// 5,020 CU - Zero-copy initialization (20% savings)
pub fn initialize_zero_copy(_ctx: Context<InitializeCounterZeroCopy>) -> Result<()> {
Ok(())
}
// 1,254 CU total for increment (52% savings!)
pub fn increment_zero_copy(ctx: Context<IncrementZeroCopy>) -> Result<()> {
let counter = &mut ctx.accounts.counter_zero_copy.load_mut()?;
counter.count = counter.count.checked_add(1).unwrap(); // 151 CU for operation
Ok(())
}
```
**Zero-copy account definition:**
```rust
#[account(zero_copy)]
#[repr(C)]
#[derive(InitSpace)]
pub struct CounterZeroCopy {
count: u64,
authority: Pubkey,
big_struct: BigStruct, // Can include large structs without stack overflow
}
```
**Benefits of zero-copy:**
- 50%+ CU savings on serialization/deserialization
- Avoids stack frame violations with large account structures
- Direct memory access without intermediate copying
- Particularly valuable for frequently updated accounts
**Trade-off**: Slightly more complex API (`load()`, `load_mut()`) and requires `#[repr(C)]` for memory layout guarantees.
### 4. Program Derived Addresses (PDAs)
PDA operations vary significantly in cost depending on the method used.
**Finding PDAs:**
```rust
// EXPENSIVE: 12,136 CU
// Iterates through nonces to find valid bump seed
let (pda, bump) = Pubkey::find_program_address(&[b"counter"], ctx.program_id);
// EFFICIENT: 1,651 CU (87% savings!)
// Uses known bump seed directly
let pda = Pubkey::create_program_address(&[b"counter", &[248_u8]], &program_id).unwrap();
```
**Optimization strategy:**
1. Use `find_program_address()` **once** during account initialization
2. Save the bump seed in the account data
3. Use `create_program_address()` with the saved bump for all subsequent operations
**Anchor implementation:**
```rust
// Account structure - save the bump
#[account]
pub struct CounterData {
pub count: u64,
pub bump: u8, // Store the bump seed here
}
// EXPENSIVE: 12,136 CU - Without saved bump
#[account(
seeds = [b"counter"],
bump // Anchor finds it every time
)]
pub counter_checked: Account<'info, CounterData>,
// EFFICIENT: 1,600 CU - With saved bump (87% savings!)
#[account(
seeds = [b"counter"],
bump = counter_checked.bump // Use the saved bump
)]
pub counter_checked: Account<'info, CounterData>,
```
### 5. Cross-Program Invocations (CPIs)
CPIs add significant overhead compared to direct operations.
**CPI to System Program:**
```rust
// 2,215 CU - CPI for SOL transfer
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info().clone(),
to: ctx.accounts.counter.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, 1_000_000)?;
```
**Direct lamport manipulation:**
```rust
// 251 CU - Direct operation (90% savings!)
let counter_account_info = ctx.accounts.counter.to_account_info();
let mut counter_lamports = counter_account_info.try_borrow_mut_lamports()?;
**counter_lamports += 1_000_000;
let payer_account_info = ctx.accounts.payer.to_account_info();
let mut payer_lamports = payer_account_info.try_borrow_mut_lamports()?;
**payer_lamports -= 1_000_000;
```
**Important caveats:**
1. **Error handling overhead**: Error paths add ~1,199 CU if triggered
2. **Safety**: Direct manipulation bypasses safety checks in the System Program
3. **Ownership**: Only safe when you control both accounts
4. **Rent exemption**: You're responsible for maintaining rent exemption
**Best practice**: Use CPIs for safety and correctness by default. Only optimize to direct manipulation when:
- You have tight CU constraints
- You fully understand the safety implications
- Both accounts are controlled by your program
### 6. Pass by Reference vs Clone
Solana's bump allocator doesn't free memory, making unnecessary cloning particularly problematic.
**Comparison:**
```rust
let balances = vec![10_u64; 100];
// EFFICIENT: 47,683 CU - Pass by reference
fn sum_by_reference(data: &Vec<u64>) -> u64 {
data.iter().sum()
}
for _ in 0..39 {
sum_reference += sum_by_reference(&balances);
}
// INEFFICIENT: 49,322 CU - Clone data (3.5% more expensive)
// WARNING: Runs out of memory at 40+ iterations!
fn sum_by_value(data: Vec<u64>) -> u64 {
data.iter().sum()
}
for _ in 0..39 {
sum_clone += sum_by_value(balances.clone());
}
```
**Memory concern**: Solana programs have a 32KB heap using a bump allocator that **never frees memory** during transaction execution. Excessive cloning leads to out-of-memory errors.
**Best practice**: Always pass by reference (`&T`) unless you explicitly need ownership transfer. Use `Copy` types for small data.
### 7. Checked Math vs Unchecked Operations
Checked arithmetic adds safety at the cost of compute units.
**Comparison:**
```rust
let mut count: u64 = 1;
// 97,314 CU - Checked multiplication with overflow protection
for _ in 0..12000 {
count = count.checked_mul(2).expect("overflow");
}
// 85,113 CU - Bit shift operation (12% savings)
// Equivalent to multiply by 2, but unchecked
for _ in 0..12000 {
count = count << 1;
}
```
**Trade-off**: Unchecked operations are faster but risk overflow bugs that can lead to serious security vulnerabilities.
**Best practice**:
- Use checked math by default for safety
- Profile your program to identify hot paths
- Only switch to unchecked math when:
- You've proven overflow is impossible
- CU savings are critical
- You've added overflow tests
**Compiler configuration** (in Cargo.toml):
```toml
[profile.release]
overflow-checks = true # Keep overflow checks even in release mode
```
## Framework Comparison
Different implementation approaches offer varying trade-offs between developer experience, safety, and performance.
| Implementation | Binary Size | Deploy Cost | Init CU | Increment CU |
|---------------|-------------|-------------|---------|--------------|
| **Anchor** | 265,677 bytes | 1.85 SOL | 6,302 | 946 |
| **Anchor Zero-Copy** | Same | 1.85 SOL | 5,020 | ~1,254 |
| **Native Rust** | 48,573 bytes | 0.34 SOL | - | 843 |
| **Unsafe Rust** | 973 bytes | 0.008 SOL | - | 5 |
| **Assembly (SBPF)** | 1,389 bytes | 0.01 SOL | - | 4 |
| **C** | 1,333 bytes | 0.01 SOL | - | 5 |
**Key insights:**
- **Anchor**: Best developer experience, automatic account validation, but highest CU and deployment costs
- **Anchor Zero-Copy**: Significant CU improvement over standard Anchor with minimal code changes
- **Native Rust**: 11% CU savings over Anchor, 82% smaller deployment size, moderate complexity
- **Unsafe Rust**: 99% CU savings, minimal size, but requires extreme care and deep expertise
- **Assembly/C**: Maximum optimization possible, but very difficult to develop and maintain
**Recommendation**: Start with Anchor or native Rust. Optimize hot paths with zero-copy. Only consider unsafe Rust or lower-level languages for critical performance bottlenecks after profiling.
## Advanced Optimization Techniques
### 1. Compiler Flags
Configure optimization in `Cargo.toml`:
```toml
[profile.release]
opt-level = 3 # Maximum optimization
lto = "fat" # Full link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
overflow-checks = true # Keep safety checks despite performance cost
```
**Trade-offs**:
- `overflow-checks = false`: Saves CU but removes critical safety checks
- Higher `opt-level`: Better performance but slower compilation
- `lto = "fat"`: Maximum optimization but much slower builds
### 2. Function Inlining
Control function inlining to balance CU usage and stack space:
```rust
// Force inlining - saves CU by eliminating function call overhead
#[inline(always)]
fn add(a: u64, b: u64) -> u64 {
a + b
}
// Prevent inlining - saves stack space at the cost of CU
#[inline(never)]
pub fn complex_operation() {
// Large function body
}
```
**Trade-off**: Inlining saves CU but increases stack usage. Solana has a 4KB stack limit, so excessive inlining can cause stack overflow.
### 3. Alternative Entry Points
The standard Solana entry point adds overhead. Alternatives:
**Standard entry point:**
```rust
use solana_program::entrypoint;
entrypoint!(process_instruction);
```
**Minimal entry points:**
- [solana-nostd-entrypoint](https://github.com/cavemanloverboy/solana-nostd-entrypoint): Ultra-minimal entry using unsafe Rust
- [eisodos](https://github.com/anza-xyz/eisodos): Alternative minimal entry point
**Warning**: These require deep understanding of Solana internals and unsafe Rust. Only use for extreme optimization needs.
### 4. Custom Heap Allocators
Solana's default bump allocator never frees memory during transaction execution.
**Problem:**
```rust
// This will eventually run out of heap space (32KB limit)
for _ in 0..1000 {
let v = vec![0u8; 1024]; // Each iteration uses more heap
// Memory is never freed!
}
```
**Solution - Custom allocators:**
- **smalloc**: Used by Metaplex programs, provides better memory management
- Prevents out-of-memory errors in memory-intensive operations
**Implementation** (advanced):
```rust
#[global_allocator]
static ALLOCATOR: custom_allocator::CustomAllocator = custom_allocator::CustomAllocator;
```
### 5. Boxing and Heap Allocation
Heap operations cost more CU than stack operations.
```rust
// Stack allocation - faster
let data = [0u8; 100];
// Heap allocation - slower, uses more CU
let data = Box::new([0u8; 100]);
```
**Best practice**: Avoid `Box`, `Vec`, and other heap allocations when stack allocation is possible and doesn't risk overflow.
## Measuring Compute Units
### Using sol_log_compute_units()
Built-in logging function to track CU consumption:
```rust
use solana_program::log::sol_log_compute_units;
pub fn my_instruction(ctx: Context<MyContext>) -> Result<()> {
sol_log_compute_units(); // Log remaining CU
// ... do some work ...
sol_log_compute_units(); // Log remaining CU again
Ok(())
}
```
**Output in transaction logs:**
```
Program consumption: 200000 units remaining
Program consumption: 195432 units remaining
```
**CU used = 200000 - 195432 = 4,568 CU**
### compute_fn! Macro
Convenient macro for measuring specific code blocks (costs 409 CU overhead):
```rust
#[macro_export]
macro_rules! compute_fn {
($msg:expr=> $($tt:tt)*) => {
::solana_program::msg!(concat!($msg, " {"));
::solana_program::log::sol_log_compute_units();
let res = { $($tt)* };
::solana_program::log::sol_log_compute_units();
::solana_program::msg!(concat!(" } // ", $msg));
res
};
}
```
**Usage:**
```rust
let result = compute_fn! { "My expensive operation" =>
expensive_computation()
};
```
**Output:**
```
Program log: My expensive operation {
Program consumption: 195432 units remaining
Program consumption: 180123 units remaining
Program log: } // My expensive operation
```
**Actual CU = (195432 - 180123) - 409 (macro overhead) = 14,900 CU**
### Using Mollusk Bencher
For native Rust programs, use Mollusk's built-in benchmarking (see main SKILL.md for details).
## Anti-Patterns to Avoid
### 1. Excessive Logging
```rust
// BAD: Logging in production
msg!("Processing user {}", user_pubkey);
msg!("Amount: {}", amount);
msg!("Timestamp: {}", Clock::get()?.unix_timestamp);
```
**Solution**: Remove logging or use conditional compilation:
```rust
#[cfg(feature = "debug")]
msg!("Processing user {}", user_pubkey);
```
### 2. Large Data Types for Small Values
```rust
// BAD: Using u64 when u8 suffices
pub struct Config {
pub fee_percentage: u64, // Only 0-100
pub max_items: u64, // Only 0-255
}
// GOOD: Use smallest type
pub struct Config {
pub fee_percentage: u8, // 0-100
pub max_items: u8, // 0-255
}
```
### 3. Cloning Large Structures
```rust
// BAD: Unnecessary clone
fn process_data(data: Vec<u8>) -> Result<()> {
let copy = data.clone(); // Wastes CU and heap
// ...
}
// GOOD: Pass by reference
fn process_data(data: &[u8]) -> Result<()> {
// Work directly with reference
}
```
### 4. Repeated PDA Derivation
```rust
// BAD: Finding bump every time
#[account(
seeds = [b"vault"],
bump // Finds bump on every call!
)]
pub vault: Account<'info, Vault>,
// GOOD: Use saved bump
#[account(
seeds = [b"vault"],
bump = vault.bump // Uses saved bump
)]
pub vault: Account<'info, Vault>,
```
### 5. Unnecessary Boxing
```rust
// BAD: Boxing adds heap overhead
let value = Box::new(calculate_value());
// GOOD: Keep on stack
let value = calculate_value();
```
### 6. String Operations
```rust
// BAD: String concatenation and formatting
let message = format!("User {} sent {} tokens", user, amount);
msg!(&message);
// GOOD: Use separate logs or remove entirely
user.log();
amount.log();
```
### 7. Deep CPI Chains
Each CPI adds significant overhead. Avoid unnecessary indirection:
```rust
// BAD: Unnecessary CPI
invoke(
&my_helper_program::process(),
&accounts,
)?;
// GOOD: Direct implementation
process_directly(&accounts)?;
```
### 8. Not Using Zero-Copy for Large Accounts
```rust
// BAD: Large account with standard serialization
#[account]
pub struct LargeData {
pub items: [u64; 1000], // Expensive to serialize/deserialize
}
// GOOD: Use zero-copy
#[account(zero_copy)]
#[repr(C)]
pub struct LargeData {
pub items: [u64; 1000], // Direct memory access
}
```
## Best Practices Summary
1. **Minimize or eliminate logging** in production code
2. **Use zero-copy** for accounts with large data structures
3. **Cache PDA bumps** - derive once, store in account, reuse
4. **Choose smallest data types** that meet your requirements
5. **Pass by reference** instead of cloning data
6. **Profile before optimizing** - measure CU usage to identify bottlenecks
7. **Consider native Rust** over Anchor for performance-critical programs
8. **Use `vec![]` initialization** instead of repeated `push()` calls
9. **Avoid unnecessary CPIs** - use direct operations when safe
10. **Balance safety vs performance** - don't sacrifice security without careful analysis
11. **Test CU usage** regularly - include benchmarks in your test suite
12. **Use checked math by default** - only optimize to unchecked when proven safe
13. **Minimize heap allocations** - prefer stack when possible
14. **Remove or conditionally compile debug code** for production builds
15. **Consider zero-copy for frequently updated accounts** - 50%+ CU savings
## Additional Resources
### Official Documentation
- [How to Optimize Compute](https://solana.com/developers/guides/advanced/how-to-optimize-compute)
- [Solana Compute Budget Documentation](https://github.com/solana-labs/solana/blob/090e11210aa7222d8295610a6ccac4acda711bb9/program-runtime/src/compute_budget.rs#L26-L87)
### Code Examples and Tools
- [solana-developers/cu_optimizations](https://github.com/solana-developers/cu_optimizations) - Official examples with benchmarks
- [hetdagli234/optimising-solana-programs](https://github.com/hetdagli234/optimising-solana-programs) - Community optimization examples
### Video Guides
- [How to optimize CU in programs](https://www.youtube.com/watch?v=7CbAK7Oq_o4)
- [Program optimization Part 1](https://www.youtube.com/watch?v=xoJ-3NkYXfY)
- [Program optimization Part 2 - Advanced](https://www.youtube.com/watch?v=Pwly1cOa2hg)
- [Writing Solana programs in Assembly](https://www.youtube.com/watch?v=eacDC0VgyxI)
### Technical Articles
- [RareSkills: Solana Compute Unit Price](https://rareskills.io/post/solana-compute-unit-price)
- [Understanding Solana Compute Units](https://www.helius.dev/blog/priority-fees-understanding-solanas-transaction-fee-mechanics)
### Advanced Tools
- [solana-nostd-entrypoint](https://github.com/cavemanloverboy/solana-nostd-entrypoint) - Minimal entry point
- [Mollusk](https://github.com/anza-xyz/mollusk) - Fast testing with CU benchmarking

View File

@@ -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<AccountMeta>,
/// Serialized instruction data
pub data: Vec<u8>,
}
pub struct AccountMeta {
/// Account public key
pub pubkey: Pubkey,
/// Is this account a signer?
pub is_signer: bool,
/// Is this account writable?
pub is_writable: bool,
}
```
---
## invoke vs invoke_signed
### invoke: Regular Signers
Use `invoke` when all required signers are regular accounts (not PDAs).
**Example: User transfers SOL**
```rust
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn user_transfer_sol(
_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let sender = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Verify sender signed the transaction
if !sender.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
sender.key,
recipient.key,
amount,
);
// Execute CPI (sender already signed the transaction)
invoke(
&transfer_ix,
&[
sender.clone(),
recipient.clone(),
system_program.clone(),
],
)?;
Ok(())
}
```
**Key Points:**
- `sender.is_signer` must be true (verified at transaction level)
- No `signers_seeds` needed
- `invoke` internally calls `invoke_signed` with empty seeds
### invoke_signed: PDA Signers
Use `invoke_signed` when a PDA needs to sign the instruction.
**Example: PDA transfers SOL**
```rust
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn pda_transfer_sol(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pda_account = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Derive PDA and verify
let (pda, bump_seed) = Pubkey::find_program_address(
&[b"vault", recipient.key.as_ref()],
program_id,
);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account.key, // From PDA (needs signing!)
recipient.key,
amount,
);
// PDA signing seeds (must match derivation)
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
recipient.key.as_ref(),
&[bump_seed], // Critical: bump must be included
]];
// Execute CPI with PDA signature
invoke_signed(
&transfer_ix,
&[
pda_account.clone(),
recipient.clone(),
system_program.clone(),
],
signer_seeds, // Runtime verifies and grants signing authority
)?;
Ok(())
}
```
**How Runtime Handles PDA Signing:**
1. Runtime receives `signers_seeds`
2. Calls `create_program_address(signers_seeds, calling_program_id)`
3. Verifies derived PDA matches an account in the instruction
4. Grants signing authority for that account
5. Executes the CPI
**Critical:** Seeds must exactly match the PDA derivation, including the bump.
---
## Account Privilege Propagation
### Privilege Extension
When making a CPI, account privileges **extend** from the caller to the callee.
```
User Transaction
│ (provides: signer=true, writable=true)
┌─────────────────────┐
│ Program A │
│ Receives accounts: │
│ - user (signer) │──┐ Privileges
│ - vault (writable) │ │ propagate
└─────────────────────┘ │
┌─────────────────────┐
│ Program B (via CPI)│
│ Can use: │
│ - user (signer) │
│ - vault (writable) │
└─────────────────────┘
```
### Propagation Rules
**Rule 1:** If an account is a signer in Program A, it remains a signer in Program B (via CPI)
**Rule 2:** If an account is writable in Program A, it remains writable in Program B (via CPI)
**Rule 3:** Programs can add PDA signers via `invoke_signed`
**Rule 4:** Programs cannot escalate privileges (can't make non-signer a signer without PDA derivation)
### Example: Privilege Propagation Chain
```rust
// User calls Program A
// Accounts: [user (signer, writable), vault (writable), data_account]
// Program A → CPI to Program B
invoke(
&instruction_for_program_b,
&[user.clone(), vault.clone()], // Both retain privileges
)?;
// Program B → CPI to Program C
invoke(
&instruction_for_program_c,
&[user.clone()], // user still a signer!
)?;
```
**Depth**: Up to 4 levels of CPI (5 total stack height including initial transaction)
---
## Common CPI Patterns
### 1. System Program: Create Account
**Most common CPI**: Creating new accounts.
```rust
use solana_program::{
program::invoke_signed,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_pda_account(
program_id: &Pubkey,
payer: &AccountInfo,
pda_account: &AccountInfo,
system_program: &AccountInfo,
space: usize,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(space);
// Create account instruction
let create_account_ix = system_instruction::create_account(
payer.key,
pda_account.key,
rent_lamports,
space as u64,
program_id,
);
// Prepare signer seeds
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
// Execute CPI
invoke_signed(
&create_account_ix,
&[payer.clone(), pda_account.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
```
### 2. System Program: Transfer SOL
```rust
use solana_program::system_instruction;
// From regular account
let transfer_ix = system_instruction::transfer(from_key, to_key, lamports);
invoke(&transfer_ix, &[from_account, to_account, system_program])?;
// From PDA
let transfer_ix = system_instruction::transfer(pda_key, to_key, lamports);
let signer_seeds: &[&[&[u8]]] = &[&[seeds, &[bump]]];
invoke_signed(&transfer_ix, &[pda_account, to_account, system_program], signer_seeds)?;
```
### 3. Custom Program CPI
**Calling another custom program:**
```rust
use borsh::BorshSerialize;
#[derive(BorshSerialize)]
struct CustomInstructionData {
amount: u64,
memo: String,
}
pub fn call_custom_program(
custom_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let target_account = next_account_info(account_info_iter)?;
let custom_program = next_account_info(account_info_iter)?;
// Serialize instruction data
let instruction_data = CustomInstructionData {
amount,
memo: "Hello from CPI".to_string(),
};
let data = instruction_data.try_to_vec()?;
// Build instruction
let instruction = Instruction {
program_id: *custom_program_id,
accounts: vec![
AccountMeta::new(*user.key, true), // signer, writable
AccountMeta::new(*target_account.key, false), // writable
],
data,
};
// Execute CPI
invoke(
&instruction,
&[user.clone(), target_account.clone(), custom_program.clone()],
)?;
Ok(())
}
```
### 4. Multiple PDAs Signing
```rust
pub fn multi_pda_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda1_seeds = &[b"pda1", &[bump1]];
let pda2_seeds = &[b"pda2", &[bump2]];
// Multiple PDA signers
let signer_seeds: &[&[&[u8]]] = &[
pda1_seeds, // First PDA
pda2_seeds, // Second PDA
];
invoke_signed(&instruction, &accounts, signer_seeds)?;
Ok(())
}
```
### 5. Chained CPIs
**Program A calls Program B, which calls Program C:**
```rust
// In Program A
pub fn program_a_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program B
let instruction_for_b = build_program_b_instruction();
invoke(&instruction_for_b, accounts)?;
Ok(())
}
// In Program B
pub fn program_b_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program C
let instruction_for_c = build_program_c_instruction();
invoke(&instruction_for_c, accounts)?;
Ok(())
}
```
**Depth tracking**: User→A→B→C = stack depth 4 (within limit)
---
## CPI Limits and Constraints
### Stack Depth Limit
**Maximum call depth:** 5 (including initial transaction)
```
Depth 1: User Transaction
Depth 2: Program A (first CPI)
Depth 3: Program B (second CPI)
Depth 4: Program C (third CPI)
Depth 5: Program D (fourth CPI)
Depth 6: ❌ ERROR - MAX_INSTRUCTION_STACK_DEPTH exceeded
```
**Constant:**
```rust
// From agave source
pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;
```
**Error when exceeded:**
```
Error: CallDepth(5)
```
### Account Limits
- **Max accounts per instruction:** 256 (practical limit ~64 without ALTs)
- **Max writable accounts:** Limited by transaction size
- **Duplicate accounts:** Allowed but share state (mutations visible to all references)
### Compute Unit Costs
CPI operations consume compute units:
| Operation | Approximate CU Cost |
|-----------|---------------------|
| `invoke` base cost | ~1,000 CU |
| `invoke_signed` base cost | ~1,000 CU |
| Per account passed | ~50-100 CU |
| PDA derivation in runtime | ~1,500 CU |
| Actual callee logic | Variable |
**Tip:** Pre-derive PDAs and store bumps to save CU.
### Data Size Limits
- **Instruction data:** No hard limit, but affects transaction size (1232 bytes max for non-ALT transactions)
- **Account data modification:** Accounts can be resized via `realloc` (up to 10 MiB)
---
## Security Considerations
### 1. Validate PDA Derivation Before CPI
**❌ Vulnerable:**
```rust
pub fn vulnerable_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda_account = &accounts[0];
// No validation!
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
```
**✅ Secure:**
```rust
pub fn secure_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
bump: u8,
) -> ProgramResult {
let pda_account = &accounts[0];
// Validate PDA before CPI
let (expected_pda, _) = Pubkey::find_program_address(&[b"vault"], program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
```
### 2. Verify Signer Requirements
**Always check `is_signer` before making CPIs that transfer value:**
```rust
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let transfer_ix = system_instruction::transfer(user.key, vault.key, amount);
invoke(&transfer_ix, &[user.clone(), vault.clone(), system_program.clone()])?;
```
### 3. Program ID Verification
**Verify the program being called is the expected program:**
```rust
const EXPECTED_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
if token_program.key.to_string() != EXPECTED_PROGRAM {
return Err(ProgramError::IncorrectProgramId);
}
```
### 4. Privilege Leakage
**Be careful about which accounts you pass in CPIs:**
```rust
// ❌ Dangerous - passes admin with signer privilege
invoke(
&untrusted_program_instruction,
&[admin.clone(), user_data.clone()], // Admin is a signer!
)?;
// ✅ Safe - only pass necessary accounts
invoke(
&untrusted_program_instruction,
&[user_data.clone()], // Admin not included
)?;
```
### 5. Reent rancy Considerations
**Solana programs are generally safe from reentrancy** because:
- Accounts are locked during instruction execution
- Runtime prevents concurrent modifications
**However, be cautious with:**
- State assumptions across CPI boundaries
- Read-modify-write patterns split across CPIs
### 6. Error Handling
**CPI errors propagate to the caller:**
```rust
// If CPI fails, entire transaction reverts
match invoke(&instruction, &accounts) {
Ok(()) => msg!("CPI succeeded"),
Err(e) => {
msg!("CPI failed: {:?}", e);
return Err(e); // Propagate error
}
}
```
**All state changes are atomic** - if CPI fails, all changes rollback.
---
## Best Practices
### 1. Derive PDAs Once
```rust
// ❌ Wasteful - derives multiple times
pub fn wasteful(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda
let (pda_again, bump_again) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda_again (same as pda!)
}
// ✅ Efficient - derive once, reuse
pub fn efficient(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// Reuse pda and bump
}
```
### 2. Store and Reuse Bumps
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultData {
pub bump: u8, // Store on creation
// ... other fields
}
// On CPI: use stored bump
let vault_data = VaultData::try_from_slice(&vault_pda.data.borrow())?;
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[vault_data.bump]]];
```
**Benefit:** Saves ~2,700 CU per operation.
### 3. Helper Functions for Common CPIs
```rust
pub mod cpi_helpers {
use super::*;
pub fn transfer_sol(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])
}
pub fn transfer_sol_from_pda(
from_pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
signer_seeds: &[&[&[u8]]],
) -> ProgramResult {
let ix = system_instruction::transfer(from_pda.key, to.key, amount);
invoke_signed(&ix, &[from_pda.clone(), to.clone(), system_program.clone()], signer_seeds)
}
}
```
### 4. Validate All CPI Inputs
**Checklist before CPI:**
- ✅ Verify signer requirements (`is_signer`)
- ✅ Validate PDA derivation
- ✅ Check program IDs match expectations
- ✅ Verify account ownership
- ✅ Validate data integrity
### 5. Document CPI Dependencies
```rust
/// Transfers SOL from program vault to recipient.
///
/// # Accounts
/// 0. `[writable]` vault_pda - Program vault (PDA, signer)
/// 1. `[writable]` recipient - Receives SOL
/// 2. `[]` system_program - System Program (11111...)
///
/// # CPIs Made
/// - System Program: transfer (from vault to recipient)
pub fn withdraw_from_vault(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
// ...
}
```
### 6. Error Context
```rust
invoke(&instruction, &accounts).map_err(|e| {
msg!("CPI to System Program failed");
e
})?;
```
### 7. Minimize CPI Depth
**Keep call chains shallow:**
- Reduces compute units
- Easier to debug
- Lower risk of hitting stack limit
- Better user experience (simpler transactions)
---
## Summary
**Key Takeaways:**
1. **CPI enables composability** - programs call other programs
2. **Use `invoke` for regular signers**, `invoke_signed` for PDAs
3. **Privileges propagate** - signers and writable flags extend through CPIs
4. **Maximum depth is 5** - including initial transaction
5. **Always validate PDAs** before using in `invoke_signed`
6. **Verify signer requirements** to prevent unauthorized operations
7. **Store bumps** in account data to save compute units
8. **CPIs are atomic** - failures rollback all changes
**Security Checklist:**
- ✅ Validate PDA derivation with canonical bump
- ✅ Verify `is_signer` for value transfers
- ✅ Check program IDs match expectations
- ✅ Only pass necessary accounts (avoid privilege leakage)
- ✅ Handle CPI errors appropriately
**Common Pattern:**
```rust
// 1. Validate inputs
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// 2. Derive and validate PDA if needed
let (pda, bump) = Pubkey::find_program_address(&seeds, program_id);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// 3. Build instruction
let ix = build_instruction();
// 4. Execute CPI
invoke_signed(&ix, &accounts, &[&[seeds, &[bump]]])?;
```
CPI is the foundation of program composability on Solana. Master it to build powerful, modular programs that leverage the entire ecosystem.

File diff suppressed because it is too large Load Diff

View File

@@ -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<NonceData>),
Current(Box<NonceData>),
}
pub struct NonceData {
pub authority: Pubkey, // Who can authorize nonce operations
pub durable_nonce: Hash, // The actual nonce value (32 bytes)
pub fee_calculator: FeeCalculator, // Historic fee data
}
```
**Account requirements:**
- **Owner**: System Program (`11111111111111111111111111111111`)
- **Size**: 80 bytes
- **Rent exemption**: Required (minimum ~0.00144768 SOL)
- **Authority**: Can be any pubkey (keypair or PDA)
### Nonce Authority
The authority pubkey controls the nonce account:
- **Can**: Advance nonce, withdraw funds, authorize nonce transactions, change authority
- **Cannot**: Execute other instructions without nonce advancement (runtime enforces this)
**Authority options:**
- **Keypair**: Direct control (hot wallet, cold wallet)
- **PDA**: Program-controlled nonces (advanced use case)
- **Multisig**: Multiple signers required (DAO wallets)
## Creating Nonce Accounts
### Using Native Rust
```rust
use solana_sdk::{
instruction::Instruction,
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction,
sysvar::rent::Rent,
transaction::Transaction,
};
use solana_client::rpc_client::RpcClient;
fn create_nonce_account(
rpc_client: &RpcClient,
payer: &Keypair,
nonce_account: &Keypair,
authority: &Pubkey,
) -> Result<(), Box<dyn std::error::Error>> {
// Calculate rent-exempt balance for nonce account
let rent = rpc_client.get_minimum_balance_for_rent_exemption(80)?;
// Create account instruction
let create_account_ix = system_instruction::create_account(
&payer.pubkey(),
&nonce_account.pubkey(),
rent, // Lamports (rent-exempt minimum)
80, // Space (nonce account size)
&solana_program::system_program::id(), // Owner (System Program)
);
// Initialize nonce instruction
let initialize_nonce_ix = system_instruction::initialize_nonce_account(
&nonce_account.pubkey(),
authority, // Nonce authority
);
// Build transaction
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[create_account_ix, initialize_nonce_ix],
Some(&payer.pubkey()),
&[payer, nonce_account], // Both payer and nonce account must sign
recent_blockhash,
);
// Send transaction
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Created nonce account: {}", signature);
Ok(())
}
```
### Single-Step Creation
There's also a convenience function that combines both steps:
```rust
let instruction = system_instruction::create_nonce_account(
&payer.pubkey(),
&nonce_account.pubkey(),
authority,
rent_lamports,
);
// This creates a single instruction that:
// 1. Creates the account
// 2. Initializes it as a nonce account
```
### Using CLI
```bash
# Generate keypair for nonce account
solana-keygen new -o nonce-account.json
# Create nonce account
solana create-nonce-account nonce-account.json 0.0015
# Verify creation
solana nonce nonce-account.json
# Output: Current nonce value (32-byte hash)
```
## Querying Nonce Accounts
### Fetching Nonce Value
```rust
use solana_sdk::account::Account;
use solana_program::system_program;
fn get_nonce_value(
rpc_client: &RpcClient,
nonce_pubkey: &Pubkey,
) -> Result<Hash, Box<dyn std::error::Error>> {
// Fetch account data
let account = rpc_client.get_account(nonce_pubkey)?;
// Verify it's a nonce account
if account.owner != system_program::id() {
return Err("Account is not owned by System Program".into());
}
// Deserialize nonce data
let nonce_data = bincode::deserialize::<NonceState>(&account.data)?;
match nonce_data {
NonceState::Current(data) => Ok(data.durable_nonce),
NonceState::Legacy(data) => Ok(data.durable_nonce),
}
}
```
### Parsing Nonce Account
```rust
use solana_program::nonce::state::{Data, State};
fn parse_nonce_account(account_data: &[u8]) -> Result<Data, Box<dyn std::error::Error>> {
let state: State = bincode::deserialize(account_data)?;
match state {
State::Initialized(data) => Ok(data),
State::Uninitialized => Err("Nonce account not initialized".into()),
}
}
// Access nonce components
fn display_nonce_info(nonce_data: &Data) {
println!("Authority: {}", nonce_data.authority);
println!("Nonce value: {}", nonce_data.blockhash);
println!("Fee calculator: {:?}", nonce_data.fee_calculator);
}
```
## Building Transactions with Durable Nonces
### Transaction Structure
**Critical requirements:**
1. **First instruction** MUST be `advance_nonce_account`
2. Use nonce value as `recent_blockhash`
3. Sign with nonce authority (in addition to other required signers)
```rust
use solana_sdk::{
hash::Hash,
instruction::Instruction,
message::Message,
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
};
fn build_nonce_transaction(
nonce_pubkey: &Pubkey,
nonce_authority: &Keypair,
nonce_value: Hash,
instructions: Vec<Instruction>,
payer: &Keypair,
) -> Transaction {
// 1. Create advance_nonce instruction (MUST BE FIRST)
let advance_nonce_ix = system_instruction::advance_nonce_account(
nonce_pubkey,
&nonce_authority.pubkey(),
);
// 2. Combine with your instructions
let mut all_instructions = vec![advance_nonce_ix];
all_instructions.extend(instructions);
// 3. Build message with nonce as blockhash
let message = Message::new_with_blockhash(
&all_instructions,
Some(&payer.pubkey()),
&nonce_value, // Use nonce value instead of recent blockhash!
);
// 4. Sign with both payer and nonce authority
let mut signers = vec![payer];
if nonce_authority.pubkey() != payer.pubkey() {
signers.push(nonce_authority);
}
Transaction::new(&signers, message, nonce_value)
}
```
### Complete Example: Transfer with Durable Nonce
```rust
fn transfer_with_nonce(
rpc_client: &RpcClient,
nonce_account: &Pubkey,
nonce_authority: &Keypair,
payer: &Keypair,
recipient: &Pubkey,
amount: u64,
) -> Result<(), Box<dyn std::error::Error>> {
// 1. Fetch current nonce value
let nonce_value = get_nonce_value(rpc_client, nonce_account)?;
// 2. Create transfer instruction
let transfer_ix = system_instruction::transfer(
&payer.pubkey(),
recipient,
amount,
);
// 3. Build transaction with nonce
let transaction = build_nonce_transaction(
nonce_account,
nonce_authority,
nonce_value,
vec![transfer_ix],
payer,
);
// 4. Can now submit immediately or store for later
// No expiration pressure!
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Transfer completed: {}", signature);
Ok(())
}
```
### Serializing for Offline Signing
```rust
use base58::ToBase58;
fn serialize_for_offline_signing(transaction: &Transaction) -> String {
// Serialize transaction to bytes
let serialized = bincode::serialize(transaction).unwrap();
// Encode as base58 for transport
serialized.to_base58()
}
fn deserialize_signed_transaction(base58_tx: &str) -> Transaction {
use base58::FromBase58;
let bytes = base58_tx.from_base58().unwrap();
bincode::deserialize(&bytes).unwrap()
}
```
## Managing Nonce Accounts
### Advancing Nonce
**Automatic advancement**: When you submit a transaction with a durable nonce, the runtime automatically advances the nonce as part of processing the `advance_nonce_account` instruction.
**Manual advancement** (without submitting transaction):
```rust
fn advance_nonce_manually(
rpc_client: &RpcClient,
nonce_account: &Pubkey,
nonce_authority: &Keypair,
payer: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
let advance_ix = system_instruction::advance_nonce_account(
nonce_account,
&nonce_authority.pubkey(),
);
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[advance_ix],
Some(&payer.pubkey()),
&[payer, nonce_authority],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction(&transaction)?;
Ok(())
}
```
**When to manually advance:**
- Before reusing nonce for a new transaction
- To invalidate a previously signed transaction
- Regular rotation for security
### Withdrawing from Nonce Account
```rust
fn withdraw_from_nonce(
rpc_client: &RpcClient,
nonce_account: &Pubkey,
nonce_authority: &Keypair,
recipient: &Pubkey,
amount: u64,
payer: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
let withdraw_ix = system_instruction::withdraw_nonce_account(
nonce_account,
&nonce_authority.pubkey(),
recipient,
amount,
);
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[withdraw_ix],
Some(&payer.pubkey()),
&[payer, nonce_authority],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction(&transaction)?;
Ok(())
}
```
**Important**: Must maintain rent-exempt minimum balance. Can only withdraw to zero if closing the account.
### Changing Nonce Authority
```rust
fn change_nonce_authority(
rpc_client: &RpcClient,
nonce_account: &Pubkey,
current_authority: &Keypair,
new_authority: &Pubkey,
payer: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
let authorize_ix = system_instruction::authorize_nonce_account(
nonce_account,
&current_authority.pubkey(),
new_authority,
);
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[authorize_ix],
Some(&payer.pubkey()),
&[payer, current_authority],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction(&transaction)?;
Ok(())
}
```
**Use cases:**
- Transfer control to PDA for program-managed nonces
- Rotate keys for security
- Transfer to multisig for DAO control
## Offline Signing Workflows
### Hardware Wallet Flow
**Setup (online device):**
```rust
// 1. Create nonce account (one-time)
create_nonce_account(&rpc_client, &payer, &nonce_account, &hw_wallet_pubkey)?;
// 2. Fetch nonce value
let nonce_value = get_nonce_value(&rpc_client, &nonce_account.pubkey())?;
// 3. Build unsigned transaction
let unsigned_tx = build_nonce_transaction(
&nonce_account.pubkey(),
&hw_wallet_keypair, // Will be replaced with actual signature
nonce_value,
vec![transfer_ix],
&payer,
);
// 4. Serialize for hardware wallet
let serialized = serialize_for_offline_signing(&unsigned_tx);
// 5. Transfer to hardware wallet (USB, QR code, etc.)
```
**Signing (air-gapped hardware wallet):**
```rust
// 1. Receive serialized transaction
let tx = deserialize_signed_transaction(&serialized);
// 2. Display to user for review (no time pressure!)
// User reviews: recipient, amount, etc.
// 3. Sign with hardware wallet private key
// (Hardware wallet handles this internally)
// 4. Export signed transaction
let signed_serialized = serialize_for_offline_signing(&signed_tx);
// 5. Transfer back to online device
```
**Submission (online device):**
```rust
// 1. Receive signed transaction
let signed_tx = deserialize_signed_transaction(&signed_serialized);
// 2. Submit to network (can be hours/days after signing!)
let signature = rpc_client.send_and_confirm_transaction(&signed_tx)?;
```
### Multi-Signature Coordination
**DAO Proposal Execution Flow:**
```rust
// 1. Proposer creates transaction with nonce
let nonce_value = get_nonce_value(&rpc_client, &dao_nonce_account)?;
let proposal_tx = build_nonce_transaction(
&dao_nonce_account,
&dao_authority, // PDA controlled by governance
nonce_value,
vec![execute_proposal_ix],
&proposer,
);
// 2. Serialize and store in DAO state
let tx_data = bincode::serialize(&proposal_tx)?;
// Store tx_data in proposal account
// 3. Members vote over time (hours/days)
// Each vote increments approval count
// 4. When threshold reached, anyone can execute
let stored_tx: Transaction = bincode::deserialize(&proposal.tx_data)?;
// 5. Submit (nonce ensures it's still valid!)
rpc_client.send_and_confirm_transaction(&stored_tx)?;
```
### CLI Multi-Sig Example
**First co-signer (offline):**
```bash
solana transfer \
--from sender.json \
--sign-only \
--nonce nonce-account.json \
--nonce-authority nonce-authority.json \
--blockhash <NONCE_VALUE> \
--fee-payer co-sender.json \
receiver.json 0.1
# Output:
# Pubkey=Signature
# 5nZ8nY5...=4SBv7Xp...
```
**Second co-signer (online, hours/days later):**
```bash
solana transfer \
--from sender.json \
--nonce nonce-account.json \
--nonce-authority nonce-authority.json \
--blockhash <NONCE_VALUE> \
--fee-payer sender.json \
--signer 5nZ8nY5...=4SBv7Xp... \
receiver.json 0.1
```
## Security Considerations
### The Neodyme Vulnerability (2020)
**Historic issue**: Before Solana v1.3, there was a critical vulnerability in how durable nonce transactions were processed:
**The bug:**
1. Transaction with durable nonce starts processing
2. Runtime advances nonce (changes state)
3. Later instruction in transaction fails
4. Runtime rolls back ALL state changes
5. **BUG**: Nonce advancement was rolled back too!
6. Attacker could replay the transaction
**The exploit:**
```rust
// Malicious transaction:
{
instructions: [
advance_nonce(...), // Advances nonce
write_arbitrary_data(...), // Attacker's payload
fail_intentionally(...), // Forces transaction to fail
]
}
// After rollback:
// - Nonce reverted to original value
// - Arbitrary data write WAS NOT rolled back
// - Can replay transaction infinitely!
```
**Impact**: Could write arbitrary data to any account by replaying failed transactions.
**Fix** (Solana v1.3+): Nonce advancement is now **permanent** even on transaction failure. The runtime explicitly handles nonce accounts separately from normal rollback logic.
**Lesson**: This demonstrates why nonce advancement MUST happen regardless of transaction success/failure.
### Best Practices
**1. Never reuse nonce without advancing**
```rust
// BAD: Reusing nonce value
let nonce = get_nonce_value(&rpc, &nonce_account)?;
let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix1], &payer);
let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix2], &payer);
// If tx1 fails, tx2 might also fail with "nonce mismatch"
// GOOD: Advance between uses
let nonce1 = get_nonce_value(&rpc, &nonce_account)?;
let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce1, vec![ix1], &payer);
rpc.send_and_confirm_transaction(&tx1)?;
// Fetch fresh nonce (it was advanced)
let nonce2 = get_nonce_value(&rpc, &nonce_account)?;
let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce2, vec![ix2], &payer);
```
**2. Protect nonce authority**
```rust
// Use cold storage for nonce authority
// OR use PDA with program logic to restrict usage
let authority_pda = Pubkey::find_program_address(
&[b"nonce_authority", dao.key().as_ref()],
program_id,
);
```
**3. Maintain rent exemption**
```rust
// Check before withdrawal
let nonce_account = rpc.get_account(&nonce_pubkey)?;
let rent = rpc.get_minimum_balance_for_rent_exemption(80)?;
if nonce_account.lamports - withdraw_amount < rent {
return Err("Would violate rent exemption".into());
}
```
**4. Verify nonce advancement in transaction**
```rust
// In your program that uses nonce transactions:
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// First account should be nonce account
let nonce_account = &accounts[0];
// Verify it's a valid nonce account
if nonce_account.owner != &system_program::id() {
return Err(ProgramError::InvalidAccountData);
}
// Verify advance_nonce was called
// (Runtime enforces this, but you can add checks)
Ok(())
}
```
**5. Monitor nonce account balance**
```rust
// Periodic check (e.g., daily job)
fn check_nonce_health(rpc: &RpcClient, nonce: &Pubkey) -> Result<(), String> {
let account = rpc.get_account(nonce)
.map_err(|_| "Nonce account not found")?;
let rent = rpc.get_minimum_balance_for_rent_exemption(80)
.map_err(|_| "Failed to fetch rent")?;
if account.lamports < rent {
return Err(format!(
"Nonce account below rent exemption: {} < {}",
account.lamports, rent
));
}
Ok(())
}
```
## Use Cases
### 1. Scheduled Payments (Vesting)
```rust
// Pre-sign monthly vesting releases
fn create_vesting_schedule(
rpc: &RpcClient,
nonce_account: &Pubkey,
nonce_authority: &Keypair,
recipient: &Pubkey,
amount_per_month: u64,
months: usize,
) -> Result<Vec<Transaction>, Box<dyn std::error::Error>> {
let mut transactions = Vec::new();
for month in 0..months {
// Fetch current nonce
let nonce = get_nonce_value(rpc, nonce_account)?;
// Create transfer
let transfer_ix = system_instruction::transfer(
&nonce_authority.pubkey(),
recipient,
amount_per_month,
);
// Build nonce transaction
let tx = build_nonce_transaction(
nonce_account,
nonce_authority,
nonce,
vec![transfer_ix],
nonce_authority,
);
transactions.push(tx);
// Advance nonce for next month's transaction
advance_nonce_manually(rpc, nonce_account, nonce_authority, nonce_authority)?;
}
Ok(transactions)
}
// Executor submits each month
fn execute_vesting_payment(
rpc: &RpcClient,
pre_signed_tx: &Transaction,
) -> Result<(), Box<dyn std::error::Error>> {
// No time pressure - can submit anytime!
rpc.send_and_confirm_transaction(pre_signed_tx)?;
Ok(())
}
```
### 2. Conditional Trades (Limit Orders)
```rust
// Pre-sign trade execution at specific price
fn create_limit_order(
nonce: &Pubkey,
authority: &Keypair,
swap_instruction: Instruction, // Execute when price reached
) -> Transaction {
let nonce_value = /* fetch nonce */;
build_nonce_transaction(
nonce,
authority,
nonce_value,
vec![swap_instruction],
authority,
)
}
// Bot monitors price and submits when condition met
fn execute_limit_order(rpc: &RpcClient, current_price: f64, limit_tx: &Transaction) {
if current_price >= target_price {
rpc.send_transaction(limit_tx).ok(); // Submit pre-signed transaction
}
}
```
### 3. Cross-Chain Bridges
```rust
// Sign Solana transaction while waiting for Ethereum finality
async fn bridge_from_ethereum_to_solana(
eth_tx_hash: H256,
solana_mint_ix: Instruction,
nonce_account: &Pubkey,
nonce_authority: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
// 1. Pre-sign Solana mint transaction
let nonce = get_nonce_value(&solana_rpc, nonce_account)?;
let mint_tx = build_nonce_transaction(
nonce_account,
nonce_authority,
nonce,
vec![solana_mint_ix],
nonce_authority,
);
// 2. Wait for Ethereum finality (12+ minutes)
wait_for_ethereum_finality(eth_tx_hash).await?;
// 3. Submit Solana transaction (still valid!)
solana_rpc.send_and_confirm_transaction(&mint_tx)?;
Ok(())
}
```
### 4. DAO Governance Execution
Already covered in multi-sig example above - proposals can be voted on over days/weeks, then executed with pre-signed transaction.
## CLI Reference
**Create nonce account:**
```bash
solana create-nonce-account <KEYPAIR_PATH> <AMOUNT>
```
**Get current nonce:**
```bash
solana nonce <NONCE_ACCOUNT>
```
**Manually advance nonce:**
```bash
solana new-nonce <NONCE_ACCOUNT>
```
**Get nonce account info:**
```bash
solana nonce-account <NONCE_ACCOUNT>
```
**Withdraw from nonce:**
```bash
solana withdraw-from-nonce-account <NONCE_ACCOUNT> <DESTINATION> <AMOUNT>
```
**Change nonce authority:**
```bash
solana authorize-nonce-account <NONCE_ACCOUNT> <NEW_AUTHORITY>
```
**Sign transaction offline:**
```bash
solana <COMMAND> \
--sign-only \
--nonce <NONCE_ACCOUNT> \
--nonce-authority <AUTHORITY_KEYPAIR> \
--blockhash <NONCE_VALUE>
```
**Submit pre-signed transaction:**
```bash
solana <COMMAND> \
--nonce <NONCE_ACCOUNT> \
--nonce-authority <AUTHORITY_KEYPAIR> \
--blockhash <NONCE_VALUE> \
--signer <PUBKEY=SIGNATURE>
```
## Limitations and Considerations
**Transaction size:**
- Adding `advance_nonce_account` instruction adds ~40 bytes
- May push transaction over size limit if already near maximum
**Extra signature requirement:**
- Nonce authority must sign (if different from fee payer)
- Increases transaction complexity
**Rent cost:**
- Each nonce account requires ~0.0015 SOL rent-exempt minimum
- For many scheduled transactions, can become expensive
**Nonce advancement overhead:**
- Compute units to advance nonce (~few hundred CU)
- Minimal but worth considering for CU-constrained transactions
**Cannot mix recent blockhashes and nonces:**
- Transaction uses either recent blockhash OR durable nonce
- Cannot use both in the same transaction
## Resources
### Official Documentation
- [Introduction to Durable Nonces](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces)
- [Durable Transaction Nonces Proposal](https://docs.anza.xyz/implemented-proposals/durable-tx-nonces)
- [CLI Nonce Examples](https://docs.anza.xyz/cli/examples/durable-nonce)
### Code Examples
- [Durable Nonces Repository](https://github.com/0xproflupin/solana-durable-nonces)
- [System Program Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/system_instruction.rs)
### Security Analysis
- [Neodyme: Nonce Upon a Time](https://neodyme.io/en/blog/nonce-upon-a-time/) - Historic vulnerability analysis
### Technical References
- [solana-sdk NonceState](https://docs.rs/solana-sdk/latest/solana_sdk/nonce/state/enum.State.html)
- [System Program Instructions](https://docs.rs/solana-sdk/latest/solana_sdk/system_instruction/)

View File

@@ -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<CustomError> for ProgramError`:
```rust
impl From<NoteError> for ProgramError {
fn from(e: NoteError) -> Self {
ProgramError::Custom(e as u32)
}
}
```
**How it works:**
1. Custom error is converted to `u32` (using `as u32` cast)
2. Wrapped in `ProgramError::Custom(u32)`
3. Returned to client as error code
**Error code mapping:**
```rust
NoteError::Forbidden ProgramError::Custom(0)
NoteError::InvalidLength ProgramError::Custom(1)
NoteError::InvalidRating ProgramError::Custom(2)
NoteError::EmptyTitle ProgramError::Custom(3)
NoteError::MaxNotesExceeded ProgramError::Custom(4)
```
### Using Custom Errors
```rust
pub fn create_note(
program_id: &Pubkey,
accounts: &[AccountInfo],
title: String,
content: String,
rating: u8,
) -> ProgramResult {
// Validation with custom errors
if title.is_empty() {
return Err(NoteError::EmptyTitle.into());
}
if content.len() > 1000 {
return Err(NoteError::InvalidLength.into());
}
if rating < 1 || rating > 5 {
return Err(NoteError::InvalidRating.into());
}
// Continue processing...
Ok(())
}
```
**The `.into()` method** automatically converts `NoteError` to `ProgramError`.
### Advanced Custom Error Types
**With additional context:**
```rust
#[derive(Error, Debug)]
pub enum GameError {
#[error("Insufficient mana: have {current}, need {required}")]
InsufficientMana { current: u32, required: u32 },
#[error("Invalid move: {0}")]
InvalidMove(String),
#[error("Player not found: {0}")]
PlayerNotFound(String),
}
```
**Note:** Errors with fields cannot derive `Copy`, only `Clone`.
---
## Error Propagation
### The `?` Operator
The `?` operator is Rust's error propagation mechanism:
```rust
pub fn complex_operation(
accounts: &[AccountInfo],
) -> ProgramResult {
// If validation fails, error is returned immediately
validate_accounts(accounts)?;
// If deserialization fails, error is propagated
let data = AccountData::try_from_slice(&accounts[0].data.borrow())?;
// If checked math fails, ArithmeticOverflow is returned
let result = data.value.checked_add(100)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}
```
**What `?` does:**
1. If `Result` is `Ok(value)`, unwraps to `value`
2. If `Result` is `Err(e)`, converts `e` and returns early
3. Conversion happens via `From` trait
### Error Conversion Chain
```rust
// Step 1: Borsh deserialization fails
let data = MyData::try_from_slice(bytes)?;
// Returns: Err(std::io::Error)
// Step 2: ? operator converts via From trait
// std::io::Error → ProgramError::BorshIoError
// Step 3: Custom error conversion
return Err(MyError::InvalidData.into());
// MyError → ProgramError::Custom(n)
```
### Manual Error Handling
```rust
// Without ?
pub fn manual_error_handling(
account: &AccountInfo,
) -> ProgramResult {
match validate_account(account) {
Ok(()) => {
// Continue processing
}
Err(e) => {
msg!("Validation failed: {:?}", e);
return Err(e);
}
}
Ok(())
}
// With ? (equivalent)
pub fn automatic_error_handling(
account: &AccountInfo,
) -> ProgramResult {
validate_account(account)?;
Ok(())
}
```
### Mapping Errors
Transform one error type to another:
```rust
pub fn map_errors(
account: &AccountInfo,
) -> ProgramResult {
// Map generic error to custom error
let data = AccountData::try_from_slice(&account.data.borrow())
.map_err(|_| NoteError::InvalidLength)?;
// Map to different ProgramError variant
let value = data.amount.checked_add(100)
.ok_or(ProgramError::ArithmeticOverflow)?;
Ok(())
}
```
### Combining Multiple Operations
```rust
pub fn chain_operations(
accounts: &[AccountInfo],
) -> ProgramResult {
// All operations must succeed or transaction fails
let account1 = validate_and_load_account(&accounts[0])?;
let account2 = validate_and_load_account(&accounts[1])?;
let combined = account1.value
.checked_add(account2.value)
.ok_or(ProgramError::ArithmeticOverflow)?;
update_account(&accounts[2], combined)?;
Ok(())
}
```
---
## Error Context and Logging
### Adding Context with `msg!`
Use `msg!` macro to log context before returning errors:
```rust
use solana_program::msg;
pub fn transfer_tokens(
from: &AccountInfo,
to: &AccountInfo,
amount: u64,
) -> ProgramResult {
if amount == 0 {
msg!("Transfer amount cannot be zero");
return Err(ProgramError::InvalidArgument);
}
let from_balance = get_balance(from)?;
if from_balance < amount {
msg!("Insufficient balance: have {}, need {}", from_balance, amount);
return Err(ProgramError::InsufficientFunds);
}
// Perform transfer...
Ok(())
}
```
### Logging Best Practices
**✅ Good logging:**
```rust
msg!("Invalid rating: got {}, expected 1-5", rating);
msg!("PDA derivation failed: expected {}, got {}", expected, actual);
msg!("Account {} not owned by program {}", account.key, program_id);
```
**❌ Poor logging:**
```rust
msg!("Error"); // Not helpful
msg!("Failed"); // What failed?
// (no logging) // Can't debug issues
```
### Conditional Logging
```rust
pub fn debug_operation(
account: &AccountInfo,
debug_mode: bool,
) -> ProgramResult {
if debug_mode {
msg!("Processing account: {}", account.key);
msg!("Owner: {}", account.owner);
msg!("Lamports: {}", account.lamports());
}
// Process...
Ok(())
}
```
### Error with Recovery
```rust
pub fn try_with_fallback(
accounts: &[AccountInfo],
) -> ProgramResult {
// Try primary method
match process_primary(accounts) {
Ok(()) => {
msg!("Primary method succeeded");
Ok(())
}
Err(e) => {
msg!("Primary method failed: {:?}, trying fallback", e);
// Try fallback
process_fallback(accounts).map_err(|fallback_err| {
msg!("Fallback also failed: {:?}", fallback_err);
fallback_err
})
}
}
}
```
---
## Client-Side Error Handling
### Error Code Interpretation
**Client receives:**
```json
{
"error": {
"InstructionError": [
0,
{
"Custom": 2
}
]
}
}
```
**Decoding:**
- Instruction index: `0` (first instruction)
- Error type: `Custom`
- Error code: `2`
### TypeScript Error Mapping
```typescript
// Define error codes matching Rust enum
enum NoteError {
Forbidden = 0,
InvalidLength = 1,
InvalidRating = 2,
EmptyTitle = 3,
MaxNotesExceeded = 4,
}
// Error messages
const NOTE_ERROR_MESSAGES = {
[NoteError.Forbidden]: "You do not own this note",
[NoteError.InvalidLength]: "Note text is too long",
[NoteError.InvalidRating]: "Rating must be between 1 and 5",
[NoteError.EmptyTitle]: "Note title cannot be empty",
[NoteError.MaxNotesExceeded]: "Maximum notes limit reached",
};
// Parse error
function parseNoteError(error: any): string {
if (error?.InstructionError) {
const [_, instructionError] = error.InstructionError;
if (instructionError?.Custom !== undefined) {
const errorCode = instructionError.Custom;
return NOTE_ERROR_MESSAGES[errorCode] || `Unknown error: ${errorCode}`;
}
}
return "Transaction failed";
}
// Usage
try {
await program.methods.createNote(title, content, rating).rpc();
} catch (error) {
const message = parseNoteError(error);
console.error(message);
}
```
### Anchor Error Handling
**With Anchor framework:**
```typescript
import { AnchorError } from "@coral-xyz/anchor";
try {
await program.methods.createNote(title, content, rating).rpc();
} catch (error) {
if (error instanceof AnchorError) {
console.error("Error code:", error.error.errorCode.code);
console.error("Error message:", error.error.errorMessage);
console.error("Error number:", error.error.errorCode.number);
}
}
```
---
## Best Practices
### 1. Fail Fast
**Return errors immediately when validation fails:**
```rust
// ✅ Good - fails fast
pub fn validate_input(rating: u8) -> ProgramResult {
if rating < 1 || rating > 5 {
return Err(NoteError::InvalidRating.into());
}
// Continue only if valid
Ok(())
}
// ❌ Bad - continues with invalid state
pub fn validate_input_bad(rating: u8) -> ProgramResult {
if rating >= 1 && rating <= 5 {
// Valid branch
}
// Continues regardless!
Ok(())
}
```
### 2. Meaningful Error Messages
```rust
// ✅ Good - specific and actionable
#[error("Username must be 3-20 characters, got {0}")]
InvalidUsernameLength(usize),
#[error("Insufficient mana: need {required}, have {current}")]
InsufficientMana { required: u32, current: u32 },
// ❌ Bad - vague
#[error("Invalid input")]
InvalidInput,
#[error("Error")]
GenericError,
```
### 3. Organize Errors by Category
```rust
#[derive(Error, Debug, Copy, Clone)]
pub enum GameError {
// Validation errors (0-99)
#[error("Invalid player name")]
InvalidPlayerName,
#[error("Invalid move")]
InvalidMove,
// State errors (100-199)
#[error("Game not started")]
GameNotStarted,
#[error("Game already finished")]
GameFinished,
// Resource errors (200-299)
#[error("Insufficient gold")]
InsufficientGold,
#[error("Inventory full")]
InventoryFull,
}
```
### 4. Consistent Error Handling Pattern
```rust
pub fn standard_operation_pattern(
program_id: &Pubkey,
accounts: &[AccountInfo],
params: Params,
) -> ProgramResult {
// 1. Parse accounts
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let data_account = next_account_info(account_info_iter)?;
// 2. Validate signers
if !user.is_signer {
msg!("User must sign the transaction");
return Err(ProgramError::MissingRequiredSignature);
}
// 3. Validate ownership
if data_account.owner != program_id {
msg!("Data account not owned by program");
return Err(ProgramError::IllegalOwner);
}
// 4. Validate input parameters
if params.amount == 0 {
msg!("Amount cannot be zero");
return Err(ProgramError::InvalidArgument);
}
// 5. Load and validate account data
let mut data = AccountData::try_from_slice(&data_account.data.borrow())?;
if !data.is_initialized {
msg!("Account not initialized");
return Err(ProgramError::UninitializedAccount);
}
// 6. Perform operation
// ...
Ok(())
}
```
### 5. Document Error Codes
```rust
/// Error codes for the Note program.
///
/// | Code | Error | Description |
/// |------|-------|-------------|
/// | 0 | Forbidden | Caller does not own the note |
/// | 1 | InvalidLength | Note text exceeds maximum length |
/// | 2 | InvalidRating | Rating not in range 1-5 |
/// | 3 | EmptyTitle | Note title is empty |
/// | 4 | MaxNotesExceeded | User has reached note limit |
#[derive(Error, Debug, Copy, Clone)]
#[repr(u32)]
pub enum NoteError {
#[error("You do not own this note")]
Forbidden = 0,
#[error("Note text is too long")]
InvalidLength = 1,
#[error("Rating must be between 1 and 5")]
InvalidRating = 2,
#[error("Note title cannot be empty")]
EmptyTitle = 3,
#[error("Maximum notes limit reached")]
MaxNotesExceeded = 4,
}
```
### 6. Error Testing
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_rating() {
let result = validate_rating(0);
assert_eq!(
result.unwrap_err(),
NoteError::InvalidRating.into()
);
let result = validate_rating(6);
assert_eq!(
result.unwrap_err(),
NoteError::InvalidRating.into()
);
}
#[test]
fn test_valid_rating() {
for rating in 1..=5 {
assert!(validate_rating(rating).is_ok());
}
}
}
```
### 7. Avoid Silent Failures
```rust
// ❌ Bad - errors ignored
pub fn bad_error_handling(accounts: &[AccountInfo]) -> ProgramResult {
let _ = validate_accounts(accounts); // Ignores error!
if let Ok(data) = load_data(accounts) {
process(data); // What if load_data failed?
}
Ok(()) // Returns success even if operations failed!
}
// ✅ Good - errors propagated
pub fn good_error_handling(accounts: &[AccountInfo]) -> ProgramResult {
validate_accounts(accounts)?;
let data = load_data(accounts)?;
process(data)?;
Ok(())
}
```
---
## Summary
**Key Takeaways:**
1. **Always return `ProgramResult`** from instruction handlers
2. **Use custom errors** for specific failure modes
3. **Implement `From` trait** to convert custom errors to `ProgramError`
4. **Use `?` operator** for clean error propagation
5. **Add context with `msg!`** for better debugging
6. **Fail fast** - return errors immediately
7. **Document error codes** for client developers
8. **Test error cases** as thoroughly as success cases
**Error Handling Pattern:**
```rust
use solana_program::{
entrypoint::ProgramResult,
program_error::ProgramError,
msg,
};
use thiserror::Error;
// 1. Define custom errors
#[derive(Error, Debug, Copy, Clone)]
pub enum MyError {
#[error("Descriptive error message")]
SpecificError,
}
// 2. Implement From conversion
impl From<MyError> for ProgramError {
fn from(e: MyError) -> Self {
ProgramError::Custom(e as u32)
}
}
// 3. Use in program
pub fn my_instruction(accounts: &[AccountInfo]) -> ProgramResult {
// Validate
if invalid_condition {
msg!("Detailed error context");
return Err(MyError::SpecificError.into());
}
// Propagate errors with ?
let data = load_data(accounts)?;
Ok(())
}
```
**Remember:** Good error handling is not optional—it's essential for security, debugging, and user experience.

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -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 <PROGRAM_ID> \
https://github.com/your-org/your-repo \
--library-name my_program
# Or specify exact commit
solana-verify verify-from-repo \
-u mainnet-beta \
--program-id <PROGRAM_ID> \
https://github.com/your-org/your-repo \
--commit-hash <COMMIT_HASH> \
--library-name my_program
```
**When prompted, upload verification data on-chain:**
```
Would you like to upload verification data on-chain? (y/n)
```
Select **yes** to enable:
- Verification badge on Solana Explorer
- OtterSec verification API listing
- SolanaFM verification display
### Step 4: Verify Hash Match (Sanity Check)
Before step 3, you can manually verify hashes match:
```bash
# Get on-chain program hash
solana-verify get-program-hash -u devnet <PROGRAM_ID>
# Get local executable hash
solana-verify get-executable-hash target/deploy/my_program.so
# These MUST match exactly
```
---
## Complete Production Deployment Checklist
### Pre-Deployment
- [ ] All tests pass (`cargo test`, `anchor test`)
- [ ] Security audit completed (for mainnet)
- [ ] `Cargo.lock` committed to git
- [ ] Git tag created for release (e.g., `v1.0.0`)
- [ ] Sufficient SOL in deployer wallet
- [ ] Multisig or governance ready (mainnet)
### Build
- [ ] Run `solana-verify build --library-name my_program`
- [ ] Verify `.so` file exists in `target/deploy/`
- [ ] Do NOT run `anchor build` after this
- [ ] Get hash: `solana-verify get-executable-hash target/deploy/my_program.so`
### Deploy
- [ ] Use `solana program deploy` (NOT `anchor deploy`)
- [ ] Specify correct program ID keypair
- [ ] Use appropriate priority fees
- [ ] Verify deployment: `solana program show <PROGRAM_ID>`
### Verify
- [ ] Run `solana-verify verify-from-repo` with your GitHub URL
- [ ] Upload verification data on-chain when prompted
- [ ] Check verification appears on explorer
- [ ] Optional: Submit remote verification job
### Post-Deployment
- [ ] Transfer upgrade authority to multisig (mainnet)
- [ ] Smoke test critical instructions on-chain
- [ ] Set up monitoring
- [ ] Announce deployment with verification link
---
## Example: Complete Mainnet Deployment
```bash
# 1. Prepare
git tag v1.0.0
git push origin v1.0.0
# 2. Build verifiably
solana-verify build --library-name cascade_splits
# 3. Check hash
solana-verify get-executable-hash target/deploy/cascade_splits.so
# Output: abc123def456...
# 4. Deploy to mainnet
solana program deploy target/deploy/cascade_splits.so \
--program-id target/deploy/cascade_splits-keypair.json \
--keypair ~/.config/solana/mainnet-deployer.json \
-u mainnet-beta \
--with-compute-unit-price 100000 \
--max-sign-attempts 100 \
--use-rpc
# Output: Program Id: SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB
# 5. Verify on-chain hash matches
solana-verify get-program-hash -u mainnet-beta SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB
# Output: abc123def456... (must match step 3!)
# 6. Verify against repository
solana-verify verify-from-repo \
-u mainnet-beta \
--program-id SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB \
https://github.com/cascade-protocol/splits \
--commit-hash v1.0.0 \
--library-name cascade_splits
# When prompted: Upload verification data on-chain? → YES
# 7. Transfer authority to multisig
SQUADS_VAULT="YourSquadsVaultAddress"
solana program set-upgrade-authority SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB \
--new-upgrade-authority $SQUADS_VAULT
# 8. Verify on explorer
# Visit: https://explorer.solana.com/address/SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB
# Should show verification badge
# 9. Check OtterSec verification
# Visit: https://verify.osec.io/status/SPL1T3rERcu6P6dyBiG7K8LUr21CssZqDAszwANzNMB
```
---
## Program Upgrades with Verified Builds
### Upgrade Workflow
```bash
# 1. Make changes, test, commit
git add .
git commit -m "feat: add new feature"
git tag v1.1.0
git push origin main v1.1.0
# 2. Build verifiably
solana-verify build --library-name my_program
# 3. Check if program size increased
OLD_SIZE=$(solana program show <PROGRAM_ID> | grep "Data Length" | awk '{print $3}')
NEW_SIZE=$(wc -c < target/deploy/my_program.so)
# 4. Extend if needed
if [ $NEW_SIZE -gt $OLD_SIZE ]; then
DIFF=$((NEW_SIZE - OLD_SIZE))
solana program extend <PROGRAM_ID> $DIFF
fi
# 5. Deploy upgrade
solana program deploy target/deploy/my_program.so \
--program-id <PROGRAM_ID> \
--upgrade-authority ~/.config/solana/deployer.json \
-u mainnet-beta \
--with-compute-unit-price 100000
# 6. Verify new version
solana-verify verify-from-repo \
-u mainnet-beta \
--program-id <PROGRAM_ID> \
https://github.com/your-org/your-repo \
--commit-hash v1.1.0 \
--library-name my_program
```
### Upgrades via Multisig
If upgrade authority is a Squads multisig:
```bash
# 1. Build verifiably
solana-verify build --library-name my_program
# 2. Create buffer (not direct upgrade)
solana program write-buffer target/deploy/my_program.so
# Output: Buffer: <BUFFER_ADDRESS>
# 3. Transfer buffer to multisig
solana program set-buffer-authority <BUFFER_ADDRESS> \
--new-buffer-authority <SQUADS_VAULT>
# 4. Create upgrade proposal in Squads UI
# - Navigate to https://v4.squads.so/
# - Create transaction for BPF Upgradeable Loader upgrade
# - Reference buffer address
# - Get approval from multisig members
# - Execute
# 5. After execution, verify
solana-verify verify-from-repo \
-u mainnet-beta \
--program-id <PROGRAM_ID> \
https://github.com/your-org/your-repo \
--commit-hash v1.1.0 \
--library-name my_program
```
---
## Troubleshooting
### Hash Mismatch After Deployment
**Problem:** On-chain hash doesn't match local hash
**Causes:**
1. Ran `anchor build` or `cargo build-sbf` after `solana-verify build`
2. Deployed wrong file
3. `Cargo.lock` not committed or out of sync
**Solution:**
```bash
# 1. Clean everything
cargo clean
# 2. Ensure Cargo.lock is committed
git add Cargo.lock
git commit -m "Add Cargo.lock"
# 3. Rebuild verifiably
solana-verify build --library-name my_program
# 4. Redeploy
solana program deploy target/deploy/my_program.so \
--program-id <PROGRAM_ID>
# 5. Verify again
solana-verify verify-from-repo ...
```
### Verification Fails: "Could not build from repository"
**Problem:** `solana-verify verify-from-repo` cannot build
**Causes:**
1. Missing `Cargo.lock` in repository
2. Wrong commit hash
3. Workspace configuration issue
4. Missing dependencies in Docker build
**Solution:**
```bash
# 1. Verify Cargo.lock exists in git
git ls-files | grep Cargo.lock
# 2. Check commit hash is correct
git log --oneline
# 3. Ensure workspace Cargo.toml exists at root
cat Cargo.toml # Should have [workspace]
# 4. Try local verification first
solana-verify verify-from-repo \
--program-id <PROGRAM_ID> \
file://$(pwd) \
--library-name my_program
```
### "anchor deploy" Used by Accident
**Problem:** Deployed with `anchor deploy` instead of verified build
**Solution:** Redeploy properly:
```bash
# 1. Build verifiably
solana-verify build --library-name my_program
# 2. Redeploy (upgrade) with verified binary
solana program deploy target/deploy/my_program.so \
--program-id <PROGRAM_ID>
# 3. Verify
solana-verify verify-from-repo \
-u <NETWORK> \
--program-id <PROGRAM_ID> \
https://github.com/your-org/your-repo
```
---
## Version-Specific Notes
### Anchor 0.32.1
- **Status:** Current stable version as of November 2024
- **Issue:** `anchor deploy` does not produce verifiable builds
- **Workaround:** Use workflow in this guide (solana-verify + solana program deploy)
- **Uses:** Solana SDK 2.2.x
### Anchor 0.30.x
- **Status:** Older stable version
- **Issue:** Same as 0.32.1
- **Workaround:** Same workflow applies
- **Uses:** Solana SDK 2.1.x
### Future: Anchor 1.0.0
- **Expected:** Better integration with verified builds
- **Possible:** `anchor deploy --verifiable` flag
- **Check:** Official Anchor docs when v1 releases
- **Until then:** Use this guide
---
## Best Practices Summary
### Always ✅
- Use `solana-verify build` for production builds
- Commit `Cargo.lock` to git
- Tag releases with git tags
- Deploy with `solana program deploy` directly
- Verify against repository after deployment
- Upload verification data on-chain
- Transfer mainnet authority to multisig
- Test entire flow on devnet first
### Never ❌
- Use `anchor deploy` for production/mainnet
- Run `anchor build` or `cargo build-sbf` after `solana-verify build`
- Deploy without verifying
- Deploy mainnet without devnet testing first
- Deploy mainnet without security audit
- Keep upgrade authority as individual wallet (mainnet)
- Skip uploading verification data
### Development Only
`anchor deploy` is fine for:
- Local validator testing
- Rapid iteration during development
- Devnet experiments
- Non-production testing
---
## Additional Resources
- **Solana Verify CLI**: https://github.com/Ellipsis-Labs/solana-verifiable-build
- **Verified Programs List**: https://verify.osec.io/verified-programs
- **Solana Explorer**: https://explorer.solana.com
- **Squads Protocol**: https://squads.so
- **Anchor Documentation**: https://www.anchor-lang.com/docs
---
## Summary
**For production Solana program deployments:**
1. Use `solana-verify build` (NOT `anchor deploy`)
2. Deploy with `solana program deploy` directly
3. Verify with `solana-verify verify-from-repo`
4. Upload verification data on-chain
This ensures transparency, verifiability, and trust in your deployed programs.

View File

@@ -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.

View File

@@ -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<Deposit>, amount: u64) -> Result<()> {
ctx.accounts.vault.balance = ctx.accounts.vault.balance + amount;
Ok(())
}
// If vault.balance = u64::MAX - 100 and amount = 200
// Result wraps to 99, losing 18.4 quintillion tokens!
```
**Fix:**
```rust
// ✅ SECURE: Checked arithmetic
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
ctx.accounts.vault.balance = ctx.accounts.vault.balance
.checked_add(amount)
.ok_or(ErrorCode::Overflow)?;
Ok(())
}
```
### Precision Loss
**Multiply before divide:**
```rust
// ❌ WRONG: Loses precision
let fee = amount / 100; // 1.5% becomes 1%
// ✅ CORRECT: Multiply first
let fee = amount
.checked_mul(15)
.and_then(|v| v.checked_div(1000))
.ok_or(ErrorCode::Overflow)?; // Exact 1.5%
```
---
## PDA Security
### Use Canonical Bumps
**Always find the canonical bump:**
```rust
// ✅ Find canonical bump
let (pda, bump) = Pubkey::find_program_address(
&[b"vault", user.key.as_ref()],
program_id,
);
// Store bump in account for later use
vault.bump = bump;
```
**Never hardcode or accept bumps from clients:**
```rust
// ❌ VULNERABLE: Accepts any bump
#[derive(Accounts)]
pub struct BadPDA<'info> {
#[account(seeds = [b"vault"], bump = user_provided_bump)]
pub vault: Account<'info, Vault>,
}
```
### Unique Seeds
Ensure seeds create unique PDAs:
```rust
// ✅ GOOD: Unique per user
seeds = [b"vault", user.key().as_ref()]
// ❌ BAD: Same PDA for everyone
seeds = [b"vault"]
```
---
## CPI Security
### Validate Target Programs
**Never accept arbitrary program IDs:**
```rust
// ❌ VULNERABLE
pub fn bad_cpi(ctx: Context<BadCPI>) -> Result<()> {
// Attacker can pass any program!
let cpi_ctx = CpiContext::new(
ctx.accounts.any_program.to_account_info(),
accounts,
);
// ... make CPI
}
// ✅ SECURE
#[derive(Accounts)]
pub struct SecureCPI<'info> {
pub token_program: Program<'info, Token>, // Type-checked!
}
```
### Reload Accounts After CPIs
If a CPI might modify an account you're using:
```rust
// ✅ Reload account after external call
let balance_before = token_account.amount;
// Make CPI that might change the account
token::transfer(cpi_ctx, amount)?;
// Reload to get fresh data
token_account.reload()?;
let balance_after = token_account.amount;
```
---
## Common Pitfalls
### 1. init_if_needed (Anchor)
**Dangerous pattern:**
```rust
// ❌ Can be exploited
#[account(init_if_needed, payer = user, space = 8 + 32)]
pub config: Account<'info, Config>,
```
**Problem:** Attacker creates the account first with malicious data.
**Fix:**
```rust
// ✅ Use init or check if exists
#[account(init, payer = user, space = 8 + 32)]
pub config: Account<'info, Config>,
// Or explicitly check
if config.is_initialized {
return Err(ErrorCode::AlreadyInitialized.into());
}
```
### 2. Missing Signer Checks
```rust
// ❌ Anyone can withdraw!
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
ctx.accounts.vault.balance -= amount;
Ok(())
}
// ✅ Authority must sign
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>, // Required!
}
```
### 3. Account Confusion
```rust
// ❌ No validation - any accounts work!
pub struct Transfer<'info> {
pub from: Account<'info, TokenAccount>,
pub to: Account<'info, TokenAccount>,
}
// ✅ Validate relationships
pub struct Transfer<'info> {
#[account(
mut,
constraint = from.owner == authority.key(),
constraint = from.mint == to.mint,
)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}
```
### 4. Unchecked Account Types
```rust
// ❌ Uses raw AccountInfo - no type safety
pub fn bad(ctx: Context<Bad>) -> Result<()> {
let data = ctx.accounts.account.try_borrow_data()?;
// What if attacker passes wrong account type?
}
// ✅ Use typed Account
pub fn good(ctx: Context<Good>) -> Result<()> {
// Anchor verifies discriminator automatically
let vault = &ctx.accounts.vault;
}
```
---
## Pre-Deployment Checklist
Before deploying to mainnet:
### Code Review
- [ ] All accounts validated (signer, owner, writable)
- [ ] All arithmetic uses `checked_*` methods
- [ ] All PDAs use canonical bumps
- [ ] All CPIs validate target programs
- [ ] No `unwrap()` or `expect()` in production code
- [ ] No `init_if_needed` without additional checks
- [ ] All error cases handled gracefully
### Testing
- [ ] Unit tests cover all instructions
- [ ] Integration tests cover instruction interactions
- [ ] Edge cases tested (zero amounts, max values, overflow)
- [ ] Error conditions tested (invalid accounts, unauthorized access)
- [ ] Fuzz testing with Trident (if possible)
### Security Audit
- [ ] Internal code review completed
- [ ] External security audit (recommended for >$100k TVL)
- [ ] Use `solana-security` skill for systematic review
- [ ] All critical/high severity findings resolved
- [ ] Medium findings assessed and documented
### Documentation
- [ ] Account structures documented
- [ ] Instruction requirements documented
- [ ] Known limitations documented
- [ ] Upgrade strategy documented
- [ ] Emergency procedures documented
### Deployment
- [ ] Tested on devnet extensively
- [ ] Tested on mainnet-beta with small amounts
- [ ] Upgrade authority secured (multisig recommended)
- [ ] Monitoring and alerts configured
- [ ] Emergency pause mechanism (if applicable)
---
## When to Use the Security Skill
Use the **`solana-security` skill** for:
- 🔍 **Comprehensive security audits** - Systematic review of entire codebase
- 🐛 **Vulnerability analysis** - Identifying exploit scenarios
- 📋 **Security checklists** - Category-by-category validation
- ⚠️ **Attack vectors** - Understanding how programs can be exploited
- 🛡️ **Framework-specific patterns** - Anchor vs native Rust security
- 📚 **Vulnerability databases** - Learning from past exploits
Use **this skill (solana-development)** for:
- 💻 **Building programs** - Implementation guidance
-**Defensive programming** - Secure coding patterns
- 🏗️ **Development workflows** - Testing, deployment, optimization
- 📖 **Framework learning** - Anchor and native Rust how-tos
---
## Quick Security Reference
### Anchor Security Checklist
```rust
#[derive(Accounts)]
pub struct Secure<'info> {
// ✅ Signer
pub authority: Signer<'info>,
// ✅ Validation + relationships
#[account(
mut,
has_one = authority,
seeds = [b"vault", user.key().as_ref()],
bump,
)]
pub vault: Account<'info, Vault>,
// ✅ Program validation
pub token_program: Program<'info, Token>,
}
pub fn secure_fn(ctx: Context<Secure>, amount: u64) -> Result<()> {
// ✅ Checked arithmetic
ctx.accounts.vault.balance = ctx.accounts.vault.balance
.checked_add(amount)
.ok_or(ErrorCode::Overflow)?;
Ok(())
}
```
### Native Rust Security Checklist
```rust
pub fn secure_fn(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let accounts = &mut accounts.iter();
let authority = next_account_info(accounts)?;
let vault = next_account_info(accounts)?;
// ✅ Signer check
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// ✅ Owner check
if vault.owner != program_id {
return Err(ProgramError::IllegalOwner);
}
// ✅ PDA validation
let (expected_pda, _) = Pubkey::find_program_address(
&[b"vault", authority.key.as_ref()],
program_id,
);
if *vault.key != expected_pda {
return Err(ProgramError::InvalidSeeds);
}
// ✅ Deserialize
let mut vault_data = Vault::try_from_slice(&vault.data.borrow())?;
// ✅ Checked arithmetic
vault_data.balance = vault_data.balance
.checked_add(amount)
.ok_or(ProgramError::ArithmeticOverflow)?;
// ✅ Serialize back
vault_data.serialize(&mut &mut vault.data.borrow_mut()[..])?;
Ok(())
}
```
---
## Remember
**Security is not optional.** Every line of code is a potential vulnerability. Validate everything, trust nothing, and when in doubt, use the `solana-security` skill for a comprehensive audit.

View File

@@ -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<u8>
let bytes = account_data.try_to_vec()?;
// Serialize to existing buffer
let mut buffer = vec![0u8; 100];
account_data.serialize(&mut buffer.as_mut_slice())?;
```
### Deserialization
**From bytes:**
```rust
// Deserialize from slice
let account_data = UserAccount::try_from_slice(&bytes)?;
// Deserialize with BorshDeserialize
let mut cursor = &bytes[..];
let account_data = UserAccount::deserialize(&mut cursor)?;
```
---
## Account Data Layout Design
### Basic Structure
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountData {
// 1. Discriminator / Type Field (1 byte)
pub account_type: u8,
// 2. Flags / State (1 byte)
pub is_initialized: bool,
// 3. Fixed-size fields (predictable layout)
pub owner: Pubkey, // 32 bytes
pub created_at: i64, // 8 bytes
pub counter: u64, // 8 bytes
// 4. Variable-size fields (at end)
pub name: String, // 4 + length
pub metadata: Vec<u8>, // 4 + length
}
```
**Size calculation:**
```
1 (type) + 1 (flag) + 32 (pubkey) + 8 (i64) + 8 (u64) + 4 (string len) + N (string) + 4 (vec len) + M (vec)
= 58 + N + M bytes
```
### Size Calculation Helper
```rust
impl AccountData {
pub const FIXED_SIZE: usize = 58; // All fixed fields
pub fn calculate_size(name_len: usize, metadata_len: usize) -> usize {
Self::FIXED_SIZE + name_len + metadata_len
}
pub fn max_size(max_name: usize, max_metadata: usize) -> usize {
Self::calculate_size(max_name, max_metadata)
}
}
// Usage
let account_size = AccountData::max_size(32, 256); // 346 bytes
```
### Fixed-Size Accounts
**Best for performance:**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct FixedAccount {
pub is_initialized: bool,
pub owner: Pubkey,
pub balance: u64,
pub last_updated: i64,
// Fixed-size array instead of Vec
pub data: [u8; 256],
}
impl FixedAccount {
pub const SIZE: usize = 1 + 32 + 8 + 8 + 256; // 305 bytes
}
```
---
## Serialization Patterns
### Pattern 1: try_from_slice (Recommended)
**Most common pattern for account deserialization:**
```rust
use borsh::BorshDeserialize;
pub fn load_account_data(
account_info: &AccountInfo,
) -> Result<UserAccount, ProgramError> {
let data = UserAccount::try_from_slice(&account_info.data.borrow())?;
Ok(data)
}
```
**Error handling:**
```rust
let data = UserAccount::try_from_slice(&account_info.data.borrow())
.map_err(|e| {
msg!("Failed to deserialize account: {}", e);
ProgramError::InvalidAccountData
})?;
```
### Pattern 2: Unchecked Deserialization
**Use when you've already validated the account:**
```rust
use borsh::try_from_slice_unchecked;
// After validation checks
let mut data = try_from_slice_unchecked::<UserAccount>(&account_info.data.borrow())
.unwrap(); // Safe because we validated
```
**⚠️ Warning:** Only use after thorough validation. Skips some safety checks.
### Pattern 3: Partial Deserialization
**Read only what you need:**
```rust
#[derive(BorshDeserialize)]
pub struct AccountHeader {
pub account_type: u8,
pub is_initialized: bool,
pub owner: Pubkey,
}
// Deserialize just the header
let header = AccountHeader::try_from_slice(&account_info.data.borrow()[..42])?;
if !header.is_initialized {
return Err(ProgramError::UninitializedAccount);
}
```
### Pattern 4: In-Place Modification
**Efficient for large accounts:**
```rust
pub fn update_balance(
account_info: &AccountInfo,
new_balance: u64,
) -> ProgramResult {
let mut data = account_info.data.borrow_mut();
// Deserialize
let mut account = UserAccount::try_from_slice(&data)?;
// Modify
account.balance = new_balance;
account.last_updated = Clock::get()?.unix_timestamp;
// Serialize back
account.serialize(&mut &mut data[..])?;
Ok(())
}
```
### Pattern 5: Bulk Operations
**Processing multiple accounts:**
```rust
pub fn process_accounts(
accounts: &[AccountInfo],
) -> ProgramResult {
let account_data: Vec<UserAccount> = accounts
.iter()
.map(|acc| UserAccount::try_from_slice(&acc.data.borrow()))
.collect::<Result<Vec<_>, _>>()?;
// Process all accounts
for (i, data) in account_data.iter().enumerate() {
msg!("Account {}: balance = {}", i, data.balance);
}
Ok(())
}
```
---
## Zero-Copy Deserialization
### When to Use Zero-Copy
**Benefits:**
- Avoids memory allocation
- Reduces compute units (50%+ savings for large structs)
- Direct access to account data
**Use when:**
- Account data is large (> 100 bytes)
- Frequent reads
- Performance-critical paths
### Bytemuck Pattern
```toml
[dependencies]
bytemuck = { version = "1.14", features = ["derive"] }
```
```rust
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct ZeroCopyAccount {
pub is_initialized: u8, // bool as u8
pub owner: [u8; 32], // Pubkey as bytes
pub balance: u64,
pub counter: u64,
}
impl ZeroCopyAccount {
pub const SIZE: usize = std::mem::size_of::<Self>();
pub fn from_account_info(account_info: &AccountInfo) -> Result<&Self, ProgramError> {
let data = account_info.data.borrow();
bytemuck::try_from_bytes(&data)
.map_err(|_| ProgramError::InvalidAccountData)
}
pub fn from_account_info_mut(
account_info: &AccountInfo,
) -> Result<&mut Self, ProgramError> {
let data = account_info.data.borrow_mut();
bytemuck::try_from_bytes_mut(&mut data)
.map_err(|_| ProgramError::InvalidAccountData)
}
}
// Usage
let account = ZeroCopyAccount::from_account_info(account_info)?;
msg!("Balance: {}", account.balance);
// Mutable access
let account = ZeroCopyAccount::from_account_info_mut(account_info)?;
account.balance += 100;
```
**⚠️ Limitations:**
- Only works with types that are `Pod` (Plain Old Data)
- No `String`, `Vec`, or other heap-allocated types
- Must be `#[repr(C)]` for stable layout
---
## Data Versioning
### Pattern 1: Version Field
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VersionedAccount {
pub version: u8,
pub data: AccountDataEnum,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub enum AccountDataEnum {
V1(AccountDataV1),
V2(AccountDataV2),
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountDataV1 {
pub balance: u64,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct AccountDataV2 {
pub balance: u64,
pub last_updated: i64, // New field
}
// Deserialization with version handling
pub fn load_versioned_account(
account_info: &AccountInfo,
) -> ProgramResult {
let versioned = VersionedAccount::try_from_slice(&account_info.data.borrow())?;
match versioned.data {
AccountDataEnum::V1(data_v1) => {
msg!("V1 account: balance = {}", data_v1.balance);
}
AccountDataEnum::V2(data_v2) => {
msg!("V2 account: balance = {}, updated = {}",
data_v2.balance, data_v2.last_updated);
}
}
Ok(())
}
```
### Pattern 2: Optional Fields
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Account {
pub balance: u64,
// V2: Added optional field
pub metadata: Option<Metadata>,
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Metadata {
pub name: String,
pub url: String,
}
// Old accounts: metadata = None
// New accounts: metadata = Some(Metadata { ... })
```
### Pattern 3: Migration Function
```rust
pub fn migrate_account_v1_to_v2(
account_info: &AccountInfo,
) -> ProgramResult {
// Load V1
let data_v1 = AccountDataV1::try_from_slice(&account_info.data.borrow())?;
// Convert to V2
let data_v2 = AccountDataV2 {
balance: data_v1.balance,
last_updated: Clock::get()?.unix_timestamp,
};
// Reallocate if needed
let new_size = data_v2.try_to_vec()?.len();
account_info.realloc(new_size, false)?;
// Serialize V2
data_v2.serialize(&mut &mut account_info.data.borrow_mut()[..])?;
Ok(())
}
```
---
## Performance Considerations
### Compute Unit Costs
**Serialization costs (approximate):**
| Operation | CU Cost |
|-----------|---------|
| Serialize small struct (< 100 bytes) | ~500 CU |
| Serialize large struct (> 1KB) | ~2,000 CU |
| Deserialize small struct | ~800 CU |
| Deserialize large struct | ~3,000 CU |
| Zero-copy access | ~100 CU |
### Optimization Tips
**1. Minimize serialization frequency:**
```rust
// ❌ Wasteful - serializes twice
let mut data = load_data(account)?;
data.field1 = value1;
save_data(account, &data)?;
data.field2 = value2;
save_data(account, &data)?; // Serialize again!
// ✅ Efficient - serialize once
let mut data = load_data(account)?;
data.field1 = value1;
data.field2 = value2;
save_data(account, &data)?;
```
**2. Use fixed-size fields:**
```rust
// ❌ Variable size - more expensive
pub struct Account {
pub name: String, // 4 + N bytes
}
// ✅ Fixed size - cheaper
pub struct Account {
pub name: [u8; 32], // Exactly 32 bytes
}
```
**3. Order fields by size:**
```rust
// ✅ Optimized layout (largest first)
#[derive(BorshSerialize, BorshDeserialize)]
#[repr(C)]
pub struct OptimizedAccount {
pub pubkey1: Pubkey, // 32 bytes
pub pubkey2: Pubkey, // 32 bytes
pub amount: u64, // 8 bytes
pub timestamp: i64, // 8 bytes
pub flags: u8, // 1 byte
}
```
---
## Common Pitfalls
### 1. Buffer Too Small
```rust
// ❌ Error: buffer too small
let mut buffer = vec![0u8; 10];
large_struct.serialize(&mut buffer.as_mut_slice())?; // Fails!
// ✅ Correct: proper size
let size = large_struct.try_to_vec()?.len();
let mut buffer = vec![0u8; size];
large_struct.serialize(&mut buffer.as_mut_slice())?;
```
### 2. Forgetting to Borrow
```rust
// ❌ Error: data moved
let data = account_info.data;
UserAccount::try_from_slice(&data)?; // Fails!
// ✅ Correct: borrow data
let data = account_info.data.borrow();
UserAccount::try_from_slice(&data)?;
```
### 3. Mismatched Schema
```rust
// Account created with V1
#[derive(BorshSerialize)]
pub struct AccountV1 {
pub balance: u64,
}
// Later, trying to deserialize as V2
#[derive(BorshDeserialize)]
pub struct AccountV2 {
pub balance: u64,
pub timestamp: i64, // New field!
}
// ❌ Fails: not enough bytes
let data = AccountV2::try_from_slice(&bytes)?; // Error!
```
**Solution:** Use versioning or optional fields.
### 4. String/Vec Limits
```rust
// ❌ No validation
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Account {
pub name: String, // Could be 10MB!
}
// ✅ Validate before deserializing
pub fn validate_name(name: &str) -> ProgramResult {
if name.len() > 32 {
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
```
### 5. Incorrect Size Calculation
```rust
// ❌ Wrong: ignores vector length prefix
let size = my_vec.len();
// ✅ Correct: includes 4-byte length prefix
let size = 4 + my_vec.len();
```
---
## Summary
**Key Takeaways:**
1. **Use Borsh** for all Solana program serialization
2. **Design fixed-size layouts** when possible for predictability
3. **Validate before deserializing** to prevent errors
4. **Use zero-copy** for large, frequently-accessed data
5. **Plan for versioning** from the start
6. **Minimize serialization frequency** to save compute units
**Common Patterns:**
```rust
// Deserialize
let data = AccountData::try_from_slice(&account_info.data.borrow())?;
// Modify
let mut data = data;
data.field = new_value;
// Serialize
data.serialize(&mut &mut account_info.data.borrow_mut()[..])?;
```
**Size Calculation:**
```rust
// Fixed fields
const FIXED_SIZE: usize = 1 + 32 + 8;
// Variable fields
let total_size = FIXED_SIZE + 4 + string.len() + 4 + vec.len();
```
Proper serialization patterns are fundamental to efficient and correct Solana programs. Master Borsh for production-ready data handling.

View File

@@ -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<u8>,
}
pub fn create_event(
event_account: &AccountInfo,
data: Vec<u8>,
) -> ProgramResult {
let clock = Clock::get()?;
let event = Event {
created_at: clock.unix_timestamp,
data,
};
event.serialize(&mut &mut event_account.data.borrow_mut()[..])?;
Ok(())
}
```
**2. Time-based logic (vesting, expiration):**
```rust
pub fn check_vesting(
vesting_account: &AccountInfo,
) -> ProgramResult {
let clock = Clock::get()?;
let vesting = VestingSchedule::try_from_slice(&vesting_account.data.borrow())?;
if clock.unix_timestamp < vesting.unlock_timestamp {
msg!("Tokens still locked until {}", vesting.unlock_timestamp);
return Err(ProgramError::Custom(1)); // Locked
}
msg!("Vesting unlocked!");
Ok(())
}
```
**3. Slot-based mechanics:**
```rust
pub fn process_epoch_transition(
state_account: &AccountInfo,
) -> ProgramResult {
let clock = Clock::get()?;
let mut state = State::try_from_slice(&state_account.data.borrow())?;
if clock.epoch > state.last_processed_epoch {
msg!("Processing epoch transition: {} -> {}",
state.last_processed_epoch, clock.epoch);
// Process epoch rewards, resets, etc.
state.last_processed_epoch = clock.epoch;
state.serialize(&mut &mut state_account.data.borrow_mut()[..])?;
}
Ok(())
}
```
### Clock Gotchas
**⚠️ unix_timestamp is approximate:**
```rust
// ❌ Don't use for precise timing
if clock.unix_timestamp == expected_timestamp { // Risky!
// Might miss by seconds
}
// ✅ Use ranges for time checks
if clock.unix_timestamp >= unlock_time {
// Safe
}
```
**⚠️ Timestamps can vary across validators:**
The `unix_timestamp` is based on validator voting and may differ slightly between validators in the same slot. Don't assume exact precision.
---
## Rent Sysvar
**Address:** `solana_program::sysvar::rent::ID`
The Rent sysvar provides rent calculation parameters.
### Rent Structure
```rust
use solana_program::rent::Rent;
pub struct Rent {
pub lamports_per_byte_year: u64, // Base rent rate
pub exemption_threshold: f64, // Multiplier for exemption (2.0 = 2 years)
pub burn_percent: u8, // Percentage of rent burned
}
```
### Accessing Rent
**Pattern 1: get() (Recommended)**
```rust
use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;
pub fn calculate_rent_exemption(
data_size: usize,
) -> Result<u64, ProgramError> {
let rent = Rent::get()?;
// Calculate minimum balance for rent exemption
let min_balance = rent.minimum_balance(data_size);
msg!("Minimum balance for {} bytes: {} lamports", data_size, min_balance);
Ok(min_balance)
}
```
**Pattern 2: From account**
```rust
use solana_program::sysvar::rent;
pub fn check_rent_exemption(
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let data_account = next_account_info(account_info_iter)?;
let rent_account = next_account_info(account_info_iter)?;
if rent_account.key != &rent::ID {
return Err(ProgramError::InvalidArgument);
}
let rent = Rent::from_account_info(rent_account)?;
if !rent.is_exempt(data_account.lamports(), data_account.data_len()) {
msg!("Account is not rent-exempt!");
return Err(ProgramError::AccountNotRentExempt);
}
Ok(())
}
```
### Common Rent Use Cases
**1. Account creation with rent exemption:**
```rust
use solana_program::rent::Rent;
use solana_program::system_instruction;
use solana_program::program::invoke_signed;
pub fn create_account_rent_exempt(
payer: &AccountInfo,
new_account: &AccountInfo,
system_program: &AccountInfo,
program_id: &Pubkey,
seeds: &[&[u8]],
space: usize,
) -> ProgramResult {
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(space);
msg!("Creating account with {} lamports for {} bytes", min_balance, space);
let create_account_ix = system_instruction::create_account(
payer.key,
new_account.key,
min_balance,
space as u64,
program_id,
);
invoke_signed(
&create_account_ix,
&[payer.clone(), new_account.clone(), system_program.clone()],
&[seeds],
)?;
Ok(())
}
```
**2. Validating account has sufficient balance:**
```rust
pub fn validate_rent_exempt_account(
account: &AccountInfo,
) -> ProgramResult {
let rent = Rent::get()?;
if !rent.is_exempt(account.lamports(), account.data_len()) {
let required = rent.minimum_balance(account.data_len());
let current = account.lamports();
msg!("Account not rent-exempt: has {} lamports, needs {}",
current, required);
return Err(ProgramError::AccountNotRentExempt);
}
Ok(())
}
```
**3. Calculating required lamports for reallocation:**
```rust
pub fn reallocate_account(
account: &AccountInfo,
new_size: usize,
) -> ProgramResult {
let rent = Rent::get()?;
let old_size = account.data_len();
let current_lamports = account.lamports();
let new_min_balance = rent.minimum_balance(new_size);
if new_size > old_size {
// Growing account - ensure sufficient lamports
if current_lamports < new_min_balance {
msg!("Need {} more lamports for reallocation",
new_min_balance - current_lamports);
return Err(ProgramError::InsufficientFunds);
}
}
account.realloc(new_size, false)?;
Ok(())
}
```
---
## EpochSchedule Sysvar
**Address:** `solana_program::sysvar::epoch_schedule::ID`
The EpochSchedule sysvar provides information about epoch timing and slot calculations.
### EpochSchedule Structure
```rust
use solana_program::epoch_schedule::EpochSchedule;
pub struct EpochSchedule {
pub slots_per_epoch: u64, // Slots per epoch after warmup
pub leader_schedule_slot_offset: u64, // Offset for leader schedule
pub warmup: bool, // Whether in warmup period
pub first_normal_epoch: Epoch, // First non-warmup epoch
pub first_normal_slot: Slot, // First slot of first normal epoch
}
```
### Accessing EpochSchedule
```rust
use solana_program::sysvar::epoch_schedule::EpochSchedule;
use solana_program::sysvar::Sysvar;
pub fn get_epoch_info() -> ProgramResult {
let epoch_schedule = EpochSchedule::get()?;
msg!("Slots per epoch: {}", epoch_schedule.slots_per_epoch);
msg!("First normal epoch: {}", epoch_schedule.first_normal_epoch);
msg!("Warmup: {}", epoch_schedule.warmup);
Ok(())
}
```
### Common EpochSchedule Use Cases
**1. Calculating epoch from slot:**
```rust
use solana_program::clock::Clock;
use solana_program::epoch_schedule::EpochSchedule;
pub fn calculate_epoch_from_slot(
slot: u64,
) -> Result<u64, ProgramError> {
let epoch_schedule = EpochSchedule::get()?;
let epoch = epoch_schedule.get_epoch(slot);
msg!("Slot {} is in epoch {}", slot, epoch);
Ok(epoch)
}
```
**2. Determining slots remaining in epoch:**
```rust
pub fn slots_until_epoch_end() -> Result<u64, ProgramError> {
let clock = Clock::get()?;
let epoch_schedule = EpochSchedule::get()?;
let current_slot = clock.slot;
let current_epoch = clock.epoch;
// Get first slot of next epoch
let next_epoch_start = epoch_schedule.get_first_slot_in_epoch(current_epoch + 1);
let remaining = next_epoch_start - current_slot;
msg!("Slots remaining in epoch: {}", remaining);
Ok(remaining)
}
```
**3. Epoch-based reward distribution:**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct RewardState {
pub last_distribution_epoch: u64,
pub total_distributed: u64,
}
pub fn distribute_epoch_rewards(
reward_state_account: &AccountInfo,
) -> ProgramResult {
let clock = Clock::get()?;
let mut state = RewardState::try_from_slice(&reward_state_account.data.borrow())?;
if clock.epoch > state.last_distribution_epoch {
let epochs_passed = clock.epoch - state.last_distribution_epoch;
msg!("Distributing rewards for {} epochs", epochs_passed);
// Distribute rewards
let reward_amount = epochs_passed * 1000; // Example
state.total_distributed += reward_amount;
state.last_distribution_epoch = clock.epoch;
state.serialize(&mut &mut reward_state_account.data.borrow_mut()[..])?;
}
Ok(())
}
```
---
## SlotHashes Sysvar
**Address:** `solana_program::sysvar::slot_hashes::ID`
The SlotHashes sysvar contains recent slot hashes for verification purposes.
### SlotHashes Structure
```rust
use solana_program::slot_hashes::SlotHashes;
// SlotHashes contains up to 512 recent (slot, hash) pairs
pub struct SlotHashes {
// Vector of (slot, hash) tuples
// Most recent first, up to MAX_ENTRIES (512)
}
```
### Accessing SlotHashes
```rust
use solana_program::sysvar::slot_hashes::SlotHashes;
use solana_program::sysvar::Sysvar;
pub fn verify_recent_slot(
claimed_slot: u64,
claimed_hash: &[u8; 32],
) -> ProgramResult {
let slot_hashes = SlotHashes::get()?;
// Check if slot is in recent history
for (slot, hash) in slot_hashes.iter() {
if *slot == claimed_slot {
if hash.as_ref() == claimed_hash {
msg!("Slot hash verified!");
return Ok(());
} else {
msg!("Slot hash mismatch!");
return Err(ProgramError::InvalidArgument);
}
}
}
msg!("Slot not found in recent history");
Err(ProgramError::InvalidArgument)
}
```
### Common SlotHashes Use Cases
**1. Verifying transaction recency:**
```rust
pub fn verify_transaction_recent(
slot_hashes_account: &AccountInfo,
claimed_slot: u64,
) -> ProgramResult {
let slot_hashes = SlotHashes::from_account_info(slot_hashes_account)?;
// Check if claimed slot is in recent 512 slots
let is_recent = slot_hashes.iter().any(|(slot, _)| *slot == claimed_slot);
if !is_recent {
msg!("Transaction too old or slot invalid");
return Err(ProgramError::Custom(1));
}
Ok(())
}
```
**2. Preventing replay attacks:**
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct ProcessedSlot {
pub slot: u64,
pub hash: [u8; 32],
}
pub fn process_once_per_slot(
state_account: &AccountInfo,
) -> ProgramResult {
let slot_hashes = SlotHashes::get()?;
let mut state = ProcessedSlot::try_from_slice(&state_account.data.borrow())?;
// Get current slot and hash
let (current_slot, current_hash) = slot_hashes.iter().next()
.ok_or(ProgramError::InvalidArgument)?;
if state.slot == *current_slot {
msg!("Already processed in this slot!");
return Err(ProgramError::Custom(2)); // Already processed
}
// Update state
state.slot = *current_slot;
state.hash = current_hash.to_bytes();
state.serialize(&mut &mut state_account.data.borrow_mut()[..])?;
Ok(())
}
```
**⚠️ Note:** SlotHashes only maintains the most recent 512 slots. For older verification, use a different approach.
---
## Other Sysvars
### StakeHistory
**Address:** `solana_program::sysvar::stake_history::ID`
Provides historical stake activation and deactivation information.
```rust
use solana_program::sysvar::stake_history::StakeHistory;
pub fn get_stake_history() -> ProgramResult {
let stake_history = StakeHistory::get()?;
// Access historical stake data by epoch
msg!("Stake history available");
Ok(())
}
```
**Use cases:**
- Stake pool programs
- Historical stake analysis
- Reward calculations
### EpochRewards
**Address:** `solana_program::sysvar::epoch_rewards::ID`
Provides information about epoch rewards distribution (if active).
```rust
use solana_program::sysvar::epoch_rewards::EpochRewards;
pub fn check_epoch_rewards() -> ProgramResult {
let epoch_rewards = EpochRewards::get()?;
msg!("Epoch rewards data available");
Ok(())
}
```
**Use cases:**
- Stake reward programs
- Validator reward tracking
### Instructions
**Address:** `solana_program::sysvar::instructions::ID`
Provides access to instructions in the current transaction.
```rust
use solana_program::sysvar::instructions;
pub fn validate_transaction_instructions(
instructions_account: &AccountInfo,
) -> ProgramResult {
// Check if current instruction is not the first
let current_index = instructions::load_current_index_checked(instructions_account)?;
msg!("Current instruction index: {}", current_index);
// Load a specific instruction
if current_index > 0 {
let prev_ix = instructions::load_instruction_at_checked(
(current_index - 1) as usize,
instructions_account,
)?;
msg!("Previous instruction program: {}", prev_ix.program_id);
}
Ok(())
}
```
**Use cases:**
- Cross-instruction validation
- Ensuring instruction order
- Detecting sandwich attacks
---
## Access Patterns
### Pattern 1: get() - Direct Access (Recommended)
**Advantages:**
- No account needed in instruction
- Saves account space
- Lower CU cost (~100 CU)
- Cleaner code
**Disadvantages:**
- Not supported for all sysvars
- Can't be passed to CPIs
```rust
use solana_program::sysvar::Sysvar;
pub fn use_sysvar_direct() -> ProgramResult {
let clock = Clock::get()?;
let rent = Rent::get()?;
msg!("Clock: {}", clock.unix_timestamp);
msg!("Rent: {}", rent.lamports_per_byte_year);
Ok(())
}
```
**Supported sysvars:**
- Clock
- Rent
- EpochSchedule
- EpochRewards
- Fees (deprecated)
### Pattern 2: from_account_info - Account Access
**Advantages:**
- Works for all sysvars
- Can be validated
- Can be passed to CPIs
- Required for some sysvars (SlotHashes, Instructions)
**Disadvantages:**
- Account must be passed in instruction
- Slightly higher CU cost (~300 CU)
- More boilerplate
```rust
use solana_program::sysvar::clock;
pub fn use_sysvar_from_account(
clock_account: &AccountInfo,
) -> ProgramResult {
// Validate account address
if clock_account.key != &clock::ID {
return Err(ProgramError::InvalidArgument);
}
let clock = Clock::from_account_info(clock_account)?;
msg!("Clock: {}", clock.unix_timestamp);
Ok(())
}
```
**Required for:**
- SlotHashes
- StakeHistory
- Instructions
- Any sysvar passed to CPI
### Pattern 3: Hybrid Approach
**Use get() when possible, account when needed:**
```rust
pub fn hybrid_sysvar_access(
accounts: &[AccountInfo],
need_cpi: bool,
) -> ProgramResult {
if need_cpi {
// Need account for CPI
let account_info_iter = &mut accounts.iter();
let clock_account = next_account_info(account_info_iter)?;
let clock = Clock::from_account_info(clock_account)?;
// Can pass clock_account to CPI
msg!("Using account access");
} else {
// Direct access is cheaper
let clock = Clock::get()?;
msg!("Using direct access");
}
Ok(())
}
```
---
## Performance Implications
### Compute Unit Costs
| Access Method | Approximate CU Cost |
|--------------|---------------------|
| Clock::get() | ~100 CU |
| Rent::get() | ~100 CU |
| EpochSchedule::get() | ~100 CU |
| Clock::from_account_info() | ~300 CU |
| SlotHashes::from_account_info() | ~500 CU |
### Optimization Tips
**1. Use get() when possible:**
```rust
// ✅ Efficient - 100 CU
let clock = Clock::get()?;
// ❌ Wasteful - 300 CU (unless needed for CPI)
let clock = Clock::from_account_info(clock_account)?;
```
**2. Cache sysvar values:**
```rust
// ❌ Wasteful - calls get() multiple times
for i in 0..10 {
let clock = Clock::get()?; // 100 CU × 10 = 1000 CU
process_item(i, clock.unix_timestamp)?;
}
// ✅ Efficient - call once
let clock = Clock::get()?; // 100 CU
let timestamp = clock.unix_timestamp;
for i in 0..10 {
process_item(i, timestamp)?;
}
```
**3. Avoid unnecessary sysvar access:**
```rust
// ❌ Wasteful - reading sysvar in every call
pub fn update_balance(account: &AccountInfo, amount: u64) -> ProgramResult {
let clock = Clock::get()?; // Not needed!
// ... no clock usage
Ok(())
}
// ✅ Efficient - only access when needed
pub fn update_with_timestamp(account: &AccountInfo, amount: u64) -> ProgramResult {
let clock = Clock::get()?; // Used below
let timestamp = clock.unix_timestamp;
// ... use timestamp
Ok(())
}
```
---
## Best Practices
### 1. Prefer get() Over from_account_info()
**Unless you need the account for CPI or validation:**
```rust
// ✅ Default choice
let clock = Clock::get()?;
// Only if needed for CPI
let clock = Clock::from_account_info(clock_account)?;
invoke(&ix, &[..., clock_account])?;
```
### 2. Validate Sysvar Accounts
**When accepting sysvar accounts, always validate:**
```rust
pub fn validate_clock_account(
clock_account: &AccountInfo,
) -> ProgramResult {
// ✅ Always validate sysvar address
if clock_account.key != &solana_program::sysvar::clock::ID {
msg!("Invalid Clock account");
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
```
### 3. Use Clock for Timestamps, Not Slot Hashes
**For simple time-based logic:**
```rust
// ✅ Simple and efficient
let clock = Clock::get()?;
if clock.unix_timestamp >= unlock_time {
// unlock
}
// ❌ Overkill - SlotHashes is for verification, not timing
let slot_hashes = SlotHashes::get()?;
// Complex slot-based timing logic
```
### 4. Cache Sysvar Values
**Read once, use multiple times:**
```rust
pub fn process_multiple_accounts(
accounts: &[AccountInfo],
) -> ProgramResult {
// ✅ Read once
let clock = Clock::get()?;
let timestamp = clock.unix_timestamp;
for account in accounts {
update_account_timestamp(account, timestamp)?;
}
Ok(())
}
```
### 5. Document Sysvar Dependencies
**Be explicit about which sysvars your program uses:**
```rust
/// Processes user staking
///
/// # Sysvars
/// - Clock: for stake timestamp
/// - Rent: for account validation
///
/// # Accounts
/// - `[writable]` stake_account
/// - `[signer]` user
pub fn process_stake(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let clock = Clock::get()?;
let rent = Rent::get()?;
// ...
Ok(())
}
```
### 6. Handle Clock Drift
**Don't assume unix_timestamp is perfectly accurate:**
```rust
// ❌ Risky - exact timestamp match
if clock.unix_timestamp == expected_time {
// May never trigger
}
// ✅ Safe - use ranges
if clock.unix_timestamp >= expected_time {
// Reliable
}
// ✅ Best - add tolerance for early/late
const TOLERANCE: i64 = 60; // 60 seconds
if clock.unix_timestamp >= expected_time - TOLERANCE {
// Handles clock drift
}
```
---
## Summary
**Key Takeaways:**
1. **Use get() when possible** for lower CU costs and simpler code
2. **Use from_account_info()** when passing to CPIs or for sysvars without get()
3. **Always validate** sysvar account addresses when accepting them
4. **Cache sysvar values** to avoid redundant reads
5. **Understand timing limitations** - unix_timestamp is approximate
**Most Common Sysvars:**
| Sysvar | Primary Use | Access Method |
|--------|------------|---------------|
| **Clock** | Timestamps, epochs, slots | `Clock::get()` |
| **Rent** | Rent exemption calculations | `Rent::get()` |
| **EpochSchedule** | Epoch/slot calculations | `EpochSchedule::get()` |
| **SlotHashes** | Recent slot verification | `from_account_info()` only |
| **Instructions** | Transaction introspection | `from_account_info()` only |
**Common Patterns:**
```rust
// Timestamp current event
let clock = Clock::get()?;
event.created_at = clock.unix_timestamp;
// Validate rent exemption
let rent = Rent::get()?;
if !rent.is_exempt(account.lamports(), account.data_len()) {
return Err(ProgramError::AccountNotRentExempt);
}
// Calculate rent for new account
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(space);
```
Sysvars provide essential cluster state to your programs. Master their access patterns for efficient, production-ready Solana development.

File diff suppressed because it is too large Load Diff

View File

@@ -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<u64> {
let fee = (amount as u128)
.checked_mul(fee_bps as u128)?
.checked_div(10_000)?;
Some(fee as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_fee_basic() {
assert_eq!(calculate_fee(1_000_000, 250), Some(25_000)); // 2.5%
assert_eq!(calculate_fee(1_000_000, 5000), Some(500_000)); // 50%
}
#[test]
fn test_calculate_fee_rounding() {
assert_eq!(calculate_fee(100, 1), Some(0)); // Rounds down
assert_eq!(calculate_fee(10_000, 1), Some(1)); // 0.01%
}
#[test]
fn test_calculate_fee_overflow() {
assert_eq!(calculate_fee(u64::MAX, 10000), None); // Would overflow
}
}
```
### Level 2: Mollusk Program Tests
**Purpose:** Test individual instructions with full account setup but without validator overhead.
**Location:** `tests/` directory or `#[cfg(test)]` modules
**Why needed:**
- Tests actual program binary execution
- Validates account constraints, signer checks, ownership
- ~100ms per test vs ~1s for full validator
- Catches instruction-level bugs
- Compute unit benchmarking
**What belongs here:**
- Each instruction handler (initialize, create_split, execute_split, etc.)
- Error conditions (wrong signer, invalid account owner)
- Account state transitions
- Cross-program invocations (CPIs)
- PDA derivation and signing
- Rent exemption validation
**Example:**
```rust
// tests/test_initialize.rs
use {
mollusk_svm::Mollusk,
my_program::{instruction::initialize, ID},
solana_sdk::{
account::Account,
instruction::Instruction,
pubkey::Pubkey,
},
};
#[test]
fn test_initialize_success() {
let mollusk = Mollusk::new(&ID, "target/deploy/my_program");
let user = Pubkey::new_unique();
let account = Pubkey::new_unique();
let instruction = initialize(&user, &account);
let accounts = vec![
(user, system_account(10_000_000)),
(account, Account::default()),
];
let result = mollusk.process_instruction(&instruction, &accounts);
assert!(result.is_ok());
}
#[test]
fn test_initialize_wrong_signer_fails() {
let mollusk = Mollusk::new(&ID, "target/deploy/my_program");
let user = Pubkey::new_unique();
let wrong_signer = Pubkey::new_unique();
let mut instruction = initialize(&user, &Pubkey::new_unique());
instruction.accounts[0].is_signer = false; // Missing signature
let accounts = vec![(user, system_account(10_000_000))];
let checks = vec![Check::instruction_err(
InstructionError::MissingRequiredSignature
)];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
```
### Level 3: SDK Integration Tests
**Purpose:** Test that SDK produces correct instructions that work end-to-end.
**Location:** Separate SDK package (`sdk/tests/`) or TypeScript tests
**Why needed:**
- Validates serialization matches program expectations
- Tests full transaction flow (multiple instructions)
- Catches SDK bugs before users hit them
- Client-perspective testing
- Ensures TypeScript/Rust SDK matches program
**What belongs here:**
- SDK instruction builders produce valid transactions
- Full flows: create → deposit → execute
- Multiple instructions in one transaction
- Account resolution (finding PDAs from SDK)
- Error handling from client side
- Event parsing and decoding
**Example (LiteSVM):**
```rust
// sdk/tests/integration_test.rs
use {
litesvm::LiteSVM,
my_program_sdk::{instructions, MyProgramClient},
solana_sdk::{
signature::Keypair,
signer::Signer,
},
};
#[test]
fn test_full_flow_create_and_execute() {
let mut svm = LiteSVM::new();
// Add program
let program_bytes = include_bytes!("../../target/deploy/my_program.so");
svm.add_program(MY_PROGRAM_ID, program_bytes);
// Create client
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap();
let client = MyProgramClient::new(&svm, &payer);
// Step 1: Initialize
let tx1 = client.initialize().unwrap();
svm.send_transaction(tx1).unwrap();
// Step 2: Deposit
let tx2 = client.deposit(1_000_000).unwrap();
svm.send_transaction(tx2).unwrap();
// Step 3: Execute
let tx3 = client.execute().unwrap();
let result = svm.send_transaction(tx3).unwrap();
// Verify final state
let account = client.get_account().unwrap();
assert_eq!(account.balance, 1_000_000);
}
```
### Level 4: Devnet/Mainnet Smoke Tests
**Purpose:** Final validation in real environment.
**Location:** Manual testing or automated CI scripts
**Why needed:**
- Real RPC, real fees, real constraints
- Validates deployment configuration
- Tests against actual on-chain state
- Catches environment-specific issues
- Verifies upgrades work correctly
**What belongs here:**
- Post-deployment smoke tests (critical paths only)
- Upgrade validation (new version works)
- Integration with other mainnet programs
- Performance under real network conditions
**Example (Manual script):**
```bash
#!/bin/bash
# scripts/smoke-test-devnet.sh
echo "Running devnet smoke tests..."
# Test 1: Initialize
solana-keygen new --no-bpf-loader-deprecated --force -o /tmp/test-user.json
solana airdrop 2 /tmp/test-user.json --url devnet
my-program-cli initialize \
--program-id $PROGRAM_ID \
--payer /tmp/test-user.json \
--url devnet
# Test 2: Execute main flow
my-program-cli execute \
--amount 1000000 \
--payer /tmp/test-user.json \
--url devnet
echo "✅ Smoke tests passed"
```
### How to Use This Pyramid
**During development:**
1. Write inline tests as you implement math/validation
2. Write Mollusk tests for each instruction
3. Run frequently: `cargo test`
**Before PR/merge:**
1. Ensure all inline + Mollusk tests pass
2. Add SDK integration tests if SDK changed
3. Run compute unit benchmarks
**Before deployment:**
1. All tests pass on devnet-compatible build
2. Deploy to devnet
3. Run manual smoke tests on devnet
4. If pass, proceed to mainnet
**After deployment:**
1. Run smoke tests on mainnet
2. Monitor for errors
3. Keep tests updated as program evolves
### Benefits of This Structure
**Fast feedback loop:**
- Level 1 tests run in milliseconds
- Catch bugs early without slow iteration
**Comprehensive coverage:**
- Pure logic (Level 1)
- Program execution (Level 2)
- Client integration (Level 3)
- Real environment (Level 4)
**Efficient CI/CD:**
- Level 1-2 in every PR (fast)
- Level 3 on merge to main
- Level 4 post-deployment
**Clear responsibilities:**
- Each level tests different concerns
- No redundant tests
- Easier to maintain
---
## Next Steps
- For implementation details on Mollusk, LiteSVM, and Anchor testing, see **[Testing Frameworks](./testing-frameworks.md)**
- For best practices, common patterns, and additional resources, see **[Testing Best Practices](./testing-practices.md)**

View File

@@ -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)**

View File

@@ -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<TransferHook>,
amount: u64,
) -> Result<()> {
msg!("Transfer hook called! Amount: {}", amount);
// Custom transfer logic here
Ok(())
}
}
#[derive(Accounts)]
pub struct TransferHook<'info> {
pub source: InterfaceAccount<'info, TokenAccount>,
pub destination: InterfaceAccount<'info, TokenAccount>,
/// CHECK: authority
pub authority: UncheckedAccount<'info>,
}
```
---
## Next Steps
- **Common Patterns**: See [tokens-patterns.md](tokens-patterns.md) for escrow, staking, NFT creation patterns
- **Security**: See [tokens-patterns.md](tokens-patterns.md) for comprehensive security best practices

View File

@@ -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<CreateMint>) -> Result<()> {
// Mint is automatically created and initialized by Anchor constraints
msg!("Mint created: {}", ctx.accounts.mint.key());
Ok(())
}
```
**Key Anchor Constraints:**
- `init` - Creates and initializes the account
- `mint::decimals` - Number of decimal places
- `mint::authority` - Who can mint tokens
- `mint::freeze_authority` - Who can freeze token accounts (optional)
- `mint::token_program` - Which token program to use
#### Using Native Rust
```rust
use spl_token::instruction::initialize_mint;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_mint(
payer: &AccountInfo,
mint_account: &AccountInfo,
mint_authority: &Pubkey,
freeze_authority: Option<&Pubkey>,
decimals: u8,
system_program: &AccountInfo,
token_program: &AccountInfo,
rent_sysvar: &AccountInfo,
) -> ProgramResult {
// Mint account size
let mint_size = 82;
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(mint_size);
// Create mint account via System Program
invoke(
&system_instruction::create_account(
payer.key,
mint_account.key,
rent_lamports,
mint_size as u64,
&spl_token::ID,
),
&[payer.clone(), mint_account.clone(), system_program.clone()],
)?;
// Initialize mint
invoke(
&initialize_mint(
token_program.key,
mint_account.key,
mint_authority,
freeze_authority,
decimals,
)?,
&[
mint_account.clone(),
rent_sysvar.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
### Initialize a Token Account (Non-ATA)
#### Using Anchor
```rust
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
#[account(
init,
payer = payer,
token::mint = mint,
token::authority = owner,
token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
/// CHECK: Can be any account
pub owner: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
pub fn create_token_account(ctx: Context<CreateTokenAccount>) -> Result<()> {
// Token account is automatically created and initialized
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::initialize_account3;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_token_account(
payer: &AccountInfo,
token_account: &AccountInfo,
mint: &AccountInfo,
owner: &Pubkey,
system_program: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
// Token account size
let token_account_size = 165;
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(token_account_size);
// Create token account
invoke(
&system_instruction::create_account(
payer.key,
token_account.key,
rent_lamports,
token_account_size as u64,
&spl_token::ID,
),
&[payer.clone(), token_account.clone(), system_program.clone()],
)?;
// Initialize token account
invoke(
&initialize_account3(
token_program.key,
token_account.key,
mint.key,
owner,
)?,
&[token_account.clone(), mint.clone(), token_program.clone()],
)?;
Ok(())
}
```
---
## Minting Tokens
### Basic Minting (User Authority)
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint_authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token_interface::mint_to(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::mint_to;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn mint_tokens(
mint: &AccountInfo,
destination: &AccountInfo,
mint_authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
// Mint authority must be a signer
if !mint_authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&mint_to(
token_program.key,
mint.key,
destination.key,
mint_authority.key,
&[], // No multisig signers
amount,
)?,
&[
mint.clone(),
destination.clone(),
mint_authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
### Minting with PDA Authority
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct MintWithPDA<'info> {
#[account(
mut,
mint::authority = mint_authority,
)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"mint-authority"],
bump,
)]
/// CHECK: PDA signer
pub mint_authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn mint_with_pda(ctx: Context<MintWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"mint-authority",
&[ctx.bumps.mint_authority],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::mint_to(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::mint_to;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn mint_tokens_from_pda(
program_id: &Pubkey,
mint: &AccountInfo,
destination: &AccountInfo,
mint_authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Validate PDA
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *mint_authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Prepare signer seeds
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&mint_to(
token_program.key,
mint.key,
destination.key,
mint_authority_pda.key,
&[],
amount,
)?,
&[
mint.clone(),
destination.clone(),
mint_authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
```
---
## Transferring Tokens
### Basic Transfer
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer};
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::transfer(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::transfer;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn transfer_tokens(
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
// Authority must be a signer
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[], // No multisig signers
amount,
)?,
&[
source.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
### Transfer with Checks (Recommended)
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked};
#[derive(Accounts)]
pub struct TransferTokensChecked<'info> {
#[account(mut)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_tokens_checked(
ctx: Context<TransferTokensChecked>,
amount: u64
) -> Result<()> {
token_interface::transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.from.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
),
amount,
ctx.accounts.mint.decimals,
)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::transfer_checked;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn transfer_tokens_checked(
source: &AccountInfo,
mint: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
amount: u64,
decimals: u8,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&transfer_checked(
token_program.key,
source.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
decimals,
)?,
&[
source.clone(),
mint.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
### Transfer with PDA Signer
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer};
#[derive(Accounts)]
pub struct TransferWithPDA<'info> {
#[account(
mut,
token::authority = authority,
)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"authority"],
bump,
)]
/// CHECK: PDA signer
pub authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_with_pda(ctx: Context<TransferWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"authority",
&[ctx.bumps.authority],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::transfer(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::transfer;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn transfer_tokens_from_pda(
program_id: &Pubkey,
source: &AccountInfo,
destination: &AccountInfo,
authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&transfer(
token_program.key,
source.key,
destination.key,
authority_pda.key,
&[],
amount,
)?,
&[
source.clone(),
destination.clone(),
authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
```
---
## Burning Tokens
### Basic Burn
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct BurnTokens<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn burn_tokens(ctx: Context<BurnTokens>, amount: u64) -> Result<()> {
let cpi_accounts = Burn {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::burn(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::burn;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn burn_tokens(
token_account: &AccountInfo,
mint: &AccountInfo,
authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&burn(
token_program.key,
token_account.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[
token_account.clone(),
mint.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
### Burn with PDA Authority
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct BurnWithPDA<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(
mut,
token::authority = authority,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"burn-authority"],
bump,
)]
/// CHECK: PDA signer
pub authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn burn_with_pda(ctx: Context<BurnWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"burn-authority",
&[ctx.bumps.authority],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Burn {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::burn(cpi_context, amount)?;
Ok(())
}
```
#### Using Native Rust
```rust
pub fn burn_tokens_from_pda(
program_id: &Pubkey,
token_account: &AccountInfo,
mint: &AccountInfo,
authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&burn(
token_program.key,
token_account.key,
mint.key,
authority_pda.key,
&[],
amount,
)?,
&[
token_account.clone(),
mint.clone(),
authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
```
---
## Closing Token Accounts
### Close Token Account
#### Using Anchor
```rust
use anchor_spl::token_interface::{self, CloseAccount, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct CloseTokenAccount<'info> {
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> {
let cpi_accounts = CloseAccount {
account: ctx.accounts.token_account.to_account_info(),
destination: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::close_account(cpi_context)?;
Ok(())
}
```
**Using Anchor Constraints (Simplified):**
```rust
#[derive(Accounts)]
pub struct CloseTokenAccount<'info> {
#[account(
mut,
close = destination,
token::authority = authority,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
pub authority: Signer<'info>,
}
pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> {
// Account is automatically closed by Anchor constraints
Ok(())
}
```
#### Using Native Rust
```rust
use spl_token::instruction::close_account;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn close_token_account(
token_account: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&close_account(
token_program.key,
token_account.key,
destination.key,
authority.key,
&[],
)?,
&[
token_account.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
---
## Next Steps
- **Validation**: See [tokens-validation.md](tokens-validation.md) for account validation patterns
- **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features
- **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and security best practices

View File

@@ -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<Pubkey>, // 36 bytes
/// Total supply of tokens
pub supply: u64, // 8 bytes
/// Number of decimals (0 for NFTs, typically 6-9 for fungible)
pub decimals: u8, // 1 byte
/// Is initialized?
pub is_initialized: bool, // 1 byte
/// Optional authority to freeze token accounts
pub freeze_authority: COption<Pubkey>, // 36 bytes
}
```
**COption Format:**
```rust
pub enum COption<T> {
None, // Represented as [0, 0, 0, 0, ...]
Some(T), // Represented as [1, followed by T bytes]
}
```
### Token Account
**Size:** 165 bytes
```rust
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey, // 32 bytes
/// The owner of this account
pub owner: Pubkey, // 32 bytes
/// The amount of tokens this account holds
pub amount: u64, // 8 bytes
/// If `delegate` is `Some` then `delegated_amount` represents
/// the amount authorized by the delegate
pub delegate: COption<Pubkey>, // 36 bytes
/// The account's state
pub state: AccountState, // 1 byte
/// If is_native.is_some, this is a native token, and the value logs the
/// rent-exempt reserve
pub is_native: COption<u64>, // 12 bytes
/// The amount delegated
pub delegated_amount: u64, // 8 bytes
/// Optional authority to close the account
pub close_authority: COption<Pubkey>, // 36 bytes
}
pub enum AccountState {
Uninitialized,
Initialized,
Frozen,
}
```
---
## Associated Token Accounts
### What are ATAs?
**Associated Token Accounts (ATAs)** are PDAs that map a wallet address to a token account for a specific mint.
**Derivation:**
```rust
ATA = PDA(
seeds: [wallet_address, TOKEN_PROGRAM_ID, mint_address],
program: ASSOCIATED_TOKEN_PROGRAM_ID
)
```
**Benefits:**
- **Deterministic**: Same wallet + mint always produces same ATA
- **Discoverable**: Easy to find a user's token accounts
- **Standard**: All wallets use this convention
**Constants:**
```rust
// Token Program ID
pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
// Associated Token Program ID
pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
```
### Finding ATA Address
#### Using Anchor
```rust
use anchor_spl::associated_token::get_associated_token_address;
// In client code or tests
let ata_address = get_associated_token_address(
&wallet_address,
&mint_address,
);
```
#### Using Native Rust
```rust
use spl_associated_token_account::get_associated_token_address;
// Derive ATA address
let ata_address = get_associated_token_address(
&wallet_address,
&mint_address,
);
```
### Creating Associated Token Accounts
#### Using Anchor
```rust
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
#[account(
init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = owner,
associated_token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
/// CHECK: Can be any account
pub owner: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
pub fn create_ata(ctx: Context<CreateTokenAccount>) -> Result<()> {
// ATA is automatically created by Anchor constraints
Ok(())
}
```
#### Using Native Rust
```rust
use spl_associated_token_account::instruction::create_associated_token_account;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
};
pub fn create_ata(
payer: &AccountInfo,
wallet: &AccountInfo,
mint: &AccountInfo,
ata: &AccountInfo,
system_program: &AccountInfo,
token_program: &AccountInfo,
associated_token_program: &AccountInfo,
) -> ProgramResult {
invoke(
&create_associated_token_account(
payer.key,
wallet.key,
mint.key,
token_program.key,
),
&[
payer.clone(),
ata.clone(),
wallet.clone(),
mint.clone(),
system_program.clone(),
token_program.clone(),
associated_token_program.clone(),
],
)?
;
Ok(())
}
```
---
## Next Steps
- **Token Operations**: See [tokens-operations.md](tokens-operations.md) for creating mints, minting, transferring, burning, and closing accounts
- **Validation**: See [tokens-validation.md](tokens-validation.md) for account validation patterns
- **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features
- **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and security best practices

View File

@@ -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<InitializeEscrow>, amount: u64) -> Result<()> {
// Transfer tokens to escrow
token_interface::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_account.to_account_info(),
to: ctx.accounts.escrow_token_account.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
amount,
)?;
// Save state
ctx.accounts.escrow_state.user = ctx.accounts.user.key();
ctx.accounts.escrow_state.amount = amount;
ctx.accounts.escrow_state.bump = ctx.bumps.escrow_state;
Ok(())
}
#[derive(Accounts)]
pub struct ReleaseEscrow<'info> {
#[account(
mut,
seeds = [b"escrow", escrow_state.user.as_ref()],
bump = escrow_state.bump,
has_one = user,
close = user,
)]
pub escrow_state: Account<'info, EscrowState>,
#[account(mut)]
pub escrow_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub recipient_token_account: InterfaceAccount<'info, TokenAccount>,
pub user: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn release_escrow(ctx: Context<ReleaseEscrow>) -> Result<()> {
let seeds = &[
b"escrow",
ctx.accounts.user.key().as_ref(),
&[ctx.accounts.escrow_state.bump],
];
let signer_seeds = &[&seeds[..]];
token_interface::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.escrow_token_account.to_account_info(),
to: ctx.accounts.recipient_token_account.to_account_info(),
authority: ctx.accounts.escrow_state.to_account_info(),
},
).with_signer(signer_seeds),
ctx.accounts.escrow_state.amount,
)?;
Ok(())
}
```
### Using Native Rust
```rust
use borsh::{BorshDeserialize, BorshSerialize};
use spl_token::instruction::transfer;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct EscrowState {
pub user: Pubkey,
pub amount: u64,
pub bump: u8,
}
pub fn initialize_escrow(
program_id: &Pubkey,
user: &AccountInfo,
user_token_account: &AccountInfo,
escrow_token_account: &AccountInfo,
escrow_state: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
// Transfer tokens to escrow
invoke(
&transfer(
&spl_token::ID,
user_token_account.key,
escrow_token_account.key,
user.key,
&[],
amount,
)?,
&[user_token_account.clone(), escrow_token_account.clone(), user.clone()],
)?;
// Save escrow state
let (pda, bump) = Pubkey::find_program_address(&[b"escrow", user.key.as_ref()], program_id);
let escrow = EscrowState {
user: *user.key,
amount,
bump,
};
escrow.serialize(&mut &mut escrow_state.data.borrow_mut()[..])?;
Ok(())
}
pub fn release_escrow(
program_id: &Pubkey,
escrow_state: &AccountInfo,
escrow_token_account: &AccountInfo,
recipient_token_account: &AccountInfo,
escrow_pda: &AccountInfo,
amount: u64,
bump: u8,
user: &Pubkey,
) -> ProgramResult {
let signer_seeds: &[&[&[u8]]] = &[&[b"escrow", user.as_ref(), &[bump]]];
invoke_signed(
&transfer(
&spl_token::ID,
escrow_token_account.key,
recipient_token_account.key,
escrow_pda.key,
&[],
amount,
)?,
&[escrow_token_account.clone(), recipient_token_account.clone(), escrow_pda.clone()],
signer_seeds,
)?;
Ok(())
}
```
---
## Pattern 2: Token Staking
Users lock tokens to earn rewards.
### Using Anchor
```rust
use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, Transfer};
#[derive(Accounts)]
pub struct StakeTokens<'info> {
#[account(
init_if_needed,
payer = user,
space = 8 + 32 + 8 + 8 + 1,
seeds = [b"stake", user.key().as_ref()],
bump,
)]
pub stake_account: Account<'info, StakeAccount>,
#[account(mut)]
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
seeds = [b"vault"],
bump,
)]
pub vault_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct StakeAccount {
pub user: Pubkey,
pub amount_staked: u64,
pub stake_timestamp: i64,
pub bump: u8,
}
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> Result<()> {
// Transfer tokens to vault
token_interface::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_account.to_account_info(),
to: ctx.accounts.vault_token_account.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
amount,
)?;
// Update stake account
let clock = Clock::get()?;
ctx.accounts.stake_account.user = ctx.accounts.user.key();
ctx.accounts.stake_account.amount_staked += amount;
ctx.accounts.stake_account.stake_timestamp = clock.unix_timestamp;
ctx.accounts.stake_account.bump = ctx.bumps.stake_account;
Ok(())
}
#[derive(Accounts)]
pub struct UnstakeTokens<'info> {
#[account(
mut,
seeds = [b"stake", user.key().as_ref()],
bump = stake_account.bump,
has_one = user,
)]
pub stake_account: Account<'info, StakeAccount>,
#[account(mut)]
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
seeds = [b"vault"],
bump,
)]
pub vault_token_account: InterfaceAccount<'info, TokenAccount>,
/// CHECK: Vault authority PDA
#[account(
seeds = [b"vault-authority"],
bump,
)]
pub vault_authority: UncheckedAccount<'info>,
pub user: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn unstake_tokens(ctx: Context<UnstakeTokens>, amount: u64) -> Result<()> {
require!(
ctx.accounts.stake_account.amount_staked >= amount,
ErrorCode::InsufficientStake
);
let seeds = &[
b"vault-authority",
&[ctx.bumps.vault_authority],
];
let signer_seeds = &[&seeds[..]];
// Transfer tokens back to user
token_interface::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault_token_account.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
).with_signer(signer_seeds),
amount,
)?;
// Update stake account
ctx.accounts.stake_account.amount_staked -= amount;
Ok(())
}
```
---
## Pattern 3: NFT Creation
Minting a non-fungible token (supply = 1, decimals = 0).
### Using Anchor
```rust
use anchor_spl::token_interface::{self, Mint, MintTo, SetAuthority, TokenAccount, TokenInterface};
use anchor_spl::token_interface::spl_token_2022::instruction::AuthorityType;
#[derive(Accounts)]
pub struct CreateNFT<'info> {
#[account(
init,
payer = payer,
mint::decimals = 0,
mint::authority = mint_authority,
mint::token_program = token_program,
)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(
init,
payer = payer,
associated_token::mint = mint,
associated_token::authority = owner,
associated_token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
/// CHECK: Owner of the NFT
pub owner: UncheckedAccount<'info>,
pub mint_authority: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
pub fn create_nft(ctx: Context<CreateNFT>) -> Result<()> {
// Mint exactly 1 token
token_interface::mint_to(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
},
),
1,
)?;
// Remove mint authority to freeze supply
token_interface::set_authority(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
SetAuthority {
account_or_mint: ctx.accounts.mint.to_account_info(),
current_authority: ctx.accounts.mint_authority.to_account_info(),
},
),
AuthorityType::MintTokens,
None,
)?;
msg!("NFT created: {}", ctx.accounts.mint.key());
Ok(())
}
```
### Using Native Rust
```rust
use spl_token::instruction::{mint_to, set_authority, AuthorityType};
pub fn create_nft(
mint: &AccountInfo,
token_account: &AccountInfo,
mint_authority: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
// 1. Mint exactly 1 token
invoke(
&mint_to(
&spl_token::ID,
mint.key,
token_account.key,
mint_authority.key,
&[],
1, // Exactly 1 token
)?,
&[mint.clone(), token_account.clone(), mint_authority.clone()],
)?;
// 2. Remove mint authority (make supply fixed)
invoke(
&set_authority(
&spl_token::ID,
mint.key,
None, // Set to None
AuthorityType::MintTokens,
mint_authority.key,
&[],
)?,
&[mint.clone(), mint_authority.clone()],
)?;
Ok(())
}
```
---
## Pattern 4: Freezing and Thawing Accounts
### Using Anchor
```rust
use anchor_spl::token_interface::{self, FreezeAccount, Mint, ThawAccount, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct FreezeTokenAccount<'info> {
#[account(
mint::freeze_authority = freeze_authority,
)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub freeze_authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn freeze_account(ctx: Context<FreezeTokenAccount>) -> Result<()> {
token_interface::freeze_account(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
FreezeAccount {
account: ctx.accounts.token_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
authority: ctx.accounts.freeze_authority.to_account_info(),
},
),
)?;
Ok(())
}
pub fn thaw_account(ctx: Context<FreezeTokenAccount>) -> Result<()> {
token_interface::thaw_account(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
ThawAccount {
account: ctx.accounts.token_account.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
authority: ctx.accounts.freeze_authority.to_account_info(),
},
),
)?;
Ok(())
}
```
### Using Native Rust
```rust
use spl_token::instruction::{freeze_account, thaw_account};
pub fn freeze_token_account(
token_account: &AccountInfo,
mint: &AccountInfo,
freeze_authority: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
invoke(
&freeze_account(
token_program.key,
token_account.key,
mint.key,
freeze_authority.key,
&[],
)?,
&[
token_account.clone(),
mint.clone(),
freeze_authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
pub fn thaw_token_account(
token_account: &AccountInfo,
mint: &AccountInfo,
freeze_authority: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
invoke(
&thaw_account(
token_program.key,
token_account.key,
mint.key,
freeze_authority.key,
&[],
)?,
&[
token_account.clone(),
mint.clone(),
freeze_authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
```
---
## Security Considerations
### 1. Always Validate Token Accounts
#### Anchor Approach
```rust
#[derive(Accounts)]
pub struct SafeTransfer<'info> {
#[account(
mut,
constraint = source.mint == mint.key() @ ErrorCode::InvalidMint,
constraint = source.owner == authority.key() @ ErrorCode::InvalidOwner,
)]
pub source: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
constraint = destination.mint == mint.key() @ ErrorCode::InvalidMint,
)]
pub destination: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
```
#### Native Rust Approach
```rust
// ❌ Dangerous - no validation
pub fn unsafe_transfer(
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
) -> ProgramResult {
// No checks! Attacker can pass any accounts
invoke(&transfer_instruction, &accounts)?;
Ok(())
}
// ✅ Safe - validates everything
pub fn safe_transfer(
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
expected_mint: &Pubkey,
) -> ProgramResult {
// Validate source
validate_token_account(source, authority.key, expected_mint)?;
// Validate destination
let dest_token = TokenAccount::unpack(&destination.data.borrow())?;
if dest_token.mint != *expected_mint {
return Err(ProgramError::InvalidAccountData);
}
invoke(&transfer_instruction, &accounts)?;
Ok(())
}
```
### 2. Check Token Program ID
#### Anchor Approach
```rust
// Anchor automatically validates via Interface type
pub token_program: Interface<'info, TokenInterface>,
```
#### Native Rust Approach
```rust
pub fn validate_token_program(token_program: &AccountInfo) -> ProgramResult {
if token_program.key != &spl_token::ID && token_program.key != &spl_token_2022::ID {
msg!("Invalid Token Program");
return Err(ProgramError::IncorrectProgramId);
}
Ok(())
}
```
### 3. Verify Mint Matches
**Attack scenario:** Attacker passes token account for wrong mint.
#### Anchor Approach
```rust
#[account(
constraint = token_account.mint == expected_mint.key() @ ErrorCode::InvalidMint,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
```
#### Native Rust Approach
```rust
// Always verify mint
let source_token = TokenAccount::unpack(&source.data.borrow())?;
let dest_token = TokenAccount::unpack(&dest.data.borrow())?;
if source_token.mint != dest_token.mint {
msg!("Mint mismatch between source and destination");
return Err(ProgramError::InvalidAccountData);
}
```
### 4. Authority Checks
#### Anchor Approach
```rust
#[account(
constraint = token_account.owner == authority.key() @ ErrorCode::Unauthorized,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>, // Automatically validates is_signer
```
#### Native Rust Approach
```rust
// Verify authority matches token account owner
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
if token_account.owner != *authority.key {
msg!("Authority doesn't own token account");
return Err(ProgramError::IllegalOwner);
}
// Verify authority signed
if !authority.is_signer {
msg!("Authority must sign");
return Err(ProgramError::MissingRequiredSignature);
}
```
### 5. Account State Checks
#### Anchor Approach
```rust
use spl_token::state::AccountState;
pub fn check_not_frozen(ctx: Context<SomeContext>) -> Result<()> {
let token_account = &ctx.accounts.token_account;
require!(
token_account.state == AccountState::Initialized,
ErrorCode::AccountFrozen
);
Ok(())
}
```
#### Native Rust Approach
```rust
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
// Check not frozen
if token_account.state == spl_token::state::AccountState::Frozen {
msg!("Token account is frozen");
return Err(ProgramError::InvalidAccountData);
}
// Check initialized
if token_account.state == spl_token::state::AccountState::Uninitialized {
msg!("Token account not initialized");
return Err(ProgramError::UninitializedAccount);
}
```
### 6. Use TransferChecked Over Transfer
**Why:** `transfer_checked` validates the mint and decimals, preventing certain attack vectors.
#### Anchor Approach
```rust
// ✅ Preferred - validates mint and decimals
token_interface::transfer_checked(
cpi_context,
amount,
decimals,
)?;
// ❌ Less secure - no mint/decimal validation
token_interface::transfer(
cpi_context,
amount,
)?;
```
#### Native Rust Approach
```rust
// ✅ Preferred
invoke(
&transfer_checked(
token_program.key,
source.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
decimals,
)?,
&accounts,
)?;
// ❌ Less secure
invoke(
&transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&accounts,
)?;
```
---
## Summary
### Key Takeaways
**Anchor Advantages:**
- Automatic account validation through constraints
- Cleaner, more concise code
- Built-in safety checks
- Type-safe account structures
- Simplified CPI with `CpiContext`
**Native Rust Advantages:**
- Full control over all operations
- No framework overhead
- Explicit validation (can be more transparent)
- Useful for understanding low-level mechanics
### Common Operations Quick Reference
| Operation | Anchor Module | Native Rust Crate |
|-----------|---------------|-------------------|
| Mint tokens | `token_interface::mint_to` | `spl_token::instruction::mint_to` |
| Transfer tokens | `token_interface::transfer` | `spl_token::instruction::transfer` |
| Transfer checked | `token_interface::transfer_checked` | `spl_token::instruction::transfer_checked` |
| Burn tokens | `token_interface::burn` | `spl_token::instruction::burn` |
| Create ATA | `associated_token` constraint | `spl_associated_token_account` |
| Close account | `token_interface::close_account` | `spl_token::instruction::close_account` |
| Freeze account | `token_interface::freeze_account` | `spl_token::instruction::freeze_account` |
### Security Checklist
- ✅ Validate token program ID
- ✅ Verify token account ownership
- ✅ Check mint matches expected
- ✅ Confirm authority is signer
- ✅ Ensure account not frozen
- ✅ Validate ATA derivation if applicable
- ✅ Use `transfer_checked` instead of `transfer`
- ✅ Validate account state (initialized/frozen)
- ✅ Check sufficient balance before operations
### Token Account Sizes
- **Mint account:** 82 bytes
- **Token account:** 165 bytes
- **Token-2022 with extensions:** 82/165 + extension sizes
Token integration is fundamental for DeFi, NFT, and gaming programs on Solana. Whether using Anchor or native Rust, understanding both approaches provides the flexibility to choose the right tool for your use case.

View File

@@ -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<ValidateTokenAccount>) -> Result<()> {
// Validation is automatic via constraints
// Additional checks if needed
require!(
ctx.accounts.token_account.amount >= 100,
ErrorCode::InsufficientBalance
);
Ok(())
}
```
### Using Native Rust
```rust
use spl_token::state::Account as TokenAccount;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
};
pub fn validate_token_account(
token_account_info: &AccountInfo,
expected_owner: &Pubkey,
expected_mint: &Pubkey,
) -> ProgramResult {
// 1. Verify owned by Token Program
if token_account_info.owner != &spl_token::ID {
msg!("Account not owned by Token Program");
return Err(ProgramError::IllegalOwner);
}
// 2. Deserialize token account
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
// 3. Verify owner
if token_account.owner != *expected_owner {
msg!("Token account owner mismatch");
return Err(ProgramError::IllegalOwner);
}
// 4. Verify mint
if token_account.mint != *expected_mint {
msg!("Token account mint mismatch");
return Err(ProgramError::InvalidAccountData);
}
// 5. Verify not frozen
if token_account.state != spl_token::state::AccountState::Initialized {
msg!("Token account is frozen or uninitialized");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
```
---
## Validate ATA Address
### Using Anchor
```rust
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct ValidateATA<'info> {
#[account(
associated_token::mint = mint,
associated_token::authority = owner,
associated_token::token_program = token_program,
)]
pub ata: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
/// CHECK: Any account
pub owner: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn validate_ata(ctx: Context<ValidateATA>) -> Result<()> {
// ATA address is automatically validated by Anchor constraints
Ok(())
}
```
### Using Native Rust
```rust
use spl_associated_token_account::get_associated_token_address;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn validate_ata(
ata_info: &AccountInfo,
wallet: &Pubkey,
mint: &Pubkey,
) -> ProgramResult {
// Derive expected ATA address
let expected_ata = get_associated_token_address(wallet, mint);
// Validate match
if expected_ata != *ata_info.key {
msg!("Invalid ATA address");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
```
---
## Check Token Balance
### Using Anchor
```rust
use anchor_spl::token_interface::TokenAccount;
pub fn check_balance(
ctx: Context<SomeContext>,
minimum_amount: u64
) -> Result<()> {
let token_account = &ctx.accounts.token_account;
require!(
token_account.amount >= minimum_amount,
ErrorCode::InsufficientBalance
);
Ok(())
}
```
### Using Native Rust
```rust
use spl_token::state::Account as TokenAccount;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
program_pack::Pack,
};
pub fn check_token_balance(
token_account_info: &AccountInfo,
minimum_amount: u64,
) -> ProgramResult {
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
if token_account.amount < minimum_amount {
msg!("Insufficient token balance: {} < {}", token_account.amount, minimum_amount);
return Err(ProgramError::InsufficientFunds);
}
Ok(())
}
```
---
## Next Steps
- **Token-2022**: See [tokens-2022.md](tokens-2022.md) for Token Extensions Program features
- **Patterns & Security**: See [tokens-patterns.md](tokens-patterns.md) for common patterns and comprehensive security best practices

View File

@@ -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<Hash, HashAge>,
max_age: usize, // Currently 151
}
// Transaction validation:
fn is_valid_blockhash(blockhash: &Hash, queue: &BlockhashQueue) -> bool {
queue.ages.contains_key(blockhash) // Must be in last 151 blockhashes
}
```
### The 151-Block Window
**How it works:**
1. Each slot produces a new blockhash (~400-600ms per slot)
2. Runtime keeps last 151 blockhashes in `BlockhashQueue`
3. Transactions checked against this queue
4. If blockhash older than 150 blocks → **REJECTED**
**Calculation:**
```
151 blockhashes × ~600ms average slot time = ~90 seconds maximum
151 blockhashes × ~400ms minimum slot time = ~60 seconds minimum
Effective window: 60-90 seconds
```
**Critical**: Once a blockhash exits the queue (>150 blocks old), transactions using it can **never** be processed. They're permanently invalid.
### Detecting Expiration
**Using `lastValidBlockHeight`:**
```rust
use solana_client::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;
async fn check_transaction_expiration(
rpc_client: &RpcClient,
last_valid_block_height: u64,
) -> bool {
// Get current block height
let current_block_height = rpc_client
.get_block_height()
.unwrap_or(0);
// Transaction expired if current height > last valid height
current_block_height > last_valid_block_height
}
```
**Getting `lastValidBlockHeight`:**
```rust
let blockhash_response = rpc_client.get_latest_blockhash()?;
let blockhash = blockhash_response.value.0;
let last_valid_block_height = blockhash_response.value.1; // Blocks until expiration
println!("Blockhash: {}", blockhash);
println!("Valid until block: {}", last_valid_block_height);
```
### Why Transactions Expire
**Design rationale:**
1. **Prevents replay attacks**: Old transactions can't be resubmitted years later
2. **Manages state bloat**: Runtime doesn't need infinite blockhash history
3. **Network spam protection**: Attackers can't flood network with ancient transactions
4. **Simplifies fee markets**: Recent activity determines current conditions
**Trade-off**: 60-90 second window requires responsive clients and reliable networking.
## How Transactions Get Dropped
### Before Processing
**1. UDP Packet Loss**
Solana uses UDP for transaction forwarding (performance over reliability):
```
Client → RPC: UDP packet
RPC → Leader: UDP packet
Packet loss rate: 0.1-5% depending on network conditions
```
**Impact**: Transaction silently dropped, never reaches leader.
**Detection**: No error, no confirmation - transaction just disappears.
**Solution**: Retry mechanism (RPC default behavior).
**2. RPC Node Congestion**
RPC nodes maintain transaction queues:
```rust
// RPC node queue limits
const MAX_TRANSACTIONS_QUEUE: usize = 10_000;
// When queue full:
if queue.len() >= MAX_TRANSACTIONS_QUEUE {
return Err("Transaction queue full, try again");
}
```
**Impact**: New transactions rejected when queue full.
**Detection**: RPC returns error immediately.
**Solution**: Back off and retry, or use different RPC endpoint.
**3. RPC Node Lag**
RPC nodes can fall behind cluster:
```rust
// Check RPC health
let processed_slot = rpc_client.get_slot()?;
let max_shred_insert_slot = rpc_client.get_max_shred_insert_slot()?;
let lag = max_shred_insert_slot.saturating_sub(processed_slot);
if lag > 50 {
println!("WARNING: RPC is {} slots behind", lag);
// Consider using different RPC node
}
```
**Impact**: Fetches stale blockhashes that expire quickly.
**Solution**: Monitor RPC health, use multiple RPC providers.
**4. Blockhash from Minority Fork**
Clusters occasionally fork temporarily (~5% of slots):
```
Majority fork: Block A → Block B → Block C
Minority fork: Block A → Block X (abandoned)
```
If you fetch blockhash from minority fork:
- Blockhash is valid on minority fork
- Majority fork has different blockhash
- Transaction **never** valid on majority fork
**Impact**: Transaction permanently invalid (never in BlockhashQueue of majority fork).
**Detection**: Transaction never confirms, blockhash never appears in majority chain.
**Solution**: Use `confirmed` commitment level when fetching blockhashes (not `processed`).
### After Processing But Before Finalization
**5. Leader on Minority Fork**
Transaction processed by leader, but leader's block abandoned by cluster:
```
1. Leader processes transaction in slot 1000
2. Cluster votes on slot 1000
3. Supermajority votes for different fork
4. Leader's block (and transaction) discarded
```
**Impact**: Transaction processed but not confirmed. Must resubmit.
**Detection**: Transaction shows as processed but never confirmed.
**Solution**: Wait for `confirmed` level before assuming success.
**6. Transaction Expiration During Retry**
Default RPC retry behavior has limitations:
```rust
// RPC retry logic (simplified):
while !finalized && !expired {
forward_to_leader();
sleep(2_seconds);
}
// Problem: What if we can't determine expiration?
// RPC may stop retrying early!
```
**Impact**: RPC stops retrying before transaction actually expires.
**Solution**: Implement custom retry logic with explicit expiration tracking.
## Commitment Levels
### Understanding Commitment
Solana has three commitment levels representing stages of finality:
```
Processed
↓ (1-2 slots later)
Confirmed
↓ (32+ slots later, ~13 seconds)
Finalized
```
### Processed
**Definition**: Transaction processed by leader and included in a block.
**Characteristics:**
- Fastest (most recent)
- Least safe (~5% chance of being on abandoned fork)
- Can be rolled back if fork abandoned
**When to use:**
- Real-time UX updates (show pending state)
- Price feeds where staleness is worse than occasional rollback
- **NOT for blockhash fetching** (risk of minority fork blockhash)
**Example:**
```rust
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::commitment_config::CommitmentLevel;
let config = RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(CommitmentLevel::Processed),
..Default::default()
};
// Risky! Blockhash might be from minority fork
let signature = rpc_client.send_transaction_with_config(&transaction, config)?;
```
### Confirmed
**Definition**: Supermajority of validators voted for the block containing the transaction.
**Characteristics:**
- Fast (~1-2 slots, ~600-1200ms)
- Safe (~<0.1% chance of rollback in normal conditions)
- **RECOMMENDED for blockhash fetching**
**When to use:**
- **Default choice** for most operations
- Blockhash fetching (balance of speed and safety)
- Transaction submission (preflight commitment)
- Confirmation monitoring
**Example:**
```rust
let commitment = CommitmentConfig::confirmed();
// Fetch blockhash at confirmed level
let recent_blockhash = rpc_client.get_latest_blockhash_with_commitment(commitment)?;
// Set preflight commitment to match
let config = RpcSendTransactionConfig {
preflight_commitment: Some(CommitmentLevel::Confirmed),
..Default::default()
};
```
### Finalized
**Definition**: 32+ confirmed blocks have been built on top (mathematically impossible to rollback).
**Characteristics:**
- Slowest (~13+ seconds)
- 100% safe (impossible to rollback)
- Guaranteed by consensus algorithm
**When to use:**
- Financial settlement
- Legal/compliance requirements
- Cross-chain bridges
- Critical state changes
**Example:**
```rust
let commitment = CommitmentConfig::finalized();
// Wait for finalization
rpc_client.confirm_transaction_with_spinner(
&signature,
&recent_blockhash,
commitment,
)?;
```
### Preflight Commitment Matching
**Critical rule**: Preflight commitment MUST match blockhash fetch commitment.
**Why:**
```rust
// Scenario: Mismatch
let blockhash = rpc.get_latest_blockhash_with_commitment(confirmed)?; // confirmed
let config = RpcSendTransactionConfig {
preflight_commitment: Some(CommitmentLevel::Processed), // processed (WRONG!)
..Default::default()
};
// RPC tries to simulate at processed level
// But blockhash only exists at confirmed level
// Result: "Blockhash not found" error
```
**Correct approach:**
```rust
let commitment = CommitmentConfig::confirmed();
// Fetch blockhash
let blockhash_response = rpc.get_latest_blockhash_with_commitment(commitment)?;
let blockhash = blockhash_response.0;
// Match preflight commitment
let config = RpcSendTransactionConfig {
preflight_commitment: Some(CommitmentLevel::Confirmed),
..Default::default()
};
let signature = rpc.send_transaction_with_config(&transaction, config)?;
```
## RPC Retry Behavior
### Default Retry Logic
RPC nodes automatically retry transactions:
```rust
// Simplified RPC retry algorithm:
const RETRY_INTERVAL: Duration = Duration::from_secs(2);
const MAX_QUEUE_SIZE: usize = 10_000;
loop {
if transaction.is_finalized() {
return Ok(signature);
}
if queue.len() >= MAX_QUEUE_SIZE {
return Err("Queue full");
}
if can_determine_expiration() {
if transaction.is_expired() {
return Err("Blockhash expired");
}
} else {
// Conservative: retry only once if can't determine expiration
if retry_count > 1 {
return Ok(signature); // Might not actually be finalized!
}
}
forward_to_current_leader();
forward_to_next_leader();
sleep(RETRY_INTERVAL);
retry_count += 1;
}
```
### Leader Forwarding
RPC forwards transactions to:
1. **Current leader**: For immediate processing
2. **Next leader**: In case current leader rotation happens
**Why both?**
- Leader rotation happens every 4 slots (~1.6-2.4 seconds)
- Transaction might arrive during rotation
- Next leader can process in upcoming slots
### Queue Pressure
During congestion:
```
Queue size: 10,000 transactions
New transaction arrives:
if queue.is_full():
reject("Transaction queue full")
else:
queue.push(transaction)
retry_until_finalized()
```
**User experience:**
- Fresh transactions rejected when queue full
- Older transactions keep retrying
- Can create priority inversion (old low-priority tx blocks new high-priority tx)
**Solution**: Use `maxRetries: 0` to take manual control during congestion.
## Custom Retry Strategies
### Manual Retry Loop
Taking full control:
```rust
use solana_client::rpc_client::RpcClient;
use solana_sdk::signature::Signature;
use std::time::Duration;
use tokio::time::sleep;
async fn send_transaction_with_retry(
rpc_client: &RpcClient,
transaction: &Transaction,
last_valid_block_height: u64,
) -> Result<Signature, Box<dyn std::error::Error>> {
let config = RpcSendTransactionConfig {
skip_preflight: true, // Already validated
max_retries: Some(0), // Manual retry control
..Default::default()
};
let signature = rpc_client.send_transaction_with_config(
transaction,
config,
)?;
// Manual retry loop
loop {
// Check if transaction confirmed
match rpc_client.get_signature_status(&signature)? {
Some(Ok(_)) => {
println!("Transaction confirmed!");
return Ok(signature);
}
Some(Err(e)) => {
return Err(format!("Transaction failed: {:?}", e).into());
}
None => {
// Not processed yet, continue
}
}
// Check expiration
let current_block_height = rpc_client.get_block_height()?;
if current_block_height > last_valid_block_height {
return Err("Transaction expired".into());
}
// Resubmit
rpc_client.send_transaction_with_config(transaction, config)?;
// Wait before next retry
sleep(Duration::from_millis(500)).await;
}
}
```
### Exponential Backoff
Reduce network load during congestion:
```rust
async fn retry_with_exponential_backoff(
rpc_client: &RpcClient,
transaction: &Transaction,
last_valid_block_height: u64,
) -> Result<Signature, Box<dyn std::error::Error>> {
let signature = rpc_client.send_transaction(transaction)?;
let mut retry_delay = Duration::from_millis(500);
const MAX_DELAY: Duration = Duration::from_secs(8);
loop {
match rpc_client.get_signature_status(&signature)? {
Some(Ok(_)) => return Ok(signature),
Some(Err(e)) => return Err(e.into()),
None => {
// Check expiration
if rpc_client.get_block_height()? > last_valid_block_height {
return Err("Expired".into());
}
// Resubmit
rpc_client.send_transaction(transaction)?;
// Exponential backoff
sleep(retry_delay).await;
retry_delay = std::cmp::min(retry_delay * 2, MAX_DELAY);
}
}
}
}
```
### Constant Interval (Mango Approach)
Aggressive resubmission:
```rust
async fn retry_constant_interval(
rpc_client: &RpcClient,
transaction: &Transaction,
last_valid_block_height: u64,
) -> Result<Signature, Box<dyn std::error::Error>> {
let signature = rpc_client.send_transaction(transaction)?;
const RETRY_INTERVAL: Duration = Duration::from_millis(500);
loop {
match rpc_client.get_signature_status(&signature)? {
Some(Ok(_)) => return Ok(signature),
Some(Err(e)) => return Err(e.into()),
None => {
if rpc_client.get_block_height()? > last_valid_block_height {
return Err("Expired".into());
}
// Constant interval resubmission
rpc_client.send_transaction(transaction)?;
sleep(RETRY_INTERVAL).await;
}
}
}
}
```
**Trade-offs:**
- **Exponential backoff**: Network-friendly, slower confirmation
- **Constant interval**: Faster confirmation, more network load
- **Choice depends on**: Application needs, RPC provider limits, congestion levels
## Confirmation Monitoring
### Polling for Confirmation
**Basic polling:**
```rust
use solana_sdk::signature::Signature;
fn wait_for_confirmation(
rpc_client: &RpcClient,
signature: &Signature,
commitment: CommitmentConfig,
) -> Result<(), Box<dyn std::error::Error>> {
loop {
match rpc_client.get_signature_status_with_commitment(
signature,
commitment,
)? {
Some(Ok(_)) => {
println!("Transaction confirmed at {:?}", commitment);
return Ok(());
}
Some(Err(e)) => {
return Err(format!("Transaction failed: {:?}", e).into());
}
None => {
std::thread::sleep(Duration::from_millis(500));
}
}
}
}
```
**With timeout:**
```rust
use std::time::{Duration, Instant};
fn wait_for_confirmation_with_timeout(
rpc_client: &RpcClient,
signature: &Signature,
timeout: Duration,
) -> Result<bool, Box<dyn std::error::Error>> {
let start = Instant::now();
while start.elapsed() < timeout {
match rpc_client.get_signature_status(signature)? {
Some(Ok(_)) => return Ok(true),
Some(Err(e)) => return Err(e.into()),
None => std::thread::sleep(Duration::from_millis(500)),
}
}
Ok(false) // Timed out
}
```
### Using `confirm_transaction`
Built-in helper with expiration tracking:
```rust
let commitment = CommitmentConfig::confirmed();
// Method 1: With blockhash context
rpc_client.confirm_transaction_with_spinner(
&signature,
&recent_blockhash,
commitment,
)?;
// Method 2: With last valid block height (recommended)
let result = rpc_client.confirm_transaction_with_commitment(
&signature,
commitment,
)?;
if result.value {
println!("Transaction confirmed!");
} else {
println!("Transaction not confirmed (might have expired)");
}
```
### WebSocket Subscriptions (Real-Time)
For real-time updates without polling:
```rust
use solana_client::pubsub_client::PubsubClient;
use solana_sdk::commitment_config::CommitmentConfig;
async fn subscribe_to_signature(
ws_url: &str,
signature: &Signature,
) -> Result<(), Box<dyn std::error::Error>> {
let pubsub_client = PubsubClient::new(ws_url).await?;
let (mut stream, unsubscribe) = pubsub_client
.signature_subscribe(signature, Some(CommitmentConfig::confirmed()))
.await?;
// Wait for notification
while let Some(response) = stream.next().await {
match response.value {
solana_client::rpc_response::RpcSignatureResult::ProcessedSignature(_) => {
println!("Transaction confirmed!");
break;
}
}
}
unsubscribe().await;
Ok(())
}
```
**Advantages:**
- Real-time notification (no polling delay)
- Lower RPC load
- Immediate feedback
**Disadvantages:**
- WebSocket connection overhead
- Need to handle disconnections
- Not all RPC providers support WebSockets
## Best Practices
### 1. Fetch Fresh Blockhashes
```rust
// BAD: Fetch once and reuse
let blockhash = rpc.get_latest_blockhash()?;
for tx in transactions {
// All use same blockhash (increases expiration risk)
send_transaction(tx, &blockhash)?;
}
// GOOD: Fetch fresh blockhash for each transaction
for tx in transactions {
let blockhash = rpc.get_latest_blockhash()?;
send_transaction(tx, &blockhash)?;
}
// BETTER: Fetch fresh blockhash right before signing
fn prepare_and_send(user_action: Action) {
// User initiates action
let blockhash = rpc.get_latest_blockhash()?; // Fetch now!
// Build and sign (fast)
let tx = build_transaction(user_action, &blockhash);
sign_transaction(&tx);
// Submit immediately
send_transaction(&tx)?;
}
```
### 2. Use Confirmed Commitment
```rust
// RECOMMENDED: Confirmed commitment
let commitment = CommitmentConfig::confirmed();
let blockhash = rpc.get_latest_blockhash_with_commitment(commitment)?;
// Risks minority fork
let blockhash = rpc.get_latest_blockhash_with_commitment(
CommitmentConfig::processed()
)?; // Avoid!
```
### 3. Match Preflight Commitment
```rust
let commitment = CommitmentConfig::confirmed();
// Fetch blockhash
let (blockhash, last_valid_block_height) = rpc
.get_latest_blockhash_with_commitment(commitment)?;
// Match preflight commitment
let config = RpcSendTransactionConfig {
preflight_commitment: Some(CommitmentLevel::Confirmed), // MATCH!
..Default::default()
};
```
### 4. Track Expiration Explicitly
```rust
// Get expiration info
let (blockhash, last_valid_block_height) = rpc.get_latest_blockhash()?;
// Check before retry
fn should_retry(rpc: &RpcClient, last_valid: u64) -> bool {
rpc.get_block_height().unwrap_or(0) <= last_valid
}
```
### 5. Monitor RPC Health
```rust
async fn check_rpc_health(rpc: &RpcClient) -> bool {
let processed = rpc.get_slot().unwrap_or(0);
let max_shred = rpc.get_max_shred_insert_slot().unwrap_or(0);
let lag = max_shred.saturating_sub(processed);
if lag > 50 {
eprintln!("RPC lagging by {} slots", lag);
return false;
}
true
}
```
### 6. Implement Proper Error Handling
```rust
match rpc.send_transaction(&tx) {
Ok(signature) => {
println!("Submitted: {}", signature);
// Wait for confirmation
}
Err(e) => {
if e.to_string().contains("BlockhashNotFound") {
// Blockhash expired, fetch fresh one
let new_blockhash = rpc.get_latest_blockhash()?;
// Re-sign transaction with new blockhash
} else if e.to_string().contains("AlreadyProcessed") {
// Transaction already submitted (safe to ignore)
} else {
// Other error, handle appropriately
return Err(e.into());
}
}
}
```
### 7. Use Skip Preflight Judiciously
```rust
// When to skip preflight:
// - During congestion (preflight adds latency)
// - When retrying (already validated once)
// - When you're confident about transaction validity
let config = RpcSendTransactionConfig {
skip_preflight: true, // Skip simulation
preflight_commitment: Some(CommitmentLevel::Confirmed),
max_retries: Some(0),
..Default::default()
};
// Still recommended: Simulate ONCE before skip_preflight
rpc.simulate_transaction(&tx)?; // Catch errors
// Then submit with skip_preflight for speed
```
## Production Patterns
### High-Throughput System
```rust
struct TransactionSubmitter {
rpc_client: Arc<RpcClient>,
retry_queue: Arc<Mutex<VecDeque<RetryableTransaction>>>,
}
struct RetryableTransaction {
transaction: Transaction,
signature: Signature,
last_valid_block_height: u64,
submitted_at: Instant,
retry_count: usize,
}
impl TransactionSubmitter {
async fn submit_transaction(&self, tx: Transaction) -> Result<Signature, Error> {
let (blockhash, last_valid) = self.rpc_client.get_latest_blockhash()?;
// Submit initial
let signature = self.rpc_client.send_transaction(&tx)?;
// Add to retry queue
let retryable = RetryableTransaction {
transaction: tx,
signature,
last_valid_block_height: last_valid,
submitted_at: Instant::now(),
retry_count: 0,
};
self.retry_queue.lock().unwrap().push_back(retryable);
Ok(signature)
}
async fn retry_worker(&self) {
loop {
sleep(Duration::from_millis(500)).await;
let mut queue = self.retry_queue.lock().unwrap();
for tx in queue.iter_mut() {
// Check if confirmed
match self.rpc_client.get_signature_status(&tx.signature) {
Ok(Some(Ok(_))) => {
// Confirmed, remove from queue (handle in cleanup pass)
continue;
}
Ok(Some(Err(_))) => {
// Failed, remove from queue
continue;
}
_ => {
// Not confirmed, check expiration
let current_height = self.rpc_client.get_block_height().unwrap_or(0);
if current_height > tx.last_valid_block_height {
// Expired, remove from queue
continue;
}
// Retry
let _ = self.rpc_client.send_transaction(&tx.transaction);
tx.retry_count += 1;
}
}
}
// Cleanup confirmed/failed/expired
queue.retain(|tx| {
matches!(
self.rpc_client.get_signature_status(&tx.signature),
Ok(None) // Still pending
)
});
}
}
}
```
### Wallet Integration
```rust
async fn wallet_send_transaction(
rpc: &RpcClient,
unsigned_tx: Transaction,
signer: &dyn Signer,
) -> Result<Signature, Error> {
// Fetch blockhash immediately before signing
let (blockhash, last_valid) = rpc.get_latest_blockhash()?;
// Update transaction with fresh blockhash
let mut tx = unsigned_tx.clone();
tx.message.recent_blockhash = blockhash;
// Sign
tx.sign(&[signer], blockhash);
// Simulate first
rpc.simulate_transaction(&tx)?;
// Submit with retry
let signature = tx.signatures[0];
send_with_retry(rpc, &tx, last_valid).await?;
Ok(signature)
}
```
## Resources
### Official Documentation
- [Transaction Retry Guide](https://solana.com/developers/guides/advanced/retry)
- [Transaction Confirmation Guide](https://solana.com/developers/guides/advanced/confirmation)
### Technical References
- [RpcClient Source](https://github.com/solana-labs/solana/blob/master/client/src/rpc_client.rs)
- [Transaction Source](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction/mod.rs)
- [BlockhashQueue Source](https://github.com/solana-labs/solana/blob/master/runtime/src/blockhash_queue.rs)
### Community Resources
- [Solana Cookbook - Transactions](https://solanacookbook.com/references/basic-transactions.html)
- [Solana Stack Exchange - Transaction Questions](https://solana.stackexchange.com/questions/tagged/transaction)

View File

@@ -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<Pubkey>, // All 32-byte addresses
pub recent_blockhash: Hash,
pub instructions: Vec<CompiledInstruction>,
}
```
**Characteristics:**
- No version byte (implicitly version "legacy")
- All accounts must be fully specified (32 bytes each)
- Maximum ~35 accounts due to packet size limits
- Still supported and widely used for simple transactions
### Version 0 Transactions
**Structure:**
```rust
pub struct MessageV0 {
pub header: MessageHeader,
pub account_keys: Vec<Pubkey>, // Directly specified accounts
pub recent_blockhash: Hash,
pub instructions: Vec<CompiledInstruction>,
pub address_table_lookups: Vec<MessageAddressTableLookup>, // NEW!
}
pub struct MessageAddressTableLookup {
pub account_key: Pubkey, // ALT address (32 bytes)
pub writable_indexes: Vec<u8>, // Writable account indices
pub readonly_indexes: Vec<u8>, // Readonly account indices
}
```
**Characteristics:**
- Starts with version byte: `0x80` (128 in decimal, version 0)
- Includes `address_table_lookups` field
- Can reference accounts from ALTs using 1-byte indices
- Enables 64+ accounts per transaction
**Transaction size calculation:**
```
Version 0 overhead:
+ 1 byte (version)
+ 1 byte (number of lookup tables)
+ 34 bytes per lookup table (32-byte address + 2 length bytes)
+ 1 byte per account index referenced
Example with 1 ALT referencing 30 accounts:
1 (version) + 1 (table count) + 34 (table) + 30 (indices) = 66 bytes
Equivalent legacy transaction:
30 accounts × 32 bytes = 960 bytes
Savings: 960 - 66 = 894 bytes!
```
## Address Lookup Tables (ALTs)
### What Are ALTs?
Address Lookup Tables are **on-chain accounts** that store collections of related addresses. They act as a lookup mechanism to compress account references in transactions.
**Key properties:**
- Managed by the Address Lookup Table Program (`AddressLookupTableProgram`)
- Store up to **256 addresses** (indexed by u8: 0-255)
- Can be created, extended, deactivated, and closed
- Addresses are append-only for security
### ALT Account Structure
```rust
pub struct AddressLookupTable<'a> {
pub meta: LookupTableMeta,
pub addresses: Cow<'a, [Pubkey]>,
}
pub struct LookupTableMeta {
pub deactivation_slot: Slot, // Slot when deactivated (u64::MAX if active)
pub last_extended_slot: Slot, // Last slot when addresses were added
pub last_extended_slot_start_index: u8, // Index where last extension started
pub authority: Option<Pubkey>, // Can add/deactivate (None = immutable)
}
```
**On-chain layout:**
```
Bytes 0-55: LookupTableMeta (56 bytes)
Bytes 56+: Raw list of Pubkey addresses (32 bytes each)
```
### Creating Address Lookup Tables
**Step 1: Create the table**
```rust
use solana_sdk::{
address_lookup_table_account::instruction as alt_instruction,
instruction::Instruction,
pubkey::Pubkey,
signer::Signer,
};
// Get recent slot for table derivation
let recent_slot = rpc_client.get_slot()?;
// Create lookup table instruction
let (create_ix, lookup_table_address) = alt_instruction::create_lookup_table(
payer.pubkey(), // Authority
payer.pubkey(), // Payer
recent_slot, // Recent slot for PDA derivation
);
// The lookup table address is derived deterministically:
// PDA(seeds=[authority, recent_slot], program=AddressLookupTableProgram)
```
**Transaction to create:**
```rust
let create_tx = Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction(&create_tx)?;
```
**Important**: Wait for the transaction to be **finalized** before extending or using the table.
**Step 2: Extend the table with addresses**
```rust
// Addresses to add to the lookup table
let addresses_to_add = vec![
pubkey1,
pubkey2,
pubkey3,
// ... up to ~20 addresses per transaction
];
let extend_ix = alt_instruction::extend_lookup_table(
lookup_table_address,
payer.pubkey(), // Authority
Some(payer.pubkey()), // Payer (optional)
addresses_to_add,
);
let extend_tx = Transaction::new_signed_with_payer(
&[extend_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction(&extend_tx)?;
```
**Batching strategy:**
- Each extend operation can add approximately **20 addresses** before hitting transaction size limits
- For more addresses, send multiple extend transactions
- Example from TeamRaccoons repo: Batch in chunks of 20
```rust
// Batch extend for large address sets
let batch_size = 20;
for chunk in addresses.chunks(batch_size) {
let extend_ix = alt_instruction::extend_lookup_table(
lookup_table_address,
authority.pubkey(),
Some(payer.pubkey()),
chunk.to_vec(),
);
// Send transaction...
rpc_client.send_and_confirm_transaction(&tx)?;
}
```
**Warmup period:**
- Newly added addresses require **1 slot** before they can be used
- Must wait for finalization before using in v0 transactions
- Check `last_extended_slot` to ensure addresses are ready
**Step 3: Fetch the lookup table**
```rust
use solana_client::rpc_client::RpcClient;
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
let lookup_table_account = rpc_client
.get_account(&lookup_table_address)?;
let lookup_table = AddressLookupTableAccount::deserialize(&lookup_table_account.data)?;
// Access addresses
println!("Table contains {} addresses", lookup_table.addresses.len());
for (index, address) in lookup_table.addresses.iter().enumerate() {
println!("Index {}: {}", index, address);
}
```
### Using ALTs in V0 Transactions
**Build a v0 transaction with ALT:**
```rust
use solana_sdk::{
message::{v0, VersionedMessage},
transaction::VersionedTransaction,
address_lookup_table_account::AddressLookupTableAccount,
};
// 1. Create your instructions (can reference >35 accounts)
let instructions = vec![
// Your program instructions
];
// 2. Fetch lookup table accounts
let lookup_table_account = rpc_client.get_account(&lookup_table_address)?;
let lookup_table = AddressLookupTableAccount::deserialize(&lookup_table_account.data)?;
// 3. Build v0 message
let v0_message = v0::Message::try_compile(
&payer.pubkey(),
&instructions,
&[lookup_table], // Pass lookup tables here
recent_blockhash,
)?;
// 4. Create versioned transaction
let versioned_tx = VersionedTransaction::try_new(
VersionedMessage::V0(v0_message),
&[&payer], // Signers
)?;
// 5. Send transaction
let signature = rpc_client.send_and_confirm_transaction(&versioned_tx)?;
```
**How accounts are referenced:**
When you create an instruction with accounts that exist in the ALT:
```rust
use solana_sdk::instruction::{AccountMeta, Instruction};
// These accounts are in the lookup table at indices 0, 1, 2
let account_in_alt_0 = Pubkey::new_unique();
let account_in_alt_1 = Pubkey::new_unique();
let account_in_alt_2 = Pubkey::new_unique();
let ix = Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![
AccountMeta::new(account_in_alt_0, false), // Index 0 in ALT
AccountMeta::new_readonly(account_in_alt_1, false), // Index 1
AccountMeta::new(account_in_alt_2, false), // Index 2
],
);
// When compiled with ALT, these become 1-byte indices instead of 32-byte addresses
```
### Deactivating and Closing ALTs
**Deactivation:**
```rust
let deactivate_ix = alt_instruction::deactivate_lookup_table(
lookup_table_address,
authority.pubkey(),
);
rpc_client.send_and_confirm_transaction(&tx)?;
```
**Why deactivate?**
- Prevents the table from being used in new transactions
- Required before closing
- Creates a safety cooldown period
**Cooldown period:**
- Must wait until the deactivation slot exits the slot hashes sysvar (~2.5 days on mainnet)
- Prevents same-slot recreation attacks
- Ensures no in-flight transactions reference the table
**Closing:**
```rust
let close_ix = alt_instruction::close_lookup_table(
lookup_table_address,
authority.pubkey(),
recipient.pubkey(), // Receives reclaimed rent
);
rpc_client.send_and_confirm_transaction(&tx)?;
```
**Requirements:**
- Table must be deactivated first
- Deactivation slot must have exited slot hashes sysvar
- Only authority can close
- Rent is returned to specified recipient
### Freezing ALTs (Making Immutable)
```rust
let freeze_ix = alt_instruction::freeze_lookup_table(
lookup_table_address,
authority.pubkey(),
);
rpc_client.send_and_confirm_transaction(&tx)?;
```
**Effect:**
- Sets authority to `None`
- Table becomes **permanently immutable**
- Cannot add more addresses
- Cannot deactivate or close
- Useful for protocol-level tables that should never change
## RPC Configuration for V0 Transactions
**Critical requirement**: When fetching transactions, you must specify support for versioned transactions:
```rust
use solana_client::rpc_config::RpcTransactionConfig;
use solana_transaction_status::UiTransactionEncoding;
let config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Json),
commitment: Some(CommitmentConfig::confirmed()),
max_supported_transaction_version: Some(0), // REQUIRED!
};
let tx = rpc_client.get_transaction_with_config(&signature, config)?;
```
**Without `max_supported_transaction_version: Some(0)`:**
- RPC calls will **fail** if they encounter a v0 transaction
- Error: "Transaction version is not supported"
- This affects: `getTransaction`, `getBlock`, `getSignaturesForAddress`, etc.
**For account subscriptions:**
```rust
use solana_client::rpc_config::RpcAccountInfoConfig;
let config = RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::JsonParsed),
commitment: Some(CommitmentConfig::confirmed()),
// No max_supported_transaction_version needed for account queries
};
```
## Limitations and Constraints
### Hard Limits
1. **256 addresses per table** (u8 index limit)
- Tables use 1-byte indices
- Cannot store more than 256 addresses
- Create multiple tables if needed
2. **256 unique accounts total per transaction**
- Solana runtime limit
- Includes both direct accounts and ALT references
- Accounts can appear multiple times in instructions
3. **~20 addresses per extend operation**
- Limited by transaction size
- Must batch large address sets
4. **Transaction signers cannot be in ALTs**
- All signers must be explicitly listed in the transaction
- Cannot reference signer accounts from lookup tables
- This is a security feature
5. **No recursive lookups**
- Cannot reference another ALT from within an ALT
- Cannot store ALT addresses in an ALT
### Security Constraints
1. **Append-only design**
- Addresses cannot be removed or modified
- Prevents front-running attacks
- Once added, addresses are permanent (until table is closed)
2. **Warmup requirement**
- New addresses need 1 slot before use
- Prevents same-slot manipulation
- Must wait for finalization
3. **Deactivation cooldown**
- Tables cannot be closed immediately after deactivation
- Must wait for slot to exit slot hashes sysvar
- Protects in-flight transactions
4. **Authority control**
- Only authority can extend or deactivate
- Set to `None` to make immutable
- Cannot change authority after freezing
### Hardware Wallet Limitations
**Issue**: Hardware wallets cannot verify accounts referenced from ALTs
**Why:**
- Hardware wallets display all transaction accounts for user verification
- They don't have access to fetch lookup table data on-chain
- Cannot show which addresses the indices reference
**Implications:**
- Users must trust that the correct lookup table is being used
- Phishing risk: Malicious apps could use attacker-controlled ALTs
- Hardware wallet UX shows: "This transaction uses address lookup tables"
**Mitigations:**
- Use well-known, immutable (frozen) ALTs when possible
- Publish ALT addresses in protocol documentation
- Verify ALT contents before use in client code
- Consider adding integrity check instructions
## Security Best Practices
### 1. Wait for Finalization
```rust
// BAD: Using immediately after creation
let (create_ix, alt_address) = alt_instruction::create_lookup_table(...);
rpc_client.send_transaction(&create_tx)?; // Not confirmed!
let extend_ix = alt_instruction::extend_lookup_table(alt_address, ...); // FAILS!
// GOOD: Wait for finalization
rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?;
// Now safe to extend
rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?;
// Now safe to use in v0 transactions
```
### 2. Verify Lookup Table Contents
```rust
// Fetch and verify before use
let lookup_table = rpc_client.get_account(&alt_address)?;
let alt = AddressLookupTableAccount::deserialize(&lookup_table.data)?;
// Verify expected addresses
assert_eq!(alt.addresses.len(), expected_count);
assert_eq!(alt.addresses[0], expected_address_0);
// Check authority if relevant
if let Some(authority) = alt.meta.authority {
assert_eq!(authority, expected_authority);
}
```
### 3. Add Integrity Check Instructions
For critical operations, add an instruction that verifies the lookup table contents:
```rust
// Your program instruction
pub fn verify_lookup_table(
ctx: Context<VerifyLookupTable>,
expected_addresses: Vec<Pubkey>,
) -> Result<()> {
let lookup_table = &ctx.accounts.lookup_table;
// Verify table contains expected addresses
for (i, expected) in expected_addresses.iter().enumerate() {
require_keys_eq!(
lookup_table.addresses[i],
*expected,
ErrorCode::InvalidLookupTable
);
}
Ok(())
}
```
### 4. Use Immutable Tables for Protocols
```rust
// After fully populating a protocol-level table
let freeze_ix = alt_instruction::freeze_lookup_table(
protocol_alt_address,
authority.pubkey(),
);
rpc_client.send_and_confirm_transaction(&freeze_tx)?;
// Now the table is permanently immutable
// Users can trust it won't change
```
### 5. Front-Running Prevention
**Why ALTs are append-only:**
```rust
// If removal were allowed, this attack would be possible:
// 1. User submits swap transaction using ALT at index 5
// 2. Attacker sees pending transaction
// 3. Attacker removes legitimate address, adds malicious address at index 5
// 4. User's transaction executes with malicious address
// Append-only design prevents this:
// - Addresses cannot be removed
// - Indices remain stable
// - Order cannot change
```
## Code Examples
### Complete Example: Multi-Swap with ALT
Based on the TeamRaccoons address-lookup-table-multi-swap example:
```rust
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
address_lookup_table_account::instruction as alt_instruction,
address_lookup_table_account::AddressLookupTableAccount,
commitment_config::CommitmentConfig,
instruction::Instruction,
message::{v0, VersionedMessage},
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::{Transaction, VersionedTransaction},
};
fn create_and_use_alt_for_swaps() -> Result<()> {
let rpc_client = RpcClient::new_with_commitment(
"https://api.devnet.solana.com".to_string(),
CommitmentConfig::confirmed(),
);
let payer = Keypair::new();
// Fund payer...
// Step 1: Collect all accounts needed for swap chain
let swap_accounts = vec![
token_program_id,
associated_token_program_id,
swap_program_1,
pool_1_address,
pool_1_authority,
pool_1_token_a,
pool_1_token_b,
swap_program_2,
pool_2_address,
pool_2_authority,
pool_2_token_a,
pool_2_token_b,
// ... many more accounts
];
// Step 2: Create lookup table
let recent_slot = rpc_client.get_slot()?;
let (create_ix, alt_address) = alt_instruction::create_lookup_table(
payer.pubkey(),
payer.pubkey(),
recent_slot,
);
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let create_tx = Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?;
println!("Created ALT at {}", alt_address);
// Step 3: Extend in batches of 20
for (batch_num, chunk) in swap_accounts.chunks(20).enumerate() {
let extend_ix = alt_instruction::extend_lookup_table(
alt_address,
payer.pubkey(),
Some(payer.pubkey()),
chunk.to_vec(),
);
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let extend_tx = Transaction::new_signed_with_payer(
&[extend_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?;
println!("Extended ALT batch {}", batch_num);
}
// Step 4: Fetch the populated lookup table
let alt_account = rpc_client.get_account(&alt_address)?;
let lookup_table = AddressLookupTableAccount::deserialize(&alt_account.data)?;
println!("ALT contains {} addresses", lookup_table.addresses.len());
// Step 5: Build multi-swap transaction using ALT
let swap_instructions = vec![
create_swap_instruction(0, 1, 2, 3, 4, 5, 6), // Indices into ALT
create_swap_instruction(7, 8, 9, 10, 11, 12, 13),
create_swap_instruction(14, 15, 16, 17, 18, 19, 20),
// Many more swaps...
];
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let v0_message = v0::Message::try_compile(
&payer.pubkey(),
&swap_instructions,
&[lookup_table],
recent_blockhash,
)?;
let versioned_tx = VersionedTransaction::try_new(
VersionedMessage::V0(v0_message),
&[&payer],
)?;
// Step 6: Send v0 transaction
let signature = rpc_client.send_and_confirm_transaction(&versioned_tx)?;
println!("Multi-swap completed: {}", signature);
Ok(())
}
fn create_swap_instruction(
swap_program: u8,
pool: u8,
authority: u8,
source: u8,
dest: u8,
pool_token_a: u8,
pool_token_b: u8,
) -> Instruction {
// Create instruction with account indices
// These will be resolved from the ALT
Instruction {
program_id: /* from ALT index swap_program */,
accounts: vec![
AccountMeta::new(/* ALT index pool */, false),
AccountMeta::new_readonly(/* ALT index authority */, false),
// ... etc
],
data: /* swap instruction data */,
}
}
```
### Example: Protocol-Level Immutable ALT
```rust
// Create a permanent lookup table for protocol accounts
fn create_protocol_alt(
authority: &Keypair,
protocol_accounts: Vec<Pubkey>,
) -> Result<Pubkey> {
let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com");
// Create table
let recent_slot = rpc_client.get_slot()?;
let (create_ix, alt_address) = alt_instruction::create_lookup_table(
authority.pubkey(),
authority.pubkey(),
recent_slot,
);
let create_tx = /* ... */;
rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?;
// Extend with all protocol accounts
for chunk in protocol_accounts.chunks(20) {
let extend_ix = alt_instruction::extend_lookup_table(
alt_address,
authority.pubkey(),
Some(authority.pubkey()),
chunk.to_vec(),
);
let extend_tx = /* ... */;
rpc_client.send_and_confirm_transaction_with_spinner(&extend_tx)?;
}
// Freeze the table (make immutable)
let freeze_ix = alt_instruction::freeze_lookup_table(
alt_address,
authority.pubkey(),
);
let freeze_tx = /* ... */;
rpc_client.send_and_confirm_transaction_with_spinner(&freeze_tx)?;
println!("Created immutable protocol ALT at {}", alt_address);
// Publish this address in documentation
// Users can trust it won't change
Ok(alt_address)
}
```
## Troubleshooting
### Common Errors and Solutions
**Error: "Transaction version is not supported"**
```rust
// Problem: RPC not configured for v0 transactions
let tx = rpc_client.get_transaction(&signature)?; // FAILS
// Solution: Set max_supported_transaction_version
let config = RpcTransactionConfig {
max_supported_transaction_version: Some(0),
..Default::default()
};
let tx = rpc_client.get_transaction_with_config(&signature, config)?; // Works
```
**Error: "Address lookup table not found"**
```rust
// Problem: Using table before creation is finalized
let (create_ix, alt_address) = alt_instruction::create_lookup_table(...);
rpc_client.send_transaction(&tx)?; // Sent but not confirmed
let extend_ix = alt_instruction::extend_lookup_table(alt_address, ...); // FAILS
// Solution: Wait for confirmation
rpc_client.send_and_confirm_transaction_with_spinner(&create_tx)?;
// Now table exists
```
**Error: "Invalid lookup table index"**
```rust
// Problem: Referencing index beyond table size
let lookup_table = /* has 10 addresses */;
let ix = Instruction {
accounts: vec![
AccountMeta::new(/* index 15 */, false), // FAILS - index out of bounds
],
// ...
};
// Solution: Verify table contents and use valid indices
assert!(index < lookup_table.addresses.len());
```
**Error: "Cannot deactivate lookup table"**
```rust
// Problem: Not the authority
let deactivate_ix = alt_instruction::deactivate_lookup_table(
alt_address,
wrong_authority.pubkey(), // Not the actual authority
);
// Solution: Use the correct authority
let alt = AddressLookupTableAccount::deserialize(&account.data)?;
let correct_authority = alt.meta.authority.expect("Table has no authority");
let deactivate_ix = alt_instruction::deactivate_lookup_table(
alt_address,
correct_authority,
);
```
**Error: "Cannot close lookup table"**
```rust
// Problem 1: Table not deactivated
let close_ix = alt_instruction::close_lookup_table(...); // FAILS
// Solution: Deactivate first, then wait
let deactivate_ix = alt_instruction::deactivate_lookup_table(...);
// ... send deactivate transaction ...
// ... wait for cooldown period (~2.5 days mainnet) ...
let close_ix = alt_instruction::close_lookup_table(...);
// Problem 2: Cooldown period not complete
// Solution: Check if deactivation slot has exited slot hashes
let slot_hashes = rpc_client.get_slot_hashes()?;
let oldest_slot = slot_hashes.last().unwrap().0;
if alt.meta.deactivation_slot < oldest_slot {
// Safe to close
}
```
## Use Cases and Patterns
### 1. DEX Aggregators
**Problem**: Multi-hop swaps require many accounts (pools, authorities, token accounts)
**Solution**: Create ALT with all pool accounts
```rust
// ALT contains:
// [0-19]: Pool 1 accounts (program, pool, authority, tokens, mint, etc.)
// [20-39]: Pool 2 accounts
// [40-59]: Pool 3 accounts
// [60-79]: Common accounts (token program, associated token program, etc.)
// Transaction can now execute 3+ swaps atomically
```
### 2. Complex Protocol Interactions
**Problem**: DeFi protocols compose multiple programs (lending, swapping, staking)
**Solution**: Protocol-specific ALT with all contract addresses
```rust
// Protocol ALT:
// [0]: Program ID
// [1]: Global config account
// [2-10]: Pool addresses
// [11-20]: Oracle addresses
// [21-30]: Treasury accounts
// etc.
```
### 3. NFT Minting/Trading
**Problem**: Minting or trading multiple NFTs requires many metadata accounts
**Solution**: Collection-specific ALT with all related accounts
```rust
// Collection ALT:
// [0]: Candy machine
// [1]: Collection mint
// [2]: Collection metadata
// [3]: Collection master edition
// [4-100]: Individual NFT addresses
```
### 4. Transaction Builder Programs
**Problem**: Building very large transactions (>64 accounts)
**Solution**: Multi-transaction pattern with ALTs
```rust
// Transaction 1: Create and populate ALT
// Transaction 2: Execute main operation using ALT
// Transaction 3: Clean up and close ALT
```
## Best Practices Summary
1. **Always wait for finalization** before using newly created or extended tables
2. **Batch extend operations** in chunks of ~20 addresses
3. **Verify table contents** before use in production
4. **Use immutable tables** for protocol-level accounts
5. **Set max_supported_transaction_version** in all RPC calls
6. **Document ALT addresses** for protocol integrators
7. **Consider hardware wallet UX** - frozen tables are more trustworthy
8. **Add integrity checks** for critical operations
9. **Plan for cooldown** when closing tables
10. **Keep signers explicit** - never try to put signers in ALTs
## Program Compatibility
**Important**: Programs are **completely unaware** of whether they were called via legacy or v0 transactions.
From the program's perspective:
- Account references work identically
- No code changes needed
- Same `AccountInfo` structures
- Same validation logic
The transaction version only affects:
- How accounts are referenced in the transaction
- Transaction size limits
- Client-side transaction construction
**This means:**
- Existing programs work with v0 transactions without modification
- New programs don't need version-specific logic
- ALTs are purely a client-side optimization
## Resources
### Official Documentation
- [Versioned Transactions Guide](https://solana.com/developers/guides/advanced/versions)
- [Address Lookup Tables Guide](https://solana.com/developers/guides/advanced/lookup-tables)
- [Versioned Transactions Proposal](https://docs.anza.xyz/proposals/versioned-transactions)
### Code Examples
- [TeamRaccoons Multi-Swap Example](https://github.com/TeamRaccoons/address-lookup-table-multi-swap)
- [Solana Program Library - Address Lookup Table](https://github.com/solana-labs/solana-program-library/tree/master/address-lookup-table)
### Technical References
- [AddressLookupTableProgram Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/address_lookup_table/instruction.rs)
- [solana-sdk VersionedTransaction](https://docs.rs/solana-sdk/latest/solana_sdk/transaction/struct.VersionedTransaction.html)
- [solana-sdk Message v0](https://docs.rs/solana-sdk/latest/solana_sdk/message/v0/struct.Message.html)
### Community Resources
- [Solana Cookbook - Versioned Transactions](https://solanacookbook.com/references/basic-transactions.html#versioned-transactions)
- [Solana Stack Exchange - ALT Questions](https://solana.stackexchange.com/questions/tagged/address-lookup-table)

View File

@@ -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.**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,386 @@
# Important Caveats
Critical limitations, quirks, and gotchas in Solana and Anchor development that every security reviewer must know.
## Anchor Framework Limitations
### 1. `init_if_needed` Re-initialization Risk
```rust
// Dangerous: Can bypass initialization logic
#[account(init_if_needed, payer = user, space = ...)]
pub user_account: Account<'info, UserAccount>,
```
**Issue:** If account already exists, initialization is skipped entirely. Existing malicious or inconsistent data is not validated.
**When to use:** Only when you explicitly validate existing accounts in instruction logic.
### 2. `AccountLoader` Missing Discriminator Check
```rust
// Does NOT validate discriminator by default!
#[account(mut)]
pub user: AccountLoader<'info, User>,
```
**Issue:** `AccountLoader` is for zero-copy accounts and doesn't check the account discriminator automatically. Enables type cosplay attacks.
**Solution:** Use `Account<'info, T>` when possible, or add manual discriminator check.
### 3. `close` Constraint Ordering
```rust
// ❌ Wrong: close must be last
#[account(
close = receiver,
mut,
has_one = authority
)]
// ✅ Correct: close is last
#[account(
mut,
has_one = authority,
close = receiver
)]
```
**Issue:** Anchor processes constraints in order. If `close` isn't last, subsequent constraints may check zeroed account.
### 4. Space Calculation Errors Are Permanent
```rust
// If this space is wrong, account is unusable!
#[account(
init,
payer = user,
space = 8 + 32 // Too small = can't deserialize later!
)]
pub user_account: Account<'info, UserAccount>,
```
**Issue:** Once initialized, account size is fixed. Too small = deserialization fails. Too large = wasted rent.
**Solution:** Always use `InitSpace` derive macro:
```rust
#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub authority: Pubkey,
#[max_len(100)]
pub name: String,
}
// Then use:
space = 8 + UserAccount::INIT_SPACE
```
### 5. `constraint` Expression Limitations
```rust
// constraint expressions can't call functions that return Results!
#[account(
constraint = some_validation(account.value)? @ ErrorCode::Invalid // Compile error!
)]
```
**Issue:** Constraint expressions must be simple boolean checks. Cannot use `?` operator.
**Solution:** Validate in instruction body for complex checks.
## Solana Runtime Quirks
### 1. Account Data Persists After Zeroing Lamports
```rust
// Within same transaction:
**account.lamports.borrow_mut() = 0;
let data = account.try_borrow_data()?; // Still readable!
```
**Issue:** Account data remains accessible within the transaction even after lamports are zeroed. Only garbage collected after transaction completes.
**Implication:** Always check lamports before reading account data.
### 2. Non-Canonical PDA Bumps
```rust
// Multiple PDAs possible with different bumps!
let (pda_255, bump_255) = Pubkey::find_program_address(seeds, program_id); // bump = 255
let (pda_254, bump_254) = Pubkey::create_program_address(&[seeds, &[254]], program_id); // Also valid!
```
**Issue:** Same seeds can derive multiple PDAs with different bumps. Creates confusion and potential exploits.
**Solution:** Always use canonical bump (255 counting down to first valid). Anchor's `bump` constraint enforces this.
### 3. Compute Budget Limits
| Network | Base Compute Units | With Optimization |
|---------|-------------------|-------------------|
| Mainnet | 200,000 | Up to 1,400,000 (with request) |
| Devnet | 200,000 | Up to 1,400,000 |
**Issue:** Complex programs can exceed compute budget, causing transaction failure.
**Optimization strategies:**
- Minimize CPIs (each costs ~1000 CU)
- Use `AccountLoader` for large accounts
- Avoid loops with variable length
- Request higher compute budget: `ComputeBudgetProgram::set_compute_unit_limit()`
### 4. Transaction Size Limit
**Hard limit:** ~1232 bytes for transaction
**Implications:**
- Limits number of accounts (~35-40 accounts typical max)
- Large instructions need Account Compression or chunking
- Can't pass large data directly in instruction
**Solutions:**
- Use PDAs to store large data
- Break operations into multiple transactions
- Use lookup tables for frequent accounts
### 5. Account Snapshot Loading
```rust
let balance_before = ctx.accounts.vault.balance;
// CPI happens here
// balance_before is STALE - account was loaded before CPI
```
**Issue:** Accounts are loaded as snapshots at transaction start. Modifications during transaction (via CPIs) don't update the loaded data.
**Solution:** Call `.reload()` after any CPI that might modify the account.
## Token Program Gotchas
### 1. ATA Addresses Are Deterministic But Not Guaranteed
```rust
let ata = get_associated_token_address(&owner, &mint);
// ata address is deterministic but account might not exist!
```
**Issue:** ATA address can be calculated but account may not be initialized.
**Solution:** Check account exists and is initialized before use, or use `init_if_needed` with proper validation.
### 2. Delegates Don't Automatically Reset
```rust
// After transfer of ownership:
token_account.owner = new_owner;
// BUT: delegate and delegated_amount are NOT reset!
```
**Issue:** Changing owner doesn't clear delegate/close authority. Old delegate can still spend.
**Solution:** Explicitly reset authorities when changing ownership:
```rust
account.delegate = COption::None;
account.delegated_amount = 0;
if account.is_native() {
account.close_authority = COption::None;
}
```
### 3. Token-2022 Extension Rent
**Issue:** Each extension adds rent cost. Account size varies by extensions enabled.
**Extensions and their sizes:**
- Transfer Fee: ~83 bytes
- Transfer Hook: ~107 bytes
- Permanent Delegate: ~36 bytes
- Interest Bearing: ~40 bytes
**Solution:** Calculate rent based on all enabled extensions.
### 4. Token-2022 Transfer Hooks Can Be Malicious
```rust
// Transfer hook can call arbitrary program!
pub struct TransferHookAccount {
pub program_id: Pubkey, // Could be malicious
}
```
**Issue:** Transfer hook extensions allow calling external program during transfers. Malicious hook can fail transaction or drain funds.
**Solution:**
- Validate transfer hook program if accepting specific tokens
- Consider disallowing tokens with transfer hooks
- Use Anchor's `TransferChecked` instruction
## Testing Blind Spots
### 1. Concurrent Transaction Ordering
**Issue:** Tests typically run transactions sequentially. In production, concurrent transactions can interleave in unexpected ways.
**Vulnerability example:**
```rust
// Transaction 1: Check balance = 100
// Transaction 2: Withdraw 80 (balance now 20)
// Transaction 1: Withdraw 80 (uses stale check, balance now -60!)
```
**Mitigation:**
- Use atomic operations
- Reload accounts before critical operations
- Design for idempotency
### 2. Account Rent Reclaim Attacks
**Issue:** When account rent falls below minimum, validator can reclaim the account. Tests don't simulate this.
**Solution:** Ensure all accounts are rent-exempt (2+ years of rent).
### 3. Sysvar Manipulation in Tests
```rust
// In tests, you can set arbitrary clock values
ctx.accounts.clock = Clock { unix_timestamp: attacker_value, ... };
```
**Issue:** Tests may not catch reliance on tamper-resistant sysvars.
**Solution:** In production, always load sysvars from official sysvar accounts:
```rust
pub clock: Sysvar<'info, Clock>, // Validated address
```
### 4. Devnet vs Mainnet Differences
| Aspect | Devnet | Mainnet |
|--------|--------|---------|
| Oracle prices | Often stale/fake | Real-time |
| Program versions | May differ | Stable versions |
| Compute limits | More lenient | Strict |
| Congestion | Minimal | Can be high |
| Token availability | Test tokens | Real value |
**Issue:** Programs tested only on devnet may fail on mainnet.
**Solution:** Test on mainnet-fork or mainnet with small amounts before full deployment.
## Rust-Specific Gotchas
### 1. `unwrap()` Panics
```rust
// Panics kill the entire transaction!
let value = some_option.unwrap(); // ❌ Never do this
```
**Solution:** Always use proper error handling:
```rust
let value = some_option.ok_or(ErrorCode::MissingValue)?;
```
### 2. Integer Division Truncation
```rust
let result = 5 / 2; // result = 2, not 2.5!
```
**Issue:** Integer division truncates, potentially causing precision loss in financial calculations.
**Solution:** Use `Decimal` type for precise calculations, or multiply before divide:
```rust
let result = (5 * PRECISION) / 2 / PRECISION;
```
### 3. Overflow in Debug vs Release
```rust
// Debug mode: panics on overflow
// Release mode: wraps silently!
let x: u8 = 255;
let y = x + 1; // Debug: panic, Release: y = 0
```
**Solution:** Always use `checked_*` methods - they work same in debug and release.
## Cross-Program Invocation (CPI) Gotchas
### 1. CPI Success Doesn't Guarantee Correct State
```rust
// CPI returns success but state may be unexpected
invoke(&transfer_instruction, &accounts)?;
// Transfer succeeded but amount might be different due to fees!
```
**Solution:** Reload and validate account state after CPI.
### 2. Signer Seeds Must Be Exact
```rust
// Seeds for signing must match PDA derivation exactly
let seeds = &[
b"vault",
user.key().as_ref(),
&[bump], // Must be same bump used to derive PDA
];
invoke_signed(&instruction, &accounts, &[seeds])?;
```
**Issue:** Wrong seeds = "signature verification failed" error.
### 3. CPI Depth Limit
**Limit:** 4 levels of CPI depth
**Issue:** Program A → Program B → Program C → Program D → Program E (fails!)
**Solution:** Design programs to minimize CPI depth.
## Common Misunderstandings
### 1. "Anchor Prevents All Security Issues"
**False:** Anchor prevents some common issues (missing discriminators, wrong account types) but doesn't validate business logic, arithmetic, or authorization.
### 2. "Devnet Testing Is Sufficient"
**False:** Mainnet has different compute limits, real oracle data, congestion, and MEV considerations.
### 3. "One Audit Makes Code Secure"
**False:** Audits find issues in a snapshot. Code changes after audit reintroduce risk. Need continuous security review.
### 4. "`checked_*` Methods Are Slower"
**False:** Rust compiler optimizes these similarly to unchecked arithmetic. Always use checked methods.
### 5. "PDAs Can't Sign"
**True for external transactions, false for CPIs:** PDAs can sign CPIs using `invoke_signed` but can't sign transactions directly.
## Version-Specific Issues
### Anchor Version Compatibility
- **< 0.28**: No `InitSpace` derive, manual space calculation error-prone
- **< 0.29**: Different constraint syntax
- **0.30+**: Breaking changes in error handling and account initialization
**Solution:** Check `Cargo.toml` for version and consult [Anchor Changelog](https://github.com/coral-xyz/anchor/blob/master/CHANGELOG.md).
### Solana Version Differences
- **Pre-1.14**: Different fee structure
- **Pre-1.16**: No Address Lookup Tables
- **Pre-1.17**: No Token-2022
**Solution:** Verify target Solana version matches deployment network.
---
**Key Takeaway:** Many "obvious" assumptions about blockchain behavior don't hold in Solana. Always validate against actual runtime behavior, not assumptions from other chains.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
# Resources
Comprehensive collection of official documentation, security guides, audit reports, and learning materials for Solana development and security.
## Official Documentation
### Solana Core
- [Solana Docs](https://solana.com/docs/) - Official Solana documentation
- [Solana Cookbook](https://solana.com/developers/cookbook) - Recipes for common Solana tasks
- [Solana Courses](https://solana.com/developers/courses/) - Official learning paths
- [Program Examples](https://github.com/solana-developers/program-examples) - Multi-framework examples
- [Developer Bootcamp 2024](https://github.com/solana-developers/developer-bootcamp-2024)
### Anchor Framework
- [Anchor Docs](https://www.anchor-lang.com/docs) - Official Anchor documentation
- [Anchor Book](https://book.anchor-lang.com/) - Comprehensive Anchor guide
- [Anchor by Example](https://examples.anchor-lang.com/) - Example programs
- [Anchor Lang Docs](https://docs.rs/anchor-lang) - API documentation
- [Anchor SPL Docs](https://docs.rs/anchor-spl) - SPL integration helpers
### SPL Programs
- [SPL Documentation](https://spl.solana.com/) - Solana Program Library docs
- [Token Program](https://github.com/solana-program/token) - SPL Token source
- [Token-2022](https://github.com/solana-program/token-2022) - Next-gen token program
- [Associated Token Account](https://github.com/solana-program/associated-token-account)
- [Token Metadata](https://github.com/solana-program/token-metadata)
- [Metaplex Token Metadata](https://github.com/metaplex-foundation/mpl-token-metadata)
## Security Resources
### Curated Security Lists
- [Awesome Solana Security (0xMacro)](https://github.com/0xMacro/awesome-solana-security) - **Actively maintained**, comprehensive resource list
- [Rektoff Security Roadmap](https://github.com/Rektoff/Security-Roadmap-for-Solana-applications) - Full lifecycle security strategy
- [SlowMist Best Practices](https://github.com/slowmist/solana-smart-contract-security-best-practices) - Common pitfalls with examples
- [Ackee Solana Handbook](https://ackee.xyz/solana/book/latest/) - Comprehensive development guide
### Security Guides & Articles
- [Helius Security Guide](https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security) - Common vulnerabilities explained
- [Neodyme Breakpoint Workshop](https://github.com/neodyme-labs/neodyme-breakpoint-workshop) - Hands-on security training
- [Solana Security Course](https://solana.com/developers/courses/program-security) - Official security course
- [Asymmetric Research CPI Vulnerabilities](https://blog.asymmetric.re/invocation-security-navigating-vulnerabilities-in-solana-cpis/)
- [Ottersec Lamport Transfers](https://osec.io/blog/2025-05-14-king-of-the-sol) - SOL transfer vulnerabilities
- [Infect3d Auditing Essentials](https://www.infect3d.xyz/blog/solana-quick-start)
### Vulnerability Collections
- [Urataps Audit Examples](https://github.com/urataps/solana-audit-examples) - Programs with vulnerabilities
- [ImmuneBytes Attack Vectors](https://github.com/ImmuneBytes-Security-Audit/Blockchain-Attack-Vectors/tree/main/Solana%20Attack%20Vectors)
- [Exvul Security Guide](https://exvul.com/rust-smart-contract-security-guide-in-solana/)
- [Nirlin Advanced Vulnerabilities](https://substack.com/inbox/post/164534668)
### Video Tutorials
- [Zigtur Security Walkthrough](https://www.youtube.com/watch?v=xd6qfY-GDYY)
- [M4rio Security Walkthrough](https://www.youtube.com/watch?v=q4z8tIi43lg)
### Token-2022 Security
- [Offside Token-2022 Part 1](https://blog.offside.io/p/token-2022-security-best-practices-part-1)
- [Offside Token-2022 Part 2](https://blog.offside.io/p/token-2022-security-best-practices-part-2)
- [Neodyme Token-2022 Security](https://neodyme.io/en/blog/token-2022)
### Deep Dives & Research
- [r0bre's 100 Daily Solana Tips](https://accretionxyz.substack.com/p/r0bres-100-daily-solana-tips)
- [Accretion Hidden IDL Instructions](https://accretionxyz.substack.com/p/hidden-idl-instructions-and-how-to)
- [Farouk ELALEM Under the Hood](https://ubermensch.blog/under-the-hood-of-solana-program-execution-from-rust-code-to-sbf-bytecode)
- [Lucrative_Panda Security History](https://medium.com/@lucrativepanda/a-comprehensive-analysis-of-solanas-security-history-all-incidents-impacts-and-evolution-up-to-1b1564c7ddfe)
## Essential Codebases to Study
Study these production codebases to learn security patterns:
### Framework & Core Programs
- [Anchor Framework](https://github.com/solana-foundation/anchor) - The framework itself
- [Solana System Program](https://github.com/solana-program/system)
- [SPL Token Program](https://github.com/solana-program/token)
- [Token-2022](https://github.com/solana-program/token-2022)
### Production Protocols
- [Raydium AMM](https://github.com/raydium-io/raydium-cp-swap) - DEX protocol
- [Kamino Lending](https://github.com/Kamino-Finance/klend) - Lending protocol
- [Squads Multisig](https://github.com/Squads-Protocol/v4) - Multisig protocol
## Audit Reports
Study real security audits to learn from actual vulnerabilities:
### Code4rena
- [Pump Science](https://code4rena.com/reports/2025-01-pump-science) - 2 High, 3 Medium
### Sherlock
- [Orderly](https://audits.sherlock.xyz/contests/524/report) - 2 High, 1 Medium
- [WOOFi](https://audits.sherlock.xyz/contests/535/report) - 2 High, 3 Medium
### Cantina
Contact `0xmorph` in Cantina Discord for read access:
- [Grass](https://cantina.xyz/competitions/3211ee0d-133f-43a0-837e-8dc1ecfaa424) - 13 High, 6 Medium
- [Olas](https://cantina.xyz/competitions/829164bf-7fba-4b84-a6b8-76652205bd97) - 2 High, 3 Medium
- [Tensor](https://cantina.xyz/competitions/21787352-de2c-4a77-af09-cc0a250d1f04) - 5 High, 10 Medium
- [ZetaChain](https://cantina.xyz/competitions/80a33cf0-ad69-4163-a269-d27756aacb5e) - 6 High, 27 Medium
- [Inclusive Finance](https://cantina.xyz/competitions/3eff5a8f-b73a-4cfe-8c54-546b475548f0) - 45 High, 25 Medium
- [Reserve Index](https://cantina.xyz/code/8b94becd-54e7-41cd-88e6-caae7becc76a) - 10 High, 11 Medium
## Learning Paths
### For EVM Developers
- [RareSkills Solana Course](https://www.rareskills.io/solana-tutorial) - Ethereum to Solana
- [0xkowloon Anchor for EVM](https://0xkowloon.gitbook.io/anchor-for-evm-developers)
### For Rust Learners
- [Rust Book](https://doc.rust-lang.org/book/)
- [Rust by Example](https://doc.rust-lang.org/rust-by-example/index.html)
### Native Rust (Non-Anchor)
- [Solana Native Rust Docs](https://solana.com/docs/programs/rust)
- [Native Development Course](https://solana.com/developers/courses/native-onchain-development)
### Blueshift Challenges
- [Blueshift Courses](https://learn.blueshift.gg/) - Anchor and Pinocchio
## Tools
### Development
- [Solana Playground](https://beta.solpg.io/) - Browser-based IDE
- [Rust Playground](https://play.rust-lang.org/) - Test Rust snippets
### Security & Analysis
- [Trident](https://github.com/Ackee-Blockchain/trident) - Fuzz testing framework
- [Certora Prover](https://docs.certora.com/en/latest/docs/solana/index.html) - Formal verification
- [Sec3 IDL Guesser](https://github.com/sec3-service/IDLGuesser) - Reverse engineer IDLs
- [Anchor X-ray](https://github.com/crytic/anchorx-ray) - Visualize accounts (Trail of Bits)
- [Anchor Version Detector](https://github.com/johnsaigle/anchor-version-detector) - Compatibility checker
### Testing
- [Anchor Test Framework](https://book.anchor-lang.com/anchor_in_depth/testing.html)
- [Solana Test Validator](https://docs.solana.com/developing/test-validator)
## CTFs & Practice
### Capture The Flag
- [Ackee Solana CTF](https://github.com/Ackee-Blockchain/Solana-Auditors-Bootcamp/tree/master/Capture-the-Flag)
### Bootcamps
- [Rektoff 6-Week Bootcamp](https://www.rektoff.xyz/bootcamp) - Free, Solana Foundation supported
- [Ackee Auditors Bootcamp](https://ackee.xyz/solana-auditors-bootcamp)
## Community & Support
### Q&A Platforms
- [Solana Stack Exchange](https://solana.stackexchange.com/)
### Blogs & Newsletters
- [Helius Blog](https://www.helius.dev/blog) - Frequent Solana content
- [Pine Analytics Substack](https://substack.com/@pineanalytics1) - Protocol deep dives
## Security Firms
Top firms for Solana security audits:
- [Runtime Verification](https://runtimeverification.com/)
- [OtterSec](https://osec.io/)
- [Neodyme](https://neodyme.io/en/)
- [Sec3](https://www.sec3.dev/)
- [Zellic](https://www.zellic.io/)
- [Ackee Blockchain](https://ackee.xyz/)
- [Hexens](https://hexens.io/)
- [Trail of Bits](https://www.trailofbits.com/)
- [Kudelski Security](https://kudelskisecurity.com/)
- [Cantina](https://cantina.xyz/)
- [Certora](https://www.certora.com/)
- [Sherlock](https://www.sherlock.xyz/)
## Version Information
- Latest Anchor version (as of 2025): 0.30+
- Recommended Solana CLI: Latest stable
- Rust minimum version: 1.70+
---
**Note:** This is a curated collection from the Awesome Solana Security repository and other trusted sources. Resources are selected for their quality, maintenance status, and relevance to modern Solana development practices.

View File

@@ -0,0 +1,291 @@
# Security Checklists
Comprehensive validation checklists for Solana program security reviews.
## Account Validation Checklist
For every account in every instruction:
- [ ] **Signer validation**: Uses `Signer<'info>` or `is_signer` check when needed
- [ ] **Owner validation**: Uses `#[account(owner = ...)]` or manual owner check
- [ ] **Writable checks**: Properly marked `mut` when account data will be modified
- [ ] **Account initialization**: Checks if account is initialized before use
- [ ] **PDA validation**: Validates seeds and uses canonical bump
- [ ] **Discriminator check**: For `AccountLoader`, validates account type
- [ ] **Account relationships**: Uses `has_one` for related accounts
```rust
// Complete account validation example
#[derive(Accounts)]
pub struct SecureInstruction<'info> {
#[account(
mut,
has_one = authority, // Relationship validation
seeds = [b"vault", authority.key().as_ref()],
bump, // Canonical bump
)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>, // Signer required
#[account(
mut,
constraint = token_account.owner == authority.key(), // Custom validation
)]
pub token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>, // Program validation
}
```
## Arithmetic Safety Checklist
For all mathematical operations:
- [ ] **Addition**: Uses `checked_add()` instead of `+`
- [ ] **Subtraction**: Uses `checked_sub()` instead of `-`
- [ ] **Multiplication**: Uses `checked_mul()` instead of `*`
- [ ] **Division**: Uses `checked_div()` instead of `/`
- [ ] **Division by zero**: Validates divisor is non-zero
- [ ] **Precision loss**: Uses `try_floor_u64()` instead of `try_round_u64()` to prevent arbitrage
- [ ] **Avoid saturating**: Does not use `saturating_*` methods (they hide errors)
- [ ] **Proper error handling**: All arithmetic wrapped in `ok_or(error)?`
```rust
// Secure arithmetic examples
let total = balance
.checked_add(amount)
.ok_or(ErrorCode::Overflow)?;
let share = total
.checked_div(denominator)
.ok_or(ErrorCode::DivisionByZero)?;
// For Decimal types (token amounts)
let liquidity = Decimal::from(collateral_amount)
.try_div(rate)?
.try_floor_u64()?; // Not try_round_u64()!
```
## PDA and Account Security Checklist
- [ ] **Canonical bump**: PDAs use `bump` in seeds constraint (not hardcoded)
- [ ] **Unique seeds**: Seeds include unique identifier (user pubkey, mint, etc.)
- [ ] **No duplicate accounts**: Same account not used twice as mutable
- [ ] **Init vs init_if_needed**: Uses `init` with proper validation, not `init_if_needed`
- [ ] **has_one constraints**: Related accounts validated with `has_one`
- [ ] **Custom constraints**: Complex validation uses `constraint` expression
- [ ] **Seed collision**: Seeds designed to prevent collisions
```rust
// Secure PDA patterns
#[account(
init,
payer = authority,
space = 8 + UserAccount::INIT_SPACE,
seeds = [
b"user",
authority.key().as_ref(), // Unique to user
mint.key().as_ref(), // Unique to mint
],
bump
)]
pub user_account: Account<'info, UserAccount>,
```
## CPI Security Checklist
For all Cross-Program Invocations:
- [ ] **Program validation**: Target program is validated (uses `Program<'info, T>`)
- [ ] **Signer seeds**: PDA signers pass seeds correctly in `invoke_signed`
- [ ] **Return value checking**: CPI success doesn't guarantee correct state
- [ ] **Account reloading**: Reload accounts after CPI that may modify them
- [ ] **No arbitrary CPI**: Program account is not user-controlled
- [ ] **Privilege escalation**: CPI doesn't grant unexpected permissions
```rust
// Secure CPI pattern
#[derive(Accounts)]
pub struct SecureCPI<'info> {
pub token_program: Program<'info, Token>, // Type-validated
// ... other accounts
}
pub fn secure_cpi(ctx: Context<SecureCPI>) -> Result<()> {
// CPI with validated program
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
),
amount,
)?;
// Reload account after CPI
ctx.accounts.from.reload()?;
// Validate expected state
require!(
ctx.accounts.from.amount == expected_amount,
ErrorCode::InvalidState
);
Ok(())
}
```
## Oracle and External Data Checklist
For Pyth, Switchboard, or other oracles:
- [ ] **Oracle status**: Validates oracle is in valid state (Trading status for Pyth)
- [ ] **Price staleness**: Checks timestamp is recent enough
- [ ] **Oracle owner**: Validates oracle account owner is correct program
- [ ] **Confidence interval**: For Pyth, checks confidence is acceptable
- [ ] **Price validity**: Validates price is within reasonable bounds
- [ ] **Fallback handling**: Has strategy for oracle failure
```rust
// Pyth oracle validation
pub fn validate_pyth_price(
pyth_account: &AccountInfo,
clock: &Clock,
) -> Result<i64> {
// Validate owner
require_keys_eq!(
*pyth_account.owner,
PYTH_PROGRAM_ID,
ErrorCode::InvalidOracle
);
let price_data = pyth_account.try_borrow_data()?;
let price_feed = load_price_feed_from_account_info(pyth_account)?;
// Check status
require!(
price_feed.agg.status == PriceStatus::Trading,
ErrorCode::InvalidOracleStatus
);
// Check staleness (e.g., max 60 seconds old)
let max_age = 60;
require!(
clock.unix_timestamp - price_feed.agg.publish_time <= max_age,
ErrorCode::StalePrice
);
// Check confidence (example: max 1% of price)
let confidence_threshold = price_feed.agg.price / 100;
require!(
price_feed.agg.conf <= confidence_threshold as u64,
ErrorCode::OracleConfidenceTooLow
);
Ok(price_feed.agg.price)
}
```
## Token Program Security Checklist
### SPL Token Checks
- [ ] **ATA validation**: Associated Token Accounts validated correctly
- [ ] **Mint authority**: Proper checks on mint authority for minting operations
- [ ] **Freeze authority**: Handles frozen accounts appropriately
- [ ] **Delegate handling**: Resets delegate when needed
- [ ] **Close authority**: Resets close authority on owner change
### Token-2022 Specific Checks
- [ ] **Transfer hooks**: Handles transfer hook extensions correctly
- [ ] **Extension data**: Validates all active extensions
- [ ] **Confidential transfers**: Properly handles confidential transfer extension
- [ ] **Transfer fees**: Respects transfer fee extension
- [ ] **Permanent delegate**: Checks for permanent delegate extension
- [ ] **Additional rent**: Accounts for extension rent requirements
```rust
// Token-2022 with extensions
use spl_token_2022::extension::{
BaseStateWithExtensions,
StateWithExtensions,
};
pub fn safe_token_2022_transfer(
/* accounts */
) -> Result<()> {
// Check for transfer hook
let mint_data = mint.try_borrow_data()?;
let mint_with_extensions = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Ok(transfer_hook) = mint_with_extensions.get_extension::<TransferHook>() {
// Handle transfer hook properly
// ... transfer hook logic
}
// Check for transfer fee
if let Ok(transfer_fee_config) = mint_with_extensions.get_extension::<TransferFeeConfig>() {
// Calculate and handle fees
// ... fee logic
}
// Proceed with transfer
Ok(())
}
```
## Architecture Review Checklist
- [ ] **PDA design**: PDAs used appropriately vs keypair accounts
- [ ] **Account space**: Space calculation uses `InitSpace` derive
- [ ] **Error handling**: Custom errors with descriptive messages
- [ ] **Event emission**: Critical state changes emit events
- [ ] **Rent exemption**: All accounts are rent-exempt
- [ ] **Transaction size**: Stays within ~1232 byte limit
- [ ] **Compute budget**: Optimized to stay under compute limits
- [ ] **Upgradeability**: Considers upgrade path and account versioning
## Testing Checklist
- [ ] **Unit tests**: Each instruction has unit tests
- [ ] **Fuzz tests**: Arithmetic operations have fuzz tests (Trident)
- [ ] **Integration tests**: Realistic multi-instruction scenarios
- [ ] **Negative tests**: Tests for expected failures
- [ ] **PDA tests**: Tests for seed collisions
- [ ] **Edge cases**: Zero amounts, max values, overflow boundaries
- [ ] **Concurrency**: Tests for transaction ordering issues
- [ ] **Devnet testing**: Deployed and tested on devnet
```rust
// Example test structure
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_case() {
// Test expected behavior
}
#[test]
#[should_panic(expected = "Overflow")]
fn test_overflow() {
// Test arithmetic overflow protection
}
#[test]
fn test_unauthorized_access() {
// Test fails with wrong signer
}
#[test]
fn test_edge_case_zero_amount() {
// Test zero amount handling
}
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,525 @@
# Common Vulnerability Patterns
Detailed examples of common Solana smart contract vulnerabilities with exploit scenarios and secure alternatives.
## 1. Missing Signer Validation
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// No check that caller is authorized!
let vault = &mut ctx.accounts.vault;
vault.balance -= amount;
Ok(())
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
pub user: AccountInfo<'info>, // Not a Signer!
}
```
### Exploit Scenario
Attacker can drain the vault by calling `withdraw` with any account as the `user` parameter. No signature verification means anyone can execute the instruction.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
has_one = authority, // Ensures vault.authority == authority.key()
)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>, // Must sign transaction
}
```
## 2. Integer Overflow/Underflow
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.balance = vault.balance + amount; // Can overflow!
Ok(())
}
```
### Exploit Scenario
If `vault.balance = u64::MAX - 100` and `amount = 200`, the addition overflows and wraps to `99`, effectively stealing funds from the vault.
### Secure Alternative
```rust
// ✅ SECURE
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.balance = vault
.balance
.checked_add(amount)
.ok_or(ErrorCode::Overflow)?;
Ok(())
}
```
## 3. PDA Substitution Attack
### Vulnerability
```rust
// ❌ VULNERABLE
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub config: Account<'info, Config>, // No PDA validation!
#[account(
mut,
seeds = [b"vault", config.key().as_ref()], // Uses unvalidated config
bump
)]
pub vault: Account<'info, Vault>,
}
```
### Exploit Scenario
Attacker creates a fake `config` account with malicious settings. The `vault` PDA is derived from this fake config, potentially accessing wrong vault or bypassing security checks.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(
seeds = [b"config"], // Global config PDA
bump,
)]
pub config: Account<'info, Config>,
#[account(
mut,
seeds = [b"vault", config.key().as_ref()],
bump
)]
pub vault: Account<'info, Vault>,
}
```
## 4. Type Cosplay Attack
### Vulnerability
```rust
// ❌ VULNERABLE
#[account(mut)]
pub user: AccountLoader<'info, User>, // Doesn't check discriminator!
```
### Exploit Scenario
Attacker passes a `UserAdmin` account instead of `User`. Since `AccountLoader` doesn't check discriminators by default, the program treats the admin account as a regular user, potentially bypassing privilege checks.
### Secure Alternative
```rust
// ✅ SECURE
#[account(mut)]
pub user: Account<'info, User>, // Enforces correct discriminator
```
## 5. Account Reloading Issues
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn complex_operation(ctx: Context<ComplexOp>) -> Result<()> {
let initial_balance = ctx.accounts.vault.balance;
// CPI that modifies vault
transfer_tokens(&ctx)?;
// Still using stale balance!
require!(
ctx.accounts.vault.balance >= initial_balance,
ErrorCode::InvalidBalance
);
Ok(())
}
```
### Exploit Scenario
The `balance` value is cached from before the CPI. If the CPI modified the vault, the check uses stale data, potentially allowing invalid state transitions.
### Secure Alternative
```rust
// ✅ SECURE
pub fn complex_operation(ctx: Context<ComplexOp>) -> Result<()> {
transfer_tokens(&ctx)?;
// Reload account to get fresh data
ctx.accounts.vault.reload()?;
require!(
ctx.accounts.vault.balance >= expected_balance,
ErrorCode::InvalidBalance
);
Ok(())
}
```
## 6. Improper Account Closing
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
**ctx.accounts.vault.to_account_info().lamports.borrow_mut() = 0;
// Data not zeroed, authority not reset!
Ok(())
}
```
### Exploit Scenario
Account data remains accessible within the same transaction even after lamports are zeroed. Attacker can read sensitive data or reuse the account in unexpected ways.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(
mut,
close = receiver // Properly closes: transfers lamports, zeros data
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub receiver: SystemAccount<'info>,
}
```
## 7. Missing Lamports Check
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn process(ctx: Context<Process>) -> Result<()> {
let data = ctx.accounts.user_data.load()?; // Can read closed account!
// ... use data
Ok(())
}
```
### Exploit Scenario
Account was closed earlier in the transaction but data is still readable. Processing closed account data can lead to inconsistent state or bypass business logic.
### Secure Alternative
```rust
// ✅ SECURE
pub fn process(ctx: Context<Process>) -> Result<()> {
require!(
**ctx.accounts.user_data.to_account_info().lamports.borrow() > 0,
ErrorCode::AccountClosed
);
let data = ctx.accounts.user_data.load()?;
// ... use data
Ok(())
}
```
## 8. Arbitrary CPI
### Vulnerability
```rust
// ❌ VULNERABLE
#[derive(Accounts)]
pub struct ArbitraryCPI<'info> {
pub token_program: AccountInfo<'info>, // Not validated!
}
pub fn transfer(ctx: Context<ArbitraryCPI>) -> Result<()> {
invoke(
&transfer_instruction,
&[
ctx.accounts.token_program.clone(), // Could be malicious!
// ...
]
)?;
Ok(())
}
```
### Exploit Scenario
Attacker passes malicious program instead of real Token program. Malicious program can emit fake events, return success without transferring, or drain funds.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct SecureCPI<'info> {
pub token_program: Program<'info, Token>, // Type-checked!
}
// Or manual validation
require_keys_eq!(
*ctx.accounts.token_program.key,
spl_token::ID,
ErrorCode::InvalidTokenProgram
);
```
## 9. Duplicate Mutable Accounts
### Vulnerability
```rust
// ❌ VULNERABLE
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
}
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
ctx.accounts.from.amount -= amount;
ctx.accounts.to.amount += amount; // Same account = double amount!
Ok(())
}
```
### Exploit Scenario
If `from` and `to` are the same account, the user can double their balance by transferring to themselves.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(
mut,
constraint = from.key() != to.key() @ ErrorCode::SameAccount
)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
}
```
## 10. Bump Seed Canonicalization
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn init_vault(ctx: Context<InitVault>, bump: u8) -> Result<()> {
// Accepts any bump from user!
let seeds = &[b"vault", user.key().as_ref(), &[bump]];
// Multiple PDAs possible for same seeds!
}
```
### Exploit Scenario
Attacker can create multiple vault PDAs with different bumps for the same user, fragmenting state or confusing off-chain systems.
### Secure Alternative
```rust
// ✅ SECURE
#[derive(Accounts)]
pub struct InitVault<'info> {
#[account(
init,
payer = user,
space = 8 + Vault::INIT_SPACE,
seeds = [b"vault", user.key().as_ref()],
bump // Anchor derives and stores canonical bump automatically
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
```
## 11. Missing Owner Check
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn read_data(ctx: Context<ReadData>) -> Result<()> {
let oracle_data = ctx.accounts.oracle.try_borrow_data()?;
// No check that oracle is owned by Pyth program!
let price = parse_price(&oracle_data)?;
Ok(())
}
```
### Exploit Scenario
Attacker creates fake oracle account owned by their own program, filled with manipulated price data. Program trusts the fake data.
### Secure Alternative
```rust
// ✅ SECURE
pub fn read_data(ctx: Context<ReadData>) -> Result<()> {
require_keys_eq!(
*ctx.accounts.oracle.owner,
PYTH_PROGRAM_ID,
ErrorCode::InvalidOracleOwner
);
let oracle_data = ctx.accounts.oracle.try_borrow_data()?;
let price = parse_price(&oracle_data)?;
Ok(())
}
```
## 12. Precision Loss / Rounding Errors
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn calculate_shares(collateral: u64, rate: Decimal) -> Result<u64> {
Decimal::from(collateral)
.try_div(rate)?
.try_round_u64() // Rounding can be exploited!
}
```
### Exploit Scenario
Attacker repeatedly deposits/withdraws small amounts. Rounding up gives slightly more shares each time, slowly draining the pool.
### Secure Alternative
```rust
// ✅ SECURE
pub fn calculate_shares(collateral: u64, rate: Decimal) -> Result<u64> {
Decimal::from(collateral)
.try_div(rate)?
.try_floor_u64() // Always round down in user's favor
}
```
## 13. Unchecked Error Returns
### Vulnerability
```rust
// ❌ VULNERABLE
spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
); // Return value ignored!
```
### Exploit Scenario
Transfer instruction fails silently but program continues as if it succeeded. State becomes inconsistent with actual token balances.
### Secure Alternative
```rust
// ✅ SECURE
invoke(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?, // Propagates error
&[
source.clone(),
destination.clone(),
authority.clone(),
],
)?;
// Or use Anchor's CPI helpers
token::transfer(ctx, amount)?;
```
## 14. Init If Needed Vulnerability
### Vulnerability
```rust
// ❌ VULNERABLE
#[account(
init_if_needed,
payer = user,
space = 8 + Account::INIT_SPACE
)]
pub user_account: Account<'info, UserAccount>,
```
### Exploit Scenario
If account already exists, initialization is skipped but existing data might be inconsistent or malicious. Can bypass initialization checks.
### Secure Alternative
```rust
// ✅ SECURE - Explicit initialization
#[account(
init,
payer = user,
space = 8 + Account::INIT_SPACE
)]
pub user_account: Account<'info, UserAccount>,
// Or if init_if_needed is truly needed, add validation
pub fn init_or_validate(ctx: Context<InitAccount>) -> Result<()> {
if ctx.accounts.user_account.is_initialized {
// Validate existing data
require!(
ctx.accounts.user_account.owner == ctx.accounts.user.key(),
ErrorCode::InvalidOwner
);
} else {
// Initialize new account
ctx.accounts.user_account.is_initialized = true;
ctx.accounts.user_account.owner = ctx.accounts.user.key();
}
Ok(())
}
```
## 15. Stale Oracle Data
### Vulnerability
```rust
// ❌ VULNERABLE
pub fn get_price(pyth_account: &AccountInfo) -> Result<i64> {
let price_feed = load_price_feed(pyth_account)?;
Ok(price_feed.agg.price) // No staleness check!
}
```
### Exploit Scenario
Oracle stopped updating hours ago due to network issues. Attacker exploits stale price to buy/sell at favorable outdated rates.
### Secure Alternative
```rust
// ✅ SECURE
pub fn get_price(
pyth_account: &AccountInfo,
clock: &Clock
) -> Result<i64> {
let price_feed = load_price_feed(pyth_account)?;
// Check publishing time
let max_age_seconds = 60;
require!(
clock.unix_timestamp - price_feed.agg.publish_time <= max_age_seconds,
ErrorCode::StaleOraclePrice
);
// Check status
require!(
price_feed.agg.status == PriceStatus::Trading,
ErrorCode::OracleNotTrading
);
Ok(price_feed.agg.price)
}
```