Solana's account model explained (for developers coming from Ethereum)

Published 2026-09-29 · 8 min read

Solana's account model explained (for developers coming from Ethereum)

If you're an Ethereum developer trying to reason about Solana for the first time, the account model is the biggest mental hurdle. On Ethereum, an ERC-20 balance is just a slot in the token contract's storage. On Solana, each user's balance of each token is a separate account. Here's why that matters, what to expect, and where the gotchas live.

The one-sentence version

Solana state = a big global key-value map from pubkey → account, where every account has an owner program that governs how its data can mutate. Users, tokens, positions, everything is an account.

Side-by-side comparison

Concept Ethereum Solana
User wallet EOA at 0x... System-owned account at base58 pubkey
Contract Bytecode + storage at 0x... Executable account (program) + separate data accounts
ERC-20 balance Slot in the token contract Separate ATA (Associated Token Account) per user
ERC-721 (NFT) tokenId → owner slot Separate token account holding balance = 1
Storage cost Paid via gas per SSTORE Rent-exempt deposit per account (refundable on close)
Executed code Contract bytecode in-place Program bytecode + accounts passed explicitly
Contract state Implicit (in contract storage) Explicit (data accounts named in the tx)

Why "one account per user per token"?

Solana's runtime is optimized for parallelism. Every transaction declares upfront which accounts it will read/write. The scheduler can then execute non-overlapping transactions in parallel across CPU cores.

If everyone's USDC balance was in a single "USDC contract storage" (Ethereum-style), any two USDC transfers would conflict on the same account — no parallelism. By giving each user their own ATA per token, transfers between different pairs of users are non-conflicting and run in parallel.

This is why Solana can theoretically process 65k TPS (in practice ~2-4k for real-world load). The account model is the enabler.

What's an ATA exactly?

An Associated Token Account (ATA) is a deterministically-derived account address:

ata_address = PDA(seeds = [wallet_pubkey, TOKEN_PROGRAM_ID, mint_pubkey])

For a given (wallet, token) pair, the ATA address is always the same. This is like Ethereum's CREATE2 predictable addresses, but built into the token standard from day one.

To hold USDC, your wallet's USDC ATA needs to exist. If it doesn't, the sender creates it (paying 0.00204 SOL as rent-exempt deposit).

Rent-exempt deposit — the analog to gas-for-storage

On Ethereum, you pay gas per SSTORE (~20k gas for a new slot, ~5k for updates). That gas is burned. Storage stays forever, subsidized by future gas payments.

On Solana, you deposit SOL when creating an account (rent-exempt minimum) and the SOL stays in the account. When you close the account, you get 100% of the deposit back. The network is essentially borrowing your SOL to guarantee storage — not consuming it.

The math for a standard SPL token account (165 bytes):

rent_exempt = (128 + 165) * 3480 * 2 = 2,039,280 lamports = 0.00203928 SOL

For an Ethereum dev, this is the biggest UX gotcha: your users are locking SOL every time they touch a new token, and that SOL is invisible in most wallet UIs.

The "hidden SOL" problem

Because ATAs stay open after balance goes to zero (unless explicitly closed), active users accumulate hundreds of empty ATAs. Each holds 0.00204 SOL that's technically theirs, but locked out of their spendable balance until they call closeAccount.

For a memecoin trader with 500 dead positions: 500 × 0.00204 = 1.02 SOL locked (~$200 at current prices).

Tools like SolDuster, jup.ag/close, and sol-incinerator help reclaim it. This is one of the most common "wait, where did my SOL go" questions from new Solana users coming from Ethereum.

Program invocation model

On Ethereum: contract.transfer(recipient, amount) — the contract's storage is implicitly used.

On Solana:

const ix = createTransferInstruction(
  sourceAta,      // your USDC ATA
  destinationAta, // their USDC ATA
  wallet,         // authority (signer)
  amount,
  [],
  TOKEN_PROGRAM_ID
);

