Understanding Solana: Account Model - part 2
I’m currently open to collaborations and development projects across blockchain, smart contracts, and full-stack systems, feel free to connect if you’re building something interesting.
After reading the previous post we have a knowledge about Solana architecture, in this post we will learn that in Solana, everything revolves around accounts.
Every piece of state from your tokens to your program data lives inside an account. Programs themselves are stateless, they only read and modify data that accounts expose to them. Once you understand that, Solana’s entire design starts to make sense.In this post, we’ll slow down and unpack what “an account” really means.
We’ll go through its structure, ownership rules, and how Solana uses Program-Derived Addresses (PDAs) to create predictable and secure addresses without private keys. You’ll also learn how different types of accounts fit together, from system accounts managed by the runtime to program state accounts that store your app’s logic or user data.
What makes an account the basic unit of state on Solana
Account Types
What is Program derived address (PDA)
How Solana enforces ownership, rent, and access
In the next blog, we’ll explore what is Anchor and see how these concepts map to real code and how Anchor’s account macros simplify safety and boilerplate.
Previous part
What makes an account the basic unit of state on Solana
The most important thing to understand about Solana is that unlike most blockchains where “contracts” hold their own state internally, Solana separates code and data completely.
Programs (smart contracts) are pure logic, they don’t own persistent storage.
Instead, all persistent data lives in accounts, which are on-chain data containers managed by the runtime.
You can think of an account as a self-contained piece of state with:
struct Account {
// num of lamports in the account
lamports: uint64,
// data held in this account
data: bytes,
// PubKey that owns this account. If executable, the program that loads this account.
owner: Pubkey,
// this account’s data contains a loaded program (and is now read-only)
executable: bool,
// the epoch at which this account will next owe rent
rent_epoch: Epoch,
}Every instruction that runs on Solana explicitly lists which accounts it wants to read or write.
The runtime enforces access rules:
Only the account’s owner program can modify its data.
Only the account’s signer (or a PDA acting as signer, PDA will be explained in next section) can authorize transfers or ownership changes.
This strict separation achieves two things:
Parallelism: since programs operate only on the accounts listed in the transaction, Solana can execute unrelated transactions concurrently.
Security and clarity: a program cannot accidentally modify global state; it must be given explicit permission to touch each account.
In short, accounts are the atomic unit of state on Solana.
If Ethereum’s equivalent is a contract with internal storage slots, Solana’s model externalizes that: each “slot” becomes its own addressable account on-chain.
Everything you interact with: users, tokens, pools, NFTs, even the Solana runtime itself is built on top of these accounts.
What is Program derived address (PDA)
Solana doesn’t allow programs to hold private keys.
Yet, sometimes your program needs an address it can own and control deterministically, for example, a vault to store user deposits, or a metadata account tied to a specific mint.
That’s exactly what Program-Derived Addresses (PDAs) provide.
A PDA is a special kind of account address that is:
Deterministically derived from a set of seeds and a program ID
Guaranteed not to collide with any address that has a valid Ed25519 private key.
Because PDAs don’t correspond to real keypairs, they cannot be signed using a private key.
Instead, the Solana runtime lets the owning program “sign” on behalf of that PDA during an instruction but only if the PDA was derived with the program’s own ID.
How PDAs are generated
A PDA is derived like that with the help of solana-sdk on rust:
Pubkey::find_program_address(seeds: &[&[u8]], program_id: &Pubkey)This function tries different “bump” values: It iterate bump values by starting at 255 and decrements by 1 until a valid PDA is found that is not on the Ed25519 curve (i.e., cannot have a private key).
That results a unique PDA is created for the combination of:
program_idand the
seedsarray.
Example:
let (vault_pda, bump) =
Pubkey::find_program_address(&[b”my_vault”, user.key().as_ref()], &program_id);Now vault_pda will always be the same for this user and program no need to store it anywhere.
Overview:

