The Ethereum Wallet & Signing Bible

Every EIP and ERC that matters for wallets, transaction signing, signature verification, and account abstraction — with history, diagrams, Solidity & TypeScript, and the 2026 ecosystem map.

EIP-155EIP-2718EIP-1559EIP-191EIP-712ERC-1271ERC-6492ERC-7739EIP-2612ERC-4337EIP-7702ERC-7579ERC-6900EIP-5792

§1
A Brief History
2008 → 2026: how we got from one key per account to programmable wallets
§2
Seeds, Keys & Accounts
BIP-39/32/44, HD derivation paths, EOAs vs contract accounts, ECDSA
§3
Transaction Anatomy
EIP-155, the typed envelope, 1559 fees, tx types 0x00–0x04
§4
Signing Messages
EIP-191 personal_sign & EIP-712 typed data, with full code
§5
Proving Ownership
ERC-1271, OpenZeppelin SignatureChecker, ERC-6492, ERC-7739
§6
Gasless UX
EIP-2612 permit, ERC-2771 meta-transactions & relayers, sponsorship models compared
§7
ERC-4337
UserOps, bundlers, EntryPoint, paymasters, factories
§8
EIP-7702
Type-0x04 transactions: your EOA becomes a smart wallet
§9
Modular Accounts
ERC-7579 vs ERC-6900 — the app store for wallets
§11
The Ecosystem
Safe, Kernel, Nexus, Coinbase Smart Wallet · Biconomy, ZeroDev, Alchemy, Pimlico
§12
End-to-End Flows
Login, gasless payment, and EOA-upgrade walkthroughs
§13
EVM Fundamentals
Storage slots, CALL vs DELEGATECALL, CREATE2, selectors
§14
Proxy & Upgrade Patterns
EIP-1967, Transparent, UUPS, Beacon, Diamond, storage-layout law
§15
Known Issues & Hacks
The DAO, Parity, Wormhole, Nomad, Ronin, Bybit — and the defect classes
§16
Sidechains & Rollups
Optimistic vs ZK rollups, blobs, sequencers, why AA lives on L2
§17
Zero-Knowledge Proofs
SNARKs vs STARKs, circuits, a worked circom example, real-world uses
§18
Consensus & Quantum
PoW/PoS/BFT security economics, slashing, MEV — and Shor vs your keys
§19
Beyond the EVM
Solana (ATAs, rent, ed25519), Tron (energy/bandwidth), Cosmos (app-chains) vs Ethereum
§20
Cheat Sheet
Every number, one memory hook each

1 · A Brief History: From One Key to Programmable Accounts

Every standard in this guide exists because of one design decision made in 2014 — and a decade spent escaping its consequences.

When Ethereum launched (Frontier, July 2015), it inherited Bitcoin's account security model: an account is a keypair, and a signature from the private key is absolute, unconditional authority. That was the simplest thing that could work. But Ethereum also introduced something Bitcoin didn't have — contract accounts, on-chain programs with their own storage and balance. From day one there were two kinds of citizens: EOAs (key-controlled, can start transactions, zero logic) and contracts (logic-controlled, infinitely programmable, but unable to act on their own).

The tension was visible almost immediately. 2016–2017: the first multisig wallets appeared (the Parity multisig — famously frozen in 2017 by a library self-destruct, losing 513k ETH — and Gnosis Safe, which survived and became the institutional standard). These proved people wanted accounts with rules: multiple approvers, daily limits, recovery. 2016: EIP-155 added chainId to signatures after the ETH/ETC split made cross-chain replay a live exploit. 2018: ERC-1271 answered "how does a contract sign things?"; EIP-1014 (CREATE2) made counterfactual wallet addresses possible — an address you can fund before the wallet exists. 2017–2020: EIP-712 gave signatures human-readable structure, and EIP-2612 used it to make token approvals gasless.

Meanwhile the "make accounts programmable at the protocol level" dream kept stalling: EIP-86, EIP-2938 (native account abstraction) both died — consensus changes are slow and risky. 2021: Vitalik Buterin and the eth-infinitism team (Yoav Weiss, Dror Tirosh et al.) published ERC-4337: account abstraction with zero protocol changes, using an alternative mempool and a singleton EntryPoint contract. The EntryPoint went live on mainnet in March 2023, and an industry grew around it — bundlers, paymasters, smart account SDKs (Biconomy, ZeroDev, Alchemy, Pimlico, Safe modules).

But 4337 accounts have new addresses — useless for the hundreds of millions of existing EOAs. 2024: EIP-3074 (AUTH/AUTHCALL opcodes) was approved for inclusion, then withdrawn after a community storm over its security model. Vitalik proposed EIP-7702 as the replacement in about a day. May 7, 2025: the Pectra upgrade shipped 7702 — EOAs can now carry code, and the same account you made in 2017 can be a smart wallet today. 2026: the mainstream architecture is "7702 + 4337": your old address, delegated to a modular (ERC-7579) implementation, transacting through bundlers with sponsored gas — while EIP-7701 (native AA, the "endgame") sits on the long-term roadmap.

2015 Frontier launch EOAs + contracts 2016–17 EIP-155 · multisigs Gnosis Safe · Parity hack 2018–20 ERC-1271 · CREATE2 EIP-712 · EIP-2612 2021 EIP-1559 fees ERC-4337 proposed 2023 EntryPoint live bundler ecosystem 2024 EIP-3074 withdrawn EIP-7702 proposed May 2025 → Pectra ships 7702 smart EOAs era
Fig. 1 — A decade of escaping the "account = keypair" decision.
Analogy
An EOA is a personal debit card: one PIN, absolute power, no recovery — lose the PIN and the money is gone; leak it and the money is also gone. A smart contract wallet is a corporate bank account: 2-of-3 director signatures, daily limits, an assistant who can pay small invoices (session keys), and the ability to replace a lost keycard without changing the account number. The entire history above is the industry building the corporate account — and then (7702) figuring out how to retrofit the corporate features onto everyone's existing debit cards.

2 · From Seed Phrase to Keys: BIP-39, BIP-32, BIP-44

Before any EIP matters, a wallet has to make keys. Every "12 words on paper" wallet you've ever used is three Bitcoin standards stacked together — Ethereum adopted them wholesale.

2.1 BIP-39: entropy → mnemonic → seed

A mnemonic is not the key — it's a human-readable encoding of randomness:

1. entropy  = 128 or 256 random bits                    (from a CSPRNG)
2. checksum = first (entropy_bits / 32) bits of SHA-256(entropy)   → 4 or 8 bits
3. concat   = entropy ‖ checksum   → 132 or 264 bits
4. split into 11-bit chunks → 12 or 24 indexes into a fixed 2048-word list
   (2048 = 2¹¹ — each word carries exactly 11 bits)

5. seed = PBKDF2-HMAC-SHA512(
            password = mnemonic,
            salt     = "mnemonic" + optional_passphrase,
            iterations = 2048)                → 512-bit seed
Analogy
The mnemonic is the DNA sample; PBKDF2 is the lab process that grows it into a master cell (the 512-bit seed). Same DNA + same process = same cell, every time, on any device — which is why 12 words restore an entire wallet anywhere.

2.2 Worked example: 16 bytes → 12 words → an address every value below is real

The algorithm above is easier to trust once you've watched every byte move. Here is one complete run, start to finish, with nothing elided.

Use these words for reading, never for funds
This is BIP-39 official test vector #2 — its entropy is the byte 0x7f repeated 16 times, and it is published in the spec, in every BIP-39 test suite, and now here. Thousands of machines know these keys. It is a teaching fixture, not a wallet. Real entropy comes from a CSPRNG you never see, and a real mnemonic is one no one has ever printed.

Step 1 — entropy. 128 bits from a CSPRNG. Ours is deliberately the least random value imaginable, so you can follow the bits by eye:

entropy (hex)  = 7f7f7f7f 7f7f7f7f 7f7f7f7f 7f7f7f7f          (16 bytes = 128 bits)
entropy (bits) = 01111111 01111111 01111111 01111111 ... (0x7f = 0111 1111, ×16)

Step 2 — checksum. Hash the entropy and keep the first 128/32 = 4 bits:

SHA-256(entropy) = 87dcde7fa6df23e15fa7ba9b2a1f31408eac832f4e615ea815ae92024e3d818b
                   ^^
                   first byte 0x87 = 1000 0111
                                     ────
                                     first 4 bits = 1000  ← the checksum

Step 3 — concatenate and slice into 11-bit words. 128 + 4 = 132 bits = exactly 12 × 11. Each chunk is an index into the 2048-word list:

128 entropy bits ‖ 1000  →  132 bits  →  twelve 11-bit indexes

  #   11 bits        index   word
  ─────────────────────────────────────
   1  01111111011     1019   legal
   2  11111011111     2015   winner
   3  11011111110     1790   thank
   4  11111110111     2039   year
   5  11110111111     1983   wave
   6  10111111101     1533   sausage
   7  11111101111     2031   worth
   8  11101111111     1919   useful
   9  01111111011     1019   legal      ← repeats: the entropy repeats every 8 bytes
  10  11111011111     2015   winner
  11  11011111110     1790   thank
  12  11111111000     2040   yellow     ← last word carries the checksum
                └── the 4 checksum bits live here, which is why word 12
                    differs from word 4 ("year") despite identical entropy bits

Word 12 is where the checksum earns its keep: change any earlier word and the final word almost certainly stops agreeing with the hash. That is the "1 in 16" check from §2.1 made concrete.

Step 4 — stretch the words into a seed. This is the step people misread. The words are not hashed into the seed directly; they are run through PBKDF2 2048 times, and the passphrase goes in the salt, not the password:

seed = PBKDF2-HMAC-SHA512(
         password   = "legal winner thank year wave sausage worth useful legal winner thank yellow",
         salt       = "mnemonic" + passphrase,     ← passphrase lands HERE
         iterations = 2048,
         dkLen      = 64 bytes)

The passphrase: same words, a different wallet

Run the identical 12 words twice, once bare and once with the passphrase trezor, and the 512-bit seeds share nothing:

salt = "mnemonic"            (no passphrase)
seed = 878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe
       5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096

salt = "mnemonictrezor"      (passphrase = "trezor")
seed = 9fad48776d59511b5d532911300fd684daf464c06865041dc045a0f3dfe9e284
       1219290013bfc454f2bb9aade52751f5e9f0cd67c0dad602df3ca4b5001b5304
       ▲ not one byte in common — this is a whole separate wallet

↔ scroll the diagram sideways

