Vivory Codex

The Ethereum Machine: EVM Architecture, Account Types, and the Life of a Transaction

1강275,409

학습 목표

  • 외부 소유 계정(EOA)과 컨트랙트 계정을 속성과 기능에 따라 구별한다
  • 트랜잭션의 생명주기를 지갑 서명부터 멤풀을 거쳐 블록 포함 및 최종 확정까지 추적한다
  • 가스가 존재하는 이유와 가스 한도, 기본 수수료, 우선순위 수수료가 트랜잭션 비용을 결정하는 방식을 설명한다
  • EVM의 세 가지 데이터 저장 위치(스택, 메모리, 스토리지)와 각각의 상대적 비용을 설명한다
  • 이더리움의 계정 기반 상태 모델과 비트코인의 UTXO 모델을 높은 수준에서 비교한다
  • Hardhat 프로젝트를 초기화하고 빈 Solidity 컨트랙트를 컴파일한다

Here's the complete lesson with the ❌ → 🤔 → ✅ pattern integrated naturally after the storage cost discussion (before the "Bytecode Execution" subsection), where it reinforces the lesson's core message about data locations:


The Ethereum Machine: EVM Architecture, Account Types, and the Life of a Transaction

On June 17, 2016, someone drained $60 million from "The DAO" — a smart contract that held roughly 14% of all Ether in existence. The attacker didn't hack a server. They didn't guess a password. They called a function on a contract, and the contract did exactly what its code told it to do. The bug wasn't in Ethereum. The bug was in how the developers misunderstood how Ethereum actually executes code.

That event split the entire chain in two (Ethereum and Ethereum Classic), and it taught me the single most important lesson in my career: before you write a single line of Solidity, understand the machine your code runs on. I've audited contracts that lost real money because the developer didn't know the difference between an EOA and a contract account. I've seen gas estimation bugs that locked funds forever.

This lesson is the foundation the rest of this course sits on. We're building a crowdfunding DApp called FundChain over 10 lessons — but today, no Solidity. Today, we understand the machine.


The Case: Ethereum's $60M Lesson in Execution Models

Background. In 2016, a collective called "The DAO" raised 12.7 million ETH (around $150M at the time) through a smart contract. Think of it as an on-chain venture capital fund — investors deposited ETH, voted on proposals, and could withdraw their share at any time by calling a splitDAO() function.

The Decision. The contract's developers wrote the withdrawal function to send ETH first, then update the balance. In traditional programming, this ordering is harmless — hand over the money, then update the ledger. But Ethereum's execution model allows the receiver of ETH to run code when they receive it. The attacker's contract received ETH, then immediately called splitDAO() again — before the balance was updated. Recursion. Drain. Repeat.

The Outcome. 3.6 million ETH stolen. The community hard-forked the chain to reverse it. Ethereum Classic (the unforked chain) still exists today as a reminder.

The root cause wasn't complex cryptography or advanced math. It was this: the developers didn't understand that on Ethereum, sending ETH to an address can trigger arbitrary code execution. That's an architectural property of the EVM. If you understand the EVM, that attack vector is obvious. If you don't, it's invisible.

🤔 Think about it: In JavaScript, when you call a function and send data to an API, the API can't "call you back" mid-execution. Why is Ethereum different?

Answer

Because Ethereum isn't a request/response system — it's a state machine. Any address on Ethereum might have executable code behind it. When a contract sends ETH to another address, the receiving address's code runs within the same transaction. There's no HTTP boundary, no process isolation. It's all one atomic execution context. This is fundamentally different from client-server architecture, and it's the single biggest thing that catches traditional web developers off guard.


Ethereum vs. Bitcoin: A Calculator vs. a Computer

The DAO hack exploited a property unique to Ethereum's architecture. To understand why that property exists — and why it has to exist — we need to understand what makes Ethereum fundamentally different from its predecessor.

I need to kill a misconception right now. People say "Ethereum is like Bitcoin but with smart contracts." That's like saying "a laptop is like a calculator but with a screen." Technically not wrong, but it misses the point entirely.

Bitcoin is a ledger. It tracks one thing: who owns how many bitcoin. Its scripting language (Script) is intentionally limited — it can do basic conditions like "this output can only be spent if signature X is provided," but it can't loop, can't store state, and can't call other scripts.

