Week 01 — Web3, Blockchain & Crypto Fundamentals

“Every Web3 exploit is, at root, a violation of an assumption the developer didn’t know they were making. Before you can audit assumptions, you need a precise mental model of the stack — what each layer guarantees, what it doesn’t, and where the trust seams are.”

Tags: web3-security foundations consensus cryptography merkle signature Learner: Developer with general programming background → professional Web3 auditor Time: 7 days (4–6h/day) Related: Tuan-02-Ethereum-EVM-Deep-Dive · Tuan-12-Wallet-AA-Key-Management · Tuan-10-Bridge-Cross-Chain-Security


1. Context & Why

1.1 What this week is and isn’t

This week is not a “what is blockchain” hand-wave. It’s the precise model an auditor uses when reasoning about a system. Specifically, by the end you can answer, for any Web3 component:

  1. What does this layer guarantee? (e.g., “PoS finality after 2 epochs” or “ECDSA signature implies knowledge of d for public key Q = dG”)
  2. What does it not guarantee? (e.g., “PoS finality does not guarantee no reorg before finalization”)
  3. What’s the trust seam between this layer and the next? (e.g., “An app trusts that the RPC node is honest about the chain head”)
  4. What breaks if the assumption fails? (e.g., “If finality is reverted, an L2 bridge that paid out optimistically loses funds”)

Auditors who skip this become “syntax auditors” — they spot tx.origin and block.timestamp patterns, but they miss the cross-chain reorg, the validator-set rotation race, the partial-finality bridge bug. The hardest bugs live in the seams.

1.2 Why blockchain at all? (the auditor’s reframe)

The non-auditor framing: “blockchain is a decentralized database”. This framing is useless for security.

The auditor’s framing: blockchain is a replicated state machine with a particular set of trust assumptions, and security work is the discipline of figuring out exactly which assumptions a given protocol relies on, then probing whether reality matches those assumptions.

Concrete example — three different “is this transfer final?” questions:

QuestionWhere the trust lives
”Did the user’s transaction execute?”EVM determinism + node honesty
”Will the transaction stay in the canonical chain?”Consensus + finality model
”Did the asset really arrive on chain B after a bridge transfer?”Bridge’s trust model (validators / light client / ZK proof)

Each question has a different failure mode. A bridge auditor who doesn’t carefully distinguish them will miss the Nomad / Ronin / Wormhole class of bug.

1.3 Learning goals for the week

By the end, you can:

  • Draw the layered architecture of Ethereum (P2P → consensus → execution → DA → app) and state what each layer guarantees.
  • Explain probabilistic finality vs deterministic finality and give one Web3 exploit that depended on misjudging finality.
  • Use keccak256 on raw bytes from the command line, derive an Ethereum address from a public key, and verify an ECDSA signature manually with a small library.
  • Compute a Merkle root by hand for ≤ 8 leaves and verify a proof.
  • State the three properties of a cryptographic hash function (preimage / second-preimage / collision resistance) and which DeFi designs rely on which.
  • Articulate at a one-paragraph level what a ZK proof gives you (succinctness + zero-knowledge) without claiming to understand the math.

1.4 Primary references

SourceWhy
Ethereum.org Developer Docs — Intro & PoShttps://ethereum.org/en/developers/docs/intro-to-ethereum/ · https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/ — official, current
Ethereum Yellow Paperhttps://ethereum.github.io/yellowpaper/paper.pdf — formal protocol spec
Mastering Ethereum (Antonopoulos & Wood)https://github.com/ethereumbook/ethereumbook — free, foundational; some parts pre-Merge, treat them as historical context
Vitalik, “The Limits to Blockchain Scalability”https://vitalik.eth.limo/general/2021/05/23/scaling.html — 2021 but conceptual content still current
Real-World Cryptography (David Wong)Book; chapters 1–8
NIST FIPS 186-5 (Digital Signature Standard)https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf — ECDSA spec
RFC 6979 (Deterministic ECDSA)https://datatracker.ietf.org/doc/html/rfc6979 — relevant to malleability discussion

2. The Replicated State Machine Model

2.1 The core abstraction

Strip away the marketing, and every public blockchain is the same thing:

state[n+1] = STF(state[n], block[n+1])
  • state[n]: the world state after block n (in Ethereum: account balances, contract storage, nonces, code).
  • block[n+1]: an ordered batch of transactions plus a header.
  • STF: the state transition function (deterministic; defined by the protocol).

Every honest node, given the same state[n] and the same block[n+1], computes the same state[n+1]. Determinism is the only thing that lets thousands of nodes agree on state without trusting each other.

This has two immediate audit implications:

  1. Anything non-deterministic in the execution layer breaks consensus. That’s why block.timestamp, block.prevrandao, block.number are deterministic for a given block but adversarially controllable. There is no real “current time” or “real random” inside the EVM.
  2. The blockchain doesn’t decide truth; it decides which transactions to apply, in what order. A smart contract is a function. Its inputs are caller, calldata, value, block context. Its output is state changes + return data + logs. Everything else (oracles, user intent, “fairness”) is outside the deterministic model and must be brought in via some trust mechanism.

2.2 The two hard problems

State machine replication has two hard problems:

  1. Membership / Sybil resistance: who is allowed to propose blocks? In a permissionless system anyone can spin up identities, so we need a costly signal: Proof-of-Work (computation) or Proof-of-Stake (capital + slashing).
  2. Agreement on ordering: even with valid block proposers, nodes can see different sets of valid blocks (due to network latency, partition). Consensus algorithms resolve this — sometimes immediately (BFT), sometimes statistically (longest chain).
PropertyBitcoin (PoW)Ethereum (PoS post-Merge)Cosmos (Tendermint)Solana (PoH + TowerBFT)
Sybil resistanceHash powerStake (32 ETH/validator)Bonded ATOMBonded SOL
Block productionSingle miner per blockSingle proposer per slot (12s)Round-robin proposerSingle leader per slot (400ms)
Finality modelProbabilisticDeterministic (Casper FFG) after 2 epochs (~12.8m)Deterministic (1 block, instant)Optimistic, with rollback
What “finality” means in practiceWait 6+ confirmations heuristicallyWait until finalized tag (~12.8m)Block height N is final at block NRollbacks have happened in production

Audit takeaway: when a bridge contract says “we trust messages from chain X with N confirmations”, check whether N is enough for that chain’s actual finality model. A 1-block confirmation on Ethereum is not the same as a 1-block confirmation on Solana.

2.3 Layered architecture

flowchart TD
  subgraph Network[Network Layer]
    P2P[P2P / devp2p / libp2p]
  end
  subgraph Consensus[Consensus Layer]
    CL[Beacon chain / fork choice / finality]
  end
  subgraph Execution[Execution Layer]
    EL[EVM / state / tx pool]
  end
  subgraph DA[Data Availability]
    Blobs[Calldata / EIP-4844 blobs / external DA]
  end
  subgraph App[Application Layer]
    SC[Smart Contracts]
    Off[Oracles / Indexers / Relayers]
    UI[Wallet / Frontend]
  end
  P2P --> CL
  CL --> EL
  EL --> DA
  EL --> SC
  Off --> SC
  UI --> Off
  UI -.signs.-> EL

Each layer makes assumptions about the layer below. Bugs hide in cross-layer assumption mismatches.

  • A smart contract assumes the EVM behaves as specified. Bug class: compiler bug (rare — but Curve/Vyper 2023).
  • A frontend assumes the RPC node tells the truth about chain head. Bug class: malicious RPC.
  • A bridge assumes a remote chain’s finality holds. Bug class: cross-chain reorg.
  • An oracle consumer assumes the oracle reports a fair market price. Bug class: price manipulation.

Track these assumption arrows. Every audit deliverable should include an explicit “trust assumptions” section that lists them.


3. Finality, Forks, and Reorgs

3.1 Why finality matters to an auditor

A “transaction that succeeded” can be undone if the chain reorgs past the block containing it. For most application logic, this doesn’t matter — the user just re-submits. For some applications, it matters enormously:

ApplicationReorg sensitivity
ERC-20 transferLow — sender just resubmits
Exchange deposit creditHigh — exchange can credit, then reorg undoes deposit, exchange loses money
Cross-chain bridgeCritical — bridge unlocks/mints on destination based on source-chain event; if source reorgs, bridge is upside down
Optimistic rollup challengeCritical — fraud-proof window must outlast worst-case reorg
LiquidationMedium — liquidator paid; if reorg undoes, liquidation must repeat