128 bits entropy (CSPRNG) 7f7f7f7f 7f7f7f7f 7f7f7f7f 7f7f7f7f + 4 checksum bits from SHA-256 12-word mnemonic legal winner thank year wave sausage worth useful … PBKDF2-HMAC-SHA512 · 2048 iterations password = mnemonic · salt = "mnemonic" + passphrase the ONLY thing that differs below is the salt salt = "mnemonic" no passphrase — the default wallet 512-bit seed 878386efb78845b3…bddfb096 HMAC-SHA512("Bitcoin seed", seed) → master key 7e56ecf5943d79e1…78b4564e m/44'/60'/0'/0/0 0x58A57ed9…55bB1b25 salt = "mnemonic" + "trezor" the "25th word" — a hidden wallet 512-bit seed 9fad48776d59511b…001b5304 HMAC-SHA512("Bitcoin seed", seed) → master key 578e0c6b44d56f83…bf432ab5 m/44'/60'/0'/0/0 0x34FF7054…651eFFD Same 12 words. Different salt. Two unrelated wallets — and no checksum protects the passphrase. Forget it, or typo it, and the funds are simply gone. A typo yields a valid, empty wallet — not an error.
Fig. 2b — The passphrase enters as PBKDF2 salt, so it re-rolls the seed and every key beneath it. Both branches are real output from the vector above.

Step 5 — the seed becomes the tree. From here §2.3 takes over: the 512-bit seed is HMAC'd into a master key + chain code, and the BIP-44 path walks down to an address:

no passphrase                          passphrase = "trezor"
────────────────────────────           ────────────────────────────
master key 7e56ecf5943d79e1…           master key 578e0c6b44d56f83…
chain code 598b4595ea728027…           chain code a8806347c85e7acb…

m/44'/60'/0'/0/0                       m/44'/60'/0'/0/0
  0x58A57ed9d8d624cBD12e2C467D…          0x34FF70540588fa9B3f7df225e…
m/44'/60'/0'/0/1                       m/44'/60'/0'/0/1
  0x0D3eB21b6b21833A4939Cfff48…          0xBDCEeacb069fD88B7f39F5Ae30…
Why the passphrase is not "just a password"
A password guards one account and can be reset. This passphrase selects which of 2512 wallets your words unlock — every possible passphrase produces a valid, empty-looking wallet, and there is no server to tell you which one was yours. That is the plausible deniability: a thief with your 12 words gets a real wallet, just not your real wallet. It is also the footgun, and it is the same property viewed from either side.

Want to check the arithmetic yourself? Every number above is reproducible in a few lines:

Reproduce it
import { mnemonicToSeedSync } from '@scure/bip39'
import { HDKey } from '@scure/bip32'

const M = 'legal winner thank year wave sausage worth useful legal winner thank yellow'

for (const pass of ['', 'trezor']) {
  const seed = mnemonicToSeedSync(M, pass)            // passphrase → PBKDF2 salt
  const addr = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0")
  console.log(pass || '(none)', Buffer.from(seed).toString('hex').slice(0, 16))
  // → ''       878386efb78845b3…  → 0x58A57ed9…
  // → 'trezor' 9fad48776d59511b…  → 0x34FF7054…
}

2.3 BIP-32: the HD (Hierarchical Deterministic) tree

The 512-bit seed becomes the root of an infinite key tree:

I = HMAC-SHA512(key = "Bitcoin seed", data = seed)      // yes, even on Ethereum
master_private_key = I[0..32]        // left half
master_chain_code  = I[32..64]       // right half — extra entropy for derivation

child derivation (index i):
  normal   (i < 2³¹):  child = f(parent_pubkey,  chain_code, i)   // xpub can derive
  hardened (i ≥ 2³¹):  child = f(parent_PRIVkey, chain_code, i)   // notated i′ or i'
Analogy
A family tree grown from one ancestor. Normal derivation is like a public genealogy registry — anyone with the registry (xpub) can list all descendants' faces (addresses) but not their house keys. Hardened derivation is a branch that opted out of the registry entirely.

2.4 BIP-44: the derivation path — one seed, every account

m / purpose' / coin_type' / account' / change / address_index
m /   44'    /    60'     /    0'    /   0    /      0        ← your first ETH account
m /   44'    /    60'     /    0'    /   0    /      1        ← MetaMask "Account 2"
m /   44'    /    0'      /    0'    /   0    /      0        ← same seed, Bitcoin
                  └── 60 = Ethereum's registered coin type (SLIP-44)

One seed, many chains. The coin_type' level (registered in SLIP-44) is how a single mnemonic drives wallets on completely different networks:

ChainTypical pathCurveAddress derivation
Bitcoinm/44'/0'/0'/0/0 (84' for segwit)secp256k1hash160(pubkey) → base58/bech32
Ethereum / all EVMm/44'/60'/0'/0/isecp256k1last20(keccak256(pubkey)) → 0x…
Tronm/44'/195'/0'/0/isecp256k1same keccak trick, then 0x41-prefix + base58check → T…
Cosmos ecosystemm/44'/118'/0'/0/i (shared by many app-chains)secp256k1bech32(ripemd160(sha256(pubkey))) → cosmos1…/osmo1…
Solanam/44'/501'/0'/0'all hardeneded25519address = base58(pubkey) itself, no hashing

Note Solana's oddity: ed25519 doesn't support BIP-32's normal (non-hardened) derivation math, so it uses SLIP-0010 with every level hardened — no xpub-style watch-only trees. Full non-EVM comparison in §19. BSC/Polygon/Arbitrum/etc. all reuse 60' — same key, same 0x address on every EVM chain (which is exactly why EIP-155 chainId matters).

BIP-39 mnemonic (12/24 words) "orbit velvet lunar … pay" PBKDF2 +passphrase 512-bit seed device-independent HMAC-SHA512 BIP-32 master key + chain code m (xprv / xpub) BIP-44 path: m / 44' / 60' / 0' / 0 / i i = 0 → privkey₀ pubkey₀ = privkey₀ × G addr: 0xA1… (Account 1) i = 1 → privkey₁ pubkey₁ = privkey₁ × G addr: 0xB2… (Account 2) … ∞ each leaf = one EOA · siblings are cryptographically unrelated · the 12 words regenerate the whole tree
Fig. 2 — BIP-39 → BIP-32 → BIP-44: one backup of 12 words, infinite deterministic EOAs.
TypeScript (viem) — the whole pipeline in code
import { english, generateMnemonic, mnemonicToAccount } from 'viem/accounts'

const mnemonic = generateMnemonic(english)          // BIP-39: entropy → 12 words

const account0 = mnemonicToAccount(mnemonic)        // default m/44'/60'/0'/0/0
const account1 = mnemonicToAccount(mnemonic, { addressIndex: 1 })   // …/0/1
const ledger3  = mnemonicToAccount(mnemonic, { accountIndex: 3 })   // m/44'/60'/3'/0/0

console.log(account0.address)   // 0x… — last20(keccak256(pubkey))
// account0.signTypedData(…), signMessage(…), signTransaction(…) all available
Security corollaries
• The 12 words ARE every key, forever — including addresses you haven't generated yet. There is no "change just one password."
• A leaked xpub leaks your entire address graph (privacy, not funds) — and xpub + any one non-hardened child private key = the parent xprv. This is why purpose/coin/account are hardened.
• None of this hierarchy exists for smart wallets (§7–8): their "key" is whatever validation logic you install — which is exactly the upgrade this guide builds toward.

2.5 Two kinds of citizens

EOA Externally Owned Account 🔑 Controlled by a private key 📦 State: nonce, balance 🚀 Can initiate transactions 🧠 Logic: none — hardcoded ECDSA ♻️ Recovery / rotation: impossible …until EIP-7702 (§8) Contract Account Smart contract wallet, DEX, token… ⚙️ Controlled by its own code 📦 State: nonce, balance, code, storage 🚀 Can initiate transactions: no* 🧠 Logic: anything — multisig, limits… ♻️ Keys rotatable, recoverable * someone must call it — the 4337 problem (§7) vs
Fig. 3 — The two account types. Everything in this guide bridges the gap between them.

2.6 ECDSA in five lines

An EOA's address is derived from its secp256k1 public key:

privkey (32 bytes, random)
  → pubkey = privkey × G            (elliptic curve point, 64 bytes)
  → address = last20( keccak256(pubkey) )

A signature is 65 bytes — r (32) ‖ s (32) ‖ v (1) — and Ethereum's superpower is public key recovery: given a hash and (v, r, s), the ecrecover precompile (address 0x01) returns the address that signed. You never store or transmit public keys.

Analogy
ECDSA recovery is a wax seal. You don't need the king's ring to verify a letter — the imprint itself proves which ring pressed it. ecrecover reads the imprint and tells you the ring.

2.7 Why EOAs hurt — the problem statement for the rest of this guide

Smart wallets fix all four, but pre-2023 they had a bootstrap problem: contracts can't start transactions, so a centralized "relayer" EOA had to poke them. ERC-4337 (§7) standardizes and decentralizes the relayer layer; EIP-7702 (§8) removes the migration cost.

3 · Transaction Anatomy & the Typed Envelope

3.1 Legacy transactions and EIP-155 replay protection · 2016

A legacy (type-0) transaction is the RLP encoding of nine fields:

rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])

The bug: originally the chain wasn't part of what you signed. After the 2016 ETH/ETC split, any transaction broadcast on one chain could be replayed verbatim on the other — same key, same nonce, same validity. EIP-155 folds chainId into the signed preimage and encodes it into v:

signed:  keccak256( rlp([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]) )
v      = chainId × 2 + 35  (or 36)     // recover chainId from v
Analogy
Writing the country name on a bank cheque so it can't be cashed abroad. Same signature, but now the paper says where it's valid.

3.2 EIP-2718: the typed transaction envelope 2020

Rather than torturing the legacy format forever, EIP-2718 declares: a transaction is simply

TransactionType (1 byte) ‖ TransactionPayload (opaque, type-defined)

One leading byte selects the format, and each new type defines its own payload and its own signature scheme. This is the extension point that made everything after 2020 possible:

0x00 legacy EIP-155 0x01 EIP-2930 access lists 0x02 EIP-1559 fees 0x03 EIP-4844 blobs 0x04 EIP-7702 set-code future… EIP-2718 envelope: first byte picks the format 2015/16 Berlin 2021 London 2021 Dencun 2024 Pectra 2025 — §8
Fig. 4 — The typed-envelope family. EIP-7702's type 0x04 is just the newest sibling.

3.3 EIP-1559: the fee market London · 2021

Type-2 transactions replace one gasPrice with two caps, against a protocol-computed, burned baseFee that rises when blocks are full and falls when they're empty:

effectiveGasPrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
validator tip     = effectiveGasPrice − baseFee        // baseFee is burned 🔥
Analogy
Surge pricing with a burn. The surge portion goes to no one (burned), you only tip the driver on top, and you pre-authorize a maximum fare (maxFeePerGas) so you can never be overcharged while you sleep.
Why wallet devs care
ERC-4337 UserOperations (§7) copy this fee model field-for-field (maxFeePerGas/maxPriorityFeePerGas inside the UserOp), and bundlers arbitrage the difference. Understanding 1559 = understanding UserOp gas math.

3.4 Gas mechanics in practice — units, limits, and who gets what

Two numbers that beginners conflate, kept strictly apart:

gas      = WORK — how many units of computation/storage the tx consumes
gasPrice = MONEY PER UNIT — what you pay for each unit, in wei

fee paid = gasUsed × effectiveGasPrice

units:  1 ether = 10⁹ gwei = 10¹⁸ wei      (gas prices are quoted in gwei)
FieldSet byMeaningGotchas
gasLimit (per tx)you / your walletmax work you authorize. Unused gas is refunded; if execution needs more, the tx reverts out-of-gas but you still pay for gas consumed up to the failurea plain ETH transfer costs exactly 21,000 (the "intrinsic" cost); calldata bytes, storage writes (SSTORE ≈ 20k for a new slot) dominate real costs
block gas limitprotocol/validatorstotal work per block (~36M gas, drifting upward via validator votes)this is the ceiling that makes blockspace scarce — the whole reason fees exist and L2s exist (§16)
gasPrice (legacy, type-0)youone flat number: bid per unitoverpays whenever the network calms down mid-flight — the problem 1559 fixed
baseFeeprotocol, per blockalgorithmic floor: ±12.5% max per block based on whether the previous block was over/under 50% fullburned 🔥 — the validator never sees it, so there's no incentive to stuff blocks and inflate it
maxPriorityFeePerGas ("tip")youthe only part the validator keeps — your bid for inclusion priority1–2 gwei is usually plenty; spikes during NFT mints/liquidation storms
maxFeePerGasyouhard cap: baseFee + tip will never exceed thiswallets typically set ≈ 2× current baseFee + tip so the tx survives base-fee climbs while pending
Analogy
gasLimit is the fuel tank size you authorize for a journey; gasUsed is fuel actually burned (the rest returns to your wallet); baseFee is the government-set toll that gets shredded at the booth; the tip is the only cash the toll operator pockets; maxFeePerGas is the note to your driver: "if tolls rise above this, park and wait."
Native currency footnote
ETH is not an ERC-20 — it has no contract, no transfer(), no allowance; it moves via the protocol's value field and is readable as address.balance. That asymmetry is why WETH (wrapped ETH) exists, why every DEX pairs against WETH internally, and why paymasters that accept "any token" still need a swap route back to real ETH to pay the validator. On other EVM chains the native asset differs (BNB, MATIC/POL, TRX-adjacent models) — same mechanics, different burn/tip policies.

4 · Signing Messages: EIP-191 & EIP-712

Wallets sign two different things: transactions (state changes, §3) and messages (proofs and intents — logins, orders, permits). If the two byte-formats could ever collide, a "harmless login" could secretly be a valid transaction. Both standards exist to make that collision impossible.

4.1 EIP-191: personal_sign 2016

digest = keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)

Weakness: the user sees a text blob or hex. Fine for "Sign in to OpenSea", terrible for "sell 3 NFTs for X" — no structure, nothing a wallet can render or a contract can decompose. Hence 712.

4.2 EIP-712: typed structured data the important one · 2017

EIP-712 lets the wallet show the user exactly what they're signing, field by field — and lets a contract rebuild the identical digest on-chain to verify it.