Ethereum is a state machine. It tracks everything: account balances, yes, but also arbitrary data stored by programs, the code of those programs, and the results of every computation. Ethereum's virtual machine (the EVM) is Turing-complete — it can compute anything, limited only by gas.

FeatureBitcoinEthereum
Primary purposeValue transferGeneral computation
ScriptingStack-based, non-Turing-completeStack-based, Turing-complete
StateUTXO (unspent transaction outputs)Account-based (balances + storage)
Block time~10 minutes~12 seconds
Consensus (current)Proof of WorkProof of Stake (since Sept 2022)
Can store arbitrary data?Limited (OP_RETURN, 80 bytes)Yes (contract storage, unlimited*)

Here's my opinionated take: Bitcoin did one thing and did it perfectly. Ethereum tried to do everything, and the tradeoff is complexity. That complexity is why we need this entire lesson — and why The DAO hack was possible. But it's also why we can build a crowdfunding DApp on-chain. You can't do that on Bitcoin (not natively, at least).


The EVM: Stack, Memory, Storage, and Bytecode

So what actually powers this general-purpose computation? The EVM — Ethereum Virtual Machine. It's the runtime environment for every smart contract. Every single node on the Ethereum network runs the EVM. When you deploy a contract, every validator executes your bytecode and reaches the same result. That's how consensus works: deterministic execution.

The EVM is a stack-based virtual machine. If you've ever used an HP calculator with Reverse Polish Notation, same idea. Instead of writing 2 + 3, you push 2, push 3, then execute ADD. The result (5) sits on top of the stack.

The Three Data Locations

This is where I see developers stumble constantly. The EVM has three distinct places to put data, and they have wildly different costs:

LocationPersistenceGas CostAnalogy
StackTemporary (within opcode)Very cheap (~3 gas)CPU registers
MemoryTemporary (within transaction)Moderate (expands quadratically)RAM
StoragePermanent (on-chain forever)Expensive (~20,000 gas for new slot)Hard drive that charges rent by the byte — forever

Storage is the killer. Writing a single 32-byte word to storage costs 20,000 gas on a fresh slot. At today's gas prices, that can be $0.50–$5.00 for one variable. I've seen contracts burn $200 in storage writes that could have been done in memory for pennies. We'll optimize this in Lesson 4, but plant this in your brain now: storage is permanent and expensive.

Here's what this looks like in practice. Below is a simplified view of what happens when the EVM processes a basic addition. You won't write bytecode directly — Solidity compiles down to it — but understanding this model will save you when debugging:

// EVM Stack Execution: What happens when Solidity compiles "uint x = 2 + 3;"

// Step 1: PUSH1 0x02    → Stack: [2]
// Step 2: PUSH1 0x03    → Stack: [3, 2]
// Step 3: ADD            → Stack: [5]
// Step 4: SSTORE         → Writes 5 to storage slot → Stack: []

// Gas cost breakdown:
// PUSH1: 3 gas × 2 = 6 gas
// ADD: 3 gas
// SSTORE (new slot): 20,000 gas
// Total: 20,009 gas
// 
// Notice: the actual computation (ADD) is nearly free.
// The STORAGE WRITE dominates the cost by 3 orders of magnitude.

🤔 Think about it: If storage is so expensive, why does Ethereum store data on-chain at all? Why not just use a traditional database?

Answer

Because the entire point of Ethereum is trustlessness. A traditional database is controlled by whoever operates the server — they can modify, delete, or censor data at will. On-chain storage is replicated across thousands of nodes worldwide and verified by consensus. Nobody can unilaterally change it. You're paying for decentralized, tamper-proof persistence, not for storage capacity. That's the tradeoff: expensive but trustless vs. cheap but requires trust.

❌ WRONG WAY → 🤔 BETTER → ✅ BEST: Choosing Where to Put Data

Now that you know storage costs 20,000 gas per new slot write, let's see how this plays out in real contract code. Imagine our FundChain contract needs to calculate the total donations from an array of contributors. Here are three approaches — the difference in gas cost is staggering.

❌ WRONG WAY: Accumulate in storage inside a loop

