Solana Programming Model: Accounts, Programs, and PDAs

Learn Solana's unique programming model with accounts, programs, and Program Derived Addresses (PDAs).

Advanced⏱️ 60 min📚 Prerequisites: 2

Solana Programming Model: Accounts, Programs, and PDAs

Solana uses a unique account-based model different from Ethereum.

Key Concepts

Accounts

In Solana, everything is an account:

  • Data Accounts: Store data
  • Program Accounts: Store executable code
  • System Accounts: Native programs

Account Structure

RUST
struct AccountInfo {
    key: Pubkey,           // Account address
    lamports: u64,         // Balance in lamports (1 SOL = 1e9 lamports)
    data: Vec<u8>,         // Account data
    owner: Pubkey,         // Program that owns this account
    executable: bool,      // Is this a program?
    rent_epoch: u64,       // Rent epoch
}

Programs

Programs are Solana's equivalent of smart contracts:

  • Written in Rust (or C)
  • Compiled to BPF (Berkeley Packet Filter) bytecode
  • Stateless: Programs don't store data
  • Data stored in separate accounts

Program Derived Addresses (PDAs)

PDAs are program-controlled addresses:

  • Derived from program ID + seeds
  • No private key (can't sign)
  • Only the program can sign for them
  • Used for program-owned accounts

Solana Program Example

RUST
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program_error::ProgramError,
};

// Program entry point
entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    
    // Get accounts
    let user_account = next_account_info(accounts_iter)?;
    let data_account = next_account_info(accounts_iter)?;
    
    // Verify ownership
    if data_account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    
    // Read/write account data
    let mut data = data_account.data.borrow_mut();
    // ... process instruction ...
    
    Ok(())
}

Key Differences from Ethereum

FeatureEthereumSolana
StateIn contractIn accounts
ExecutionSequentialParallel
FeesGasCompute units
Finality~15s~400ms
Throughput~15 TPS~65,000 TPS

Account Ownership

RUST
// Transfer ownership
pub fn transfer_ownership(
    account: &AccountInfo,
    new_owner: &Pubkey,
) -> ProgramResult {
    // Only current owner can transfer
    account.assign(new_owner);
    Ok(())
}

// Check ownership
pub fn is_owned_by(account: &AccountInfo, owner: &Pubkey) -> bool {
    account.owner == owner
}

PDA (Program Derived Address)

RUST
use solana_program::pubkey::Pubkey;

// Find PDA
pub fn find_pda(
    program_id: &Pubkey,
    seeds: &[&[u8]],
) -> (Pubkey, u8) {
    Pubkey::find_program_address(seeds, program_id)
}

// Example: User's token account PDA
let (user_token_pda, bump) = find_pda(
    &program_id,
    &[b"token", user_pubkey.as_ref()],
);

Cross-Program Invocations (CPI)

Programs can call other programs:

RUST
use solana_program::program::invoke;

pub fn call_other_program(
    instruction: &Instruction,
    account_infos: &[AccountInfo],
) -> ProgramResult {
    invoke(instruction, account_infos)?;
    Ok(())
}

Real-World Examples

  • Serum DEX: Decentralized exchange on Solana
  • Raydium: AMM on Solana
  • Magic Eden: NFT marketplace
  • Jupiter: DEX aggregator

Advantages

  • High Throughput: Parallel execution
  • Low Fees: ~$0.00025 per transaction
  • Fast Finality: Sub-second confirmation
  • Composability: Programs can call each other

Challenges

  • Account Model: Different from Ethereum
  • Rent: Accounts must pay rent
  • Size Limits: Accounts have size limits
  • Learning Curve: Different programming model

Code Examples

Solana Account Structure

Basic account structure in Solana

RUST
// Simplified Solana account model
use std::collections::HashMap;

#[derive(Clone)]
pub struct Pubkey([u8; 32]);

struct Account {
    key: Pubkey,
    lamports: u64,
    data: Vec<u8>,
    owner: Pubkey,
    executable: bool,
}

struct SolanaState {
    accounts: HashMap<Pubkey, Account>,
}

