Program-Owned State
Solana programs are stateless executables. All durable state lives in program-owned accounts - separate addresses whose owner field is your program ID.
Recipe
#[account]
pub struct Escrow {
pub maker: Pubkey,
pub amount: u64,
}
#[account(
init,
payer = maker,
space = 8 + Escrow::INIT_SPACE,
seeds = [b"escrow", maker.key().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,When to reach for this:
- Designing any protocol that stores user or global configuration data
- Replacing EVM
mapping(address => Data)patterns - Separating hot per-user state from global config accounts
- Enabling parallel transactions on non-overlapping user accounts
Working Example
use anchor_lang::prelude::*;
declare_id!("Escrow1111111111111111111111111111111111111");
#[program]
pub mod escrow_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>, amount: u64) -> Result<()> {
let escrow = &mut ctx.accounts.escrow;
escrow.maker = ctx.accounts.maker.key();
escrow.amount = amount;
Ok(())
}
pub fn release(ctx: Context<Release>) -> Result<()> {
let escrow = &ctx.accounts.escrow;
require!(escrow.amount > 0, EscrowError::Empty);
// transfer lamports via CPI...
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = maker,
space = 8 + Escrow::INIT_SPACE,
seeds = [b"escrow", maker.key().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,
#[account(mut)]
pub maker: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct Escrow {
pub maker: Pubkey,
pub amount: u64,
}What this demonstrates:
- Escrow state is an account owned by
escrow_program - PDA seeds tie the account to a specific maker
- Program bytecode never stores
amount- only the escrow account does
Deep Dive
State Patterns
| Pattern | Seeds | Use |
|---|---|---|
| Per-user PDA | [b"user", pubkey] | Profiles, positions |
| Global config | [b"config"] | Admin settings |
| Per-mint vault | [b"vault", mint] | Token custody |
Why Stateless Programs
- Same program code serves unlimited users via separate accounts
- Parallel execution when transactions touch different accounts
- Upgrades replace code without migrating embedded storage
Gotchas
- Global singleton for all users - contention and size limits. Fix: shard state across PDAs.
- Storing state in instruction data only - not durable. Fix: persist to accounts.
- Missing PDA bump storage - recomputation cost and seed canonicality issues. Fix: store bump in account or use
seeds::programwithbump. - Owner mismatch on init - another program could impersonate. Fix: Anchor
initsets owner automatically. - Not closing stale accounts - rent locked forever. Fix:
closeconstraint when lifecycle ends.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| PDA per entity | User-specific mutable state | Tiny global flags |
| Single config account | Protocol-wide settings | High-write user data |
| Zero-copy account | Large account data | Small structs |
| State compression | Mass mints / claims | Frequently updated records |
FAQs
Where does my program store a user's balance?
In a program-owned account (often a PDA) - not inside the program binary.
Can programs read other programs' state accounts?
Yes, if passed in the transaction and deserialization is known. Writing still requires owner program logic.
How many accounts can one program own?
Unlimited distinct addresses. Each is a separate account on-chain.
What is the global account anti-pattern?
One account holding all user records. Causes size limits, CU blowups, and parallelization failure.
Do PDAs count as program-owned state?
Yes. PDAs are accounts owned by the deriving program.
How does upgrade affect state?
State accounts persist across program upgrades. Only bytecode changes.
Can I migrate state to a new layout?
Yes via realloc, new accounts, or deserialization versioning in instruction handlers.
Who pays to create state accounts?
The payer in init - user or subsidizing protocol.
Is program-owned the same as user-owned?
Program owns the account; user controls via signer checks on instructions that mutate it.
How do indexers find all accounts for a program?
getProgramAccounts RPC with memcmp filters on discriminators.
Can state accounts hold SOL?
Yes - lamports field. Common in escrow and vault patterns.
What is InitSpace in Anchor 0.32.1?
Derive macro calculating Borsh serialized size for space attribute.
Related
- System Accounts vs. Program Accounts - account types
- Data Accounts & Layout - serialization
- Creating & Closing Accounts - lifecycle
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.