// WRONG — writes to storage on EVERY iteration
// If 100 donors: 100 × 5,000 = 500,000 gas just for SSTORE updates

contract FundChainWrong {
    uint256 public totalRaised;  // storage variable

    function tallyDonations(uint256[] calldata amounts) external {
        for (uint256 i = 0; i < amounts.length; i++) {
            totalRaised += amounts[i];  // SSTORE every iteration!
            // Each += is: SLOAD (2,100) + ADD (3) + SSTORE (5,000) = 7,103 gas
        }
    }
    // 100 donors → ~710,300 gas for the loop alone
}

Every iteration reads from storage, adds, and writes back to storage. You're paying the "hard drive" price on every single loop pass. This is the #1 gas mistake I see in audit after audit.

🤔 BETTER: Accumulate in memory, write to storage once

// BETTER — uses a local variable (memory/stack), writes storage once

contract FundChainBetter {
    uint256 public totalRaised;  // storage variable

    function tallyDonations(uint256[] calldata amounts) external {
        uint256 sum = 0;  // local variable → lives on the stack (3 gas)
        for (uint256 i = 0; i < amounts.length; i++) {
            sum += amounts[i];  // ADD on stack → 3 gas per iteration
        }
        totalRaised = sum;  // ONE storage write at the end → 5,000 gas
    }
    // 100 donors → ~5,300 gas for the loop + 5,000 SSTORE = ~10,300 gas
    // That's a 98.5% reduction vs. the wrong way!
}

One read from storage, one write. The loop itself costs almost nothing because stack operations are ~3 gas each.

✅ BEST: Accumulate locally, write once, AND emit an event for off-chain indexing

// BEST — minimal storage writes + events for off-chain data access

contract FundChainBest {
    uint256 public totalRaised;

    event DonationsTallied(uint256 newTotal, uint256 donorCount);

    function tallyDonations(uint256[] calldata amounts) external {
        uint256 sum = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            sum += amounts[i];
        }
        totalRaised = sum;

        // Events cost ~375 gas + 256 gas per indexed topic
        // WAY cheaper than storing extra data in contract storage
        emit DonationsTallied(sum, amounts.length);
    }
    // ~10,300 gas for computation + ~631 gas for the event = ~10,931 gas
    // Your frontend reads the event from transaction logs — no extra SLOAD needed
}

The key insight: events are stored in transaction logs, not in contract storage. They cost a fraction of SSTORE and are readable by off-chain applications (like our FundChain frontend). If data only needs to be read off-chain and not used by other contracts, events are almost always the right choice.

// Gas comparison summary:
// ❌ WRONG  (storage in loop):    ~710,300 gas — $7-70 at typical gas prices
// 🤔 BETTER (local + 1 write):     ~10,300 gas — $0.10-1.00
// ✅ BEST   (local + event):       ~10,931 gas — $0.11-1.00 + off-chain data
//
// Same result. Same security guarantees. 98.5% cheaper.
// This is what understanding the EVM buys you.

We'll apply exactly this pattern when building FundChain's contribution tracking in Lesson 5. For now, tattoo this on your brain: read from storage once, compute locally, write to storage once, use events for everything your frontend needs.

Bytecode Execution: What Really Happens When You Deploy

When you write Solidity and compile it, the compiler produces bytecode — a sequence of EVM opcodes. That bytecode is what gets stored on-chain. Here's a concrete look at what that means:

// You can inspect any contract's bytecode using ethers.js
// Let's look at a real deployed contract on Ethereum mainnet

// This is the bytecode of the USDT (Tether) contract's first 40 bytes:
const usdtBytecode = "0x606060405260008054600160a060020a...";

// When the EVM encounters this:
// 0x60 = PUSH1 (push 1 byte onto stack)
// 0x60 = the byte to push (0x60 = 96 in decimal)
// 0x40 = PUSH1 again
// 0x52 = MSTORE (store to memory)

// You never write this by hand. Solidity does it for you.
// But knowing it exists explains WHY:
// - Contract deployment costs gas (bytecode must be stored on-chain)
// - Longer contracts = more gas to deploy
// - Every function call = executing these opcodes

console.log("EVM opcodes are the CPU instructions of Ethereum");
console.log("Solidity is the high-level language that compiles to them");
// Output:
// EVM opcodes are the CPU instructions of Ethereum
// Solidity is the high-level language that compiles to them

