Skip to content

Two Kinds of Account: EOAs and Contracts

The previous page settled why Ethereum keeps a mutable map from address to account instead of a set of coins: a world computer needs somewhere to remember things, and coins are designed to be forgotten. But that raises a sharper question. If the ledger is a map of accounts, and a “smart contract” is supposedly a program that lives on the chain, then where does the program live? The answer is disarmingly simple, and it is the whole subject of this page: a program lives inside an account. Ethereum does not keep contracts in a separate registry. It stores them in the same address-to-account map that holds ordinary wallets.

So the state map has exactly two kinds of entry. Some accounts are plain wallets, steered from the outside by whoever holds a private key. Others are programs, steered by their own code. This page defines both, states the single rule that separates them, shows how each comes into existence, and proves the payoff: that calling an account with code runs that code. Get this distinction crisp and the rest of the part — the account record, the global state map, and state transitions — falls into place.

Externally Owned Accounts: a wallet with a key

Section titled “Externally Owned Accounts: a wallet with a key”

An Externally Owned Account (EOA) is what most people mean by “a wallet.” It is an account whose authority lives outside the chain, in a private key that some human (or their software) holds. Whoever can produce a valid signature for that key controls the account. There is no code inside an EOA; there is nothing to run. An EOA can do exactly one interesting thing: originate a transaction — sign a message that says “advance the world state this way” and broadcast it.

An EOA carries only two pieces of mutable state:

  • a nonce — a counter of how many transactions this account has already sent. It increments by one every time the account originates a transaction, and it is what stops someone from replaying a signed transaction twice (more on replay protection when we dissect the account record).
  • a balance — how much native currency (ether, denominated in wei) the account holds.

That is the entire footprint of a wallet: a counter and a number, guarded by a key that never touches the chain. Crucially, only an EOA can start a transaction. Every state change on Ethereum — every token transfer, every contract call, every deployment — traces back to a transaction signed by some EOA’s private key. Contracts, as we are about to see, can never kick things off by themselves.

Externally Owned Account (a wallet)
┌───────────────────────────────────┐
│ nonce: 7 │ ← txs sent so far
│ balance: 4_000_000_000_000_000 │ ← wei
│ code: (empty) │ ← nothing to run
│ storage: (empty) │
└───────────────────────────────────┘
│ controlled by a private key held off-chain
│ can SIGN and ORIGINATE transactions

Contract Accounts: a program with a memory

Section titled “Contract Accounts: a program with a memory”

A Contract Account is an account whose authority lives inside the chain, as code. Where an EOA is steered by an off-chain private key, a contract is steered by its own bytecode: it has no key, nobody can “sign as it,” and it does nothing on its own. A contract is a passive thing until something calls it. When a call arrives, the network runs the contract’s code, and that code can read and write the contract’s own persistent memory, move the contract’s balance around, and even call other contracts.

A contract carries everything an EOA has — a nonce and a balance — plus two things a wallet never has:

  • code — the contract’s bytecode, installed once at creation and thereafter immutable. This is the program.
  • storage — a persistent key/value map that survives between calls. This is the program’s memory. It does not live in any server’s RAM; it lives in the world state, so the value a contract wrote yesterday is exactly the value the next call reads today.
Contract Account (a program)
┌───────────────────────────────────┐
│ nonce: 1 │
│ balance: 250_000_000_000_000_000 │ ← wei the contract holds
│ code: 60 00 54 60 01 01 … │ ← bytecode: THE PROGRAM
│ storage: { 0 → 42, 1 → 99, … } │ ← persistent memory
└───────────────────────────────────┘
│ controlled by its OWN code
│ runs only when called; cannot originate a tx

The most important limitation to internalise: a contract cannot originate a transaction. It can react — when a transaction (ultimately signed by some EOA) calls into it, its code may in turn call other contracts, and those are called internal or message calls. But there is no way for a contract to wake up on its own, on a timer, or “in the background.” Nothing happens on Ethereum until an EOA signs something. This is why patterns like “run this contract every hour” require an off-chain EOA (a bot, a keeper) to send the triggering transaction — the chain has no heartbeat of its own.

Here is the rule the entire page turns on, and it is smaller than you might expect:

An account is a contract if and only if it has non-empty code.

