Files
2025-11-30 09:01:25 +08:00

893 lines
25 KiB
Markdown

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