3.2 Probabilistic vs deterministic finality

Probabilistic finality: the chance of reversion decreases exponentially with confirmations but is never exactly zero. Bitcoin works this way. Practice: “6 confirmations” is a social convention, not a protocol guarantee.

Deterministic / economic finality: at a specific block height, reversion costs a known economic penalty (slashed stake). Ethereum post-Merge works this way. The finalized tag returned by JSON-RPC marks blocks that cannot be reverted without massive slashing (>1/3 of validator set).

flowchart LR
  H[head] --> S[safe<br>justified] --> F[finalized]
  H -.can reorg.-> H
  S -.can reorg<br>but rare.-> H
  F -.essentially never reverts.-> F
  style F fill:#90ee90
  style S fill:#ffe69c
  style H fill:#ffcccc

Three tags an Ethereum RPC returns:

TagMeaningWhen safe to act on
latestmost recent block proposer publishedfor low-stakes UX
safehead of last justified epochfor medium-stakes flows
finalizedblock past 2 epoch confirmationsfor value-bearing cross-chain flows

Audit rule: any contract that mints on chain B based on an event on chain A must wait for chain A’s finalized block (or stricter). Reading from latest is the Nomad/Ronin class of bug.

3.3 Reorgs in practice (you must know this)

  • Ethereum reorgs of 1–2 blocks happened regularly pre-Merge; post-Merge they are rare but possible until finalization.
  • Polygon PoS, BSC, and some L2 sequencers have had multi-block reorgs in 2022–2023. [verify recent state]
  • Solana has had multiple multi-block-equivalent rollbacks in 2022 due to network halts.

When auditing, ask: “what’s the deepest plausible reorg on this chain in the past 2 years?” Then design assumptions around that, not around the spec’s ideal.


4. Cryptography for Auditors

Auditors do not implement primitives. They reason about misuse, mis-parameterization, and assumption violation. This section is the “what could go wrong” catalog.

4.1 Hash functions

The three properties:

PropertyDefinitionBroken example consequence
Preimage resistanceGiven h, hard to find any m such that H(m) = hPassword / commitment scheme breaks
Second-preimage resistanceGiven m1, hard to find m2 ≠ m1 with H(m1) = H(m2)Signature spoofing
Collision resistanceHard to find any m1, m2 with H(m1) = H(m2)Merkle proofs forgeable

Hash functions you’ll see:

FunctionOutputWhere usedNotes
keccak256256-bitEthereum (everything: addresses, slot derivation, signatures’ message hash, Merkle in many systems)Distinct from SHA-3 — same Keccak family, different padding
SHA-256256-bitBitcoin, some Ethereum precompiles, generally outside EVM
PoseidonconfigurableZK circuits”ZK-friendly” because arithmetic-circuit-cheap; do not use outside circuits unless you understand the constants
ripemd160, blake2, MiMCvariousSpecial-purposeAudit-relevant in specific protocols

Auditor pitfall — non-deterministic encoding before hashing:

keccak256(abi.encode(a, b))     // length-prefixed, no ambiguity
keccak256(abi.encodePacked(a, b)) // concatenation, collisions possible!

abi.encodePacked can produce the same bytes for different (a, b) pairs when types are variable-length (string, bytes, dynamic arrays). Real bug class — every audit should flag encodePacked use over variable-length types.

// Both produce the same hash — collision!
keccak256(abi.encodePacked("aaa", "bbb")) // "aaabbb"
keccak256(abi.encodePacked("aa",  "abbb")) // "aaabbb"

4.2 Asymmetric crypto: ECDSA on secp256k1

Ethereum uses ECDSA over the secp256k1 curve. You need to remember:

  • Private key d: a 256-bit integer in [1, n-1] where n is the curve order.
  • Public key Q = d · G where G is the curve generator.
  • Address = keccak256(Q_x || Q_y)[12:] — the last 20 bytes of the public key’s hash.

Signing a message m:

  1. Compute e = H(m) (typically keccak256).
  2. Pick random nonce k (or deterministic per RFC 6979).
  3. Compute R = k · G, take r = R.x mod n.
  4. Compute s = k⁻¹ · (e + r·d) mod n.
  5. Signature = (r, s, v) where v is the recovery id.

