Solana for people who think in Ethereum
September 25, 2026
I spent the last few weeks porting a cross-chain feature from Ethereum and Starknet to Solana. I went in expecting a slightly different virtual machine. What I found was a different way of thinking about state entirely, and for the first week almost nothing made sense.
Then one idea clicked, and everything else fell out of it. This post is that idea, built up from the ground, for someone who already understands how Ethereum or Starknet works. One thing it deliberately is not: a guide to writing Solana programs in Rust — there are plenty of good tutorials for that. This is about the model you need before any of those tutorials make sense.
Forget contracts. Think files.
On Ethereum, a contract is one thing: code and storage bundled together at one address. The USDC contract contains a mapping of every holder’s balance. Your balance lives inside someone else’s contract.
Solana takes that bundle and splits it apart. Everything on Solana is an account, and an account is best understood as a file:
Ethereum: Solana:
┌─────────────────────┐ ┌───────────────┐ ┌──────────────┐
│ USDC contract │ │ Token program │ │ your balance │
│ ├── code │ │ (executable │ │ (data file, │
│ └── storage │ │ file, code │ │ 165 bytes, │
│ ├── your bal │ │ only) │ │ just yours) │
│ ├── my bal │ └───────────────┘ └──────────────┘
│ └── ... │ ┌──────────────┐
└─────────────────────┘ │ my balance │
│ (another │
code + all state, │ data file) │
one address └──────────────┘
code in one file,
each piece of state in its own file
Every account has the same shape: an address, some bytes of data, a lamport (SOL) balance, and two important flags. One flag says whether the file is executable — a program — in which case its bytes are compiled code and nothing else. Programs on Solana are stateless: they cannot store anything inside themselves. All state lives in separate, non-executable data files.
If you come from Starknet, you already know contracts whose class (code) is declared separately from the instances that hold storage. Solana pushes that split much further: not just code vs. storage, but every user’s storage in its own account.
The owner rule
The second field every account has is an owner, and this is the single most important rule on Solana:
Only the program that owns an account may modify its data or deduct its lamports. Everyone can read; exactly one program can write.
Your USDC balance file is owned by the Token program. The Token program’s code is the only code on the entire chain that can change the numbers inside it. Not you, not another program, not even the validator. When you “hold” tokens, what you actually hold is a file whose contents only the Token program will edit, following its own rules.
Notice what this means about you. Your wallet key does not own your balance file in the protocol’s eyes — the Token program does. Your key is recorded inside the file, in a field the Token program calls the authority:
your USDC token account (a 165-byte file)
┌────────────────────────────────────────┐
│ owner: Token program ← protocol-level: who may write the file
├────────────────────────────────────────┤
│ data: │
│ mint: USDC mint address │
│ authority: YOUR wallet pubkey ← app-level: whose signature the
│ amount: 1,000,000 │ Token program's code demands
└────────────────────────────────────────┘
The protocol enforces “only the Token program writes here.” The Token program’s code enforces “and I only do it when the authority has signed.” Two layers, and keeping them separate in your head resolves half of Solana’s apparent weirdness. The protocol guards the file; the program guards the trigger.
New accounts start out owned by a built-in program called the System program (plain wallets stay that way forever — that’s why the System program is what moves SOL between wallets). A program takes ownership of a file when the file is created for it, and there is a small storage cost: each account must keep a SOL deposit proportional to its size, called rent. It’s refundable — close the file, get the deposit back. Think of it as paying for your own storage slot instead of the contract deployer paying for a mapping that grows forever.
One struct, many disguises
It’s worth pausing on how little an account actually is. Every account on the chain — your wallet, the Token program, a billion-dollar escrow — is the same five fields:
┌─────────────────────────────────────────────────────────┐
│ address where the file lives (its name) │
│ lamports SOL held by the file (balance and/or rent) │
│ data the bytes — meaning depends on the owner │
│ owner the ONE program allowed to write it │
│ executable is this file code? │
└─────────────────────────────────────────────────────────┘
Everything you meet on Solana is this struct wearing a different disguise. What changes is who owns it and what the bytes mean:
| address comes from | data holds | owner | executable | |
|---|---|---|---|---|
| wallet | your keypair | nothing (0 bytes) | System program | no |
| program | its deploy keypair | compiled code | BPF loader | yes |
| mint account | a keypair at creation | supply, decimals, mint authority | Token program | no |
| token account | usually derived (ATA) | mint, authority, amount, delegate | Token program | no |
| PDA state account | derived from seeds — no key exists | your program’s structs | your program | no |
A few things this table quietly teaches:
- A wallet is the emptiest possible account: no data at all. Your SOL
balance is just its
lamportsfield, and it stays owned by the System program forever. Everything interesting about “your account” on Solana lives in other files that merely point back at your pubkey. - Even programs obey the owner rule. A program doesn’t own itself — a
built-in loader does, and that loader is what writes new code during an
upgrade. Code is data too; it just has the
executableflag set. - The rows differ in where the address comes from: wallets and programs are backed by real keypairs, while token accounts and program state usually sit at derived addresses that anyone can recompute (more on that derivation later).
- “Whose is this?” always has two answers. The owner column is the protocol’s answer; the authority buried in the data column is the application’s. The mint and token account rows are both “owned by the Token program,” but they answer to completely different people.
Why, though?
My honest first reaction was: why? Why not let the Token program keep one big dictionary of balances, like every other chain? Why scatter state across millions of tiny files?
The answer is parallelism, and it shows up in how transactions work.
A Solana transaction must declare, up front, every account it will touch, and whether it needs each one read-only or writable. The runtime never discovers state accesses during execution the way the EVM does — it knows the full footprint before running a single instruction. That lets the scheduler do something the EVM can’t:
tx A: writes [alice-usdc, bob-usdc] ┐
tx B: writes [carol-usdc, dave-usdc] ├── disjoint files
tx C: writes [alice-usdc, erin-usdc] ┘
A and B touch different files → run in parallel, different cores
A and C both write alice-usdc → serialized
If balances lived in one dictionary inside one account, every token transfer in the world would write the same file and everything would serialize. The many-small-files design isn’t an aesthetic choice; it’s the price of running transactions concurrently. Once I saw that, the design stopped feeling arbitrary and started feeling inevitable.
The transaction envelope
That up-front account list is worth a closer look, because it doubles as the permission system. Each account in the list carries two bits — am I a signer, and am I writable — giving four kinds of entries:
writable read-only
┌───────────────────────┬─────────────────────┐
signer │ your wallet paying │ "prove you approve, │
│ fees, your token acct │ but nothing here │
│ being debited │ changes" (rare) │
├───────────────────────┼─────────────────────┤
non-signer │ the recipient's │ programs being │
│ account being │ called, config │
│ credited │ being read │
└───────────────────────┴─────────────────────┘
The recipient of a transfer doesn’t sign — their file just gets marked writable and credited. And programs themselves appear in the list too, as read-only entries, because a program is just another file the runtime needs to load.
Beyond the account list, a transaction carries instructions (which program to call, with which accounts and which bytes of arguments), a recent blockhash as an expiry mechanism, and a fee. Fees are tiny and predictable: a flat per-signature fee plus a compute budget, rather than an EVM-style gas auction.
Tokens, concretely
With the file model in place, Solana’s token standard is almost obvious. There is one Token program, deployed once, that runs every token on the chain — nobody redeploys ERC-20 code. A specific token is just two kinds of files it owns:
┌────────────────────┐ ┌──────────────────────────┐
│ mint account │ │ token account │
│ "the currency" │ │ "one holder's balance" │
│ │ │ │
│ supply │◀─────────┼─ mint │
│ decimals │ │ authority: a wallet │
│ mint authority │ │ amount │
└────────────────────┘ └──────────────────────────┘
one per token one per holder
Your wallet address never holds tokens directly. For each token you hold, you have a separate token account. To keep those addresses predictable, there’s a convention called the associated token account: a deterministic address computed from (your wallet, the mint), so anyone can figure out where to send you USDC without asking.
One more piece that matters in practice: a token account can name a
delegate — another party allowed to spend up to an approved amount from
your file. It’s ERC-20 approve/transferFrom, but stored in your own
account instead of a mapping in the token contract.
PDAs: accounts a program controls
Here’s the puzzle that leads to Solana’s most distinctive idea. Say you’re building an escrow: users deposit tokens, your program releases them later. The escrow’s token account needs an authority — and authorities prove themselves by signing. Programs don’t have private keys. So who holds the escrow?
Normal Solana addresses are points on the ed25519 curve — every one of them has a corresponding private key that someone could hold. What you want is an address that provably has no key at all, that only your program can act for.
That’s a Program Derived Address (PDA). You derive it by hashing some seeds of your choosing together with your program’s ID, and the derivation deliberately lands off the curve:
seeds ("escrow", token-mint) + program ID
│
hash ── bumped until the result
│ falls OFF the ed25519 curve
▼
an address with no possible private key
PDAs solve two problems at once:
-
Deterministic addresses. Anyone can recompute the PDA from the seeds, so your program’s state files sit at well-known addresses — like a Starknet contract address derived from its class hash and salt, or a CREATE2 address on Ethereum. No registry needed.
-
Program signatures. The runtime lets a program “sign” as its own PDAs when calling other programs. Since no private key exists, and only the program that the address was derived from gets this privilege, a PDA is an identity that your program — and nothing else in the universe — can wield. The escrow token account’s authority is a PDA, and suddenly a keyless program can own funds.
The same two-layer rule from earlier applies: the protocol guarantees only your program can sign for the PDA, but when it signs is decided entirely by your program’s code. The protocol guards the address; your code guards the trigger.
Programs calling programs
Solana programs call each other constantly — your program never touches token balances itself; it asks the Token program to. This is a cross-program invocation (CPI), and it has one elegant property: privilege flows down.
When a user signs your transaction, and your program CPIs into the Token program, the accounts you pass along keep their signer and writable flags:
user signs tx ──▶ your program ──CPI──▶ Token program
│ │
│ passes accounts │ sees "user's token acct,
│ it received, │ authority signed" and
│ flags intact │ performs the transfer
└──── + may add its own PDA "signature"
Your program can’t invent a signature the user didn’t give, but it can forward one, and it can add signatures for its own PDAs. Calls can nest a few levels deep, and every hop re-checks the same rules.
There’s a security instinct hiding here that Ethereum developers don’t need:
on Ethereum, when your contract calls USDC.transfer(...), the address of
USDC is baked into your code. On Solana, the caller supplies every account,
including the ones your program will treat as “the token program” or “the
config.” Nothing stops a user from handing you a hostile lookalike file with
the right shape and the wrong contents. Solana programs therefore spend their
opening lines verifying accounts: is this file owned by who I expect? Is its
address the PDA I would derive? Does its data point back to the mint I was
told? Most real Solana exploits are a missing one of these checks, not a
buggy state transition.
Programs are files too — and files can be replaced
One last inversion of Ethereum intuition. Ethereum contracts are immutable by default, and upgradeability is an opt-in trick (proxies). Solana programs are upgradeable by default: a program account has an upgrade authority that can swap out the code, and immutability is what you opt into, by burning that authority.
This changes what “trust the contract” means. On Solana you’re always implicitly trusting whoever holds the upgrade authority — which is why serious projects move it to a multisig or burn it, and why the first thing I check on any Solana program now is who can rewrite it.
The whole picture
┌────────────────────────────┐
│ everything is │
│ an account (file) │
└─────────────┬──────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
programs = executable state = data files one owner per file
files, stateless (rent-paid, closable) (the owner rule)
│ │ │
▼ ▼ ▼
upgradeable by default declared up front authority (in data)
via upgrade authority in every tx ≠ owner (protocol)
│
┌─────────┴──────────┐
▼ ▼
parallel execution PDAs: keyless files
(disjoint files a program alone controls,
run concurrently) signed via derivation in CPIs
Every design decision on this chain traces back to the top box. Accounts are files; files have exactly one owner; transactions must name their files in advance. Parallelism, the token model, PDAs, CPI privilege rules, even the exploit patterns — all of it is downstream of those three sentences.
It took me an embarrassing number of “wait, why?” questions to get here. If you’re coming from Ethereum or Starknet, my advice is to resist mapping Solana concepts onto contracts-and-storage for as long as you can. Start from the filesystem, and let the rest be derived — on this chain, derivation is kind of the whole point.