web3.path

PHASE 05 Ethereum · ~4 hours

Ethereum Deep Dive

Ethereum = a global, single-threaded, deterministic VM (the EVM) whose memory is the world state and whose billing unit is gas. Once you see it that way, everything else clicks.

Goal — stand up a local chain with Hardhat, deploy a contract, inspect tx, receipt, and storage slots.

1. Two kinds of accounts

EOA (Externally Owned)Contract
Controlled byPrivate keyCode
Can initiate tx?YesNo (only in response)
Has code?NoYes (immutable bytecode)
AnalogyUser's emailLambda function with a balance

2. The EVM, in SWE terms

Analogy — imagine an AWS Lambda with three key differences: (1) the filesystem (storage) is shared with every other Lambda on Earth; (2) every line you execute charges from the caller's wallet; (3) the entire world re-runs your code to double-check. That's the EVM.

3. Gas — the rate limiter

Every opcode has a gas cost. ADD is 3 gas; reading a storage slot (SLOAD) is 2,100 cold / 100 warm; writing a new slot (SSTORE) is 20,000. Your tx pays gas_used × gas_price.

OpGasNotes
ADD, SUB3Arithmetic is free-ish
KECCAK25630 + 6/wordHashing isn't free
SLOAD (cold)2,100Reading chain state
SSTORE (new)20,000Writing new state
SSTORE (update)2,900Mutating existing
CREATE32,000 + codeDeploying
CALL~100+Calling another contract
Gas is the primary design constraint. Sort arrays off-chain. Cache storage reads in memory. Every loop iteration costs real money.

4. EIP-1559: base fee + tip

effective_gas_price = min(max_fee, base_fee + max_priority_fee)
// base_fee is burned, tip goes to validator

Base fee adjusts ±12.5% per block to target 50% block usage. It's an auto-scaling load balancer for blockspace — very much like AWS spot pricing.

5. A transaction's lifecycle

wallet.sign(tx) → JSON-RPC eth_sendRawTransaction │ ▼ Node validates: sig, nonce, balance ≥ gas × price │ ▼ Mempool (public) ──► MEV searchers / builders reorder │ ▼ Validator includes tx in block (12s slot) │ ▼ EVM executes → state_root updates → receipt has logs + gas_used │ ▼ 2 epochs later (~13 min) → finalized

6. Storage layout & the state trie

Each contract has a map from 32-byte slot → 32-byte value. Solidity variables are packed into slots in declaration order. A mapping(k => v) stores v at keccak256(k || slot_index).

contract Foo {
  uint256 a;                     // slot 0
  uint128 b; uint128 c;          // slot 1 (packed)
  mapping(address => uint256) d; // slot 2 — entries at keccak256(addr || 2)
}
Analogy — Solidity storage is a giant Redis hash where keys are 32-byte blobs. Mappings are Redis hashes within the hash, addressed by a deterministic hash function.

7. Hands-on: local chain in 5 minutes

npm init -y
npm i --save-dev hardhat
npx hardhat init           # pick "JavaScript project"
npx hardhat node           # local chain on :8545, 20 funded accounts
# in another terminal:
npx hardhat console --network localhost
> const [a,b] = await ethers.getSigners()
> (await ethers.provider.getBalance(a.address)) / 10n**18n
10000n   // 10,000 test ETH

This is your dev loop for the rest of the course. Hardhat gives you console.log inside Solidity — yes, really.

8. Reading a receipt

const tx = await contract.doThing(42);
const r  = await tx.wait();
console.log({
  status: r.status,        // 1 = success, 0 = reverted
  gasUsed: r.gasUsed,
  logs: r.logs,            // events
  blockNumber: r.blockNumber,
});

9. Project

Deliverable — deploy the default Lock.sol from Hardhat's init, call it from the console, read storage slot 0 with provider.getStorage(addr, 0), and match the raw bytes against the Solidity variable.

Quiz

Q. Why does reading the same storage slot twice in one tx cost 2,100 gas the first time and 100 gas the second?
Cold vs warm access — same trick as a CPU cache. Helps gas-price real work while making dodgy spam (pinging random slots) expensive.
← Phase 4Phase 6: Smart Contracts →