You explicitly pass every account the program will read/write. This is verbose but enables the parallel scheduler.

Programs are stateless

Ethereum contracts have their own storage. Solana programs are pure code — no state. State lives in data accounts owned by the program.

Example: a Uniswap V2 pool on Ethereum is one contract instance with its own storage (reserves, LP token supply, etc.). On Solana (Raydium AMM), the "program" is one deployed instance shared across all pools; each pool is a separate data account owned by that program.

This means: - You can upgrade a program without migrating state (state is in separate accounts) - Multiple "instances" of the same protocol are just multiple accounts, not multiple deploys - Program size is fixed at deploy time (no runtime storage growth)

Signers and rent-payers

Every transaction has: - 1+ signers (their keypair signs the tx) - 1+ instructions (each references N accounts) - 1 fee payer (pays the ~5000-lamport tx fee)

If an instruction creates a new account, someone has to pay its rent-exempt deposit. Usually the fee payer, but can be any account owned by the system program with enough SOL.

This is where things like create_associated_token_account get subtle: if Alice sends USDC to Bob and Bob doesn't have a USDC ATA yet, Alice's tx creates it — and Alice pays the 0.00204 SOL deposit. Some wallets warn about this, some don't.

Cross-program invocations (CPI)

Ethereum has CALL, DELEGATECALL, STATICCALL.

Solana has CPI — one program calls another program's instruction. The caller passes accounts through; the callee reads/writes them within its own program's rules.

Example: a DEX program calls the Token Program to transfer tokens. The Token Program checks the signer authority, then updates the ATA balances.

Token-2022 vs legacy SPL Token

There are two token programs on Solana: - Legacy SPL Token: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA (2020, most tokens) - Token-2022: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb (2023, new features)

Both implement the same core operations (mint, transfer, close). Token-2022 adds extensions like transfer fees, interest-bearing, non-transferable, confidential transfers, metadata pointer, permanent delegate.

If you're scanning for a user's token holdings, you must query BOTH program IDs. Ethereum has one ERC-20 standard — Solana effectively has two.

Practical devkit tips

Read a user's SPL token holdings:

const { value } = await connection.getTokenAccountsByOwner(
  wallet,
  { programId: TOKEN_PROGRAM_ID }
);

// Also query Token-2022:
const { value: v22 } = await connection.getTokenAccountsByOwner(
  wallet,
  { programId: TOKEN_2022_PROGRAM_ID }
);

Derive the ATA address for a given wallet + mint:

import { getAssociatedTokenAddress } from '@solana/spl-token';
const ata = await getAssociatedTokenAddress(mint, wallet);

Send a token (creates the destination ATA if needed):

import { getOrCreateAssociatedTokenAccount, transfer } from '@solana/spl-token';
const destAta = await getOrCreateAssociatedTokenAccount(
  connection, payer, mint, destination
);
await transfer(connection, payer, sourceAta.address, destAta.address, sourceOwner, amount);

Close an empty account (reclaim rent):

import { createCloseAccountInstruction } from '@solana/spl-token';
const ix = createCloseAccountInstruction(ata, destination, owner);

Common surprises for Ethereum devs

  1. Every token means opening an account — first-time buyers pay a 0.00204 SOL "onboarding tax" per new token
  2. Empty accounts persist — sell a token to zero, the account stays open (this is why users have "hidden SOL")
  3. Transactions can be too big — Solana caps txs at 1232 bytes. Complex compositions might not fit
  4. PDAs are derived, not created — you compute a PDA address deterministically, but the account at that address needs a create step to exist on-chain
  5. Owner vs signer — an account is "owned" by a program (governs what data changes are legal), separate from being "controlled" by a keypair (who can sign to modify it via the owner program)

If you're onboarding to Solana from Ethereum and want a quick scan of the "hidden SOL" issue on your own wallet: solduster.com.


Scan your wallet on solduster.com →