Two Types of Accounts: EOAs and Contract Accounts

Now that you understand the machine, let's meet the actors that interact with it. This is the most fundamental distinction on Ethereum, and I'm going to be blunt: if you confuse these two, you will lose money. I've seen it happen. A developer sent tokens to a contract address that had no withdrawal function — gone forever. Millions of dollars in ETH sit permanently locked in contracts because someone treated a contract address like a regular wallet.

Ethereum has exactly two types of accounts:

1. Externally Owned Accounts (EOAs)

This is what MetaMask gives you. It's controlled by a private key, has no code, and is the only account type that can initiate transactions.

// An EOA's properties — let's inspect one using ethers.js
const ethers = require("ethers");

// Vitalik Buterin's public address (this is public information)
const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

// An EOA has these properties:
const eoaProperties = {
  address: vitalikAddress,          // Derived from public key
  balance: "varies",                 // ETH held
  nonce: "number of txs sent",      // Prevents replay attacks
  codeHash: "0xc5d2...empty",       // EMPTY — no code
  storageRoot: "0x56e8...empty"     // EMPTY — no storage
};

console.log("EOA Properties:");
console.log(JSON.stringify(eoaProperties, null, 2));

// Output:
// EOA Properties:
// {
//   "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
//   "balance": "varies",
//   "nonce": "number of txs sent",
//   "codeHash": "0xc5d2...empty",
//   "storageRoot": "0x56e8...empty"
// }

2. Contract Accounts

Created when you deploy a smart contract. Controlled by code, not a private key. Cannot initiate transactions on their own — they only execute when called by an EOA (or by another contract that was ultimately triggered by an EOA).

// A Contract Account's properties
const contractProperties = {
  address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT contract
  balance: "ETH held by contract",
  nonce: "number of contracts created BY this contract",
  codeHash: "0x3f6b...hash of bytecode",   // HAS CODE
  storageRoot: "0xa4c2...root of storage"   // HAS STORAGE (all USDT balances!)
};

console.log("Contract Account Properties:");
console.log(JSON.stringify(contractProperties, null, 2));

// Output:
// Contract Account Properties:
// {
//   "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
//   "balance": "ETH held by contract",
//   "nonce": "number of contracts created BY this contract",
//   "codeHash": "0x3f6b...hash of bytecode",
//   "storageRoot": "0xa4c2...root of storage"
// }

The critical differences:

PropertyEOAContract Account
Controlled byPrivate keyCode (immutable after deploy)
Has code?NoYes
Has storage?NoYes
Can initiate tx?YesNo — only responds
Can receive ETH?AlwaysOnly if code allows it
CreationGenerate key pairDeploy via transaction
Nonce meaningTx countContracts created count

🤔 Think about it: Our FundChain crowdfunding contract will hold ETH from donors. Who "owns" that ETH? Can the contract decide on its own to send it back?

Answer

The contract holds the ETH in its balance. But the contract cannot decide on its own to do anything — it has no private key and cannot initiate transactions. The ETH can only move if:

  1. An EOA sends a transaction that calls a function on the contract
  2. That function's code contains logic to transfer ETH (like a withdraw() or refund() function)

This is exactly why our FundChain contract needs carefully designed withdrawal and refund functions — we'll build those in Lessons 7 and 8. If we forget to include a withdrawal function, the ETH is locked forever. This has happened to real contracts holding millions of dollars.


The Life of a Transaction: From Click to Finality

You know the machine. You know the actors. Now let's trace what happens when they interact.

You click "Send" in MetaMask. What actually happens? Follow this path once, and gas fees, failed transactions, and that maddening "pending" state will all make sense.

Each step carries exact data. Here's the structure:

The Transaction Object

Every Ethereum transaction contains these fields. No exceptions.

