Introduction
Solana's programming model diverges fundamentally from the EVM's contract-centric architecture. Instead of smart contracts that encapsulate both logic and state, Solana separates the two: programs are stateless executables, and accounts are the universal primitive for storing state. Mastering the account model is non-negotiable for writing secure, efficient Solana programs.
Anatomy of a Solana Account
Every account on Solana is a contiguous buffer of bytes stored in the cluster's global state. The runtime exposes the following metadata for each account:
pubkey– The 32-byte Ed25519 public key that uniquely identifies the account.lamports– Au64balance denominating the account's SOL holdings (1 SOL = 1e9 lamports).data– A variable-length byte array (Vec) holding arbitrary serialized state.owner– The program ID (pubkey) that has exclusive write authority overdataandlamportsdeduction.executable– Boolean flag. Whentrue, the runtime treatsdataas BPF bytecode.rent_epoch– Legacy field tracking rent collection; largely irrelevant since mandatory rent-exemption (post v1.17+).
Ownership Semantics
Ownership is Solana's access-control backbone:
1. Only the owning program may modify an account's data bytes or debit its lamports.
2. Any program may credit lamports to any account.
3. The System Program (11111111111111111111111111111111) owns all newly created wallet accounts.
4. Ownership transfer is possible: the current owning program can reassign owner to another program ID.
This means a token account is owned by the Token Program, not by the user's wallet. The wallet is merely the authority encoded inside the account's data—a critical distinction that trips up newcomers.
Account Creation and Allocation
Accounts are created via the System Program's CreateAccount instruction, which requires three parameters:
lamports– Enough to satisfy rent exemption.space– Bytes to allocate (max 10 MB as of 2025, though practical CU limits constrain this).owner– The program that will own the new account.
`rust
// Pseudocode: Creating an account in an Anchor program
let cpi_accounts = system_program::CreateAccount {
from: payer.to_account_info(),
to: new_account.to_account_info(),
};
let space = 8 + MyStruct::INIT_SPACE; // 8-byte discriminator + data
let rent = Rent::get()?.minimum_balance(space);
system_program::create_account(
CpiContext::new(system_program.to_account_info(), cpi_accounts),
rent,
space as u64,
&program_id,
)?;
`
Rent Exemption
Since the deprecation of rent collection, all accounts must be rent-exempt at creation. The minimum balance is calculated as:
`
minimum_balance = 19.055441478 lamports/byte-year × 2 years × (128 + data_len)
`
The 128-byte overhead covers the account metadata header stored by the runtime. Attempting to create an account with insufficient lamports will fail.
Program Derived Addresses (PDAs)
PDAs are deterministic addresses that do not lie on the Ed25519 curve, meaning no private key exists. They are derived via:
`
(pda, bump) = find_program_address([seed1, seed2, ...], program_id)
`
Internally, the runtime appends a single byte (bump) and hashes SHA256(seeds ++ program_id ++ "ProgramDerivedAddress"), decrementing the bump from 255 until the result is off-curve.
Key Properties and Edge Cases
- Canonical bump:
find_program_addressreturns the highest valid bump. Always store and reuse it to avoid computing against lower bumps, which are valid but non-canonical and open front-running vectors. - Seed length limits: Each individual seed ≤ 32 bytes; maximum 16 seeds per derivation.
- PDA signing: Programs can sign for their PDAs via
invoke_signed, enabling CPIs where the PDA acts as authority. - Collision risk: If two different seed schemes across programs produce the same PDA, only the owning program can sign for it—cross-program collision is functionally impossible to exploit.
Account Types in Practice
| Account Type | Owner | Executable | Typical Use |
|---|---|---|---|
| Wallet (system account) | System Program | No | Hold SOL |
| Program account | BPF Loader | Yes | Store bytecode |
| Program data account | BPF Loader | No | Upgradeable program buffer |
| Token account | Token Program | No | SPL token balance |
| PDA data account | Your program | No | Custom program state |
| Mint account | Token Program | No | Token mint config |
Advanced Patterns and Edge Cases
Zero-Copy Deserialization
For large accounts (> a few KB), Borsh deserialization copies all data onto the stack/heap. Anchor's #[account(zero_copy)] uses bytemuck to reinterpret the raw byte buffer as a struct reference, eliminating copies and reducing CU usage.
`rust
#[account(zero_copy)]
#[repr(C)]
pub struct OrderBook {
pub orders: [Order; 512],
}
`
Edge case: Zero-copy structs must be #[repr(C)] with no padding ambiguity. Misaligned fields will cause runtime panics.
Account Reallocation
Since the realloc syscall, programs can resize accounts dynamically:
`rust
account_info.realloc(new_size, false)?;
`
The boolean controls zero-initialization of new bytes. When increasing size, you must transfer additional lamports to maintain rent-exemption. Shrinking does not automatically refund lamports—you must explicitly transfer the excess.
Closing Accounts Safely
The canonical pattern to close an account:
1. Transfer all lamports to a recipient.
2. Zero out the data buffer.
3. Optionally reassign owner to the System Program.
Revival attack: If you only drain lamports without zeroing data, a malicious actor can in the same transaction re-fund the account, and it retains its old data and owner. Anchor's close constraint handles this by zeroing + reassigning.
Account Confusion Attacks
Since accounts are just byte buffers, a program must validate:
- The owner matches the expected program.
- The discriminator (first 8 bytes in Anchor) matches the expected type.
- The account is not executable when data accounts are expected.
Omitting these checks enables type-confusion exploits where an attacker substitutes a malicious account with crafted data.
Data Serialization
Solana imposes no serialization format—programs choose their own. Common choices:
- Borsh: Default in Anchor. Deterministic, compact, well-supported in Rust/JS.
- Zero-copy (bytemuck): For performance-critical, fixed-size structs.
- Custom: Some programs use hand-rolled serialization to minimize CU.
Comparison with EVM
| Dimension | Solana | EVM |
|---|---|---|
| State location | Explicit accounts passed to tx | Implicit in contract storage slots |
| Parallelism | Possible (non-overlapping accounts) | Sequential within block |
| State cost | Rent-exempt deposit (refundable) | Gas for SSTORE (non-refundable*) |
| Program upgrades | Native via upgrade authority | Proxy patterns |
Solana's explicit account model enables the Sealevel runtime to execute transactions in parallel when their account sets don't overlap—a key throughput advantage.
Conclusion
Solana's account model is both its superpower and its steepest learning curve. The separation of state and logic, combined with ownership rules, PDAs, and explicit account passing, creates a powerful but unforgiving environment. Mastering the nuances—canonical bumps, revival attacks, zero-copy alignment, realloc rent math—separates production-grade programs from audit-failing prototypes. Build with these invariants in mind, and Solana's performance model will reward you.