System Accounts vs. Program Accounts
System-owned accounts are simple lamport holders. Program-owned accounts have structured data interpreted by their owner. Program (executable) accounts contain BPF bytecode. Each type has different mutation rules.
Recipe
| Type | Owner | Executable | Typical Use |
|---|---|---|---|
| Wallet | System Program | false | Hold SOL |
| Data PDA | Your program | false | App state |
| SPL Token Acct | Token Program | false | Token balances |
| Program | BPF Loader | true | On-chain logic |
Working Example
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[derive(Accounts)]
pub struct TransferSol<'info> {
#[account(mut)]
pub from: SystemAccount<'info>,
#[account(mut)]
pub to: SystemAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct MutateState<'info> {
#[account(mut, owner = crate::ID)]
pub state: Account<'info, GameState>,
}What this demonstrates:
SystemAccountwraps plain lamport-holding accounts- Custom state requires
owner = your_program_id - System Program handles SOL transfers; your program handles its own data accounts
Deep Dive
Mutation Rules
- System Program can create accounts, assign owners, transfer lamports
- Owner program can write
dataand debit lamports (within rules) - Non-owner programs can only read unless owner delegates via CPI
- Executable accounts cannot be written as data during normal invocations
Native Programs
| Program | ID (well-known) | Role |
|---|---|---|
| System | 11111...1111 | Account lifecycle, SOL transfer |
| SPL Token | Tokenkeg... | Fungible tokens |
| BPF Loader | BPFLoaderUpgradeab1e... | Deploy and upgrade programs |
Gotchas
- Passing wallet as
Account<T>- deserialization fails on empty data. Fix: useSystemAccountorSigner. - Writing to SPL token accounts directly - only Token Program can. Fix: CPI to SPL Token instructions.
- Treating program ID account as state - program account is executable, not your PDA. Fix: store state in PDAs owned by your program.
- Wrong owner constraint - security vulnerability. Fix: always set
owner = program_idor use AnchorAccount<T>auto-check. - Invoking executable accounts as data - runtime error. Fix: pass program accounts only as
Program<'info, T>orAccountInfofor CPI.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
SystemAccount | SOL-only accounts | Structured data |
Account<T> | Typed program state | Raw unknown layouts |
InterfaceAccount | Token-2022 / MPL compatibility | Plain system accounts |
FAQs
What owns a new wallet account?
The System Program. Data is empty; lamports hold SOL.
Can the System Program write token data?
No. SPL Token program owns token accounts.
What is an executable account?
A deployed program. executable = true, owned by a BPF loader.
How does ownership transfer?
Via System Program assign instruction, called by the current owner program.
Can one transaction mix system and program accounts?
Yes. Transactions commonly include wallets, PDAs, token accounts, and program IDs.
What is the BPF Upgradeable Loader?
Owns upgradeable program accounts on Agave 4.1.1. Separates program ID from program data.
Are PDAs system-owned?
No. PDAs are owned by the program that derived them.
Can users own their own data accounts?
The account is owned by your program; users control via signer authority stored in data or as PDA seeds.
What happens if owner is wrong at init?
Another program could write your data. Critical security bug.
How do I verify owner in Anchor?
Account<'info, T> checks owner == program ID automatically. Add explicit owner = X for cross-program accounts.
Is the System Program special-cased?
Yes. It alone can create accounts and assign initial owners without being the current owner.
Can program accounts hold lamports?
Yes. Programs accumulate lamports for rent; withdrawals require program logic.
Related
- Account Anatomy - owner and executable fields
- Program-Owned State - state account pattern
- Account Ownership & Permissions - write rules
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.