// Anatomy of an Ethereum transaction (post-EIP-1559)
const transaction = {
  // Identity & ordering
  from: "0xYourEOA...",        // Sender's address (derived from signature)
  nonce: 42,                    // Sender's 43rd transaction (0-indexed)
  
  // Destination
  to: "0xContractOrEOA...",    // Recipient (null for contract deployment!)
  value: 1000000000000000000n, // 1 ETH in wei (10^18 wei = 1 ETH)
  
  // Gas economics (EIP-1559)
  gasLimit: 21000,              // Max gas units you'll pay for
  maxFeePerGas: 30000000000n,  // Max total per gas unit (30 Gwei)
  maxPriorityFeePerGas: 2000000000n, // Tip to validator (2 Gwei)
  
  // Payload
  data: "0x",                   // Empty for simple ETH transfer
                                // Contains function selector + args for contract calls
  
  // Chain identification
  chainId: 1,                   // 1 = mainnet, 11155111 = Sepolia testnet
  type: 2                       // EIP-1559 transaction type
};

console.log(`Sending ${Number(transaction.value) / 1e18} ETH`);
console.log(`Max gas cost: ${Number(transaction.gasLimit * transaction.maxFeePerGas) / 1e18} ETH`);
console.log(`Nonce ensures this tx can only be processed ONCE, in order`);

// Output:
// Sending 1 ETH
// Max gas cost: 0.00063 ETH
// Nonce ensures this tx can only be processed ONCE, in order

The nonce is critical. It's a counter that increments with every transaction you send. If your nonce is 42, the network will only accept a transaction with nonce 42 from your address — not 41 (already used), not 43 (not yet). This prevents replay attacks and guarantees ordering. I've had transactions stuck for hours because I accidentally created a nonce gap.

The to field being null means contract deployment. When you deploy our FundChain contract in the project update, the to field will be empty, and the data field will contain the contract's bytecode. The network creates a new contract account at a deterministic address.

Gas: The Fuel That Prevents Infinite Loops

Why does gas exist? Because the EVM is Turing-complete, which means it can run infinite loops. Without gas, a malicious contract could execute while(true) {} and every node on the network would spin forever. Gas is Ethereum's kill switch: every computation costs fuel, and you set a hard limit on how much you'll burn.

Post-EIP-1559 (August 2021), gas pricing has three components:

// Gas cost calculation — this is how your MetaMask fee is computed

const baseFee = 15n;          // Set by protocol (Gwei) — adjusts per block
const priorityFee = 2n;       // Your tip to the validator (Gwei)
const gasUsed = 21000n;        // Simple ETH transfer always costs exactly 21,000

// What you pay:
const totalCostGwei = gasUsed * (baseFee + priorityFee);
const totalCostETH = Number(totalCostGwei) / 1e9;

// What the validator gets:
const validatorReward = gasUsed * priorityFee;

// What gets burned (removed from supply forever):
const burned = gasUsed * baseFee;

console.log(`Total cost: ${totalCostGwei} Gwei (${totalCostETH} ETH)`);
console.log(`Validator gets: ${validatorReward} Gwei`);
console.log(`Burned forever: ${burned} Gwei`);

// Output:
// Total cost: 357000 Gwei (0.000357 ETH)
// Validator gets: 42000 Gwei
// Burned forever: 315000 Gwei

The base fee burn is why Ethereum can be deflationary — when more ETH is burned than issued, the total supply shrinks. This was one of the most significant economic changes in Ethereum's history.

🔍 Deep Dive: What happens when your transaction runs out of gas?

This is one of the most frustrating things in Ethereum development. If your transaction runs out of gas mid-execution:

  1. All state changes are reverted — nothing you computed is saved
  2. You still pay for the gas used — the validator did the work, so they keep the fee
  3. Your nonce still increments — the transaction was processed (it just failed)

A failed transaction costs you real money with zero result. I once burned $40 in gas on a failed deployment because I set the gas limit too low. The contract was large, needed more gas than I estimated, and I got nothing back.

Pro tip: Always set your gas limit ~20% higher than the estimate. You only pay for what you actually use — the surplus is refunded. Setting it too low risks failure; setting it high costs nothing extra.


Block Structure and Post-Merge Proof of Stake

Transactions don't exist in isolation — they're bundled into blocks. And the way those blocks are produced changed dramatically on September 15, 2022. Since "The Merge," Ethereum no longer uses mining. Validators stake 32 ETH, and the protocol randomly selects one to propose each block.

// Ethereum Consensus Evolution

// 2015-2022: Proof of Work (PoW)
// - Miners solved SHA-256 puzzles
// - ~13 second block time
// - ~13,000 ETH/day issuance
// - Energy usage: ~same as a small country

