# 05 — Execution and Smart Contracts
[[04 Consensus]] picks one canonical block. This module is about the next step in the pipeline from [[01 Replicated State Machines]]:
```text
log → apply → state
^
this module
```
Every node replays the same block through the same `apply`. If two honest nodes disagree on the result — even by one bit — their `state_root`s diverge ([[03 Blocks, Hashes, and History]]) and the chain forks without an attacker. Execution must be **deterministic**: same inputs, same outputs, everywhere.
### What ordinary programming gets wrong
A normal program can read the wall clock, call an API, roll random numbers, or use floating point. Fine on one machine. Fatal here — each node runs at a slightly different moment, on slightly different hardware, and nobody coordinates the outside world.
| Source of variation | Why two honest nodes can disagree |
|---|---|
| Wall-clock time | Node A and B apply the same block milliseconds apart; `Date.now()` differs |
| Randomness | Ten thousand independent dice rolls are ten thousand different numbers |
| Network I/O | Each node fetches live; the server may answer differently or not at all |
| Floating point | Compilers and CPUs have historically disagreed on rounding at the edges |
Solidity sidesteps the clock problem by reading `block.timestamp` from the header — data every node already agreed on when they agreed on the block. Everything else from the outside world must arrive as transaction input ([[08 Nodes, RPCs, and Indexers]]), not be fetched mid-execution.
Remove those four escape hatches and you are left with a deliberately small execution environment. Ethereum calls it the EVM.
## The EVM
A smart contract is code stored on-chain:
```text
tx → EVM → contract code → state changes
```
**The design question:** how do you specify a program so that independent implementations — written in Go, Rust, Java — always produce bit-identical results?
| Approach | Problem |
|---|---|
| Native machine code (x86, ARM) | Different CPUs, OSes, and compilers disagree at the edges |
| Interpret Python or JavaScript | The runtime itself must match bit-for-bit across versions and platforms — a harder problem, not an easier one |
| **Define a tiny abstract machine** | Specify every opcode exactly; multiple teams build interpreters that must agree or the network forks |
Ethereum takes the third path. The **Ethereum Virtual Machine** is that machine: a **stack** of values, **opcodes** that manipulate it, and rules tight enough that two implementations have almost nowhere to diverge. Not portable in the JVM sense — **identical** everywhere.
Properties that fall out of that choice:
- **Deterministic** — same block, same result, on every node
- **Stack-based** — minimal spec surface; hard for implementers to disagree quietly
- **Isolated** — no outside I/O; oracles bring external facts in via transactions
- **Atomic** — a transaction's state changes all commit or all revert (fees for work done still apply)
- **Gas-metered** — every opcode has a price *(why, below)*
**Bitcoin's alternative.** Bitcoin scripts have no loops — not Turing-complete, so every script provably halts. Ethereum wanted contracts with state, composition, and arbitrary logic. That requires Turing-completeness, which reopens the **halting problem**: you cannot decide in general whether a program will finish.
So ten thousand nodes may execute a stranger's program as part of agreeing on state. Without a bound, `while (true) {}` freezes the network at no cost to the attacker. Static analysis cannot solve this in general. The pragmatic fix: charge per step, require prepaid budget, stop when it runs out. The program halts or the money does — either way, every node finishes in bounded time.
What can you build under these rules? Tokens, exchanges, lending, NFTs, DAOs, bridges, games — more than skeptics expected, less than the marketing promised.
## Gas
The EVM section named the problem; this is the mechanism.
Each transaction carries a prepaid budget:
```text
gas_limit
max_fee
priority_fee
```
Every opcode costs gas. Run out midway, and execution reverts — but the fee is still charged. That might feel unfair until you notice the alternative: if failed transactions were free, denial-of-service attacks would be free too. Gas doesn't try to predict whether `while (true) {}` would ever finish; it just guarantees that either the program finishes, or the prepaid budget runs out first. Either way, every node stops in bounded time, and the attacker paid for the privilege of trying.
Gas prices everything scarce — computation, storage, data, and ultimately blockspace itself. The economics live in [[Gas and Blockspace]]; the sentence worth keeping is that blockspace is the scarce commodity a blockchain sells.
## Tokens
### Accounts before tokens
Module 02 gave you keys and signatures; Module 03 named two kinds of on-chain account. Before tokens make sense, those two need to be in the same picture.
```text
private_key → signs → transaction → changes chain state
| |
v v
(off-chain) address (on-chain)
```
An **address** is a name in chain state — derived from a public key in Ethereum's case ([[02 Cryptographic Primitives]]). An **externally owned account (EOA)** is an address controlled by whoever holds the matching private key. A **contract account** is an address controlled by code deployed at that address.
```text
EOA — controlled by a private key; can initiate transactions
Contract — controlled by bytecode; only changes state when called
```
In the examples below, **Alice** and **Bob** are EOAs: two addresses. When Alice "buys" something, what actually happens is a signed transaction from her address edits storage in one or more contracts. The software that holds Alice's key, builds that transaction, and broadcasts it — what people call a **wallet** — is a separate layer we haven't opened yet; it lives in [[07 Wallets and Account Abstraction]]. Stay at chain level here: addresses, signatures, storage maps, blocks.
### Where a token actually lives
Start concrete, because "token" stays ethereal until you see it sitting somewhere physical.
A **contract account** has an address and **storage**: a private key-value space on the chain, same as any other account's fields — but for contracts, the interesting part is that storage map. When people say "USDC" or "an NFT collection," they mean one specific contract at one specific address whose storage contains a map from address (or token id) to a number. That map *is* the token. There is no coin, no file, no picture living anywhere else — just entries in that contract's storage.
```text
USDC contract (address 0xA0b8...)
storage: { alice: 500_000000, bob: 12_000000, ... } ← this map is "USDC balances"
CoolCats NFT contract (address 0xB3c1...)
storage: { 4471: alice, 4472: bob, ... } ← this map is "who owns which cat"
```
Nothing about either map is special-cased by the EVM. They're ordinary contract storage, exactly like any other contract's data. "Owning a token" means: some contract's storage has your address next to a number (or a token id) you care about.
### Buying an NFT, transaction by transaction
Here's the part that ties it to everything from modules 03 and 04. Say Alice wants to buy Bob's NFT `#4471` for 50 USDC through a marketplace contract.
**Before the purchase, two setup transactions** (each its own block, or the same one — doesn't matter):
```typescript
// Alice authorizes the marketplace to pull 50 USDC from her, later
USDC.approve(marketplaceAddress, 50_000000)
// Bob authorizes the marketplace to move his NFT, later
CoolCats.approve(marketplaceAddress, 4471)
```
Each of these is a normal transaction. It gets broadcast, picked up by a block proposer, included in a block, and executed by every node — exactly the pipeline from modules 01–04. All it does is write one entry into an `Allowances` map inside each contract's storage.
**Then the actual purchase — one transaction, one block:**
```typescript
marketplace.buy(CoolCats, tokenId: 4471, priceInUSDC: 50_000000)
```
This transaction lands in a block like any other. Inside the EVM, executing it triggers the marketplace's own code, which makes two further calls — not new transactions, just function calls happening inside this one:
```typescript
USDC.transferFrom(alice, bob, 50_000000) // moves money: edits USDC's storage map
CoolCats.transferFrom(bob, alice, 4471) // moves the NFT: edits CoolCats' storage map
```
Recall **atomic** from the EVM properties above: both calls succeed, or the whole transaction reverts — Alice can't lose her USDC without receiving the NFT, and Bob can't lose the NFT without receiving payment. If Alice's balance were only 40 USDC, `transferFrom` would fail, the whole transaction reverts, and neither storage map changes at all.
**Now connect it to the block.** This one transaction sits in the block's transaction list; its execution changes two lines across two different contracts' storage. Once the block executes, the new values of *every* contract's storage — including these two edited maps — get folded into the new **world state**, and `state_root` in the block header ([[03 Blocks, Hashes, and History]]) commits to all of it at once. A token balance changing is not a separate event from "a block was produced" — it is a block being produced, viewed at the resolution of one contract's storage.
### Why the interfaces matter
Notice what the marketplace contract did *not* need to know: what USDC's code looks like internally, how CoolCats stores its metadata, or anything specific to either contract at all. It only needed to know that both contracts expose `transferFrom` with a particular signature — the **interface**. That's the entire value of writing it down as a TypeScript-style interface: it's a promise about shape, not implementation, and any contract satisfying that promise can plug into any other code written against it.
This is why the same marketplace contract can sell USDC-priced NFTs, DAI-priced NFTs, or a token that didn't exist when the marketplace was deployed — as long as the new token implements the agreed interface, it composes automatically. That property has a name in this world: **composability**, and it's the reason ERC standards exist at all. Without a shared interface, every marketplace would need custom code per token; with one, the marketplace is written once, against the shape, and works with anything that shape matches. Ordinary interfaces do exactly this in normal software — the twist here is that the "implementations" plugging into your interface are contracts deployed by total strangers, and you're trusting the interface instead of trusting them.
With the concrete picture in hand, here's the vocabulary for the three shapes that recur. TypeScript is a convenient way to write them down, because the real differences between these three standards aren't in the method names. They're in the *shape of the state* each one implies. Watch the types, not just the signatures.
```typescript
type Address = string;
type TokenId = bigint;
```
**Fungible (ERC-20).** One balance per address — stablecoins, governance tokens, wrapped assets. Units are interchangeable: your dollar and my dollar are the same dollar.
```typescript
interface ERC20 {
totalSupply(): bigint;
balanceOf(owner: Address): bigint;
transfer(to: Address, amount: bigint): boolean;
approve(spender: Address, amount: bigint): boolean;
transferFrom(from: Address, to: Address, amount: bigint): boolean;
}
// the state this interface is a view onto:
type ERC20State = Record<Address, bigint>; // owner -> amount
```
Unlike a one-shot bank transfer, ERC-20 adds a standing permission on top of that flat map — a second map, one layer deeper:
```typescript
type Allowances = Record<Address, Record<Address, bigint>>; // owner -> spender -> amount
```
`approve` writes into `Allowances`; `transferFrom` checks it before moving funds in `ERC20State`. Convenient and composable — and, because the allowance quietly outlives the intent that created it, also how many drains happen. Worth filing away: `approve` is the root of an entire category of attacks we'll meet in [[12 Security and Incentive Design]].
**Non-fungible (ERC-721).** Every unit has its own identity: collectibles, memberships, tickets, onchain identity. Your token `#4471` isn't interchangeable with anyone else's `#4471`-shaped token, because there isn't one — there's exactly one `#4471`, and it has exactly one owner.
```typescript
interface ERC721 {
ownerOf(tokenId: TokenId): Address;
transferFrom(from: Address, to: Address, tokenId: TokenId): void;
approve(to: Address, tokenId: TokenId): void;
tokenURI(tokenId: TokenId): string;
}
// the state this interface is a view onto:
type ERC721State = Record<TokenId, Address>; // tokenId -> single owner
```
The types already tell the story: `ERC20State` maps address → amount; `ERC721State` maps id → address. Fungibility isn't a special extra rule bolted onto ERC-20 — it's just what you get from *not* tracking individual units. ERC-721 tracks them; ERC-20 doesn't bother.
**Semi-fungible (ERC-1155).** Many token *types* in one contract, each internally fungible — the natural fit for games and inventories. Your 3 "Wood" and my 3 "Wood" are interchangeable; your "Wood" and your "Stone" are not.
```typescript
interface ERC1155 {
balanceOf(owner: Address, id: TokenId): bigint;
balanceOfBatch(owners: Address[], ids: TokenId[]): bigint[];
safeTransferFrom(from: Address, to: Address, id: TokenId, amount: bigint): void;
}
// the state this interface is a view onto:
type ERC1155State = Record<TokenId, Record<Address, bigint>>; // id -> (owner -> amount)
```
Now the unifying moment the types have been building toward. Look at that nested `Record` again:
```typescript
type ERC1155State = Record<TokenId, ERC20State>;
// ^ one ERC-20-shaped balance map, per token id
```
**ERC-1155 is a contract running many ERC-20s side by side, indexed by id.** And ERC-721 is the special case of ERC-1155 where every id's inner map is constrained to exactly one entry, worth exactly `1n`:
```typescript
// ERC-721, viewed through the ERC-1155 lens:
// for every tokenId: exactly one owner, exactly one unit
type ERC721AsERC1155 = Record<TokenId, { [owner: Address]: 1n }>;
```
Three standards, one underlying shape — a `Record` of `Record`s, or a flattened special case of it — with different constraints switched on or off. Fungibility, uniqueness, and batching are properties of *which map you build and how tightly you constrain it*, not three unrelated designs. Every one of these maps is still just contract storage, exactly like the USDC and CoolCats examples above — nothing new physically happened, you've only named the shapes precisely.
## Upgradeability
Contracts are immutable by default. Software, famously, is not done when it ships. Protocols square this circle with upgrade patterns:
- **Proxy contracts** — storage stays in place while the logic pointer changes; if new code expects a different storage layout, reads and writes hit the wrong slots.
- Timelocked upgrades
- Multisig- or governance-controlled admin
- Immutable core, upgradeable periphery
| **Immutable** | **Upgradeable** |
|---|---|
| Strong trust minimization | Can fix bugs |
| Bugs are permanent | Admin key risk |
| Simple mental model | Governance complexity |
The instinct worth developing: whenever you meet a contract, ask who can change it. Upgradeability solves a real operational problem, but it does so by quietly weakening the immutability users think they were promised — it's a [[Trust Assumptions|trust assumption]] dressed as an implementation detail.
## Learning outcomes
Questions worth trying on:
- Could you list what the EVM forbids, and derive each prohibition from the determinism requirement alone?
- Could you explain to a backend engineer why a failed transaction still costs money — and why the system genuinely couldn't work otherwise?
- What is an ERC-20 balance, physically? Could you explain a token transfer without using the word "send"?
- Given a real protocol, could you find out who can upgrade it — and say what that means for its users?
- And the thread to pull forward: contracts execute when transactions arrive. But how does a signed transaction get from someone's key into a block? The journey is more eventful than you'd think — and the wallet layer comes in [[07 Wallets and Account Abstraction]].
## Checkpoint
1. Why no `random()`, no reliable `now()`, no HTTP in the EVM? One reason covers all three. *(Hint: determinism — every node must get the same result. If unsure, revisit "What ordinary programming gets wrong" or [[Distributed Systems Bridges]].)*
2. What's the difference between `transfer` and `approve` + `transferFrom`, and why does the second pattern exist? *(If unsure, revisit "Tokens" — the risk side returns in [[12 Security and Incentive Design]].)*
3. A proxy upgrade changes logic but keeps storage. What could go wrong? *(If unsure, revisit "Upgradeability" and look up storage layout collisions.)*
**Exercise.** Write a minimal ERC-20 in Solidity — or read OpenZeppelin's canonical one, line by line. Deploy it to a testnet, call `approve` then `transferFrom`, and inspect exactly which storage slots changed and why. The mechanics stop being abstract the first time you watch the slots move.
## Resources
- **Ethereum.org docs: Smart contracts, Gas, EVM** — the canonical reference.
- **Mastering Ethereum** — the EVM and smart contract chapters; use for fundamentals, not roadmap.
- **Ethernaut** — start it now. Small puzzles that teach how contracts break: reentrancy, delegatecall, tx.origin, access control. The fastest way to really learn the EVM is watching it get exploited.
---
Next: a signed transaction hits the network. What happens between broadcast and inclusion in a block? → [[06 Transaction Lifecycle and MEV]]