That’s it. There is no is_contract flag stored on the chain, no type tag, no separate table of “contracts” versus “wallets.” Both kinds are the same record shape, and the network decides which behaviour to apply by asking one question: is the code field empty? In real Ethereum this is phrased in terms of the account’s codeHash — the hash of its code. An account whose codeHash equals the hash of the empty string has no code and is an EOA; any other codeHash means there is code, and the account is a contract.

Our companion crate, ethmini, makes the rule literal. An account is four fields, and the predicate that separates a wallet from a program is a single line:

pub struct Account {
pub nonce: u64, // txs sent (EOA) / contracts created (contract)
pub balance: u128, // native currency, in the smallest unit
pub storage: BTreeMap<u64, u64>, // persistent memory — empty for a wallet
pub code: Vec<u8>, // bytecode — empty for a wallet
}
impl Account {
/// Does this account hold contract code? If so, calling it *runs* that code;
/// if not, a call is just a value transfer.
pub fn is_contract(&self) -> bool {
!self.code.is_empty()
}
}

Read is_contract again: it does not consult a registry or a type field. It asks whether code is empty. The presence of code is the only thing that turns an account from a wallet into a program. Everything else — that a contract has storage, that calling it runs logic, that it can’t be signed for — follows from that one bit of information.

The two kinds are also born differently, and the difference is again about code.

You do not “create” an EOA in any on-chain sense. An EOA is just an address, and an address is derived from a public key, which is derived from a private key. Generating a key pair is a purely local act — no transaction, no fee, no network involved. The address exists as a mathematical possibility the moment the key exists.

On-chain, an EOA “appears” the first time it is used: the first time someone sends ether to it, or the first time it originates a transaction. Before that, the state map simply has no entry for it — and an address with no entry is treated as an account with a zero balance, a zero nonce, and no code. There is no ceremony. An address is an EOA by default, precisely because the default account has empty code.

A contract is installed by a deploy transaction

Section titled “A contract is installed by a deploy transaction”

A contract, by contrast, must be deployed. Because a contract is code sitting in an account, creating one means running a special transaction whose effect is: “create a new account and put this bytecode in its code field.” The new contract does not get to pick its own address — the address is derived from the creator and the creator’s nonce, so it is deterministic and collision-free:

TxKind::Deploy { code, value } => {
let addr = contract_address(tx.from, tx.nonce); // derived, not chosen
if *value > 0 { self.transfer_value(tx.from, addr, *value); }
self.account_mut(addr).code = code.clone(); // the account now HAS code
Ok(Receipt { status: Success, created: Some(addr), .. })
}

The load-bearing line is self.account_mut(addr).code = code.clone();. That single assignment is what makes the new account a contract — it flips code from empty to non-empty, and by the rule above, an account with non-empty code is a contract. Deployment is not a magical act of creation; it is an ordinary state edit that happens to write the code field. (Real Ethereum adds a wrinkle: the bytecode you send in a deploy is init code that runs once and returns the runtime code to install. The idea is the same — a transaction ends with bytecode written into a fresh account.)

The payoff: why calling a contract runs its code

Section titled “The payoff: why calling a contract runs its code”

Now we can see why all of this matters. On Ethereum there is only one kind of action that touches another account — a call carrying some value and some data. What that call does depends entirely on whether the target has code. The network branches on exactly the is_contract bit we defined above:

TxKind::Call { to, value, gas_limit } => {
if *value > 0 { self.transfer_value(tx.from, *to, *value); }
let target = self.account_mut(*to);
if !target.is_contract() { // ← no code? DONE. It was just a transfer.
return Ok(Receipt { status: Success, .. });
}
// has code → RUN it against the account's own storage
let (code, storage) = (target.code.clone(), target.storage.clone());
match vm::run(&code, &storage, *gas_limit) {
Ok(exec) => { // clean halt → commit the new storage
self.account_mut(*to).storage = exec.storage;
Ok(Receipt { status: Success, gas_used: exec.gas_used, .. })
}
Err(reason) => Err(Reverted { gas_used: *gas_limit, reason }),
}
}

Two paths, one branch:

  • Target has no code (an EOA). The value moves and the call is over. Sending ether to a friend’s wallet is literally a call to a codeless account: nothing runs, one balance goes down, another goes up.
  • Target has code (a contract). The network loads that code and executes it against the contract’s own storage. The code can read storage (SLOAD), write it (SSTORE), and thereby change the world state persistently. Send a call to a token contract and its transfer logic runs; send a call to a wallet and nothing runs at all.