impl SolanaState {
    fn new() -> Self {
        SolanaState {
            accounts: HashMap::new(),
        }
    }
    
    fn create_account(&mut self, key: Pubkey, owner: Pubkey, data: Vec<u8>) -> Account {
        let account = Account {
            key: key.clone(),
            lamports: 0,
            data,
            owner,
            executable: false,
        };
        self.accounts.insert(key, account.clone());
        account
    }
    
    fn get_account(&self, key: &Pubkey) -> Option<&Account> {
        self.accounts.get(key)
    }
    
    fn transfer_lamports(&mut self, from: &Pubkey, to: &Pubkey, amount: u64) -> Result<(), String> {
        let from_account = self.accounts.get_mut(from)
            .ok_or("From account not found")?;
        
        if from_account.lamports < amount {
            return Err("Insufficient balance".to_string());
        }
        
        from_account.lamports -= amount;
        
        let to_account = self.accounts.get_mut(to)
            .ok_or("To account not found")?;
        to_account.lamports += amount;
        
        Ok(())
    }
}

fn main() {
    let mut state = SolanaState::new();
    
    let alice_key = Pubkey([1; 32]);
    let bob_key = Pubkey([2; 32]);
    
    // Create accounts
    state.create_account(alice_key.clone(), Pubkey([0; 32]), vec![]);
    state.create_account(bob_key.clone(), Pubkey([0; 32]), vec![]);
    
    // Set initial balance
    if let Some(account) = state.accounts.get_mut(&alice_key) {
        account.lamports = 1_000_000_000; // 1 SOL
    }
    
    // Transfer
    state.transfer_lamports(&alice_key, &bob_key, 100_000_000).unwrap();
    
    println!("Solana account model: Everything is an account!");
}

Explanation:

In Solana, everything is an account. Programs are accounts with executable=true. Data is stored in separate accounts. This enables parallel execution and high throughput.

Program Derived Address

PDA concept and usage

RUST
// Simplified PDA concept
use std::collections::HashMap;

struct Pubkey([u8; 32]);

struct PDA {
    program_id: Pubkey,
    seeds: Vec<Vec<u8>>,
    address: Pubkey,
    bump: u8,
}

fn find_pda(program_id: &Pubkey, seeds: &[&[u8]]) -> (Pubkey, u8) {
    // Simplified: In real Solana, uses find_program_address
    // Combines seeds + program_id + bump to find valid address
    let combined: Vec<u8> = seeds.iter()
        .flat_map(|s| s.iter().copied())
        .chain(program_id.0.iter().copied())
        .collect();
    
    // Simplified hash
    let mut address = [0u8; 32];
    for (i, &byte) in combined.iter().take(32).enumerate() {
        address[i] = byte;
    }
    
    (Pubkey(address), 255) // Simplified bump
}

struct Program {
    id: Pubkey,
    pdas: HashMap<Pubkey, PDA>,
}

impl Program {
    fn new(id: Pubkey) -> Self {
        Program {
            id,
            pdas: HashMap::new(),
        }
    }
    
    fn create_pda(&mut self, seeds: &[&[u8]]) -> Pubkey {
        let (address, bump) = find_pda(&self.id, seeds);
        
        let pda = PDA {
            program_id: self.id.clone(),
            seeds: seeds.iter().map(|s| s.to_vec()).collect(),
            address: address.clone(),
            bump,
        };
        
        self.pdas.insert(address.clone(), pda);
        address
    }
}

fn main() {
    let program_id = Pubkey([1; 32]);
    let mut program = Program::new(program_id);
    
    // Create PDA for user token account
    let user_pubkey = b"user123";
    let pda = program.create_pda(&[b"token", user_pubkey]);
    
    println!("Created PDA for user token account");
    println!("PDA can only be controlled by the program!");
}

Explanation:

PDAs are addresses derived from a program ID and seeds. They have no private key, so only the program can sign for them. This enables programs to own accounts and manage state.

Exercises

Simple Account Transfer

Create a simple account transfer function!

Medium

Starter Code:

RUST
struct Account {
    lamports: u64,
}

fn main() {
    // Create two accounts
    // Transfer lamports between them
}