digest = keccak256( 0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message) ) domainSeparator — WHERE is this valid? name: "OrbitXPay" version: "1" chainId: 1 verifyingContract: 0xOrders… kills replay across apps, chains, contracts hashStruct — WHAT is being agreed? typeHash: keccak256("Order(address maker,…)") maker: 0xSedhu… amount: 500e6 nonce: 7 deadline: 1789millis… wallet renders each field to the human 32-byte digest → sign with ECDSA …or hand to isValidSignature for smart wallets (§5)
Fig. 5 — The two halves of every EIP-712 digest: the domain answers "where", the struct answers "what".
Analogy
EIP-191 is signing a napkin; EIP-712 is signing a standardized printed form with labeled boxes. The notary (wallet) reads every box aloud to you before you sign; the court (contract) can reprint the identical form later and check the seal matches.
Solidity — verify an EIP-712 order (OpenZeppelin)
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA}  from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract Orders is EIP712 {
    bytes32 private constant ORDER_TYPEHASH =
        keccak256("Order(address maker,uint256 amount,uint256 nonce,uint256 deadline)");

    mapping(address => mapping(uint256 => bool)) public usedNonces;

    constructor() EIP712("OrbitXPay", "1") {}   // builds the domain separator

    function fill(Order calldata o, bytes calldata sig) external {
        require(block.timestamp <= o.deadline, "expired");
        require(!usedNonces[o.maker][o.nonce], "replayed");

        bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
            ORDER_TYPEHASH, o.maker, o.amount, o.nonce, o.deadline)));

        require(ECDSA.recover(digest, sig) == o.maker, "bad sig");
        usedNonces[o.maker][o.nonce] = true;
        // …execute the order
    }
}
TypeScript (viem) — produce the same signature client-side
const signature = await walletClient.signTypedData({
  domain: { name: 'OrbitXPay', version: '1', chainId: 1, verifyingContract: ordersAddr },
  types: {
    Order: [
      { name: 'maker',    type: 'address' },
      { name: 'amount',   type: 'uint256' },
      { name: 'nonce',    type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  primaryType: 'Order',
  message: { maker, amount: 500_000_000n, nonce: 7n, deadline },
})

Where you've already met 712: EIP-2612 permit, Permit2, Safe's SafeTx, Uniswap orders, OpenSea Seaport, CoW Swap, Sign-In-With-Ethereum variants, and — since EntryPoint v0.8 — the ERC-4337 userOpHash itself.

4.3 Signature pitfalls checklist interview-grade

AttackDefense
Replay the same signature twiceInclude a nonce; mark it consumed on-chain
Replay years laterInclude a deadline; check block.timestamp <= deadline
Replay on another chain / another contract / another dAppEIP-712 domain separator (chainId + verifyingContract). Never verify raw unhashed data
Malleability: for any sig (r,s,v), (r, n−s, v′) is also valid ECDSAReject high-s values. OZ ECDSA.recover does this; raw ecrecover does NOT. Never use a signature as a unique ID or nonce
ecrecover returns address(0) on garbageCheck recovered ≠ 0 AND == expected signer — or just use OZ, which reverts
Signing a hash the wallet can't render ("blind signing")Prefer 712 over 191 for anything with value; wallets increasingly warn on raw hash signing

5 · Proving Wallet Ownership: ERC-1271, SignatureChecker, ERC-6492

This is the standard you remembered as "EIP-1270" — it's ERC-1271, and the OpenZeppelin helper is SignatureChecker. It answers: how does a wallet with no private key "sign" anything?

5.1 The problem

ecrecover answers "which private key made this signature?" — but a smart contract wallet has no private key. A Safe with 3 owners: whose handwriting is "the Safe's"? If your dApp verifies logins or orders with ecrecover alone, every smart-wallet user is locked out.

5.2 ERC-1271: flip the question isValidSignature · 2018

Instead of recovering a signer, ask the account itself: "is this signature valid, by your own rules?"

The entire standard
interface IERC1271 {
    /// @return magicValue 0x1626ba7e when valid
    function isValidSignature(bytes32 hash, bytes calldata signature)
        external view returns (bytes4 magicValue);
}
Analogy
Verifying a person's signature = comparing handwriting (ecrecover). Verifying a company's approval: you can't compare handwriting — you call the company's front desk and ask "do you honor this document?", and their internal bylaws (two directors? CFO above $10k?) produce the yes/no. ERC-1271 is the front desk's phone number, standardized.
Minimal single-owner implementation
contract MiniWallet is IERC1271 {
    using ECDSA for bytes32;
    address public owner;
    bytes4 constant MAGIC = 0x1626ba7e;

    function isValidSignature(bytes32 hash, bytes calldata sig)
        external view returns (bytes4)
    {
        return hash.recover(sig) == owner ? MAGIC : bytes4(0xffffffff);
    }
}

5.3 OpenZeppelin SignatureChecker — the one-liner your dApp should use

Verifies EOAs and smart wallets with one call
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

bool ok = SignatureChecker.isValidSignatureNow(signer, digest, signature);
isValidSignatureNow (signer, hash, sig) does signer have code? no → EOA path ECDSA.recover(hash, sig) recovered == signer ? yes → 1271 path staticcall signer.isValidSignature returned 0x1626ba7e ? true / false "Now" = validity is as-of-this-moment: owners can rotate, thresholds can change
Fig. 6 — SignatureChecker's decision tree: one call handles both account types.
Off-chain (viem) — handles EOA + 1271 + 6492 automatically
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const client = createPublicClient({ chain: mainnet, transport: http() })

const valid = await client.verifyTypedData({
  address: signer,          // EOA, deployed smart wallet, or undeployed smart wallet
  domain, types, primaryType, message,
  signature,
})   // ✅ ecrecover → ERC-1271 → ERC-6492, all attempted for you

5.4 ERC-6492: verifying wallets that don't exist yet counterfactual

Modern smart wallets are counterfactual: CREATE2 fixes the address before any code is deployed; deployment happens lazily with the first transaction. So a brand-new user signs "Sign in" → your backend calls isValidSignaturethere's no code there yet → verification fails, even though the wallet is real.

ERC-6492 wraps the signature with its own deployment recipe:

sig' = abi.encode(factory, factoryCalldata, originalSig) ‖ magicSuffix
magicSuffix = 0x6492649264926492649264926492649264926492649264926492649264926492

A 6492-aware verifier that spots the suffix: simulate the factory deployment inside an eth_call, then run the 1271 check against the now-materialized code — nothing is actually deployed. viem/ethers and the universal validator contract handle this transparently.

Analogy
A cheque from a company that's incorporated but hasn't opened its office yet. 6492 staples the incorporation papers to the cheque so the bank can verify: "the moment this office exists, this cheque is good."

5.5 ERC-7739: defensive rehashing current best practice

A naive 1271 wallet that just runs ECDSA.recover(hash, sig) == owner creates a replay hole: the owner's one signature validates against every account that trusts that key (same EOA owning several smart accounts), and an identical 712 digest can mean different things to different apps. ERC-7739 nests the app's typed data inside a TypedDataSign envelope bound to the account's own domain, so each signature is valid for exactly one account. Solady and OpenZeppelin 5.x ship implementations; Kernel, Coinbase Smart Wallet, and Alchemy accounts adopted it. Know it exists — and use library code, not hand-rolled 1271.

6 · Gasless UX: permit, Gas Relayers & Gas Sponsorship

The classic UX wart: to let a contract spend your USDC you must first send approve(spender, amount) — a full gas-costing transaction — before the action you actually wanted.

6.1 EIP-2612 — approval by signature

Added to the ERC-20 surface
function permit(address owner, address spender, uint256 value,
                uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);

The owner signs an EIP-712 Permit struct off-chain (free); anyone — typically the dApp contract, inside the same transaction as the swap/deposit — submits it. The token verifies the 712 digest via ecrecover and sets the allowance. Approve + action collapse into one transaction, signed once, gas payable by someone else.

Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
Analogy
Instead of driving to the bank to register a standing instruction (approve), you sign a letter of authorization at home and hand it to the merchant, who presents it to the bank together with the payment. One trip — and it's their trip.
Caveats
• Only tokens that implement it (USDC ✓, mainnet USDT ✗) — hence Permit2 (Uniswap): approve one singleton once, and it manages signature-based sub-approvals for any ERC-20, with expirations.
• DAI's permit is nonstandard (allowed/expiry instead of value/deadline).
• EIP-2612 verifies with ecrecover only → smart accounts can't use it (their "signature" is 1271). Permit2 supports 1271 — one more reason it won.

6.2 Gas relayers: meta-transactions & ERC-2771 the pre-4337 era

permit only moves the gas cost of one function on one token. The general problem — "user signs, someone else pays, for any call" — was solved before 4337 with meta-transactions: the user signs an EIP-712 request off-chain, a relayer EOA wraps it in a real transaction and pays the gas.

The catch: inside the target contract, msg.sender is now the relayer, not the user. ERC-2771 standardizes the fix — a trusted forwarder verifies the user's signature + nonce, then calls the target with the real sender's address appended to the last 20 bytes of calldata:

ERC-2771 — both sides of the trick
// Forwarder (OpenZeppelin ERC2771Forwarder): verifies EIP-712 ForwardRequest
// (from, to, value, gas, nonce, deadline, data), then:
//   target.call{...}(abi.encodePacked(req.data, req.from))   // ← sender appended

// Recipient inherits ERC2771Context instead of using msg.sender directly:
abstract contract ERC2771Context {
    function _msgSender() internal view returns (address sender) {
        if (msg.sender == TRUSTED_FORWARDER && msg.data.length >= 20) {
            assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) }
        } else {
            sender = msg.sender;
        }
    }
}
// Rule: a 2771-aware contract must use _msgSender() EVERYWHERE, never msg.sender
Analogy
A meta-transaction is a reply-paid envelope: you write and sign the letter (EIP-712 request), the company's mailroom (relayer) pays the postage, and the forwarder stamps "actually from: you" on the inside flap so the recipient files it under your name, not the mailroom's.
Known issue — the 2771 + multicall bug (Dec 2023)
If a 2771-aware contract also exposes multicall(bytes[] calls), an attacker can go through the trusted forwarder with a crafted inner call whose last 20 bytes are a forged address — the inner delegatecall slices calldata and trusts the appended bytes it shouldn't. This hit dozens of protocols built on the affected library (the Thirdweb disclosure) and forced mass migrations/pauses. Lesson: address-in-calldata is a fragile trust channel — audit every calldata-slicing path (multicall, batch, fallback) in any 2771 contract.

6.3 Gas sponsorship — every way to pay someone else's gas, compared

MechanismWho paysWorks forTrust modelStatus 2026
EIP-2612 permit / Permit2 (§6.1)whoever submits the txtoken approvals onlytoken contract verifies sigstandard for approvals
ERC-2771 meta-tx + relayer (§6.2)relayer (reimbursed off-chain / not at all)contracts that opted in (_msgSender())⚠ trusted forwarder can impersonate anyone for that contractlegacy but alive; niche gasless functions
GSNGSN paymaster contract2771-style recipientsRelayHub escrow + relayer networklargely superseded by 4337
ERC-4337 paymaster (§7.4)paymaster (verifying / ERC-20 mode)any action of a smart accountstaked contract; validation-rule sandbox; ERC-7677 service APIthe mainstream answer
EIP-7702 sponsored upgrade (§8)whoever submits the type-0x04 txthe delegation tx itself — the authorization tuple is signed by the EOA, but anyone can broadcast itauthorization signature binds chain+noncehow wallets upgrade users who own zero ETH
Protocol-native (§19)any co-signer (Solana fee payer) / granter (Cosmos feegrant)everything on those chainsbuilt into the tx formatthe "EVM needed 4337 for this" comparison point
The through-line
Every row is the same idea maturing: separate the intent-signer from the fee-payer. permit does it for one function, 2771 for one contract, GSN for a network, 4337 for the whole account model — and Solana/Cosmos just shipped it in the protocol. When someone says "gasless", your first question should be: which row?

7 · ERC-4337: Account Abstraction Without a Fork 2021 → live Mar 2023

"Account abstraction" = decouple who pays, who validates, and what logic approves from the protocol's hardcoded ECDSA+nonce rules. After native attempts (EIP-86, EIP-2938) died, ERC-4337 rebuilt the entire transaction pipeline in contract-land + off-chain infrastructure — zero consensus changes.

7.1 The cast of characters

The restaurant analogy
A UserOperation is an order slip. The alt-mempool is the order rail in the kitchen. A bundler is a waiter collecting many tables' slips and firing them to the kitchen at once. The EntryPoint is the kitchen pass — every single plate crosses it and gets quality-checked. Your smart account is a membership card with its own rules. A paymaster is the company card behind the bar tab. The factory pre-prints your membership card number before you've ever visited.
👤 User signs userOpHash UserOperation not a transaction! alt-mempool P2P, separate rail 🧺 Bundler simulates, batches, pays gas handleOps(ops[], beneficiary) — ONE real tx EntryPoint (singleton, one address per version) Loop 1 — validation: account.validateUserOp(op, hash, missingFunds) paymaster.validatePaymasterUserOp(…) Loop 2 — execution: account.execute(callData) paymaster.postOp(…) Smart Account validateUserOp = YOUR rules passkey · multisig · session key Paymaster sponsors gas, or takes USDC instead of ETH Factory CREATE2 deploy on first op (counterfactual address)
Fig. 7 — The ERC-4337 pipeline: a parallel transaction lane built entirely from contracts + off-chain infra.

7.2 The UserOperation struct EntryPoint v0.7/v0.8 packed form

struct PackedUserOperation {
    address sender;              // the smart account
    uint256 nonce;               // 2D: 192-bit key ‖ 64-bit sequence (parallel nonces!)
    bytes   initCode;            // factory address + calldata — only on first-ever op
    bytes   callData;            // what the account should execute
    bytes32 accountGasLimits;    // packed: verificationGasLimit ‖ callGasLimit
    uint256 preVerificationGas;  // bundler overhead compensation
    bytes32 gasFees;             // packed: maxPriorityFeePerGas ‖ maxFeePerGas (1559-style)
    bytes   paymasterAndData;    // paymaster + its gas limits + its data
    bytes   signature;           // ⭐ OPAQUE — whatever your validation logic understands
}
The key mental shift
signature is opaque bytes. The protocol doesn't care if it's one ECDSA sig, three concatenated for a multisig, a P-256 passkey/WebAuthn assertion, or a BLS aggregate. Only your account contract's validateUserOp interprets it. That is the abstraction in "account abstraction".
The account-side interface
interface IAccount {
    function validateUserOp(
        PackedUserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds        // pay EntryPoint what you owe
    ) external returns (uint256 validationData);
    // validationData packs: authorizer(0=ok,1=fail) ‖ validUntil ‖ validAfter
}

Why two loops? The bundler must be guaranteed payment even if your execution reverts, and must be able to simulate ops cheaply. So validation runs first for the whole bundle (restricted by ERC-7562 rules: limited opcodes/storage so one op can't invalidate another — DoS protection), execution second. Your op failing mid-swap still pays the bundler.

7.3 Paymasters — the onboarding superpower

interface IPaymaster {
    function validatePaymasterUserOp(PackedUserOperation calldata, bytes32, uint256 maxCost)
        external returns (bytes memory context, uint256 validationData);
    function postOp(PostOpMode mode, bytes calldata context,
                    uint256 actualGasCost, uint256 actualUserOpFeePerGas) external;
}

7.4 EntryPoint versions — know the addresses

VersionAddressHighlights
v0.6 (2023)0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789The original mainnet launch; unpacked struct; legacy
v0.7 (2024)0x0000000071727De22E5E9d8BAf0edAc6f37da032Packed struct, paymaster gas limits, unused-gas penalty
v0.8 (2025)0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108EIP-712 userOpHash (readable signing!), native EIP-7702 support

(A v0.9 with further paymaster parallelization exists; v0.7/v0.8 dominate production. Check which your bundler supports.)

7.5 Honest tradeoffs

8 · EIP-7702: Your EOA Becomes a Smart Wallet Pectra · May 7, 2025

8.1 The mechanism: type-0x04 + authorization tuples

A new transaction type 0x04 (SetCode) carries an authorization_list. Each entry is a tuple the EOA's key signed:

authorization = [chain_id, address, nonce, y_parity, r, s]
signed over:    keccak256( 0x05 ‖ rlp([chain_id, address, nonce]) )

When processed, the EOA's account code becomes a delegation designator:

code = 0xef0100 ‖ <20-byte implementation address>     (23 bytes total)
Your EOA (same address!) 0xSedhu…1997 code: 0xef0100‖impl delegation designator storage: ✓ yours balance · history · approvals: ✓ kept calls execute… Implementation contract (deployed once, shared by everyone) • execute / executeBatch • validateUserOp (4337-compatible!) • isValidSignature (1271) • session keys · passkeys · limits • modules (7579) …in YOUR account's context (your storage, your balance) Root key still exists, can always re-delegate or clear (auth to address(0)) can't be rotated away — key philosophical limit
Fig. 8 — EIP-7702 delegation: a built-in proxy. Same address, borrowed brains.
Analogy
Your old debit card gets a programmable chip installed. Same card number, same account, same history — but now it can enforce limits, batch payments, and let merchants cover the fees. And the bank keeps a master override (your private key) that no chip setting can ever disable — for better and for worse.

8.2 Why 7702 + 4337 is the 2026 architecture, not 7702 vs 4337

Point your delegation at an ERC-4337-compatible implementation and your EOA gains the entire 4337 stack — bundlers, paymasters, session keys, passkeys, batching — without changing address. EntryPoint v0.8 supports this natively. Wallets (MetaMask's delegator framework, Coinbase, OKX, Trust, Ambire) ship vetted implementation lists and surface capabilities via wallet_getCapabilities (EIP-5792, §10).

TypeScript (viem) — upgrade an EOA and batch two calls through it
import { walletClient } from './client'

// 1. sign the authorization tuple with the EOA's key
const authorization = await walletClient.signAuthorization({
  contractAddress: KERNEL_V3_IMPLEMENTATION,   // vetted implementation
})

// 2. include it in a type-0x04 transaction (can be sent by a relayer!)
const hash = await walletClient.sendTransaction({
  type: 'eip7702',
  authorizationList: [authorization],
  to: walletClient.account.address,            // call yourself…
  data: encodeFunctionData({                   // …now running smart-account code
    abi: kernelAbi,
    functionName: 'executeBatch',
    args: [[approveCall, swapCall]],           // ⚡ two actions, one signature
  }),
})

8.3 Security model — what YOU must re-check

Breaking assumptions
require(msg.sender == tx.origin) no longer proves "plain EOA" — an EOA with code can originate a transaction and run logic. Every contract using this as a flash-loan/reentrancy/bot guard needs review.
Storage collisions on redelegation: switch implementation A → B, and B reads A's leftover slots. Mitigation: ERC-7201 namespaced storage.
A malicious delegation drains everything in one signature. Signing an authorization tuple is now the most dangerous signature in Ethereum. Wallets whitelist implementations; never sign raw 7702 auths from a dApp.
Initialization front-running: delegated code usually needs initialize() — good implementations bind init to a signature from the EOA key itself.

8.4 The road not taken (context)

EIP-3074 (AUTH/AUTHCALL — per-call invoker model) was approved, then withdrawn in 2024 after security controversy; 7702 replaced it with a persistent, proxy-like, 4337-compatible design. EIP-2938 (native AA) was abandoned in 2020. EIP-7701 (native account abstraction — every account a contract, no more special-cased ECDSA) remains the roadmap "endgame": 4337 and 7702 are the migration path toward it.

9 · Modular Smart Accounts: ERC-7579 & ERC-6900

Once everyone has a smart account, you want an app-store model: install a spending-limit module, a 2FA module, a recovery module — without redeploying the wallet.

Analogy
The smart account is a smartphone; modules are apps; the standard is the app-store API that lets a module written once run on any compliant account (Kernel, Nexus, Safe…).

9.1 ERC-7579 — minimal, won the market

// Module types: 1 = validator · 2 = executor · 3 = fallback · 4 = hook
interface IERC7579Account {
    function execute(bytes32 mode, bytes calldata executionCalldata) external;
    function installModule(uint256 typeId, address module, bytes calldata init) external;
    function uninstallModule(uint256 typeId, address module, bytes calldata deinit) external;
    function isModuleInstalled(uint256 typeId, address module, bytes calldata ctx)
        external view returns (bool);
}
Module typePowerExamples
Validator (1)Decides if a UserOp / 1271 signature is authorizedECDSA owner, passkey/WebAuthn, multisig, session keys
Executor (2)Can initiate actions on the accountDCA bot, auto-repay loans, subscriptions
Fallback (3)Handles unknown selectorsERC-721/1155 receivers, extensions
Hook (4)Pre/post execution checksSpending limits, compliance, allowlists

Backers: Rhinestone, ZeroDev (Kernel v3+), Biconomy (Nexus), Safe (via the Safe7579 adapter), OKX, Etherspot. ERC-7484 adds an on-chain module registry — accounts check audited-module attestations before installing.

9.2 ERC-6900 — Alchemy's richer, stricter spec

More prescriptive: plugins carry manifests; validation/execution/hooks are wired per function selector. Powers Alchemy's Modular Account. Heavier to implement and less ecosystem traction than 7579 — but influential (its lineage feeds ERC-7715 permission requests). Rule of thumb 2026: build modules against 7579 unless you live in Alchemy's stack.

10 · Wallet↔dApp Plumbing You'll Touch

StandardWhat it does
EIP-5792 wallet_sendCallsThe JSON-RPC layer where a dApp says "batch these 3 calls, maybe with sponsored gas" and wallets advertise capabilities (atomic, paymasterService, auxiliaryFunds). Works identically over 4337 accounts, 7702 EOAs, and Safe — the dApp doesn't care which. Also wallet_getCapabilities, wallet_getCallsStatus.
ERC-7677Standard paymaster web service API (pm_getPaymasterStubData / pm_getPaymasterData) — how a dApp points any wallet at its gas sponsorship.
ERC-7715 (draft)dApp requests granular permissions / session keys: "let this game move up to 10 USDC per day for a week."
EIP-6963Multi-wallet discovery in the browser — ends the window.ethereum fistfight between extensions.
CAIP-25 / WalletConnectSession protocol between dApps and remote wallets (mobile, hardware).
EIP-5792 from the dApp side
const caps = await provider.request({ method: 'wallet_getCapabilities', params: [account] })
// { "0x1": { atomic: { status: "supported" }, paymasterService: { supported: true } } }

const { id } = await provider.request({
  method: 'wallet_sendCalls',
  params: [{
    version: '2.0.0', chainId: '0x1', from: account, atomicRequired: true,
    calls: [
      { to: USDC,   data: approveCalldata },
      { to: ROUTER, data: swapCalldata },
    ],
    capabilities: { paymasterService: { url: 'https://paymaster.orbitxpay.com' } },
  }],
})   // one popup, one atomic batch, gas sponsored — regardless of wallet type

11 · The Ecosystem: Who Builds What (2026)

11.1 Safe (formerly Gnosis Safe) — the elder statesman

Launched 2017–18, Safe predates every AA standard and still secures the most institutional value on EVM chains (DAO treasuries, protocol multisigs, exchanges). Architecture: a minimal proxy per user pointing at a shared singleton, with n-of-m ECDSA owner signatures checked over an EIP-712 SafeTx digest, plus its own pre-7579 extension points (Modules = executors, Guards = hooks, fallback handlers). It implements ERC-1271 (a Safe can "sign" for governance votes, orders, logins), gained ERC-4337 support via the Safe4337Module, and joins the modular world through the Safe7579 adapter (with Rhinestone). If you hear "smart wallet" in an institutional context, it's probably a Safe.

How a Safe validates — conceptually
// SafeTx is an EIP-712 struct: to, value, data, operation, nonce, gas params…
bytes32 txHash = keccak256(abi.encodePacked(
    hex"1901", domainSeparator(), safeTxStructHash));
checkNSignatures(txHash, signatures, threshold);
// each 65-byte slice: ECDSA owner sig, OR approved-hash flag, OR
// v=0 → nested ERC-1271: the "owner" is itself a contract wallet 🤯

11.2 Smart account implementations compared

AccountTeamStandardsSignersNotes
SafeSafeown + Safe7579 adapter, 4337 modulen-of-m ECDSA, nested 1271Battle-tested since 2018; institutional default; Modules/Guards predate 7579
KernelZeroDevERC-7579 native (v3+), 7739pluggable: ECDSA, passkeys, multisig, session keysMost-deployed 4337 account family; 7702-ready
NexusBiconomyERC-7579, 7739pluggable validatorsSuccessor to Smart Account v2; strong gas benchmarks; Rhinestone module ecosystem
Modular Account / Light AccountAlchemyERC-6900 / minimalECDSA + plugins, passkeysLight = cheap minimal; Modular = full 6900
Coinbase Smart WalletCoinbaseown + 7739passkeys (P-256/WebAuthn) first-classConsumer flagship for passkey UX; heavy 1271+6492 user
MetaMask smart accountConsensys7702 + delegation frameworkECDSA root + granted delegationsBrings the upgrade to the largest EOA user base
Simple7702Account / OZ accountseth-infinitism / OpenZeppelin7702 + 4337 referenceECDSA, P-256 variantsAudited starting points if you build your own

11.3 Infra providers — Biconomy · ZeroDev · Alchemy · Pimlico

ProviderBundlerPaymasterAccount / SDKHow they differentiate
PimlicoAlto (open-source, TS)verifying + ERC-20permissionless.js — account-agnostic, viem-styleNeutral rails. Works with any account (Safe, Kernel, Nexus…); the popular self-host path
ZeroDevown + provider aggregationKernel + @zerodev/sdkBest developer abstraction on its own account. Session keys, recovery, multi-chain ops, chain abstraction, 7702
Biconomyown✓ (pioneered pay-gas-in-ERC-20)Nexus + AbstractJS / MEECross-chain orchestration ("supertransactions"); strong sponsored-gas products
AlchemyRundler (open-source, Rust)Gas ManagerAccount Kit (aa-sdk) + Modular AccountVertically integrated platform: node infra + bundler + paymaster + embedded wallets; ERC-6900's home
OthersEtherspot (Skandha), Candide (Voltaire), Particle, thirdweb, Openfort; Stackup was the early pioneerShared-mempool work; embedded-wallet niches

One sentence each: Pimlico sells neutral rails, ZeroDev sells the nicest developer experience on its own account, Biconomy sells cross-chain execution products, Alchemy sells the integrated platform. Because all of them speak ERC-4337 + EIP-5792 + ERC-7677, accounts and infra are mix-and-match — that is the entire point of the standards.

Taste of each SDK — the same "sponsored batch" in three flavors
// — Pimlico (permissionless.js): bring-your-own account
const account = await toSafeSmartAccount({ client, owners: [owner], version: '1.4.1' })
const smartClient = createSmartAccountClient({
  account, bundlerTransport: http(PIMLICO_URL),
  paymaster: pimlicoClient,      // sponsorship policy checked server-side
})
await smartClient.sendUserOperation({ calls: [approveCall, swapCall] })

// — ZeroDev: Kernel + session keys in a few lines
const kernel = await createKernelAccount(client, { plugins: { sudo: ecdsaValidator } })
const kernelClient = createKernelAccountClient({ account: kernel, paymaster: zerodevPaymaster })
await kernelClient.sendUserOperation({ calls: [approveCall, swapCall] })

// — Alchemy Account Kit
const alchemyClient = await createModularAccountV2Client({
  chain, transport: alchemy({ apiKey }), signer,
  policyId: GAS_POLICY_ID,       // Gas Manager sponsorship
})
await alchemyClient.sendUserOperation({ uo: [approveCall, swapCall] })

11.4 Choosing, quickly

12 · End-to-End Flows: Putting It All Together

Flow A — "Sign in with your wallet" (any wallet type)

Backend issues a random nonce; user signs an EIP-191/712 message (SIWE format) containing it.
EOA?ecrecover path (viem verifyMessage).
Deployed smart wallet? → ERC-1271 isValidSignature via eth_call / SignatureChecker.
Undeployed smart wallet? → ERC-6492 wrapper: simulate deploy, then 1271.
viem's publicClient.verifyMessage() does all three automatically. Never hand-roll this.

Flow B — Gasless USDC payment from a brand-new smart account

Factory computes the counterfactual address (CREATE2) — the user already "has" a wallet with $0 spent.
User signs the userOpHash — with EntryPoint v0.8 it's an EIP-712 digest, so the wallet can render it. Signer can be a passkey.
UserOp assembled: initCode = factory call, callData = execute(usdc.transfer(merchant, 50e6)), paymasterAndData = verifying paymaster (the dApp sponsors gas).
Bundler simulates, bundles, submits handleOps. EntryPoint deploys the account, the validator module checks the signature, the paymaster pays, the transfer executes.
User paid zero gas, never held ETH, signed once with Face ID. This is the onboarding 4337 was built for.

Flow C — Existing MetaMask user gets batching + sponsorship (7702)

The wallet offers an "upgrade": the user's key signs the authorization tuple [chain_id, implementation, nonce].
A type-0x04 transaction sets the designator 0xef0100‖impl on the EOA. Same address as 2019.
A dApp calls wallet_getCapabilities → sees atomic + paymasterService.
dApp sends wallet_sendCalls([approve, swap]) → wallet builds one UserOp from the EOA's own address → bundler → EntryPoint v0.8 → done.
The 2019 EOA is now a smart wallet with sponsored, atomic batches. No migration ever happened.

13 · The EVM & Smart Contract Fundamentals

To understand proxies, 7702 delegation, and half the hacks in §15, you need three EVM primitives cold: storage slots, the four call types, and CREATE2.

13.1 The machine in one screen

Per-transaction (ephemeral)              Per-account (persistent)
────────────────────────────             ────────────────────────
stack     1024 × 32-byte words           storage   2²⁵⁶ slots × 32 bytes
memory    byte array, grows, wiped       code      immutable after deploy*
calldata  read-only input bytes          balance / nonce
                                         (* except selfdestruct†, 7702)
logs/events: write-only receipts — contracts can never read them back

13.2 The four call types — the heart of everything

CALL / STATICCALL "go run YOUR code with YOUR storage" Contract A storage A ✓ used Contract B code B runs storage B ✓ used msg.sender = A STATICCALL = same, but any state write reverts DELEGATECALL "lend me YOUR code to run on MY storage" Proxy P storage P ✓ used msg.sender / value kept address(this) = P Implementation I only code borrowed storage I ✗ untouched = every proxy (§14), every 7702 delegation (§8), every Safe module
Fig. 9 — CALL runs their code on their state; DELEGATECALL runs their code on your state. All upgradeability — and several nine-figure hacks — live in that difference.

The fourth type, CREATE/CREATE2: CREATE derives the new address from (deployer, nonce); CREATE2 from keccak256(0xff ‖ deployer ‖ salt ‖ keccak256(initcode)) — deterministic, nonce-independent, the enabler of counterfactual wallets (§5.4, §7) and of the "redeploy different code at the same address" trick used in the Metamorphic-contract attacks (§15).

Analogy
CALL is hiring a caterer to cook in their kitchen and deliver the food. DELEGATECALL is inviting the caterer into your kitchen: your ingredients, your pantry gets rearranged, and anything they burn is your house. Proxies are a house that keeps no chef on staff — every meal is a delegatecall to a chef whose address is written on a note in the hallway (an EIP-1967 slot). Upgrading = replacing the note.

14 · Upgradeability: Proxy Patterns in Solidity

Deployed bytecode is immutable, but storage is forever. Proxies split the two: a permanent address holding state, delegatecalling into replaceable logic. Here is the whole pattern family, how each works, and how each fails.

14.1 The core mechanism + EIP-1967

Every proxy is ~this
contract Proxy {
    // EIP-1967: implementation pointer lives at a "random" slot so it can
    // never collide with the implementation's own slot-0,1,2… variables:
    // bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    fallback() external payable {
        address impl;
        assembly { impl := sload(IMPL_SLOT) }
        assembly {
            calldatacopy(0, 0, calldatasize())
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok case 0 { revert(0, returndatasize()) }
                      default { return(0, returndatasize()) }
        }
    }
}

Constructors don't work through proxies — a constructor runs during the implementation's deployment, writing the implementation's storage, which the proxy never sees. Hence initializers:

contract VaultV1 is Initializable {
    function initialize(address owner_) public initializer {   // callable once
        __Ownable_init(owner_);
    }
    constructor() { _disableInitializers(); }  // ⚠️ brick the RAW implementation
}                                              // so nobody can initialize it directly (§15)

14.2 The pattern family

PatternWhere upgrade logic livesMechanismUse whenSharp edges
EIP-1167 minimal proxy ("clone")nowhere — NOT upgradeable55-byte bytecode that delegatecalls one fixed implementationMass-deploying identical cheap instances (one per user/pair)Immutable forever; needs initializer discipline
Transparent proxyin the proxy + external ProxyAdminif msg.sender == admin → route to admin functions, else always delegateClassic choice; simple mental modelSolves selector clash (admin's upgradeTo() vs an implementation function with the same 4-byte selector) by never letting admin reach the implementation; costs extra gas per call + a ProxyAdmin contract
UUPS (EIP-1822)in the implementation (upgradeToAndCall inherited from UUPSUpgradeable)proxy is minimal; implementation checks _authorizeUpgradeToday's default (OZ recommends it): cheaper calls, upgrade rules are custom codeShip an implementation whose upgrade path is broken/absent → permanently bricked; the raw implementation must be initializer-locked (Wormhole, §15)
Beacon proxyin a shared beacon contracteach proxy reads the implementation address from the beacon on every call; upgrade the beacon → every proxy upgrades at onceFleets of identical contracts (all users' smart wallets, all vaults)One beacon key controls everyone; an extra SLOAD+CALL of gas per call
Diamond (EIP-2535)diamondCut facetselector→facet routing table; one address, many implementation "facets"Contracts too big for the 24KB limit (EIP-170); modular teamsComplexity tax: tooling, audits, storage discipline (AppStorage/namespaces)
Transparent / UUPS Proxy (state) 1967 slot → impl Impl V1 → V2… logic only 1 proxy : 1 impl pointer Beacon Proxy A Proxy B Proxy C… Beacon implementation() Impl V1→V2 upgrade beacon once → whole fleet upgrades Diamond (2535) Diamond (state) selector → facet table FacetA FacetB FacetC…
Fig. 10 — The proxy family. Note the rhyme with §8: a 7702 EOA (0xef0100‖impl) is literally a protocol-native proxy, and MetaMask/Kernel 7702 deployments often point it at… a beacon-style shared implementation.
UUPS in practice (OpenZeppelin)
contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public totalDeposits;                       // slot 0 — FROZEN forever now

    function initialize(address owner_) public initializer {
        __Ownable_init(owner_); __UUPSUpgradeable_init();
    }
    function _authorizeUpgrade(address) internal override onlyOwner {}
    constructor() { _disableInitializers(); }
}

contract VaultV2 is VaultV1 {
    mapping(address => uint256) public rewards;         // NEW slots APPENDED — never
    function version() external pure returns (string memory) { return "2"; }
}   // upgrade: proxy.upgradeToAndCall(address(new VaultV2()), "")

14.3 Storage-layout law (where upgrades die)

15 · Known Issues & Famous Hacks — the Scar-Tissue Catalog

Every rule in §14 and every "wallets must…" in this guide was paid for. Study the receipts.

15.1 The canon, chronologically

YearIncidentLossRoot causeThe lesson it bought
2016The DAO3.6M ETH (~$60M then)Reentrancy: balance updated after the external call; attacker's fallback re-entered withdraw recursivelyChecks-Effects-Interactions, ReentrancyGuard — and the ETH/ETC fork itself (which then necessitated EIP-155, §3.1)
2017Parity multisig #1~150k ETH ($30M)Wallet delegatecalled a shared library; initWallet was reachable → attacker re-initialized and became ownerAccess-control every init path; delegatecall exposes EVERYTHING public
2017Parity freeze #2513k ETH frozen foreverThe shared library was itself an uninitialized contract; a user became its owner and selfdestructed it — every wallet delegatecalling it brickedInitialize (or brick) implementations; don't put selfdestruct in library code. Direct ancestor of _disableInitializers()
2020bZx, Harvest, & the flash-loan era$10M+ each, repeatedlyPrice oracles read from manipulable spot pools; flash loans made "whale for one tx" freeTWAP/Chainlink oracles; assume any caller can wield infinite capital for one transaction
2022Wormhole uninitialized UUPS (whitehat, $10M bounty)near-miss on the bridgeRaw implementation left uninitialized after an upgrade → anyone could initialize, take ownership, and selfdestruct it — freezing the proxy Parity-style_disableInitializers() in every implementation constructor, forever
2022Audius$6MStorage collision: proxy's own admin variables sat in slots the implementation also used → re-initialization became possibleEIP-1967 randomized slots exist for a reason; verify layouts (ERC-7201)
2022Nomad bridge$190MAn upgrade initialized the trusted-root to 0x00every unproven message verified; first crowd-sourced exploitReview initializer values, not just code; upgrades are consensus-critical deploys
2022Ronin bridge$624M5-of-9 validator multisig; attacker spear-phished keys for 5 (4 run by one operator)Multisig math is meaningless without key independence; §11's Safe threshold discipline
2022Wintermute / Profanity$160MVanity-address generator seeded with 32-bit entropy → private keys brute-forceable (§2's "entropy is everything")Never use cute key generators; the mnemonic pipeline (§2.1) exists for a reason
2023Curve read-only reentrancy wave$70M+ ecosystem-wideProtocols read an LP price during another contract's reentrancy window (view functions aren't guarded by the victim's lock)Reentrancy isn't only about writes; check locks before trusting cross-contract reads
2025Bybit / Safe UI~$1.5B — largest everCompromised Safe{Wallet} frontend showed signers a benign transfer while they actually signed a delegatecall that swapped the Safe's implementation (§14 + blind signing, §4.3)Verify EIP-712 digests on the hardware device, not the web UI; treat operation=delegatecall in a Safe as radioactive; simulate before co-signing

15.2 The recurring defect classes

ClassOne-line mechanicsStandard defense
Reentrancy (classic, cross-function, cross-contract, read-only)External call hands control to attacker mid-state-changeChecks-Effects-Interactions; nonReentrant; pull-payments; snapshot reads
Delegatecall misuseUntrusted/replaceable code run on your storageOnly delegatecall immutable, audited targets; EIP-1967 slots; 7702 implementation whitelists (§8.3)
Uninitialized implementation / re-initializationConstructor ≠ initializer through a proxyinitializer modifier + _disableInitializers(); init in the same tx as deploy
Storage collisionTwo code versions disagree about what slot N meansAppend-only layouts, __gap, ERC-7201, upgrade-safety tooling
Selector clashTwo functions share a 4-byte selector; proxy routes the wrong oneTransparent-proxy admin split; tooling that checks clashes (it's why that pattern exists)
Signature bugsMissing nonce/deadline/domain → replay; malleability; ecrecover(0); blind-signed 712 digestsThe whole §4.3 checklist; hardware-verified signing
Approval abuse / permit phishingUnlimited approve or a phished permit/Permit2 signature drains later, silentlyExact-amount approvals, allowance revoking, wallet simulation & warnings (§6)
Oracle / price manipulationSpot price moved within one tx (flash loan)TWAP, Chainlink, multi-source medians
Access control omissionsA public function that should not be (Parity's initWallet; Poly Network 2021, $611M)Default-deny modifiers; negative tests; fuzz the "who can call this" matrix
Metamorphic contractsCREATE2 + selfdestruct let code change at a "verified" address (largely closed by EIP-6780 in Dencun: selfdestruct now only works in the creation tx)Check for the pattern when trusting addresses; post-Dencun this class is mostly historical
The meta-lesson for wallet builders
Trace this chapter back through the guide: Parity is why implementations get initialized-and-locked (§14), which is why your 7702 delegate needs init-front-running protection (§8.3). The DAO's fork created the replay problem EIP-155 solved (§3.1). Blind signing enabled Bybit, which is the case for EIP-712 (§4.2) and hardware-verified digests. Profanity is why BIP-39 entropy discipline (§2.1) is non-negotiable. The standards are the scar tissue.

16 · Scaling: Sidechains vs Rollups

The block gas limit (§3.4) makes L1 blockspace scarce and expensive. Every scaling design answers one question differently: who do you trust when the operator misbehaves?

16.1 Sidechains — a different chain wearing an EVM costume

Polygon PoS, BNB Chain, Gnosis Chain: independent blockchains with their own validator set and consensus, connected to Ethereum only by a bridge contract. Security = their validators, full stop. If the sidechain's validators collude, your bridged funds are gone and Ethereum can't help — the bridge just holds locked tokens against the sidechain's word. Cheap, fast, EVM-compatible… and trust-them-shaped. (This trust surface is why bridges headline §15's loss tables — Ronin was a sidechain bridge.)

16.2 Rollups — execute elsewhere, settle on Ethereum

A rollup executes transactions on its own fast chain, then posts (a) the compressed transaction data and (b) a state commitment to L1. Ethereum stores the data (since Dencun, in cheap EIP-4844 blobs — §3.2), so anyone can reconstruct the rollup's state and check the operator. The two families differ in how the state commitment is checked:

Optimistic rollup Arbitrum · OP Mainnet · Base "assume valid — anyone can submit a fraud proof within ~7 days" ZK rollup zkSync · Scroll · Linea · Starknet "every batch carries a validity proof — L1 verifies math, not people" batch + state root batch + SNARK/STARK Ethereum L1 data in 4844 blobs (§3.2) settlement + data availability Sidechain own validators, own security only a bridge touches L1
Fig. 11 — Rollups inherit Ethereum's security by posting data + proofs; sidechains only borrow its brand.
OptimisticZKSidechain
State validityfraud proofs (challenge window ~7 days)validity proof per batch, verified by an L1 contracttrust the validator set
Withdrawal to L1~7 days native (or instant via liquidity providers)minutes–hours (prover latency)bridge-dependent
EVM fidelitynear-perfect (same bytecode)zkEVMs vary from bytecode-level to language-levelusually full EVM
Trust residue≥1 honest challenger + sequencer livenessmath + prover livenessthe validators, entirely

16.3 What changes for wallet & contract developers

Analogy
Ethereum L1 is a supreme court: absolute authority, slow, expensive per case. A rollup is a lower court that files complete transcripts with the supreme court — anyone can appeal with the transcript as evidence (fraud proof), or in ZK's case, every verdict arrives pre-notarized by a mathematician. A sidechain is a private arbitration firm across the street: fast and cheap, but if its judges collude, the supreme court never saw your case.

17 · Zero-Knowledge Proofs: SNARKs, STARKs & Friends

§16 waved at "validity proofs" — here's what's inside. A zero-knowledge proof lets a prover convince a verifier that a statement is true without revealing why it's true — and, just as importantly for Ethereum, without the verifier re-doing the work.

17.1 The idea, three ways

Formally, a proof system needs three properties: completeness (true statements convince), soundness (false statements can't, except with negligible luck), and zero-knowledge (the verifier learns nothing beyond "it's true").

17.2 From riddles to circuits

Real systems prove statements of one canonical form:

"I know a secret witness  w  such that  C(x, w) = true"
   x = public inputs (everyone sees)      w = private witness (never revealed)
   C = an arithmetic circuit — the statement compiled into addition/multiplication
       gates over a finite field (R1CS / PLONKish constraint systems)

Examples of C: "hash(w) equals this public commitment", "w is a valid Merkle path into this tree root", "w is a batch of 10,000 transactions whose execution moves state root A to state root B" — that last one is a ZK rollup (§16).

Prover (off-chain) has secret witness w runs C(x,w), builds proof π heavy: seconds–minutes, GPUs for big circuits π (~200 bytes – 100 KB) + public inputs x Verifier (on-chain) a Solidity contract checks π against x — milliseconds, ~200–300k gas via pairing precompiles (0x06/07/08) The asymmetry proving: expensive verifying: cheap = do work once off-chain, convince everyone forever
Fig. 12 — The economic trick behind all ZK on Ethereum: crushing off-chain work into a constant-size, cheap-to-verify receipt.

17.3 SNARK vs STARK vs the rest

Groth16 (SNARK)PLONK family (SNARK)STARK
AcronymSuccinct Non-interactive ARgument of KnowledgeScalable Transparent ARgument of Knowledge
Proof size~200 bytes (3 curve points)~400 B–1 KB~50–200 KB
Trusted setup⚠ per-circuit ceremonyuniversal, updatable ("powers of tau" — one ceremony, any circuit)none — hashes only
Crypto assumptionselliptic-curve pairings — not post-quantumhash functions — plausibly post-quantum
Verify cost on L1cheapest (~200k gas)cheapexpensive → usually wrapped in a SNARK before landing on L1
Used byTornado Cash, Zcash (orig.), many circom appszkSync, Scroll, Linea, Aztec (variants: Halo2, UltraPLONK…)Starknet, StarkEx, zkVMs (RISC Zero, SP1)

17.4 Production example #1 — Tornado Cash: the commitment/nullifier machine

The canonical ZK dApp, and the pattern half the ZK ecosystem reuses. The problem: deposit 1 ETH from address A, withdraw it to fresh address B, with no on-chain link between A and B — while making double-withdrawal impossible.

Deposit — publish a commitment, reveal nothing
wallet generates two random 31-byte secrets:   nullifier k,  secret s
commitment C = PedersenHash(k ‖ s)

deposit(C) { 1 ETH in }  → contract inserts C as a leaf in an on-chain
                            Merkle tree (height 20, ~1M leaves, MiMC hash)
// A's deposit is now one anonymous leaf among everyone else's.
Withdraw — the actual statement Tornado's circuit proves
public inputs:  root          (a recent Merkle root the contract knows)
                nullifierHash = PedersenHash(k)
                recipient, relayer, fee                       ← note these!
private witness: k, s, merklePath[20], pathIndices[20]

constraints:
  C == PedersenHash(k ‖ s)                       // I know the secrets behind SOME leaf
  MerkleVerify(C, merklePath, pathIndices) == root   // that leaf is in the tree
  nullifierHash == PedersenHash(k)               // and this is ITS unique serial number
  // recipient/relayer/fee are "squeezed" into the circuit as constrained
  // public inputs — the proof is only valid for THIS exact payout
The contract side (simplified from the real Tornado core)
function withdraw(bytes calldata proof, bytes32 root, bytes32 nullifierHash,
                  address payable recipient, address payable relayer, uint256 fee) external {
    require(!nullifierHashes[nullifierHash], "already spent");   // double-spend gate
    require(isKnownRoot(root), "stale root");                    // one of last 30 roots
    require(verifier.verifyProof(proof,
        [uint256(root), uint256(nullifierHash),
         uint256(uint160(recipient)), uint256(uint160(relayer)), fee]), "bad proof");
    nullifierHashes[nullifierHash] = true;
    recipient.transfer(1 ether - fee);
    if (fee > 0) relayer.transfer(fee);
}

Why each piece exists — this is the part worth internalizing:

17.4b Production example #2 — how a zk-rollup batch actually lands on L1

§16 said "validity proof per batch." Concretely, a zkSync/Scroll-style pipeline does this:

Sequence: the sequencer executes ~thousands of user txs on L2, producing stateRoot_new from stateRoot_old.
Commit: an L1 tx (commitBatches) posts the compressed tx data as an EIP-4844 blob (§3.2) plus the claimed new root. Data is now available; the claim is not yet trusted.
Prove: GPU provers run the batch through a zkEVM circuit — the statement is "executing the txs committed in blob X against stateRoot_old yields stateRoot_new". Thousands of per-tx/per-opcode proofs get recursively aggregated into one final SNARK.
Verify: proveBatches(proof) — the L1 verifier contract checks the aggregated proof in one ~300–500k-gas call. A million L2 transactions just got verified for the L1 cost of a couple of Uniswap swaps. That division is the entire economics of ZK rollups.
Execute/finalize: after verification the new root is final — withdrawals against it can be processed immediately. No 7-day window: the fraud-proof waiting game (§16.2) is replaced by math that already ran.

Where you can see it: zkSync Era's commitBatches/proveBatches/executeBatches on its L1 diamond contract, Scroll's ScrollChain.finalizeBatchWithProof — open Etherscan and watch the cadence: many commits, periodic proofs covering several batches each (proving lags sequencing by minutes to hours; that gap is "soft finality" and it's why bridges quote two different finality times).

Real circuit bugs, real money
Zcash (2018): a subtle error in the BCTV14 setup parameters would have allowed infinite undetectable counterfeiting — found internally, patched silently, disclosed later. The proof system itself was the bug.
Under-constrained circuits: a signal the circuit computes but never constrains lets the prover choose it freely — the class behind forged-deposit exploits on early Tornado forks and the 2022–23 audit wave in circom projects.
"Frozen Heart" (2022): multiple libraries implemented Fiat–Shamir (the interactive→non-interactive transform) with incomplete transcripts, making forgeable proofs across several production codebases.
The lesson mirrors §15: the math is rarely wrong; the statement or the implementation is. Audit constraints like consensus code.

17.5 Where you'll meet ZK in the wild

UseThe statement being provedExamples
ZK rollups (§16)"executing this batch moves state root A→B"zkSync, Scroll, Linea, Starknet
Private payments"I own an unspent note in this commitment tree" (Merkle membership + nullifier, without revealing which note)Zcash, Tornado Cash, Aztec, Privacy Pools ("I'm in the tree AND not from these tainted deposits")
Identity / credentials"a valid government/issuer signature exists over attributes satisfying P" — age, citizenship, uniqueness — without the documentzkPassport-style apps, World ID (one human, one proof), Semaphore anonymous voting/signaling
zkEmail / zkTLS"this email/TLS session was genuinely signed by google.com's DKIM key and contains X"proof-of-Twitter-handle account recovery, off-chain data oracles
Light clients & bridges"this block header is valid under chain X's consensus"zk light-client bridges (Succinct, Polyhedra) — replacing multisig bridges from §15's loss table
Wallet-adjacentproof-of-reserves ("assets ≥ liabilities" without the ledger), private airdrop eligibility, zkML ("this model produced this output")exchange PoR schemes, Worldcoin's iris pipeline
Analogy to close the loop
A signature (§4) proves who said something. A zero-knowledge proof proves that something is true. Ethereum's endgame stacks them: you sign an intent (712), a smart account validates it (1271/4337), a rollup executes it, and a SNARK convinces L1 the whole thing happened correctly — one wax seal, one sealed envelope, one notarized transcript, each doing a different job.

18 · Consensus, Chain Security & the Quantum Question

Everything above assumed the chain itself is honest. This chapter is about why that's (economically) true — and about the one scheduled event that breaks the cryptography under every EOA on the planet.

18.1 What consensus actually does

A blockchain needs agreement on two things only: ordering (which tx came first — the double-spend question) and validity (state transitions follow the rules). Every consensus family is a different answer to "who gets to order, and what does cheating cost?"

FamilyWho ordersFinalityCheating costsExamples
PoW / Nakamotowhoever finds the hash lottery ticketprobabilistic — 6 confirmations = "deep enough"electricity + hardware, external to the chainBitcoin, pre-2022 Ethereum
PoS / Ethereum (Gasper = Casper-FFG + LMD-GHOST)a validator (32 ETH stake) pseudo-randomly picked per 12s slot; ~1M validators attesteconomic finality in 2 epochs (~13 min): reverting a finalized block requires burning ≥⅓ of all stakeslashing — provably contradictory votes destroy the validator's own capital, in-protocolEthereum today
BFT-style (Tendermint)rotating proposer + ⅔ pre-commit votes per blockinstant — a block is final or doesn't existslashing; but halts if >⅓ offline (liveness traded for safety)Cosmos chains (§19)
DPoS / small committee27–100 elected block producersfast, committee-finalreputation/stake of few entitiesTron (§19), BNB
Analogy
PoW is a lottery where tickets cost electricity — rewriting history means re-buying every winning ticket faster than the world combined. PoS is a security deposit system: validators post bail, and lying leaves cryptographic evidence that lets anyone burn their bail. BFT is a boardroom vote — instant and final, but the meeting stops if a third of the board doesn't show.

18.2 Attack economics — what "secure" quantitatively means

18.3 Quantum computing vs your private keys

Blockchain cryptography stands on two different pillars, and quantum computers hit them very differently:

PillarQuantum algorithmDamageVerdict
Hashes (keccak256, SHA-256, Pedersen/Poseidon…) — addresses, Merkle trees, PoW, commitments, BIP-39 seedsGroveronly a quadratic speedup: 256-bit security → ~128-bit effective✅ fine — 128 bits is still unreachable; at most, double output sizes someday
Elliptic-curve signatures (secp256k1 ECDSA, ed25519) — every EOA, every §4 signature, BLS validator keysShorsolves the discrete-log problem outright: public key → private key in polynomial time❌ broken the day a large fault-tolerant quantum computer ("CRQC") exists

So the question becomes: who has revealed their public key? Remember §2.6 — an address is keccak(pubkey)[12:], and a hash is quantum-safe. The pubkey itself only becomes public when you sign:

The escape routes (and one you already know)

Analogy
An address is a mailbox with your face behind frosted glass (the hash). Every signature you make hands out a clear photo (the pubkey). Today, nobody can pick a lock from a photo. Shor's algorithm is a machine that carves a working key from the photo — and the blockchain is a permanent public photo album. The fix isn't better photos; it's replaceable locks — which is what account abstraction turned wallets into.

19 · Beyond the EVM: Solana, Tron, Cosmos

Everything so far assumed Ethereum's model: secp256k1 keys, account-state, gas markets, contract storage. Other major chains made different bets — and each difference changes how "wallets" and "signing" even work.

13.1 Solana — everything is an account, and storage pays rent

Keys & signing. Solana uses ed25519, not secp256k1. Derivation is SLIP-0010, all-hardened (m/44'/501'/0'/0'), so there are no xpub watch-only trees. The address is simply the base58-encoded public key — no hashing step, no 0x, no checksum casing (base58 excludes look-alike characters instead). Signing is plain ed25519 over the serialized message — fast, deterministic, no malleability drama, and no ecrecover: verification takes the pubkey as input rather than recovering it.

The account model — inverted from Ethereum. On Ethereum, a token contract owns a giant mapping(address => uint256) inside its own storage. On Solana, programs are stateless: code lives in one executable account, and every piece of state lives in its own separate account owned by that program. A transaction must declare up front every account it will touch (which is what lets the runtime execute non-overlapping transactions in parallel — think EIP-2930 access lists made mandatory and turned into a scheduler).

ATAs — Associated Token Accounts

Because state is externalized, your USDC balance is not an entry in the USDC program — it's a separate token account holding {mint, owner, amount}. The Associated Token Account program standardizes where it lives: a PDA (program-derived address) computed deterministically from seeds:

ATA = find_program_address(
        seeds = [wallet_pubkey, TOKEN_PROGRAM_ID, mint_pubkey],
        program_id = ASSOCIATED_TOKEN_PROGRAM_ID )
// PDAs are addresses WITHOUT private keys — bumped off the ed25519 curve.
// Only the owning program can "sign" for them. (Compare: a contract wallet
// with no key that ERC-1271 lets speak — same idea, protocol-native.)

Rent — storage as a leased resource

Every account must keep a lamport balance proportional to its data size — the rent-exempt minimum (~0.002 SOL for a token account). In practice "rent" is a refundable storage deposit: close the account and the lamports come back. Ethereum makes you pay gas once to write storage forever; Solana makes you escrow for as long as the bytes exist. This is why Solana wallets have a "close empty token accounts, reclaim SOL" button — and why dust-token spam literally locks up attacker capital.

Other wallet-relevant differences: fees are tiny and mostly fixed (plus priority fees); the fee payer is just the first signer — someone else can pay for your transaction natively, no ERC-4337 apparatus needed; replay protection uses a recent blockhash (~60–90s validity) instead of a nonce, with "durable nonces" as the escape hatch; and there's no protocol distinction between "EOA" and "contract account" to abstract away in the first place.

13.2 Tron — the EVM cousin with a staking-based gas model

13.3 Cosmos — not a chain, a kit for building chains

13.4 Side-by-side: what "different from EVM" actually means

Ethereum / EVMSolanaTronCosmos SDK
Curvesecp256k1ed25519secp256k1secp256k1 (mostly)
HD path44'/60'/0'/0/i44'/501'/0'/0' all-hardened44'/195'/0'/0/i44'/118'/0'/0/i shared
Addresskeccak(pubkey)[12:] → 0xpubkey itself, base580x41+hash → base58check "T…"bech32, per-chain prefix
Token balances live…inside the token contract's mappingin your own ATA (rent-funded)in the TRC-20 contract (EVM-style)native bank module / CW20
Fees1559 auction, burned base feetiny fixed + priority; fee payer = any signerstaked Bandwidth/Energyper-chain gas prices, fee grants
Replay protectionaccount nonce + chainIdrecent blockhash (or durable nonce)tx expiration + ref blockaccount sequence + chain-id in sign doc
"Smart wallet" storyERC-4337 / EIP-7702 retrofit (this guide)native fee-payer split, PDAs, multisig programsEVM-style contracts, little AA activityauthz/feegrant modules, ICA over IBC
Analogy
Ethereum keeps everyone's balances in the bank's ledger book (contract storage). Solana hands you a safe-deposit box per asset (ATA) and charges a refundable deposit for the box (rent). Tron runs an EVM-style bank but sells prepaid transit passes (frozen TRX) instead of charging per trip. Cosmos doesn't give you a bank at all — it gives every community a kit to open its own bank, plus a courier network between them (IBC).

20 · Cheat Sheet — Every Number, One Memory Hook

NumberSay it out loudMemory hook
EIP-155chainId replay protectioncheque with the country written on it
EIP-2718typed tx envelopeone byte to rule the formats
EIP-1559fee marketsurge pricing, but the surge is burned
EIP-191personal_sign0x19 makes messages ≠ transactions
EIP-712typed data signingsigning a labeled form, not a napkin
ERC-1271isValidSignature → 0x1626ba7ecall the company's front desk
ERC-6492sigs for undeployed walletscheque + incorporation papers stapled
ERC-7739defensive rehashing1271, replay-hardened
EIP-2612permit()signed letter of authorization
ERC-4337AA via alt-mempoolrestaurant: slips, waiter, kitchen pass
EIP-7702set EOA code (type 0x04)programmable chip in your old debit card
ERC-7579minimal modular accountsapp store API for wallets
ERC-6900Alchemy's modular accountsthe stricter app store
EIP-5792wallet_sendCallsthe dApp↔wallet batching language
ERC-7677paymaster web servicethe gas-sponsorship phone line
EIP-3074withdrawn AUTH/AUTHCALLthe road not taken
EIP-7701native AA (future)the endgame

Suggested learning order

  1. §2–3 (keys, transactions) then §4 (191/712) — the foundation everything sits on
  2. §5 (1271 / 6492 / SignatureChecker) — your original question; every dApp needs it
  3. §7 (4337) — the big one; internalize the restaurant
  4. §8 (7702) — short, but it changes assumptions everywhere
  5. §6, §9–11 — permit, modules, ecosystem, as your build requires
  6. §13–15 (EVM, proxies, hacks) — before you ship or audit any contract
  7. §16–19 — scaling, ZK, consensus/quantum, multi-chain, as your product grows