Why this matters to auditors: signature properties and their abuse patterns.

Property / pitfallAudit implication
k reuseTwo signatures with the same k reveal d. PlayStation 3 case (2010), some early Ethereum wallets affected. Use RFC 6979 deterministic k.
s malleabilityFor any valid (r, s), (r, n−s) is also a valid signature on the same message. Pre-EIP-2 this allowed transaction-hash mutation. EIP-2 restricts s to lower half — ecrecover does NOT enforce this, the application must.
v ambiguityv is 27 or 28 (sometimes encoded as 0/1). Wrong v returns a different (valid) address, which can match an unrelated account. Always validate signer matches expected address, not just “is non-zero”.
No domain separationSame signed payload can be replayed across contracts / chains. EIP-712 solves this with a domainSeparator of (name, version, chainId, verifyingContract).
Zero address from ecrecoverBad signatures return address(0). If your access-control logic checks signer == someStoredAddress and someStoredAddress is uninitialized (address(0)), bug. Always check signer != address(0).

Why tx.origin is unsafe (and what AA changes):

tx.origin returns the EOA that initiated the transaction, not the immediate caller. If a contract uses require(tx.origin == owner), then any contract the owner interacts with can call back and pass that check. Famous historical bug.

ERC-4337 changes some of this: under AA, the “EOA” concept is replaced by a smart-contract account. tx.origin in a UserOp context is the EntryPoint contract, not a user-controlled address. Implications:

  • Phishing patterns shift from “trick EOA into bad signature” to “trick smart account into approving bad UserOp via a session key or paymaster trick”.
  • msg.sender is now reliably the smart account, not an EOA, which breaks naive msg.sender == EOA reasoning (some contracts check via tx.origin == msg.sender to “ensure not a contract” — this fails for AA users).

4.3 Merkle trees

A Merkle tree binds many leaves to a single 32-byte root via repeated pairwise hashing.

flowchart TD
  R[Root = h(h12, h34)]
  R --> H12[h12 = h(h1,h2)]
  R --> H34[h34 = h(h3,h4)]
  H12 --> H1[h1 = h(L1)]
  H12 --> H2[h2 = h(L2)]
  H34 --> H3[h3 = h(L3)]
  H34 --> H4[h4 = h(L4)]

Merkle proof of inclusion: to prove leaf L2 is in the tree, supply [h1, h34] and the position bits. Verifier recomputes the root.

Common in audits:

  • Airdrops: Merkle root posted on-chain; users submit proofs to claim. Pitfall: re-claim via different (leaf-encoding, proof) pairs if leaf encoding is ambiguous. Always include a unique-per-user nonce in the leaf.
  • Light client proofs: rollup state roots committed on L1; users prove storage values via Merkle-Patricia proofs.
  • Bridge attestations: messages batched into a Merkle root; signed by a committee.

Pitfall — second-preimage attacks on Merkle: If your verifier hashes leaves and internal nodes with the same hash function and no domain separation, an attacker may construct an internal-node value that looks like a leaf and prove inclusion of a “leaf” that was never added. Mitigation: prefix leaf hashes with 0x00 and internal hashes with 0x01 (OpenZeppelin MerkleProof does this).

4.4 Multisig, threshold, MPC, BLS — at a glance

SchemeWhat it isWhere in Web3
Multisig (m-of-n)n keys, m required to sign a txSafe (formerly Gnosis Safe) — most DAO treasuries
Threshold signaturen parties hold shares; m can produce a single signature (no on-chain reveal that m participated)MPC custody products, some L2 sequencer setups
MPC (Multi-Party Computation)Generalization: keys are never materialized at one placeFireblocks, Web3Auth, Particle Network
BLS signatures (BLS12-381)Pairing-based; aggregable (many sigs → one short sig); used in Ethereum consensus (validators attest with BLS, aggregated by block proposer)Ethereum consensus, EigenLayer AVS attestation

Audit-relevant facts:

  • A 2-of-3 multisig where one key is compromised + one signer is bribed = single point of failure.
  • BLS sigs are not strict-uniqueness if the implementation forgets to bind to a domain separator — leads to rogue-key attacks.
  • MPC products have moved key risk from “where is the key” to “where is the share + threshold” — different threat model.

