The Solana Mental Model vs. EVM
If you come from Ethereum, the biggest shift is where state lives. EVM contracts bundle code and storage. Solana separates programs (code) from accounts (data). This drives parallelism, account sizing, and security patterns.
Recipe
Quick-reference recipe card - copy-paste ready.
| EVM Concept | Solana Equivalent |
|---|---|
| Contract | Program (executable account) |
| Contract storage | Data accounts owned by a program |
msg.sender | Signer account in Accounts struct |
| Contract address | Program ID (or PDA for derived state) |
mapping | Multiple accounts keyed by seeds |
| Gas | Compute units (CU) + priority fees |
When to reach for this:
- Porting an EVM dapp architecture to Solana
- Explaining Solana to Ethereum-native teammates
- Designing account layouts instead of storage mappings
- Understanding why transactions list all accounts explicitly
- Avoiding "single contract owns everything" anti-patterns
Working Example
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
// EVM equivalent: contract UserProfile { mapping(address => Profile) profiles; }
// Solana: each profile is its own account, owned by this program
#[account]
pub struct UserProfile {
pub authority: Pubkey,
pub name: String,
pub score: u64,
}
#[derive(Accounts)]
pub struct CreateProfile<'info> {
#[account(
init,
payer = authority,
space = 8 + 32 + (4 + 32) + 8,
seeds = [b"profile", authority.key().as_ref()],
bump,
)]
pub profile: Account<'info, UserProfile>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[program]
pub mod profiles {
use super::*;
pub fn create_profile(ctx: Context<CreateProfile>, name: String) -> Result<()> {
let profile = &mut ctx.accounts.profile;
profile.authority = ctx.accounts.authority.key();
profile.name = name;
profile.score = 0;
Ok(())
}
}What this demonstrates:
- State is a separate
UserProfileaccount, not storage inside the program - PDA seeds (
b"profile", authority) replacemapping(address => Profile) initcreates and funds the account with rent-exempt lamports- The program is stateless - it only reads/writes accounts passed in the instruction
Deep Dive
How It Works
- Programs are bytecode in executable accounts. They do not store application data.
- Data accounts hold serialized bytes. A program writes only to accounts it owns.
- Transactions declare every account touched across all instructions.
- Sealevel schedules non-conflicting transactions in parallel based on account locks.
EVM vs Solana Comparison
| Aspect | EVM | Solana |
|---|---|---|
| State location | Inside contract | Separate accounts |
| Parallelism | Sequential per contract | Parallel across non-overlapping accounts |
| Account model | Contract has balance + storage | Every entity is an account |
| Upgradeability | Proxy patterns | Upgradeable loader (BPF Upgradeable) |
| Identity | Contract address | Program ID + PDAs |
Design Implications
- Size accounts upfront (or
realloc) - no unbounded mappings - Pass all accounts explicitly - no
CALLwith implicit context - Use PDAs as deterministic account addresses per user/entity
- Programs validate ownership on every instruction
Rust Notes
// EVM: require(msg.sender == owner)
// Solana: constraint on Accounts struct
#[account(has_one = authority)]
pub data: Account<'info, MyData>,
pub authority: Signer<'info>,Gotchas
- One big account for all users - hits size limits and prevents parallelism. Fix: one account per user/entity with PDA seeds.
- Expecting implicit caller context - Solana has no
msg.senderglobal. Fix: pass signer accounts and validate with constraints. - Calling programs without declaring accounts - transactions fail at runtime. Fix: list every account the callee needs in the transaction.
- Storing balances inside program binary - impossible; programs are read-only executables. Fix: use token accounts or lamport balances on data accounts.
- Assuming atomic cross-program state - CPI shares the transaction's account locks. Fix: design instructions so all needed accounts are in one transaction.
- Ignoring rent - accounts need lamport deposits. Fix: fund accounts to rent-exempt minimum at creation.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| PDA-per-entity | User-specific state | Global config (use single PDA) |
| Zero-copy accounts | Large datasets, CU savings | Small structs where Borsh is fine |
| State compression | Millions of NFTs/holders | Frequently mutated hot state |
| EVM on Solana (Alloy/SVM experiments) | Legacy Solidity port | Greenfield Solana-native apps |
FAQs
Where does my program store data on Solana?
In separate accounts that your program owns. The program itself holds only executable bytecode.
What replaces an EVM mapping?
PDAs derived from seeds (e.g., [b"user", user_pubkey]). Each entry is its own account.
Is there a Solana equivalent of msg.sender?
The signer account passed to your instruction. Validate it with Signer<'info> or has_one constraints.
Can two programs write the same account?
Only the owner program can write data. Other programs can read if the account is passed and marked writable by the owner program via CPI.
Why must transactions list all accounts upfront?
Sealevel uses the account list to schedule parallel execution and enforce lock rules before execution begins.
Do Solana programs have balances?
Program accounts hold lamports for rent but application token/SOL balances live in user/token accounts, not in the program.
How does upgradeability compare to EVM proxies?
Solana uses the BPF Upgradeable Loader - a program buffer and program data account. Authority can deploy new bytecode.
What replaces ERC-20 on Solana?
SPL Token program with mint and token accounts. Not a per-token contract.
Is gas the same as compute units?
Similar concept. CU measures on-chain compute. Fees include base signature cost plus priority fee for CU price.
Can I deploy one program per user like factory contracts?
Technically yes but wasteful. Use one program with many data accounts (PDAs) per user instead.
How do events work compared to EVM logs?
Programs emit logs via msg! and Anchor emit!. Clients parse transaction logs - there is no persistent log store on-chain.
Does Solana have contract constructors?
No. Use an init instruction that creates and configures accounts on first use.
Related
- Solana Basics - account model overview
- How a Transaction Flows - transaction-centric design
- SOL, Lamports & Units - native currency handling
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.