// 2022-present: Proof of Stake (PoS) — "The Merge"
// - Validators stake 32 ETH
// - Exactly 12 second block time (slots)
// - ~1,600 ETH/day issuance (88% reduction!)
// - Energy usage: reduced by ~99.95%

// Key PoS concepts:
// Slot: 12-second window for one block
// Epoch: 32 slots = 6.4 minutes
// Finality: 2 epochs = ~12.8 minutes
//   → After finality, reversing the block would require
//     burning 1/3 of all staked ETH (~$10B+ at current prices)

console.log("Slots per epoch: 32");
console.log("Seconds per slot: 12");
console.log("Time to finality: ~12.8 minutes (2 epochs)");

// Output:
// Slots per epoch: 32
// Seconds per slot: 12
// Time to finality: ~12.8 minutes (2 epochs)

🤔 Think about it: If a validator is randomly chosen to propose a block, what stops them from including only their own transactions or censoring specific addresses?

Answer

This is the censorship resistance problem, and it's an active area of research. Currently, validators can choose which transactions to include, and there have been documented cases of validators censoring transactions related to sanctioned addresses (e.g., OFAC compliance). However:

  1. Other validators can include the censored transaction in the next block (12 seconds later)
  2. Proposer-Builder Separation (PBS) is being developed to separate "who orders transactions" from "who proposes blocks"
  3. Slashing — validators who provably misbehave can lose their 32 ETH stake

In practice, a transaction might be delayed by one or two blocks due to censorship, but permanently censoring a transaction is extremely difficult because you'd need to control every validator selected for every future block.


Numbers That Matter

These are the benchmarks you should have memorized as an Ethereum developer:

MetricValueWhy It Matters
Simple ETH transfer gas21,000Minimum possible gas cost
SSTORE (new slot)20,000Writing one storage variable
SSTORE (update)5,000Updating existing storage
SLOAD2,100Reading storage
Contract deploy (small)~200,000–500,000Our FundChain will be here
Contract deploy (large)5,000,000–24,576,00024,576 bytes max (EIP-170)
Block gas limit~30,000,000Max computation per block
Block time12 secondsOne slot
Finality time~12.8 minutes2 epochs, 64 slots
Max contract size24,576 bytesEnforced by EIP-170

The number I check daily: Base fee. It fluctuates based on network demand. I deployed a contract at 2 AM UTC once because the base fee dropped to 8 Gwei vs. 50 Gwei during peak hours. That saved me $120 on a single deployment. Timing matters.


Actionable Takeaways

1. Treat contract addresses differently from EOA addresses. Before sending ETH or tokens, verify the target is what you expect. A contract address with no withdrawal function is a black hole. In our FundChain project, we'll build explicit withdrawal and refund functions — never assume ETH can be retrieved just because it was sent.

2. Gas estimation is not optional — it's architectural. Every line of Solidity you write compiles to opcodes that cost gas. Storage writes dominate cost by orders of magnitude. When we design FundChain's data structures in Lesson 3, we'll make storage-conscious decisions that save real money.

3. Understand finality before building anything with real stakes. A transaction with 1 confirmation is not the same as a finalized transaction. For our crowdfunding DApp, we'll need to decide: does a contribution count at inclusion, or at finality? The answer depends on the use case and the amounts involved.


🔨 Project Update

This is Lesson 1, so we're starting from zero. By the end of this step, you'll have a Hardhat development environment with an empty FundChain contract that compiles.

Prerequisites: Node.js v18+ installed. Verify with:

node --version
# Expected output: v18.x.x or higher (v20+ recommended)

Step 1: Create and initialize the project.

mkdir fundchain && cd fundchain
npm init -y
npm install --save-dev hardhat
npx hardhat init
# Select: "Create a JavaScript project"
# Accept all defaults (press Enter through prompts)

Step 2: Delete the sample contract and create FundChain.sol.

rm contracts/Lock.sol
rm test/Lock.js

Now create the contract file. This is the entire content for today:

// contracts/FundChain.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title FundChain — Decentralized Crowdfunding
/// @notice We'll build this contract over Lessons 2-8
contract FundChain {
    // Empty for now — the EVM still compiles this to bytecode!
}