4.5 Commitment schemes

A commitment is: c = Commit(m, r) where:

  • c reveals nothing about m (hiding)
  • c cannot later open to a different m' (binding)

Simple example: c = keccak256(m || r) where r is a random salt.

Used for:

  • Commit-reveal patterns for randomness, sealed-bid auctions, governance.
  • ZK proof witness binding.
  • MEV-resistant order flow (commit order, reveal later).

Pitfall: forgetting the salt makes the scheme not hiding (attacker brute-forces small message space).

4.6 Zero-knowledge proofs — auditor’s mental model

You do not need to do the math. You do need this mental model:

A ZK proof system lets a prover demonstrate that they know a witness w satisfying some predicate f(public_input, w) = true, without revealing w, and the proof is short and fast to verify.

ZK PropertyWhat it givesFailure mode
CompletenessA valid prover with a valid witness always convinces verifierHard to break; usually a correctness bug
SoundnessA malicious prover without a valid witness cannot convince verifierUnder-constrained circuits leak this; the famous “missing constraint” bug
Zero-knowledgeProof reveals nothing beyond validityUsually about randomness in proving; less common bug source
SuccinctnessProof size and verifier time are sub-linear in circuit sizePerformance property

The dominant ZK bug class is under-constrained circuits: the circuit fails to enforce some constraint, so an attacker constructs a witness that’s invalid by intent but valid by missing-constraint. The proof verifies fine, but the application semantics are violated. Examples: missing range checks, missing field-size enforcement, signed/unsigned confusion.

4.7 Cryptographic assumptions you rely on

AssumptionHolds in 2026?Threat horizon
keccak256 collision resistanceYesFar
ECDSA secp256k1 hardness (discrete log)YesThreatened by large-scale quantum computers (decades, possibly)
Pairing-based crypto (BLS12-381)YesSimilar quantum horizon
Trusted setup ceremonies (Groth16)Trust assumption, not math; honest if ≥1 participant deletes toxic wastePer-circuit; covered by ceremony quality

Audit takeaway: when a protocol says “post-quantum safe”, verify the claim against the primitives in use. Most Web3 today is not post-quantum.


5. Wallets, Keys, and Address Derivation

5.1 EOA vs contract account

EOAContract account
Controlled byPrivate keyCode
Has nonceYesYes (incremented per CREATE)
Can initiate txYesNo (called by EOA or another contract)
Has codeNoYes

Pre-AA, only EOAs can initiate transactions. Contract accounts can only react.

5.2 BIP-32 / BIP-39 / BIP-44 hierarchical wallets

