Initial commit
This commit is contained in:
892
skills/solana-development/references/accounts.md
Normal file
892
skills/solana-development/references/accounts.md
Normal 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(¬e_pda.data.borrow())?;
|
||||
if note_data.author != *author.key {
|
||||
msg!("Author mismatch");
|
||||
return Err(ProgramError::IllegalOwner);
|
||||
}
|
||||
|
||||
// Safe to proceed with update
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Writable Check
|
||||
|
||||
**Purpose:** Verify an account is marked as writable.
|
||||
|
||||
**When to use:**
|
||||
- Before modifying account data
|
||||
- Before changing lamport balances
|
||||
- Enforced automatically by runtime, but explicit checks improve clarity
|
||||
|
||||
```rust
|
||||
if !account.is_writable {
|
||||
msg!("Account must be writable");
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Initialization Check
|
||||
|
||||
**Purpose:** Prevent re-initialization or use of uninitialized accounts.
|
||||
|
||||
**Pattern: Flag-based initialization**
|
||||
|
||||
```rust
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub struct DataAccount {
|
||||
pub is_initialized: bool,
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
impl DataAccount {
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.is_initialized
|
||||
}
|
||||
}
|
||||
|
||||
// On creation - check NOT initialized
|
||||
if account_data.is_initialized() {
|
||||
msg!("Account already initialized");
|
||||
return Err(ProgramError::AccountAlreadyInitialized);
|
||||
}
|
||||
|
||||
// On update - check IS initialized
|
||||
if !account_data.is_initialized() {
|
||||
msg!("Account not initialized");
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. PDA Validation
|
||||
|
||||
**Purpose:** Verify a provided PDA matches expected derivation.
|
||||
|
||||
**Critical for security:** Always validate PDAs using canonical bump.
|
||||
|
||||
```rust
|
||||
pub fn validate_pda(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
note_id: u64,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let author = next_account_info(account_info_iter)?;
|
||||
let note_pda = next_account_info(account_info_iter)?;
|
||||
|
||||
// Derive expected PDA
|
||||
let (expected_pda, _bump) = Pubkey::find_program_address(
|
||||
&[
|
||||
author.key.as_ref(),
|
||||
note_id.to_le_bytes().as_ref(),
|
||||
],
|
||||
program_id,
|
||||
);
|
||||
|
||||
// Validate match
|
||||
if expected_pda != *note_pda.key {
|
||||
msg!("Invalid PDA - seeds don't match");
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Why use `find_program_address` instead of accepting a bump?**
|
||||
- Prevents bump seed manipulation attacks
|
||||
- Ensures canonical bump is used
|
||||
- Eliminates category of security vulnerabilities
|
||||
|
||||
### 6. Account Type Validation
|
||||
|
||||
**Purpose:** Ensure account contains expected data type.
|
||||
|
||||
**Pattern: Discriminator/Type Field**
|
||||
|
||||
```rust
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub enum AccountType {
|
||||
Uninitialized,
|
||||
UserProfile,
|
||||
GameState,
|
||||
}
|
||||
|
||||
#[derive(BorshSerialize, BorshDeserialize)]
|
||||
pub struct AccountData {
|
||||
pub account_type: AccountType,
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
// Validation
|
||||
let account_data = AccountData::try_from_slice(&account.data.borrow())?;
|
||||
if !matches!(account_data.account_type, AccountType::UserProfile) {
|
||||
msg!("Wrong account type");
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Always Validate Before Trusting
|
||||
|
||||
**Never assume accounts are correct.** Always validate:
|
||||
|
||||
```rust
|
||||
pub fn secure_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let user = next_account_info(account_info_iter)?;
|
||||
let user_data_pda = next_account_info(account_info_iter)?;
|
||||
|
||||
// ✅ Signer check
|
||||
if !user.is_signer {
|
||||
return Err(ProgramError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
// ✅ Ownership check
|
||||
if user_data_pda.owner != program_id {
|
||||
return Err(ProgramError::IllegalOwner);
|
||||
}
|
||||
|
||||
// ✅ PDA validation
|
||||
let (expected_pda, _) = Pubkey::find_program_address(
|
||||
&[b"user_data", user.key.as_ref()],
|
||||
program_id,
|
||||
);
|
||||
if expected_pda != *user_data_pda.key {
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
|
||||
// ✅ Initialization check
|
||||
let data = UserData::try_from_slice(&user_data_pda.data.borrow())?;
|
||||
if !data.is_initialized {
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
|
||||
// Now safe to proceed
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Fail Fast with Meaningful Errors
|
||||
|
||||
Return errors immediately when validation fails:
|
||||
|
||||
```rust
|
||||
// ✅ Good - fail fast
|
||||
if !account.is_signer {
|
||||
msg!("User must sign the transaction");
|
||||
return Err(ProgramError::MissingRequiredSignature);
|
||||
}
|
||||
|
||||
// ❌ Bad - continues with invalid state
|
||||
if account.is_signer {
|
||||
// process...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Type Safety
|
||||
|
||||
Leverage Rust's type system for compile-time guarantees:
|
||||
|
||||
```rust
|
||||
// Define a validated account type
|
||||
pub struct ValidatedUserAccount<'a> {
|
||||
info: &'a AccountInfo<'a>,
|
||||
data: UserAccountData,
|
||||
}
|
||||
|
||||
impl<'a> ValidatedUserAccount<'a> {
|
||||
pub fn validate(
|
||||
account: &'a AccountInfo<'a>,
|
||||
program_id: &Pubkey,
|
||||
) -> Result<Self, ProgramError> {
|
||||
// Ownership check
|
||||
if account.owner != program_id {
|
||||
return Err(ProgramError::IllegalOwner);
|
||||
}
|
||||
|
||||
// Deserialize and validate
|
||||
let data = UserAccountData::try_from_slice(&account.data.borrow())?;
|
||||
if !data.is_initialized {
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
|
||||
Ok(Self { info: account, data })
|
||||
}
|
||||
}
|
||||
|
||||
// Usage guarantees validated account
|
||||
pub fn process_with_validated_account(
|
||||
validated: ValidatedUserAccount,
|
||||
) -> ProgramResult {
|
||||
// No need to re-validate!
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Check Arithmetic Operations
|
||||
|
||||
Always use checked math to prevent overflow/underflow:
|
||||
|
||||
```rust
|
||||
// ❌ Dangerous - can overflow
|
||||
let total = amount1 + amount2;
|
||||
|
||||
// ✅ Safe - returns error on overflow
|
||||
let total = amount1
|
||||
.checked_add(amount2)
|
||||
.ok_or(ProgramError::ArithmeticOverflow)?;
|
||||
```
|
||||
|
||||
### 5. Validate Data Constraints
|
||||
|
||||
Check business logic constraints:
|
||||
|
||||
```rust
|
||||
pub fn allocate_points(
|
||||
character_account: &AccountInfo,
|
||||
new_strength: u8,
|
||||
) -> ProgramResult {
|
||||
let mut character = Character::try_from_slice(&character_account.data.borrow())?;
|
||||
|
||||
// Validate attribute cap
|
||||
if character.strength.checked_add(new_strength).ok_or(ProgramError::ArithmeticOverflow)? > 100 {
|
||||
msg!("Attribute cannot exceed 100");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
|
||||
// Validate allowance
|
||||
if new_strength > character.available_points {
|
||||
msg!("Insufficient available points");
|
||||
return Err(ProgramError::InsufficientFunds);
|
||||
}
|
||||
|
||||
character.strength += new_strength;
|
||||
character.available_points -= new_strength;
|
||||
character.serialize(&mut &mut character_account.data.borrow_mut()[..])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Vulnerabilities
|
||||
|
||||
### 1. Missing Ownership Check
|
||||
|
||||
**Vulnerability:**
|
||||
```rust
|
||||
// ❌ No ownership validation
|
||||
pub fn update_data(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
new_value: u64,
|
||||
) -> ProgramResult {
|
||||
let data_account = &accounts[0];
|
||||
|
||||
// Dangerous - could be any account!
|
||||
let mut data = MyData::try_from_slice(&data_account.data.borrow())?;
|
||||
data.value = new_value;
|
||||
data.serialize(&mut &mut data_account.data.borrow_mut()[..])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Exploit:**
|
||||
Attacker passes an account they control that happens to deserialize correctly, modifying arbitrary data.
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
// ✅ With ownership check
|
||||
if data_account.owner != program_id {
|
||||
return Err(ProgramError::IllegalOwner);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Missing Signer Check
|
||||
|
||||
**Vulnerability:**
|
||||
```rust
|
||||
// ❌ No signer validation
|
||||
pub fn withdraw(
|
||||
accounts: &[AccountInfo],
|
||||
amount: u64,
|
||||
) -> ProgramResult {
|
||||
let user_account = &accounts[0];
|
||||
let vault = &accounts[1];
|
||||
|
||||
// Dangerous - anyone can drain anyone's funds!
|
||||
**user_account.lamports.borrow_mut() += amount;
|
||||
**vault.lamports.borrow_mut() -= amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Exploit:**
|
||||
Attacker calls instruction with victim's account, draining their funds without signature.
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
// ✅ With signer check
|
||||
if !user_account.is_signer {
|
||||
return Err(ProgramError::MissingRequiredSignature);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. PDA Substitution Attack
|
||||
|
||||
**Vulnerability:**
|
||||
```rust
|
||||
// ❌ Accepts PDA without validation
|
||||
pub fn update_user_data(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
user: &AccountInfo,
|
||||
user_pda: &AccountInfo,
|
||||
) -> ProgramResult {
|
||||
// No PDA derivation check!
|
||||
let mut data = UserData::try_from_slice(&user_pda.data.borrow())?;
|
||||
data.balance += 100;
|
||||
data.serialize(&mut &mut user_pda.data.borrow_mut()[..])?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Exploit:**
|
||||
Attacker passes a different user's PDA, crediting that user's balance instead.
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
// ✅ Validate PDA derivation
|
||||
let (expected_pda, _) = Pubkey::find_program_address(
|
||||
&[b"user_data", user.key.as_ref()],
|
||||
program_id,
|
||||
);
|
||||
if expected_pda != *user_pda.key {
|
||||
return Err(ProgramError::InvalidSeeds);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Integer Overflow/Underflow
|
||||
|
||||
**Vulnerability:**
|
||||
```rust
|
||||
// ❌ Unchecked arithmetic
|
||||
pub fn add_rewards(
|
||||
account: &AccountInfo,
|
||||
reward: u64,
|
||||
) -> ProgramResult {
|
||||
let mut user = UserData::try_from_slice(&account.data.borrow())?;
|
||||
user.total_rewards = user.total_rewards + reward; // Can overflow!
|
||||
user.serialize(&mut &mut account.data.borrow_mut()[..])?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Exploit:**
|
||||
Overflow wraps around: u64::MAX + 1 = 0, causing balance to reset.
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
// ✅ Checked arithmetic
|
||||
user.total_rewards = user.total_rewards
|
||||
.checked_add(reward)
|
||||
.ok_or(ProgramError::ArithmeticOverflow)?;
|
||||
```
|
||||
|
||||
### 5. Unvalidated Account Reuse
|
||||
|
||||
**Vulnerability:**
|
||||
```rust
|
||||
// ❌ No initialization check
|
||||
pub fn update_score(
|
||||
accounts: &[AccountInfo],
|
||||
score: u64,
|
||||
) -> ProgramResult {
|
||||
let score_account = &accounts[0];
|
||||
let mut data = ScoreData::try_from_slice(&score_account.data.borrow())?;
|
||||
|
||||
// What if account was never initialized?
|
||||
data.score = score;
|
||||
data.serialize(&mut &mut score_account.data.borrow_mut()[..])?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Exploit:**
|
||||
Reusing uninitialized memory can lead to undefined behavior or data corruption.
|
||||
|
||||
**Fix:**
|
||||
```rust
|
||||
// ✅ Check initialization
|
||||
if !data.is_initialized {
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Critical Account Validation Checklist:**
|
||||
|
||||
- ✅ **Ownership check**: Verify `account.owner == expected_program_id`
|
||||
- ✅ **Signer check**: Verify `account.is_signer` for privileged operations
|
||||
- ✅ **PDA validation**: Use `find_program_address` with expected seeds
|
||||
- ✅ **Initialization check**: Verify account is initialized before use
|
||||
- ✅ **Type validation**: Ensure account contains expected data structure
|
||||
- ✅ **Rent exemption**: Calculate and enforce rent-exempt balances
|
||||
- ✅ **Arithmetic safety**: Use `checked_add`, `checked_sub`, etc.
|
||||
- ✅ **Data constraints**: Validate business logic rules
|
||||
|
||||
**Think Like an Attacker:**
|
||||
For every account your program receives, ask:
|
||||
- "What if this is the wrong account?"
|
||||
- "What if this account isn't owned by my program?"
|
||||
- "What if the user didn't sign for this?"
|
||||
- "What if this account is uninitialized?"
|
||||
- "What if these seeds derive a different PDA?"
|
||||
|
||||
Validate everything. Trust nothing.
|
||||
1647
skills/solana-development/references/anchor.md
Normal file
1647
skills/solana-development/references/anchor.md
Normal file
File diff suppressed because it is too large
Load Diff
931
skills/solana-development/references/builtin-programs.md
Normal file
931
skills/solana-development/references/builtin-programs.md
Normal 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.
|
||||
680
skills/solana-development/references/compute-optimization.md
Normal file
680
skills/solana-development/references/compute-optimization.md
Normal 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
|
||||
824
skills/solana-development/references/cpi.md
Normal file
824
skills/solana-development/references/cpi.md
Normal 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.
|
||||
1828
skills/solana-development/references/deployment.md
Normal file
1828
skills/solana-development/references/deployment.md
Normal file
File diff suppressed because it is too large
Load Diff
961
skills/solana-development/references/durable-nonces.md
Normal file
961
skills/solana-development/references/durable-nonces.md
Normal 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,
|
||||
¤t_authority.pubkey(),
|
||||
new_authority,
|
||||
);
|
||||
|
||||
let recent_blockhash = rpc_client.get_latest_blockhash()?;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[authorize_ix],
|
||||
Some(&payer.pubkey()),
|
||||
&[payer, current_authority],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
rpc_client.send_and_confirm_transaction(&transaction)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Transfer control to PDA for program-managed nonces
|
||||
- Rotate keys for security
|
||||
- Transfer to multisig for DAO control
|
||||
|
||||
## Offline Signing Workflows
|
||||
|
||||
### Hardware Wallet Flow
|
||||
|
||||
**Setup (online device):**
|
||||
```rust
|
||||
// 1. Create nonce account (one-time)
|
||||
create_nonce_account(&rpc_client, &payer, &nonce_account, &hw_wallet_pubkey)?;
|
||||
|
||||
// 2. Fetch nonce value
|
||||
let nonce_value = get_nonce_value(&rpc_client, &nonce_account.pubkey())?;
|
||||
|
||||
// 3. Build unsigned transaction
|
||||
let unsigned_tx = build_nonce_transaction(
|
||||
&nonce_account.pubkey(),
|
||||
&hw_wallet_keypair, // Will be replaced with actual signature
|
||||
nonce_value,
|
||||
vec![transfer_ix],
|
||||
&payer,
|
||||
);
|
||||
|
||||
// 4. Serialize for hardware wallet
|
||||
let serialized = serialize_for_offline_signing(&unsigned_tx);
|
||||
|
||||
// 5. Transfer to hardware wallet (USB, QR code, etc.)
|
||||
```
|
||||
|
||||
**Signing (air-gapped hardware wallet):**
|
||||
```rust
|
||||
// 1. Receive serialized transaction
|
||||
let tx = deserialize_signed_transaction(&serialized);
|
||||
|
||||
// 2. Display to user for review (no time pressure!)
|
||||
// User reviews: recipient, amount, etc.
|
||||
|
||||
// 3. Sign with hardware wallet private key
|
||||
// (Hardware wallet handles this internally)
|
||||
|
||||
// 4. Export signed transaction
|
||||
let signed_serialized = serialize_for_offline_signing(&signed_tx);
|
||||
|
||||
// 5. Transfer back to online device
|
||||
```
|
||||
|
||||
**Submission (online device):**
|
||||
```rust
|
||||
// 1. Receive signed transaction
|
||||
let signed_tx = deserialize_signed_transaction(&signed_serialized);
|
||||
|
||||
// 2. Submit to network (can be hours/days after signing!)
|
||||
let signature = rpc_client.send_and_confirm_transaction(&signed_tx)?;
|
||||
```
|
||||
|
||||
### Multi-Signature Coordination
|
||||
|
||||
**DAO Proposal Execution Flow:**
|
||||
|
||||
```rust
|
||||
// 1. Proposer creates transaction with nonce
|
||||
let nonce_value = get_nonce_value(&rpc_client, &dao_nonce_account)?;
|
||||
let proposal_tx = build_nonce_transaction(
|
||||
&dao_nonce_account,
|
||||
&dao_authority, // PDA controlled by governance
|
||||
nonce_value,
|
||||
vec![execute_proposal_ix],
|
||||
&proposer,
|
||||
);
|
||||
|
||||
// 2. Serialize and store in DAO state
|
||||
let tx_data = bincode::serialize(&proposal_tx)?;
|
||||
// Store tx_data in proposal account
|
||||
|
||||
// 3. Members vote over time (hours/days)
|
||||
// Each vote increments approval count
|
||||
|
||||
// 4. When threshold reached, anyone can execute
|
||||
let stored_tx: Transaction = bincode::deserialize(&proposal.tx_data)?;
|
||||
|
||||
// 5. Submit (nonce ensures it's still valid!)
|
||||
rpc_client.send_and_confirm_transaction(&stored_tx)?;
|
||||
```
|
||||
|
||||
### CLI Multi-Sig Example
|
||||
|
||||
**First co-signer (offline):**
|
||||
```bash
|
||||
solana transfer \
|
||||
--from sender.json \
|
||||
--sign-only \
|
||||
--nonce nonce-account.json \
|
||||
--nonce-authority nonce-authority.json \
|
||||
--blockhash <NONCE_VALUE> \
|
||||
--fee-payer co-sender.json \
|
||||
receiver.json 0.1
|
||||
|
||||
# Output:
|
||||
# Pubkey=Signature
|
||||
# 5nZ8nY5...=4SBv7Xp...
|
||||
```
|
||||
|
||||
**Second co-signer (online, hours/days later):**
|
||||
```bash
|
||||
solana transfer \
|
||||
--from sender.json \
|
||||
--nonce nonce-account.json \
|
||||
--nonce-authority nonce-authority.json \
|
||||
--blockhash <NONCE_VALUE> \
|
||||
--fee-payer sender.json \
|
||||
--signer 5nZ8nY5...=4SBv7Xp... \
|
||||
receiver.json 0.1
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### The Neodyme Vulnerability (2020)
|
||||
|
||||
**Historic issue**: Before Solana v1.3, there was a critical vulnerability in how durable nonce transactions were processed:
|
||||
|
||||
**The bug:**
|
||||
1. Transaction with durable nonce starts processing
|
||||
2. Runtime advances nonce (changes state)
|
||||
3. Later instruction in transaction fails
|
||||
4. Runtime rolls back ALL state changes
|
||||
5. **BUG**: Nonce advancement was rolled back too!
|
||||
6. Attacker could replay the transaction
|
||||
|
||||
**The exploit:**
|
||||
```rust
|
||||
// Malicious transaction:
|
||||
{
|
||||
instructions: [
|
||||
advance_nonce(...), // Advances nonce
|
||||
write_arbitrary_data(...), // Attacker's payload
|
||||
fail_intentionally(...), // Forces transaction to fail
|
||||
]
|
||||
}
|
||||
|
||||
// After rollback:
|
||||
// - Nonce reverted to original value
|
||||
// - Arbitrary data write WAS NOT rolled back
|
||||
// - Can replay transaction infinitely!
|
||||
```
|
||||
|
||||
**Impact**: Could write arbitrary data to any account by replaying failed transactions.
|
||||
|
||||
**Fix** (Solana v1.3+): Nonce advancement is now **permanent** even on transaction failure. The runtime explicitly handles nonce accounts separately from normal rollback logic.
|
||||
|
||||
**Lesson**: This demonstrates why nonce advancement MUST happen regardless of transaction success/failure.
|
||||
|
||||
### Best Practices
|
||||
|
||||
**1. Never reuse nonce without advancing**
|
||||
|
||||
```rust
|
||||
// BAD: Reusing nonce value
|
||||
let nonce = get_nonce_value(&rpc, &nonce_account)?;
|
||||
let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix1], &payer);
|
||||
let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce, vec![ix2], &payer);
|
||||
// If tx1 fails, tx2 might also fail with "nonce mismatch"
|
||||
|
||||
// GOOD: Advance between uses
|
||||
let nonce1 = get_nonce_value(&rpc, &nonce_account)?;
|
||||
let tx1 = build_nonce_transaction(&nonce_account, &auth, nonce1, vec![ix1], &payer);
|
||||
rpc.send_and_confirm_transaction(&tx1)?;
|
||||
|
||||
// Fetch fresh nonce (it was advanced)
|
||||
let nonce2 = get_nonce_value(&rpc, &nonce_account)?;
|
||||
let tx2 = build_nonce_transaction(&nonce_account, &auth, nonce2, vec![ix2], &payer);
|
||||
```
|
||||
|
||||
**2. Protect nonce authority**
|
||||
|
||||
```rust
|
||||
// Use cold storage for nonce authority
|
||||
// OR use PDA with program logic to restrict usage
|
||||
let authority_pda = Pubkey::find_program_address(
|
||||
&[b"nonce_authority", dao.key().as_ref()],
|
||||
program_id,
|
||||
);
|
||||
```
|
||||
|
||||
**3. Maintain rent exemption**
|
||||
|
||||
```rust
|
||||
// Check before withdrawal
|
||||
let nonce_account = rpc.get_account(&nonce_pubkey)?;
|
||||
let rent = rpc.get_minimum_balance_for_rent_exemption(80)?;
|
||||
|
||||
if nonce_account.lamports - withdraw_amount < rent {
|
||||
return Err("Would violate rent exemption".into());
|
||||
}
|
||||
```
|
||||
|
||||
**4. Verify nonce advancement in transaction**
|
||||
|
||||
```rust
|
||||
// In your program that uses nonce transactions:
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
// First account should be nonce account
|
||||
let nonce_account = &accounts[0];
|
||||
|
||||
// Verify it's a valid nonce account
|
||||
if nonce_account.owner != &system_program::id() {
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
|
||||
// Verify advance_nonce was called
|
||||
// (Runtime enforces this, but you can add checks)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**5. Monitor nonce account balance**
|
||||
|
||||
```rust
|
||||
// Periodic check (e.g., daily job)
|
||||
fn check_nonce_health(rpc: &RpcClient, nonce: &Pubkey) -> Result<(), String> {
|
||||
let account = rpc.get_account(nonce)
|
||||
.map_err(|_| "Nonce account not found")?;
|
||||
|
||||
let rent = rpc.get_minimum_balance_for_rent_exemption(80)
|
||||
.map_err(|_| "Failed to fetch rent")?;
|
||||
|
||||
if account.lamports < rent {
|
||||
return Err(format!(
|
||||
"Nonce account below rent exemption: {} < {}",
|
||||
account.lamports, rent
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Scheduled Payments (Vesting)
|
||||
|
||||
```rust
|
||||
// Pre-sign monthly vesting releases
|
||||
fn create_vesting_schedule(
|
||||
rpc: &RpcClient,
|
||||
nonce_account: &Pubkey,
|
||||
nonce_authority: &Keypair,
|
||||
recipient: &Pubkey,
|
||||
amount_per_month: u64,
|
||||
months: usize,
|
||||
) -> Result<Vec<Transaction>, Box<dyn std::error::Error>> {
|
||||
let mut transactions = Vec::new();
|
||||
|
||||
for month in 0..months {
|
||||
// Fetch current nonce
|
||||
let nonce = get_nonce_value(rpc, nonce_account)?;
|
||||
|
||||
// Create transfer
|
||||
let transfer_ix = system_instruction::transfer(
|
||||
&nonce_authority.pubkey(),
|
||||
recipient,
|
||||
amount_per_month,
|
||||
);
|
||||
|
||||
// Build nonce transaction
|
||||
let tx = build_nonce_transaction(
|
||||
nonce_account,
|
||||
nonce_authority,
|
||||
nonce,
|
||||
vec![transfer_ix],
|
||||
nonce_authority,
|
||||
);
|
||||
|
||||
transactions.push(tx);
|
||||
|
||||
// Advance nonce for next month's transaction
|
||||
advance_nonce_manually(rpc, nonce_account, nonce_authority, nonce_authority)?;
|
||||
}
|
||||
|
||||
Ok(transactions)
|
||||
}
|
||||
|
||||
// Executor submits each month
|
||||
fn execute_vesting_payment(
|
||||
rpc: &RpcClient,
|
||||
pre_signed_tx: &Transaction,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// No time pressure - can submit anytime!
|
||||
rpc.send_and_confirm_transaction(pre_signed_tx)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Conditional Trades (Limit Orders)
|
||||
|
||||
```rust
|
||||
// Pre-sign trade execution at specific price
|
||||
fn create_limit_order(
|
||||
nonce: &Pubkey,
|
||||
authority: &Keypair,
|
||||
swap_instruction: Instruction, // Execute when price reached
|
||||
) -> Transaction {
|
||||
let nonce_value = /* fetch nonce */;
|
||||
|
||||
build_nonce_transaction(
|
||||
nonce,
|
||||
authority,
|
||||
nonce_value,
|
||||
vec![swap_instruction],
|
||||
authority,
|
||||
)
|
||||
}
|
||||
|
||||
// Bot monitors price and submits when condition met
|
||||
fn execute_limit_order(rpc: &RpcClient, current_price: f64, limit_tx: &Transaction) {
|
||||
if current_price >= target_price {
|
||||
rpc.send_transaction(limit_tx).ok(); // Submit pre-signed transaction
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Cross-Chain Bridges
|
||||
|
||||
```rust
|
||||
// Sign Solana transaction while waiting for Ethereum finality
|
||||
async fn bridge_from_ethereum_to_solana(
|
||||
eth_tx_hash: H256,
|
||||
solana_mint_ix: Instruction,
|
||||
nonce_account: &Pubkey,
|
||||
nonce_authority: &Keypair,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1. Pre-sign Solana mint transaction
|
||||
let nonce = get_nonce_value(&solana_rpc, nonce_account)?;
|
||||
let mint_tx = build_nonce_transaction(
|
||||
nonce_account,
|
||||
nonce_authority,
|
||||
nonce,
|
||||
vec![solana_mint_ix],
|
||||
nonce_authority,
|
||||
);
|
||||
|
||||
// 2. Wait for Ethereum finality (12+ minutes)
|
||||
wait_for_ethereum_finality(eth_tx_hash).await?;
|
||||
|
||||
// 3. Submit Solana transaction (still valid!)
|
||||
solana_rpc.send_and_confirm_transaction(&mint_tx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 4. DAO Governance Execution
|
||||
|
||||
Already covered in multi-sig example above - proposals can be voted on over days/weeks, then executed with pre-signed transaction.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
**Create nonce account:**
|
||||
```bash
|
||||
solana create-nonce-account <KEYPAIR_PATH> <AMOUNT>
|
||||
```
|
||||
|
||||
**Get current nonce:**
|
||||
```bash
|
||||
solana nonce <NONCE_ACCOUNT>
|
||||
```
|
||||
|
||||
**Manually advance nonce:**
|
||||
```bash
|
||||
solana new-nonce <NONCE_ACCOUNT>
|
||||
```
|
||||
|
||||
**Get nonce account info:**
|
||||
```bash
|
||||
solana nonce-account <NONCE_ACCOUNT>
|
||||
```
|
||||
|
||||
**Withdraw from nonce:**
|
||||
```bash
|
||||
solana withdraw-from-nonce-account <NONCE_ACCOUNT> <DESTINATION> <AMOUNT>
|
||||
```
|
||||
|
||||
**Change nonce authority:**
|
||||
```bash
|
||||
solana authorize-nonce-account <NONCE_ACCOUNT> <NEW_AUTHORITY>
|
||||
```
|
||||
|
||||
**Sign transaction offline:**
|
||||
```bash
|
||||
solana <COMMAND> \
|
||||
--sign-only \
|
||||
--nonce <NONCE_ACCOUNT> \
|
||||
--nonce-authority <AUTHORITY_KEYPAIR> \
|
||||
--blockhash <NONCE_VALUE>
|
||||
```
|
||||
|
||||
**Submit pre-signed transaction:**
|
||||
```bash
|
||||
solana <COMMAND> \
|
||||
--nonce <NONCE_ACCOUNT> \
|
||||
--nonce-authority <AUTHORITY_KEYPAIR> \
|
||||
--blockhash <NONCE_VALUE> \
|
||||
--signer <PUBKEY=SIGNATURE>
|
||||
```
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
**Transaction size:**
|
||||
- Adding `advance_nonce_account` instruction adds ~40 bytes
|
||||
- May push transaction over size limit if already near maximum
|
||||
|
||||
**Extra signature requirement:**
|
||||
- Nonce authority must sign (if different from fee payer)
|
||||
- Increases transaction complexity
|
||||
|
||||
**Rent cost:**
|
||||
- Each nonce account requires ~0.0015 SOL rent-exempt minimum
|
||||
- For many scheduled transactions, can become expensive
|
||||
|
||||
**Nonce advancement overhead:**
|
||||
- Compute units to advance nonce (~few hundred CU)
|
||||
- Minimal but worth considering for CU-constrained transactions
|
||||
|
||||
**Cannot mix recent blockhashes and nonces:**
|
||||
- Transaction uses either recent blockhash OR durable nonce
|
||||
- Cannot use both in the same transaction
|
||||
|
||||
## Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Introduction to Durable Nonces](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces)
|
||||
- [Durable Transaction Nonces Proposal](https://docs.anza.xyz/implemented-proposals/durable-tx-nonces)
|
||||
- [CLI Nonce Examples](https://docs.anza.xyz/cli/examples/durable-nonce)
|
||||
|
||||
### Code Examples
|
||||
- [Durable Nonces Repository](https://github.com/0xproflupin/solana-durable-nonces)
|
||||
- [System Program Source](https://github.com/solana-labs/solana/blob/master/sdk/program/src/system_instruction.rs)
|
||||
|
||||
### Security Analysis
|
||||
- [Neodyme: Nonce Upon a Time](https://neodyme.io/en/blog/nonce-upon-a-time/) - Historic vulnerability analysis
|
||||
|
||||
### Technical References
|
||||
- [solana-sdk NonceState](https://docs.rs/solana-sdk/latest/solana_sdk/nonce/state/enum.State.html)
|
||||
- [System Program Instructions](https://docs.rs/solana-sdk/latest/solana_sdk/system_instruction/)
|
||||
800
skills/solana-development/references/error-handling.md
Normal file
800
skills/solana-development/references/error-handling.md
Normal 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.
|
||||
2042
skills/solana-development/references/native-rust.md
Normal file
2042
skills/solana-development/references/native-rust.md
Normal file
File diff suppressed because it is too large
Load Diff
796
skills/solana-development/references/pda.md
Normal file
796
skills/solana-development/references/pda.md
Normal 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.
|
||||
498
skills/solana-development/references/production-deployment.md
Normal file
498
skills/solana-development/references/production-deployment.md
Normal 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.
|
||||
187
skills/solana-development/references/resources.md
Normal file
187
skills/solana-development/references/resources.md
Normal 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.
|
||||
613
skills/solana-development/references/security.md
Normal file
613
skills/solana-development/references/security.md
Normal 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.
|
||||
620
skills/solana-development/references/serialization.md
Normal file
620
skills/solana-development/references/serialization.md
Normal 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.
|
||||
992
skills/solana-development/references/sysvars.md
Normal file
992
skills/solana-development/references/sysvars.md
Normal 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.
|
||||
1255
skills/solana-development/references/testing-frameworks.md
Normal file
1255
skills/solana-development/references/testing-frameworks.md
Normal file
File diff suppressed because it is too large
Load Diff
406
skills/solana-development/references/testing-overview.md
Normal file
406
skills/solana-development/references/testing-overview.md
Normal 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)**
|
||||
528
skills/solana-development/references/testing-practices.md
Normal file
528
skills/solana-development/references/testing-practices.md
Normal 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)**
|
||||
172
skills/solana-development/references/tokens-2022.md
Normal file
172
skills/solana-development/references/tokens-2022.md
Normal 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
|
||||
982
skills/solana-development/references/tokens-operations.md
Normal file
982
skills/solana-development/references/tokens-operations.md
Normal 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
|
||||
301
skills/solana-development/references/tokens-overview.md
Normal file
301
skills/solana-development/references/tokens-overview.md
Normal 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
|
||||
860
skills/solana-development/references/tokens-patterns.md
Normal file
860
skills/solana-development/references/tokens-patterns.md
Normal 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.
|
||||
221
skills/solana-development/references/tokens-validation.md
Normal file
221
skills/solana-development/references/tokens-validation.md
Normal 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
|
||||
978
skills/solana-development/references/transaction-lifecycle.md
Normal file
978
skills/solana-development/references/transaction-lifecycle.md
Normal 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)
|
||||
953
skills/solana-development/references/versioned-transactions.md
Normal file
953
skills/solana-development/references/versioned-transactions.md
Normal 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)
|
||||
Reference in New Issue
Block a user