Step 3: Compile.

npx hardhat compile
# Expected output:
# Compiled 1 Solidity file successfully (solc-0.8.24).

What just happened under the hood?

  1. The Solidity compiler (solc) parsed your .sol file
  2. It generated EVM bytecode (the actual machine code deployed on-chain) and an ABI (a JSON interface describing the contract's functions)
  3. These artifacts are stored in artifacts/contracts/FundChain.sol/
# Check the generated artifacts
cat artifacts/contracts/FundChain.sol/FundChain.json | head -5
# Expected output (approximately):
# {
#   "_format": "hh-sol-artifact-1",
#   "contractName": "FundChain",
#   "sourceName": "contracts/FundChain.sol",
#   "abi": [],

Cumulative project code so far:

fundchain/
├── contracts/
│   └── FundChain.sol        ← Created this lesson
├── test/                     ← Empty for now (Lesson 9)
├── hardhat.config.js         ← Generated by Hardhat
├── package.json
└── node_modules/

Run the project you've built so far:

npx hardhat compile
# Expected output:
# Compiled 1 Solidity file successfully (solc-0.8.24).

If you see that output, you're set. That empty contract compiled to real EVM bytecode — the same kind of bytecode that runs The DAO, USDT, and Uniswap. Starting next lesson, we'll fill it with Solidity.


Summary Diagram


Difficulty Fork

🟢 That was comfortable — give me the quick version

Core concepts nailed:

  • Ethereum = Turing-complete state machine, Bitcoin = ledger
  • EVM has stack (free), memory (moderate), storage (expensive)
  • EOAs have keys, contracts have code — only EOAs initiate transactions
  • Gas prevents infinite loops and pays validators
  • Post-Merge: 12s blocks, PoS, ~13 min finality

Next lesson: We write real Solidity — uint, address, mapping, function visibility (public, external, internal, private), and deploy from Remix.

🟡 Some parts were fuzzy — explain it differently

Think of Ethereum as a global shared spreadsheet:

  • EOA = A person holding a pen (private key). Only they can write entries.
  • Contract = A formula cell. It can compute and change values, but only when someone triggers it by writing to its cell.
  • Gas = You pay per character you write. Complex formulas cost more.
  • Storage = Cells that keep their values forever (expensive). Memory = scratch paper you throw away after each calculation (cheap).
  • The EVM = The spreadsheet engine that evaluates all formulas identically on every computer.

The DAO hack? A formula that sent money before updating its own cell. The attacker triggered the formula recursively before the cell ever changed.

Extra practice: Go to etherscan.io, search for any transaction, and identify: the from (EOA), the to (could be EOA or contract), the gas used, and whether it succeeded or failed.

🔴 Challenge: Interview-level question

Question: A contract at address 0xAAA calls a function on contract 0xBBB, which calls a function on contract 0xCCC, which sends ETH to EOA 0xDDD. The total gas limit is 100,000. Contract 0xCCC's call to 0xDDD fails because of insufficient gas.

  1. Which state changes are reverted? Only 0xCCC → 0xDDD? Or everything?
  2. Who pays the gas?
  3. What would be different if 0xBBB used a try/catch block around the call to 0xCCC?

Answer: (1) By default, the entire transaction reverts — all state changes across 0xAAA, 0xBBB, and 0xCCC are undone. Ethereum transactions are atomic. (2) The EOA that originally initiated the transaction pays for all gas consumed up to the point of failure. (3) If 0xBBB used try/catch, the failure in 0xCCC would be caught — only 0xCCC's changes revert, while 0xAAA and 0xBBB's changes persist. This is why error handling patterns matter enormously in Solidity — we'll cover this in Lesson 6.

코드 실습

JavaScriptWhen you write Solidity and compile it, the compiler produces **bytecode** — a sequence of EVM opcodes. That bytecode is what gets stored on-chain. Here's a concrete look at what that means:
JavaScriptThis is what MetaMask gives you. It's controlled by a private key, has no code, and is the **only** account type that can initiate transactions.
JavaScript
JavaScriptEvery Ethereum transaction contains these fields. No exceptions.
JavaScriptPost-EIP-1559 (August 2021), gas pricing has three components:

질문 & 토론