T5 · The EVM Account Model
What really distinguishes an EOA from a contract account?
- Capabilities practiced
- BuilderProtocol Literacy
- Hands on
- Read a contract's storage slot directly via RPC and compare it with the getter's return value.
- AI Lab
- Have AI calculate the gas breakdown of code, then compare it with an actual transaction receipt.
A Real-World Problem
Your product needs a team vault: funds may leave only when two of three people agree.
At the design meeting, one person says, "Receive funds at a multisig wallet address." Another says, "Deploy a vault contract." From outside, the outputs look identical: each is 40 hexadecimal characters after 0x, can receive funds, and has a balance visible in a block explorer.
You ask three questions, but nobody answers clearly:
- Can this address initiate a transaction on its own?
- Can a transfer into this address fail?
- Who can change the rules at this address?
There is an even more basic question: **where do deployed contract code and the data it remembers actually live?**Can you read them as you would read a database?
What distinguishes two addresses that look identical?
Thought Experiment
Imagine the entire chain as one globally shared computer with one enormous table.
Each row's key is an address. Its value has four fields:
nonce a counter
balance balance
codeHash hash of a piece of code
storageRoot hash of this address's own small tableThe entire distinction is whether codeHash is empty.
If it is empty, the address is the location controlled by a key. Its owner can sign and initiate transactions with a private key, and its small table is always empty.
If it is not empty, the address holds code and its own small table. It cannot initiate anything by itself. When someone calls it, the code runs and can read or write that table.
Now derive three things:
**First: this computer has no clock.**No cron, timers, or background threads. Its whole life is "someone sends a transaction → execute → state changes → wait for another." It is completely still between transactions.
Second: every computation on this computer costs money. Every node worldwide runs the same code and stores the result, so each operation has a price. Writing to the small table is especially expensive because every node must retain that data permanently.
**Third: only the first kind of address can pay to start the process.**A contract account has no private key and cannot sign. Every execution path ultimately starts with a transaction signed by a private-key holder. Complex contracts move only because that path pulls them into action.
Return to the opening problem. The two solutions are not interchangeable: the third field in that table differs, changing whether the address can act, whether code runs when funds arrive, and where its rules live.
Your Decision
Requirement: a deposit contract should pay depositors interest once a day. How do you build it?
What Happens
The four options share one rule: every state change needs a private-key holder to sign a transaction and pay for the change.
| Approach | Who triggers it? | Who pays? | New dependency |
|---|---|---|---|
| Contract schedules itself | Nobody; impossible | — | — |
| Settle on interaction | Next person to interact | That unlucky caller | None, but timing is uncontrolled |
| Run a scheduled service | Your service | You | Centralized always-on service |
| Let users claim | Users | Users | None, but users must know and want to pay |
Two broader lessons follow:
**1. "Who pays?" is a product decision, not an implementation detail.**Placing the cost on a party changes the shape of the feature. If you want zero fees for users, you absorb them and inherit a hot wallet and operations work.
2. Gas constraints shape contract data structures. "Loop through all users" is one ordinary backend loop but an on-chain transaction likely to fail. Cumulative indexes, lazy settlement, and Merkle lists all arise from that constraint.
Build a Model
One table, four fields:
address → {
nonce EOA: transactions sent; contract: contracts created
balance native coin balance in wei
codeHash empty → EOA; nonempty → contract account
storageRoot root of this address's storage tree
}Compare the two account types:
| EOA | Contract account | |
|---|---|---|
| How created | Derived from a private key, without an on-chain action | Deployed by a transaction |
| Can initiate a transaction? | Yes; the only starting point | No |
| Has code? | No | Yes |
| Has storage? | No | Yes |
| Meaning of nonce | Sequence of transactions sent | Number of contracts created |
| Receives native coins | Always succeeds | May fail, depending on its code |
| Who can change the rules? | Private-key holder | Depends on the contract: perhaps nobody, perhaps one address |
The last two rows answer the vault question. Sending native coins to a contract may fail, particularly if it has no way to receive them. The transfer then reverts while the fee is still charged, as T3 explained.
What storage looks like:
Every contract has its own address space. Slots start at 0, and each holds 32 bytes. Layout follows deterministic rules:
| Variable type | Where it lives |
|---|---|
| Fixed-size variable | Numbered in declaration order from 0; small variables may be packed into one 32-byte slot |
| Dynamic array | Its declaration slot stores length; elements start at the hash of that slot number |
| Mapping | Its declaration slot stays empty; a key's value lives at the hash of the key concatenated with the slot number |
**There are no truly private variables on-chain.**Solidity's private prevents other contracts from accessing a variable through code, but anyone off-chain can read its slot directly. A Real-World Case below shows a costly failure from assuming otherwise.
How gas is priced:
A transaction's components, roughly from more expensive to less:
Write a storage slot from zero to nonzero most expensive: every node stores new data forever
Modify a nonzero storage slot expensive, but notably less than a new slot
Read a storage slot moderate; repeat reads in the same transaction cost less
Compute in memory cheap; discarded at transaction end
Read calldata cheapest; it is already in the transaction
Base transaction cost + per-byte calldata fixed costs independent of your codeThree practical deductions:
- The first gas-saving priority is fewer storage writes, then fewer storage reads, and only then cheaper computation. Moving one storage write out of a loop may save more than changing a loop from O(n) to O(log n).
- **The first access to a slot and later accesses within one transaction have different costs.**T8 covers cold and warm access. Measure gas instead of guessing.
- **Exact prices change with protocol upgrades; do not memorize them.**Remember relative costs and the habit of measuring with
eth_estimateGasand actual receipts.
What It Is Called
An account controlled by a private key. Its address is derived from a public-key digest, as T2 explained. It exists without any on-chain operation; you can send funds to a never-used address.
It is the starting point for execution: follow any on-chain call path back far enough and you reach a transaction signed by an EOA.
An account created by a deployment transaction with code and storage. It has no private key and cannot initiate transactions.
It executes its code only when called. Code is normally immutable after deployment; an "upgrade" forwards calls through a proxy to another address. T8 explains how that can go wrong.
The term means different things for EOAs and contracts. For EOAs, it is the number of transactions sent—the counter that can stall transactions in T3. For contracts, it is the number of contracts they have created.
This explains why a newly created contract's address is predictable: it is derived from the creator's address and nonce.
One position in a contract's table, addressed by a 32-byte index and containing 32 bytes.
It is among the chain's most expensive resources, and the one that persists after a transaction ends. Writing it costs so much because every node must store those 32 bytes indefinitely.
The unit for pricing computation and storage use. Each operation consumes gas; the payment is gas consumed times the price per unit.
Its purpose is more than collecting fees: it prices resources on a shared computer so an infinite loop cannot stall every node worldwide. T3 described how its two limit parameters can fail in different ways.
The sum of the big table of all accounts. A block header stores its digest, the state root, rather than the entire table.
This explains T1's point: state is not separately "written" into a chain; it results from executing all transactions in order. The chain commits to a digest of that result.
Hands-on
**Read-only throughout: no transactions, private keys, or real assets.**A testnet or a public read-only mainnet endpoint works; reads cost no gas fee.
First, learn to distinguish account types.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode",
"params":["<an-address>","latest"]}'A response of 0x means EOA; a long bytecode response means a contract account. This is the reliable way to distinguish them. Their address strings look the same.
Try your own wallet, a token contract, and a made-up address. The made-up address also returns 0x: a never-used address and an EOA look alike in chain state.
Check what a contract account's nonce means.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount",
"params":["<a-contract-address>","latest"]}'Find a factory contract that creates child contracts and try it. You may see a nonzero value. This does not count "transactions sent by the contract," which it cannot send; it counts contracts it created.
Read slot 0 of a contract.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getStorageAt",
"params":["<a-token-contract-address>","0x0","latest"]}'The response is 32 bytes. Find a token contract with verified source code in a block explorer. Look up its first state variable and try interpreting the returned bytes as that value.
If it is a string, or several small variables packed together, the result will not be obvious. That is the lesson: storage is organized into 32-byte slots, not directly into variables.
Use eth_call to read the getter for the same data.
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"<the-same-token-contract-address>",
"data":"<four-byte-selector-for-totalSupply>"},"latest"]}'A function selector is the first four bytes of keccak256 applied to its signature string. totalSupply() has a fixed selector; calculate it yourself to verify. This is the chapter's first direct use of the hash from T2.
Find the slot that holds totalSupply.
In the verified source, count state variables by declaration order to estimate the slot, allowing for packing. Read it with eth_getStorageAt and compare with the previous eth_call response.
**The values should match exactly.**One comes from the contract's getter, the other from reading storage directly.
If they do not, you may have counted the wrong slot or encountered a proxy: the data lives in proxy storage while you are reading the implementation contract. That is a useful discovery in itself.
Read a mapping value—the most valuable step.
balanceOf is a mapping. Where does an address's balance live? The rule is:
slot = keccak256( address left-padded to 32 bytes ++ mapping declaration slot left-padded to 32 bytes )Concatenate two 32-byte values into 64 bytes and apply keccak256. Use any keccak256 tool. If you installed Foundry, see whether cast has a command for mapping slots; otherwise concatenate and hash manually.
Then query:
curl -s -X POST <your-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getStorageAt",
"params":["<token-contract-address>","<the-slot-you-calculated>","latest"]}'Use eth_call to invoke balanceOf and compare. When they match, you have grasped how contract storage works.
Common causes of mismatch: address case, failing to left-pad to 32 bytes, reversing the concatenation order—key comes first, slot number second—or miscounting slots because of inheritance. Investigating each teaches something useful.
Add a block tag to see its effect.
Replace latest in any query above with a historical height to read state at that moment.
If the endpoint errors, it is not an archive node: you have encountered T4's cost trap firsthand.
AI Lab
Ask in three steps.
Step one:
Break down the gas consumed by one call to this Solidity code into
base transaction cost, calldata, storage reads, storage writes, memory and computation, and logs.
Give a figure and basis for each, plus a total. State which protocol version sets the prices.
Step two:
How much gas differs between the first and second call from the same address?
Which components differ, and why?
Step three:
Suggest three rewrites. Estimate the saving and describe the trade-off for each.Then measure. Deploy the code on a testnet, call it twice, and fetch both receipts.
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<hash>"]}'Compare gasUsed with the estimate.
**The second step and the difference are the heart of this Lab.**Gas pricing has changed through upgrades, including cold/warm access and sharply reduced refunds. A model may blend figures from different versions into a plausible but inconsistent total.
Show it the real receipt and ask which component explains the gap. Its second answer is often better because you supplied a verifiable anchor rather than requesting memorized numbers.
This is the right way to use a model for gas: form hypotheses with it and verify them against the chain.
Verify these points yourself after using AI
- Its gas number for each operation reflects the latest protocol upgrade, not an older price
- It distinguishes writing a zero slot from modifying a nonzero slot; their costs differ greatly
- It accounts for cold and warm access; a second access to a slot in one transaction costs less
- It distinguishes zero and nonzero bytes when pricing calldata
- It uses the current refund cap and does not rely on a removed refund rule
- Its opcode names, compiler options, and test-framework commands exist in official documentation
- You compare its total with receipt gasUsed and investigate any gap over ten percent
Real-World Case
In an upgrade pattern, users call a proxy address, which forwards to an implementation contract. Code comes from the implementation; storage belongs to the proxy.
Both contracts number variables starting at slot 0. If the proxy stores its implementation address at slot 0 and the implementation's first variable also uses slot 0, an ordinary assignment in the implementation can overwrite the proxy's implementation pointer.
The standard fix places critical proxy variables at pseudorandom slots derived from hashes of fixed strings, making collisions negligibly likely. Mainstream proxy implementations follow a standardized form of this pattern.
Remember this: storage belongs to an address, not to its code.
A contract stores a random seed, password hash, or unpublished list in a state variable marked private, assuming nobody outside can read it.
Anyone can fetch it with eth_getStorageAt. private is a compiler-level visibility modifier limiting other contracts' code, not off-chain reads.
This mistake recurs in on-chain lotteries, prediction games, and sealed auctions. Someone can learn the outcome early and bet on it. There are no secrets on-chain; use commit-and-reveal structures for information that must stay hidden.
Earlier EVM rules offered substantial refunds for clearing a storage slot. Some people wrote meaningless data when gas was cheap, then cleared it when gas was expensive to claim refunds—effectively storing cheap gas for later.
These tokens once traded in real markets. A later protocol upgrade sharply reduced refunds and made the strategy unworkable.
**The lesson is how fragile a business built on a gas-pricing detail can be.**Pricing is part of consensus rules, and those rules can change.
Liquidation in a lending protocol is not automatic. The contract allows anyone to liquidate a qualifying position and rewards the liquidator.
In extreme markets, rewards have sometimes failed to cover fees. Nobody wanted to submit the call. Positions were underwater, but nothing happened on-chain, leaving the protocol with the loss.
This is an expensive illustration of "there is no cron on-chain." Any design relying on "someone will call" must calculate that caller's profit. T22 covers the mechanism in full.
Change One Variable
Everyone fills the chain with arbitrary data. State expands indefinitely; node disk and memory requirements soon exceed ordinary hardware until only a few institutions can operate nodes.
Then "anyone can independently verify" would no longer be true.
Gas is a gate that helps preserve decentralization, not just a fee mechanism. It also explains the ongoing debate about charging rent for existing state: one payment in exchange for permanent storage does not balance the books.
The boundary between "transaction initiator" and "account with logic" disappears. A wallet address could temporarily have code in a transaction, supporting batches, sponsored fees, spending limits, and social recovery.
Proposals along these lines are advancing and would rewrite several rows in the comparison table. But the underlying model remains: the table, its four fields, and the computer with no clock.
**Learn the model before the API.**APIs change yearly; the four fields have endured.
This model cannot do that directly. You learn which account slots a transaction will change only by executing it, as T1 explained. A scheduler cannot know in advance whether two transactions conflict.
An optional mechanism lets a transaction declare which addresses and slots it expects to access, but it affects gas pricing rather than scheduling and is not mandatory.
**Make advance declarations mandatory and group work by them, and you get a different model.**T6 introduces it.
Every promise that "code is the rule" would fail. Users could not know what rules will apply a moment after they interact.
Reality is a compromise: code is immutable, but a proxy can forward calls to a new address. The question becomes who may switch that address—often a multisig, sometimes a single key.
**To judge a protocol's decentralization, inspect who holds upgrade authority rather than only its white paper.**This is one of T23's core questions.
Takeaway Questions
Someone pays for every on-chain state change, and the payer starts from an EOA. The four options ask who pays: the protocol, requiring a hot wallet and operations; the next user, perhaps at great cost; or the beneficiary, whose reward may not justify claiming. The choice shapes the product, not just its implementation.
Anyone worldwide can read storage. Users bear the risk when developers put sensitive data in a private variable, often without knowing. Similarly, whoever controls contract upgrades controls every depositor's exposure. Document both at design time, not only when an audit begins.
Gas creates incentives: wasting shared resources becomes expensive, and "someone will submit the call" must be worth their time. Unperformed liquidations, untriggered settlements, and unclaimed rewards all show this incentive failing at a boundary. Before relying on an outside caller, calculate whether they profit.
Chapter Check
Call eth_getCode. 0x indicates no deployed code; bytecode indicates a contract. The address string itself cannot tell you.
There is a trap: while a contract's deployment transaction is executing, eth_getCode returns empty for that contract. Contract checks intended to block other contracts can be bypassed by calls made in a constructor.
The EVM is a deterministic state machine: every node must reach the same result. An autonomous trigger would turn "when did it trigger?" into a consensus problem.
There is no on-chain cron. Under every apparently automatic action, someone sends a transaction and pays: a user, a protocol service, or a rewarded third party.
No. private prevents other contracts' code from referencing them, but off-chain observers can read slots directly through eth_getStorageAt.
**There are no secrets on-chain.**If data must stay hidden, use commit-and-reveal: submit a hash first, then disclose the original later.
Two effects can combine.
A storage change from zero to nonzero costs far more than nonzero to nonzero: the first adds persistent state, the second updates it.
There is also cold versus warm access. The first touch of an address or slot in one transaction costs extra; later touches in that transaction are cheaper.
This is why gas optimization needs measurement: the same code line may cost differently in different states.
There is no one standard answer. Check these points:
- A multisig wallet is itself a contract account. Both options use the same account type; the difference is the code's rules.
- Whether it accepts a native-coin transfer depends on its receiving logic; verify this during design.
- It cannot initiate a transaction, so automatic payouts require another party to trigger and pay.
- Document whether rules can change and who can change them.
- Balances and rules in storage are publicly readable; do not store secrets there.
- If the design permits upgrades, whoever controls upgrades ultimately controls the vault.
A design that clearly answers points 4 and 6 is complete.
Key idea
The EVM is a global state machine: accounts index its state, and storage is the most expensive part.