The standard wallet stack:

  • BIP-39: mnemonic → seed (typically 12 or 24 English words → 512-bit seed).
  • BIP-32: seed → tree of keypairs (HD wallet) via key derivation.
  • BIP-44: tree structure conventions (m / purpose' / coin_type' / account' / change / index).

For Ethereum the typical path is m/44'/60'/0'/0/n for the n-th account.

Audit-relevant:

  • Mnemonic = root secret. Compromise once → all derived accounts compromised forever.
  • Many wallets share seed across chains. A leaked seed exposed on chain X compromises chain Y addresses too.
  • Some L2s use the same address as L1 (good UX, but means key compromise spans chains automatically).

5.3 Address derivation gotchas

  • Ethereum addresses are case-insensitive but EIP-55 introduced a mixed-case checksum encoding. UIs that accept lower-case-only addresses lose this protection.
  • Vanity addresses with leading zero bytes save gas on calldata (per EIP-2028) — small but real incentive for trading firms.
  • CREATE2 addresses are deterministic from (deployer, salt, init_code) — auditors must check that contracts deployed at predictable addresses cannot have an attacker frontrun the deployment to a different init_code (the address squatting bug).

6. The Trust Model of Web3 Applications

6.1 The end-to-end trust path

flowchart LR
  U[User] --> UI[dApp Frontend]
  UI --> W[Wallet]
  W -->|signs| RPC[RPC Node]
  RPC --> M[Mempool / Relay]
  M --> P[Block Proposer]
  P --> N[Network]
  N --> SC[Smart Contract State]
  OR[Oracle / off-chain data] --> SC
  IDX[Indexer / Subgraph] --> UI
  SC --> IDX
  style UI fill:#fff2cc
  style W fill:#fff2cc
  style RPC fill:#fff2cc
  style M fill:#fff2cc
  style OR fill:#fff2cc
  style IDX fill:#fff2cc

Yellow = trust seam. Each is a place where an honest user can be victimized without breaking the core protocol.

Trust seamFailure modeReal incident class
FrontendWrong code served (DNS hijack, CDN compromise, malicious npm dep)BadgerDAO 2021, Galxe 2023, Curve frontend incidents
WalletWallet shows different intent than what’s signed (esp. blind-signing hardware wallets)Many phishing campaigns
RPC nodeLies about chain head, censors txs, leaks tx dataMalicious public RPC
MempoolReordering for MEVSandwich attacks (continuous, not “incidents”)
Block proposerCensorship, MEV extractionInclusion-list / PBS discussion
OracleReports stale or manipulated pricebZx 2020, Mango Markets 2022
IndexerWrong UI display from wrong dataSoft failures, occasional user loss

Auditor mandate: every project should have explicit answers to:

  • “Who runs the frontend?”
  • “Where is the frontend hosted?”
  • “What RPC does the wallet use by default?”
  • “What oracle?”
  • “Who upgrades the contracts?”

Missing or hand-wavy answers = finding.

6.2 Permissionless / permissioned / hybrid

Not every Web3 system is fully permissionless. Spectrum:

TypeExamplesAudit angle
Fully permissionlessBitcoin, Ethereum L1, most DeFi protocols once renouncedThreat model includes anyone in the world
Permissioned validatorsMany L2 sequencers (1-of-1 today), bridge committeesThreat model centers on validator set integrity
Permissioned contractsProtocol-controlled upgrade keys, oracles with admin pauseThreat model includes the admin
HybridMost real DeFi: permissionless usage, permissioned upgradeNeed both surfaces

The most under-reported audit finding: protocols claim “decentralized” but have an admin key that can drain. Always trace upgrade authority and ownership to a real entity.


7. Lab — Set up the environment + reproduce a tiny exploit primitive

7.1 Lab goal

Build a working dev environment and execute the following auditor primitives:

  1. Hash an arbitrary string with keccak256 from the CLI.
  2. Derive an Ethereum address from a private key.
  3. Sign a message and recover the signer.
  4. Reproduce the abi.encodePacked collision and explain why it’s a real exploit primitive.
  5. Verify a Merkle proof against a root.

Outcome: you have Foundry installed, you can use cast, and you have hands-on intuition for the primitives behind every signature-based vulnerability.

7.2 Setup

# Install Foundry (foundry-rs)
curl -L https://foundry.paradigm.xyz | bash
foundryup
 
# Verify
forge --version
cast --version
anvil --version

Create the lab directory:

mkdir -p ~/web3-sec-lab/wk01 && cd ~/web3-sec-lab/wk01
forge init --no-commit hash-and-sig-lab
cd hash-and-sig-lab

7.3 Exercise 1 — keccak256 from the CLI

# Hash a UTF-8 string
cast keccak "hello"
# 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8
 
# Hash raw bytes (note 0x prefix)
cast keccak 0xdeadbeef
# 0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1

Task: compute keccak256("Ethereum") and verify against an online reference. Stretch: confirm keccak256 differs from SHA-3 by hashing a known SHA-3 test vector and comparing.

7.4 Exercise 2 — Address from private key

# Random private key (for lab only, never in production)
PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
cast wallet address $PK
# 0x70997970C51812dc3A010C7d01b50e0d17dc79C8

The relationship: address = keccak256(secp256k1_pubkey)[-20:].

Task: pick three different private keys and derive each address. Note that any 32-byte value < curve order is a valid private key — there is no “address creation” step; addresses pre-exist for every possible key.

7.5 Exercise 3 — Sign and recover

PK=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
MSG=$(cast keccak "I authorize transfer")
SIG=$(cast wallet sign --private-key $PK $MSG)
echo $SIG
# 0x... 65 bytes (r,s,v)
 
# Recover the signer
cast wallet verify --address $(cast wallet address $PK) $MSG $SIG
# true

Task: take a signature, mutate the last byte (v flip), re-verify. Observe what cast reports.

Discussion: this is the basis of every “off-chain signature on-chain verification” pattern (permit, ERC-4337 UserOp signing, EIP-712 typed data). Misuse appears in 30%+ of audits with off-chain components.

7.6 Exercise 4 — abi.encodePacked collision PoC

Write a Foundry test demonstrating the collision:

// test/PackedCollision.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import "forge-std/Test.sol";
 
contract PackedCollisionTest is Test {
    function test_collision() public {
        bytes32 h1 = keccak256(abi.encodePacked("aaa", "bbb"));
        bytes32 h2 = keccak256(abi.encodePacked("aa",  "abbb"));
        assertEq(h1, h2, "Expected same hash");
        emit log_named_bytes32("h1", h1);
        emit log_named_bytes32("h2", h2);
    }
}

Run:

forge test --match-test test_collision -vv

Discussion: imagine a contract verifies a signature over keccak256(abi.encodePacked(name, action)). A signature for ("aaa", "bbb") is valid for ("aa", "abbb"). Real exploit primitive.

Mitigation in production code:

  • Use abi.encode (length-prefixed) for variable-length inputs.
  • Or use EIP-712 typed data where the type hash binds the structure.

7.7 Exercise 5 — Verify a Merkle proof

// test/MerkleVerify.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import "forge-std/Test.sol";
 
contract MerkleVerifyTest is Test {
    // Build a tree with 4 leaves: h(a), h(b), h(c), h(d)
    bytes32 leafA = keccak256(abi.encodePacked("a"));
    bytes32 leafB = keccak256(abi.encodePacked("b"));
    bytes32 leafC = keccak256(abi.encodePacked("c"));
    bytes32 leafD = keccak256(abi.encodePacked("d"));
 
    function _hashPair(bytes32 x, bytes32 y) internal pure returns (bytes32) {
        return x < y
            ? keccak256(abi.encodePacked(x, y))
            : keccak256(abi.encodePacked(y, x));
    }
 
    function test_verify_inclusion_of_b() public {
        bytes32 hAB = _hashPair(leafA, leafB);
        bytes32 hCD = _hashPair(leafC, leafD);
        bytes32 root = _hashPair(hAB, hCD);
 
        // Proof of inclusion for leafB: siblings [leafA, hCD]
        bytes32[] memory proof = new bytes32[](2);
        proof[0] = leafA;
        proof[1] = hCD;
 
        bytes32 computed = leafB;
        for (uint256 i = 0; i < proof.length; i++) {
            computed = _hashPair(computed, proof[i]);
        }
 
        assertEq(computed, root, "Merkle proof failed");
    }
}

Stretch: modify the test to break the second-preimage protection by removing the x < y ordering — show that a forged proof can now verify. Then re-add the ordering and observe the forgery fails.

7.8 Expected learning outcome

After this lab, you should be able to:

  • Use cast for routine crypto primitives without thinking.
  • Recognize abi.encodePacked(string|bytes|...) as a red flag on sight.
  • Read a Merkle-proof verification loop and immediately ask: “is this protected against second-preimage?“

8. Anti-patterns (cataloged for the audit checklist)

Add to your developing audit checklist:

  • Uses abi.encodePacked with variable-length types in a hashed signature payload — flag for collision risk.
  • Uses tx.origin for access control — fails under contract callers and AA.
  • ecrecover result not compared against address(0) — invalid signatures map to zero address.
  • Signature payload missing chain-id binding — replay across chains.
  • Signature payload missing contract-address binding — replay across contracts.
  • Signature payload missing nonce / expiry — replay over time.
  • block.timestamp used as randomness — miner / proposer-influenceable.
  • block.prevrandao used as randomness without commit-reveal delay — proposer can bias.
  • Reads from latest block when bridging assets — finality assumption violation.
  • Address checksum not validated on user input (frontend / off-chain code) — user types wrong address.
  • CREATE2 deployment with attacker-controllable salt + init code — possible address squatting.
  • Merkle verification without leaf/internal-node domain separation — second-preimage forgery.

9. Trade-offs and Open Debates

DecisionOption AOption BAuditor view
Finality waitsafe tag (fast UX)finalized tag (safer)For value-bearing cross-chain: always finalized. For UX-only: safe.
Custom Merkle vs OZ MerkleProofInline customUse OZ libraryDefault to OZ. Custom adds risk for no upside unless circuit-constrained.
EIP-712 vs simple hashed payloadEIP-712keccak256(abi.encode(...))Default to EIP-712. Wallet UX is better, replay protection is structural.
AA support in protocolFirst-class (isValidSignature per EIP-1271)EOA-onlyFirst-class. ERC-4337 adoption is growing; EOA-only contracts are accumulating future debt.

10. Quiz (self-check, ≥80% to advance)

  1. Q: What’s the difference between safe and finalized RPC block tags on Ethereum post-Merge? A: safe = head of last justified epoch; can theoretically still reorg in adversarial conditions but is very unlikely. finalized = block past two epoch confirmations (Casper FFG finality); reverting requires >1/3 of stake to be slashed.
  2. Q: Why is tx.origin == owner broken even before AA? A: Any contract the owner interacts with can call your contract within the same tx, and tx.origin will be the owner. Phishing-friendly.
  3. Q: Give one concrete failure mode of keccak256(abi.encodePacked(string, string)). A: Collisions across (a, b) and (a', b') where a||b == a'||b'. A signature on one pair is reusable on the other.
  4. Q: What’s a second-preimage attack on a Merkle tree, and the standard mitigation? A: Attacker constructs an internal node that also functions as a leaf. Mitigation: prefix leaf hashes with one byte (e.g., 0x00) and internal hashes with another (0x01) — i.e., domain separation per OZ MerkleProof.
  5. Q: What’s the dominant bug class in ZK circuits? A: Under-constrained circuits — the circuit fails to enforce some constraint, allowing valid proofs of invalid statements (soundness failure).
  6. Q: An auditor reviews a bridge that emits an event on chain A and credits an account on chain B once the bridge contract sees the event “confirmed” 5 blocks later. What’s the audit question? A: Does “5 blocks” suffice for chain A’s actual finality model? On Ethereum, 5 blocks does not guarantee finality (need ~2 epochs). On a chain with weaker finality, even more blocks may not be enough.
  7. Q: A contract uses block.prevrandao as a random number for a lottery. Why is this insecure? A: The block proposer can choose to publish or withhold a block depending on whether the random outcome favors them — biasing the distribution.
  8. Q: What does EIP-712 give you that bare keccak256(abi.encode(...)) does not? A: A standardized typed-data structure with a domainSeparator that binds the signature to a specific chain id, contract address, and version — preventing cross-chain and cross-contract replay. Plus better wallet UX (human-readable signing prompts).
  9. Q: Why is address(0) a special address every signature-verification path must check for? A: ecrecover returns address(0) for invalid signatures. If your stored authorized address is uninitialized, it’s also address(0), and an invalid signature would “match”.
  10. Q: A protocol claims “fully decentralized” but the audit reveals a setOracle() function callable by a single EOA. Where in the report does this go and at what severity? A: Trust assumptions section + at least Medium severity (potentially High depending on impact of oracle swap). The discrepancy between marketing and reality is itself a finding because users base risk decisions on the claim.

11. Week 01 Deliverables

  • All Lab exercises 1–5 reproduced; tests passing.
  • Notes file: ~/web3-sec-lab/wk01/notes.md with your written answers to all quiz questions in your own words.
  • Personal “trust seam diagram” of a real Web3 app you use (e.g., Uniswap) — annotate each seam with what could go wrong.
  • One paragraph: “If I were auditing a bridge, what would I check about finality assumptions?“

12. Where this leads

Next week: Tuan-02-Ethereum-EVM-Deep-Dive. You’ll go from the layered model into the execution layer specifically — accounts, transactions, opcodes, gas, storage layout. Everything from this week (signatures, hashes, address derivation) reappears as concrete EVM mechanics.

The shift in mindset is: this week, “blockchain” was an abstract state machine; next week, it’s a specific computer with 256-bit words, 16-bit-addressed memory, and specific opcodes whose gas costs encode the security/performance trade-offs of the platform.


Last updated: 2026-05-16 See also: Roadmap · References · MOC-Web3-Security-Mastery