How PDAs “sign”
When your program makes a cross-program invocation (CPI) (Don’t worry it will be covered in later chapters, now we only want to understand the sign effect) that needs the PDA to act as a signer, for example, to transfer tokens from a PDA’s account
you use the function invoke_signed() and pass the same seeds and bump used to derive the PDA.
Example:
invoke_signed(
&instruction,
&account_infos,
&[&[b”my_vault”, user.key().as_ref(), &[bump]]],
)?;The runtime checks that those seeds and bump match a valid PDA for your program ID. If they do, it lets your program “sign” as that PDA.
No private keys, no risk of exposure.
Why PDAs matter
PDAs are at the heart of how complex Solana programs structure state.
They let you:
Create per-user or per-pool state accounts deterministically (like
[”profile”, user]or[”pool”, token_a, token_b]).Build vaults or treasuries controlled solely by your program.
Prevent arbitrary users from hijacking your storage addresses.
Avoid having to store mapping data on-chain (you can recompute addresses instead).
They are what make Solana’s stateless programs practical, safe, and composable.
Account Types
There are two basic categories that accounts fall into:
Program accounts: Accounts that contain executable code
Data accounts: Accounts that do not contain executable code, but holds information
This separation means that a program’s executable code and its state are stored in separate accounts.
Program accounts
A program account stores the compiled bytecode that defines how a Solana program behaves.
Key characteristics
Executable flag:
executable = trueOwner: always the BPF Loader
Data: the actual code of the program
Program accounts do not hold user state or variables.
When your transaction calls a program, Solana loads this executable account into memory and executes the code inside it.
Data accounts
Data accounts store all persistent state for your application: user profiles, pools, vaults, mints, and so on.
They are owned by a program, and only that program can modify their contents.
Within this broad category, you’ll encounter several important sub-types:
Program state account:
Custom accounts that hold the on-chain data for your program.
They represent things like: liquidity pool, user’s staking position, configuration record and so on…
Key properties
owner= your program’s public keyexecutable = falsemust hold enough lamports to be rent-exempt (will be covered in next section)
usually Program-Derived Addresses (PDAs) to ensure deterministic, secure ownership (Explained above)
Example
Address: pool_pda (derived from [”pool”, token_a, token_b])
Owner: MyDex111111111111111111111111111111111111111
Data: { token_a, token_b, reserves, bump }System accounts:
Accounts that are managed by the System Program.
These include:
Wallet accounts that hold SOL and can sign transactions
Temporary storage or uninitialized accounts before they’re assigned to a program
Upgrade or admin authorities (often simple keypair accounts)
System accounts are the backbone of Solana’s runtime. They can send SOL, pay rent, and be reassigned to another program as their owner.
Sysvar accounts
Read-only system-provided accounts that expose internal blockchain data to programs.
Instead of embedding runtime data into every instruction, Solana provides them through these predefined accounts. More data can be found here.
How Solana enforces ownership, rent, and access
Now that we’ve seen the different types of accounts, it’s important to understand how Solana controls who can read, write, or move lamports and how it ensures the network stays economically stable through rent.
Ownership
Every account has an owner field a public key that identifies the program authorized to modify its data.
Only the owning program can change an account’s
databytes.The System Program can change an account’s owner (using
assign) or lamport balance (usingtransfer).User wallets are just system accounts whose owner is the System Program.
When your custom program creates an account, it sets itself as the owner, making it the only program that can write to that account’s data.
Example:
account.owner = <your program id>means that only your program can modify this account’s internal data field no other program can. This strict owner–writer rule prevents accidental or malicious modification of another program’s state.
Access Control (Signers and Writable Flags)
When a transaction executes, it must explicitly list all accounts the instruction will use.
For each account, the transaction specifies:
Whether the account is read-only or writable
Whether it is a signer
The runtime enforces these flags:
Writable accounts are the only ones that can have their data or lamports modified.
Signer accounts are those that have authorized the transaction typically user wallets or PDAs through
invoke_signed.Programs cannot arbitrarily access any on-chain data. They only see what’s passed into the instruction’s context.
This explicit access model enables parallel transaction execution, since Solana knows which accounts each instruction touches.
Rent and Rent Exemption
Every account must pay rent, which is a small cost to occupy on-chain storage. However, most programs make accounts rent-exempt by depositing enough lamports upfront.
The rent-exempt minimum depends on:
the account’s data size (number of bytes), and
the current rent rate (available via the
SysvarRentaccount).
Key points:
If an account’s lamports drop below the rent-exempt threshold, the runtime can reclaim it.
Rent-exempt accounts never lose lamports or get purged.
Programs typically calculate rent like this:
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);Up Next
Once you have this foundation, we’ll move from theory to practice.
In the next post, we’ll open up Anchor in Rust, create our first on-chain program, and see how all these ideas: ownership, seeds, PDAs, and account sizing come together in real code.
You’ll learn how Anchor’s macros automatically enforce the same rules we explored conceptually here, making Solana development both safer and more ergonomic.




