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:
- What does this layer guarantee? (e.g., “PoS finality after 2 epochs” or “ECDSA signature implies knowledge of
dfor public keyQ = dG”) - What does it not guarantee? (e.g., “PoS finality does not guarantee no reorg before finalization”)
- 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”)
- 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:
| Question | Where 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
keccak256on 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
| Source | Why |
|---|---|
| Ethereum.org Developer Docs — Intro & PoS | https://ethereum.org/en/developers/docs/intro-to-ethereum/ · https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/ — official, current |
| Ethereum Yellow Paper | https://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 blockn(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:
- Anything non-deterministic in the execution layer breaks consensus. That’s why
block.timestamp,block.prevrandao,block.numberare deterministic for a given block but adversarially controllable. There is no real “current time” or “real random” inside the EVM. - 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:
- 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).
- 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).
| Property | Bitcoin (PoW) | Ethereum (PoS post-Merge) | Cosmos (Tendermint) | Solana (PoH + TowerBFT) |
|---|---|---|---|---|
| Sybil resistance | Hash power | Stake (32 ETH/validator) | Bonded ATOM | Bonded SOL |
| Block production | Single miner per block | Single proposer per slot (12s) | Round-robin proposer | Single leader per slot (400ms) |
| Finality model | Probabilistic | Deterministic (Casper FFG) after 2 epochs (~12.8m) | Deterministic (1 block, instant) | Optimistic, with rollback |
| What “finality” means in practice | Wait 6+ confirmations heuristically | Wait until finalized tag (~12.8m) | Block height N is final at block N | Rollbacks 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:
| Application | Reorg sensitivity |
|---|---|
| ERC-20 transfer | Low — sender just resubmits |
| Exchange deposit credit | High — exchange can credit, then reorg undoes deposit, exchange loses money |
| Cross-chain bridge | Critical — bridge unlocks/mints on destination based on source-chain event; if source reorgs, bridge is upside down |
| Optimistic rollup challenge | Critical — fraud-proof window must outlast worst-case reorg |
| Liquidation | Medium — 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:
| Tag | Meaning | When safe to act on |
|---|---|---|
latest | most recent block proposer published | for low-stakes UX |
safe | head of last justified epoch | for medium-stakes flows |
finalized | block past 2 epoch confirmations | for 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:
| Property | Definition | Broken example consequence |
|---|---|---|
| Preimage resistance | Given h, hard to find any m such that H(m) = h | Password / commitment scheme breaks |
| Second-preimage resistance | Given m1, hard to find m2 ≠ m1 with H(m1) = H(m2) | Signature spoofing |
| Collision resistance | Hard to find any m1, m2 with H(m1) = H(m2) | Merkle proofs forgeable |
Hash functions you’ll see:
| Function | Output | Where used | Notes |
|---|---|---|---|
keccak256 | 256-bit | Ethereum (everything: addresses, slot derivation, signatures’ message hash, Merkle in many systems) | Distinct from SHA-3 — same Keccak family, different padding |
SHA-256 | 256-bit | Bitcoin, some Ethereum precompiles, generally outside EVM | |
Poseidon | configurable | ZK circuits | ”ZK-friendly” because arithmetic-circuit-cheap; do not use outside circuits unless you understand the constants |
ripemd160, blake2, MiMC | various | Special-purpose | Audit-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]wherenis the curve order. - Public key
Q = d · GwhereGis the curve generator. - Address =
keccak256(Q_x || Q_y)[12:]— the last 20 bytes of the public key’s hash.
Signing a message m:
- Compute
e = H(m)(typicallykeccak256). - Pick random nonce
k(or deterministic per RFC 6979). - Compute
R = k · G, taker = R.x mod n. - Compute
s = k⁻¹ · (e + r·d) mod n. - Signature =
(r, s, v)wherevis the recovery id.
Why this matters to auditors: signature properties and their abuse patterns.
| Property / pitfall | Audit implication |
|---|---|
k reuse | Two signatures with the same k reveal d. PlayStation 3 case (2010), some early Ethereum wallets affected. Use RFC 6979 deterministic k. |
s malleability | For 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 ambiguity | v 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 separation | Same signed payload can be replayed across contracts / chains. EIP-712 solves this with a domainSeparator of (name, version, chainId, verifyingContract). |
Zero address from ecrecover | Bad 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.senderis now reliably the smart account, not an EOA, which breaks naivemsg.sender == EOAreasoning (some contracts check viatx.origin == msg.senderto “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
| Scheme | What it is | Where in Web3 |
|---|---|---|
| Multisig (m-of-n) | n keys, m required to sign a tx | Safe (formerly Gnosis Safe) — most DAO treasuries |
| Threshold signature | n 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 place | Fireblocks, 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:
creveals nothing aboutm(hiding)ccannot later open to a differentm'(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 Property | What it gives | Failure mode |
|---|---|---|
| Completeness | A valid prover with a valid witness always convinces verifier | Hard to break; usually a correctness bug |
| Soundness | A malicious prover without a valid witness cannot convince verifier | Under-constrained circuits leak this; the famous “missing constraint” bug |
| Zero-knowledge | Proof reveals nothing beyond validity | Usually about randomness in proving; less common bug source |
| Succinctness | Proof size and verifier time are sub-linear in circuit size | Performance 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
| Assumption | Holds in 2026? | Threat horizon |
|---|---|---|
keccak256 collision resistance | Yes | Far |
| ECDSA secp256k1 hardness (discrete log) | Yes | Threatened by large-scale quantum computers (decades, possibly) |
| Pairing-based crypto (BLS12-381) | Yes | Similar quantum horizon |
| Trusted setup ceremonies (Groth16) | Trust assumption, not math; honest if ≥1 participant deletes toxic waste | Per-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
| EOA | Contract account | |
|---|---|---|
| Controlled by | Private key | Code |
| Has nonce | Yes | Yes (incremented per CREATE) |
| Can initiate tx | Yes | No (called by EOA or another contract) |
| Has code | No | Yes |
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.
CREATE2addresses 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 differentinit_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 seam | Failure mode | Real incident class |
|---|---|---|
| Frontend | Wrong code served (DNS hijack, CDN compromise, malicious npm dep) | BadgerDAO 2021, Galxe 2023, Curve frontend incidents |
| Wallet | Wallet shows different intent than what’s signed (esp. blind-signing hardware wallets) | Many phishing campaigns |
| RPC node | Lies about chain head, censors txs, leaks tx data | Malicious public RPC |
| Mempool | Reordering for MEV | Sandwich attacks (continuous, not “incidents”) |
| Block proposer | Censorship, MEV extraction | Inclusion-list / PBS discussion |
| Oracle | Reports stale or manipulated price | bZx 2020, Mango Markets 2022 |
| Indexer | Wrong UI display from wrong data | Soft 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:
| Type | Examples | Audit angle |
|---|---|---|
| Fully permissionless | Bitcoin, Ethereum L1, most DeFi protocols once renounced | Threat model includes anyone in the world |
| Permissioned validators | Many L2 sequencers (1-of-1 today), bridge committees | Threat model centers on validator set integrity |
| Permissioned contracts | Protocol-controlled upgrade keys, oracles with admin pause | Threat model includes the admin |
| Hybrid | Most real DeFi: permissionless usage, permissioned upgrade | Need 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:
- Hash an arbitrary string with
keccak256from the CLI. - Derive an Ethereum address from a private key.
- Sign a message and recover the signer.
- Reproduce the
abi.encodePackedcollision and explain why it’s a real exploit primitive. - 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 --versionCreate 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-lab7.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
# 0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1Task: 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
# 0x70997970C51812dc3A010C7d01b50e0d17dc79C8The 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
# trueTask: 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 -vvDiscussion: 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
castfor 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.encodePackedwith variable-length types in a hashed signature payload — flag for collision risk. - Uses
tx.originfor access control — fails under contract callers and AA. -
ecrecoverresult not compared againstaddress(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.timestampused as randomness — miner / proposer-influenceable. -
block.prevrandaoused as randomness without commit-reveal delay — proposer can bias. - Reads from
latestblock 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
| Decision | Option A | Option B | Auditor view |
|---|---|---|---|
| Finality wait | safe tag (fast UX) | finalized tag (safer) | For value-bearing cross-chain: always finalized. For UX-only: safe. |
| Custom Merkle vs OZ MerkleProof | Inline custom | Use OZ library | Default to OZ. Custom adds risk for no upside unless circuit-constrained. |
| EIP-712 vs simple hashed payload | EIP-712 | keccak256(abi.encode(...)) | Default to EIP-712. Wallet UX is better, replay protection is structural. |
| AA support in protocol | First-class (isValidSignature per EIP-1271) | EOA-only | First-class. ERC-4337 adoption is growing; EOA-only contracts are accumulating future debt. |
10. Quiz (self-check, ≥80% to advance)
- Q: What’s the difference between
safeandfinalizedRPC 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. - Q: Why is
tx.origin == ownerbroken even before AA? A: Any contract the owner interacts with can call your contract within the same tx, andtx.originwill be the owner. Phishing-friendly. - Q: Give one concrete failure mode of
keccak256(abi.encodePacked(string, string)). A: Collisions across(a, b)and(a', b')wherea||b == a'||b'. A signature on one pair is reusable on the other. - 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. - 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).
- 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.
- Q: A contract uses
block.prevrandaoas 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. - Q: What does EIP-712 give you that bare
keccak256(abi.encode(...))does not? A: A standardized typed-data structure with adomainSeparatorthat 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). - Q: Why is
address(0)a special address every signature-verification path must check for? A:ecrecoverreturnsaddress(0)for invalid signatures. If your stored authorized address is uninitialized, it’s alsoaddress(0), and an invalid signature would “match”. - 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.mdwith 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