This is the sentence to carry out of the page: calling an account with no code is a value transfer; calling an account with code is a program invocation. Same operation, same transaction shape — the only thing that decides which one you get is whether the target account has code. The storage map the code runs against is the contract’s private memory, and because it lives in the world state, its changes persist across calls; the next page, Inside an Account, opens up storageRoot and codeHash and shows how the state commits to both.

Under the hood — why a contract can’t just “run itself”

Section titled “Under the hood — why a contract can’t just “run itself””

It is tempting to imagine a contract as a little daemon, always alive, waiting. It is not. A contract’s code is inert bytes in the state map, exactly like the text of a program on a disk that no one has launched. Execution only happens because a transaction pays for it: a signed message from an EOA supplies the gas, and the network — while processing that transaction — loads the target’s code and runs it. Remove the transaction and there is no execution, no thread, no timer. This is a deliberate design choice that keeps Ethereum deterministic: the state can only change as a function of the ordered list of transactions in a block, and never spontaneously. It is also why “autonomous” contracts in practice lean on off-chain EOAs to poke them — the world computer computes only when someone signs the request and pays the bill. We will see why the bill exists at all when this book reaches gas.

  • Why does it exist? Ethereum wanted programmable money — accounts that can hold logic, not just value. Rather than bolt on a separate “contract system” beside the wallet ledger, it made programs a kind of account, so the same state map, the same addressing, and the same call mechanism serve both wallets and code.
  • What problem does it solve? It collapses “who can hold funds” and “what can run logic” into one uniform record. A contract can hold a balance and be sent money exactly like a wallet; a wallet can be called exactly like a contract (it just does nothing). No translation layer, no registry lookup, no second addressing scheme.
  • What are the trade-offs? Uniformity costs safety rails. Because a call to any address might or might not run code, sending value is never purely a transfer — the target could be a contract that runs arbitrary logic on receipt. That single fact is the root of reentrancy and a whole family of “the recipient did something unexpected” bugs.
  • When should I avoid it? When you need genuinely autonomous, always-on behaviour, a contract is the wrong mental model on its own — it can’t self-trigger. You must pair it with an off-chain EOA (a keeper/bot) to originate the transactions that wake it. And for pure value custody, an EOA (or a smart-contract wallet you understand) is simpler than a bespoke contract.
  • What breaks if I remove it? Remove the “code makes it a contract” rule and you remove smart contracts entirely — Ethereum collapses back into an account-based payment system with no way to store or run on-chain logic. The world computer loses its programs and keeps only its money.
  1. What are the two mutable fields an EOA carries, and what is the one action an EOA can perform that a contract cannot?
  2. State the single rule that distinguishes a contract account from an EOA. In real Ethereum, which stored field encodes that rule, and against what is it compared?
  3. How does an EOA come into existence compared with a contract account? Why does one require a transaction while the other does not?
  4. You send a transaction that transfers 1 ETH to some address X. Describe both possible outcomes and explain what single property of X decides which one happens.
  5. A friend says “my contract runs automatically every day.” What is technically wrong with that claim, and what must actually be happening for the contract to do work daily?
Show answers
  1. An EOA carries a nonce (a counter of transactions it has sent) and a balance (its ether holdings). The action it alone can perform is originating a transaction — signing a message with its private key to start a state change. A contract can only react when called; it can never kick off a transaction itself.
  2. An account is a contract if and only if it has non-empty code (otherwise it is an EOA). Real Ethereum encodes this in the account’s codeHash: if codeHash equals the hash of the empty string, the account has no code and is an EOA; any other value means it has code and is a contract.
  3. An EOA exists implicitly — it is just an address derived from a key pair (a local, off-chain act), and it “appears” in state the first time it is used; the default account has empty code, so an unused address is already an EOA. A contract must be deployed by a transaction that installs bytecode into a fresh account’s code field. The deploy transaction is needed precisely because writing that code field is the on-chain edit that turns the new account into a program.
  4. If X has no code (an EOA), the call is just a value transfer: one balance goes down, X’s goes up, and nothing runs. If X has code (a contract), the network executes that code against X’s own storage — the code may do anything, including reject the transfer or call other contracts. The single deciding property is whether X has non-empty code.
  5. Nothing on Ethereum runs on its own — a contract is inert until called, and only an EOA can originate a transaction. So the contract cannot self-trigger “every day.” What must be happening is that some off-chain EOA (a bot or keeper) signs and sends a transaction each day that calls the contract, paying the gas to run its code.