Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Module 2: EVM-Level Changes

Difficulty: Intermediate

Estimated reading time: ~65 minutes | Exercises: ~4-5 hours

πŸ“š Table of Contents

Foundational EVM Concepts

Dencun Upgrade (March 2024)

Pectra Upgrade (May 2025)

Looking Ahead


πŸ’‘ Foundational EVM Concepts

These pre-Dencun EVM changes underpin everything else in this module. The gas table above references β€œcold” and β€œwarm” costs β€” this section explains where those numbers come from, along with other foundational concepts every DeFi developer must know.

πŸ’‘ Concept: EIP-2929 β€” Cold/Warm Access Model

Why this matters: Every time your DeFi contract reads or writes storage, calls another contract, or checks a balance, the gas cost depends on whether the address/slot has already been β€œaccessed” in the current transaction. This is the single most important concept for gas optimization.

Introduced in EIP-2929, activated with the Berlin upgrade (April 2021)

The model:

Before EIP-2929, SLOAD cost a flat 800 gas regardless of access pattern. After EIP-2929, the EVM maintains an access set β€” a list of addresses and storage slots that have been touched during the transaction. The first access to any address or slot is β€œcold” (expensive), subsequent accesses are β€œwarm” (cheap).

Access Set (maintained per-transaction by the EVM):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Addresses:                                         β”‚
β”‚    0xUniswapRouter  ← accessed (warm)               β”‚
β”‚    0xWETH           ← accessed (warm)               β”‚
β”‚    0xDAI            ← NOT accessed yet (cold)       β”‚
β”‚                                                     β”‚
β”‚  Storage Slots:                                     β”‚
β”‚    (0xWETH, slot 5)   ← accessed (warm)             β”‚
β”‚    (0xWETH, slot 12)  ← NOT accessed yet (cold)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Gas costs with cold/warm model:

OperationCold (first access)Warm (subsequent)Before EIP-2929
SLOAD2,100 gas100 gas800 gas (flat)
CALL / STATICCALL2,600 gas100 gas700 gas (flat)
BALANCE / EXTCODESIZE2,600 gas100 gas700 gas (flat)
EXTCODECOPY2,600 gas100 gas700 gas (flat)

Step-by-step: How cold/warm affects a Uniswap swap

function swap(address tokenIn, uint256 amountIn) external {
    // 1. SLOAD balances[msg.sender]
    //    First access to this slot β†’ COLD β†’ 2,100 gas
    uint256 balance = balances[msg.sender];

    // 2. SLOAD balances[msg.sender] again (in require)
    //    Same slot, already accessed β†’ WARM β†’ 100 gas ✨
    require(balance >= amountIn);

    // 3. SLOAD reserves[tokenIn]
    //    Different slot, first access β†’ COLD β†’ 2,100 gas
    uint256 reserve = reserves[tokenIn];

    // 4. CALL to tokenIn.transferFrom()
    //    First call to tokenIn address β†’ COLD β†’ 2,600 gas
    IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);

    // 5. CALL to tokenIn.transfer()
    //    Same address, already accessed β†’ WARM β†’ 100 gas ✨
    IERC20(tokenIn).transfer(recipient, amountOut);
}

πŸ’» Quick Try:

See cold/warm access in action. Deploy this in Remix or run with Foundry:

contract ColdWarmDemo {
    uint256 public valueA;
    uint256 public valueB;

    /// @dev Call this, then check gas β€” the second SLOAD is ~2000 gas cheaper
    function readTwice() external view returns (uint256, uint256) {
        uint256 a = valueA;   // Cold SLOAD: ~2,100 gas
        uint256 b = valueA;   // Warm SLOAD: ~100 gas (same slot!)
        return (a, b);
    }

    /// @dev Compare gas with readTwice β€” both SLOADs here are cold (different slots)
    function readDifferent() external view returns (uint256, uint256) {
        uint256 a = valueA;   // Cold SLOAD: ~2,100 gas
        uint256 b = valueB;   // Cold SLOAD: ~2,100 gas (different slot)
        return (a, b);
    }
}

Call both functions and compare gas. readTwice costs ~2,200 total (2,100 + 100). readDifferent costs ~4,200 total (2,100 + 2,100). That 2,000 gas difference per slot is why DeFi protocols pack related data together.

Optimization: Access Lists (EIP-2930)

EIP-2930 introduced access lists β€” a way to pre-declare which addresses and storage slots your transaction will touch. Pre-declared items start β€œwarm,” avoiding the cold surcharge at a smaller upfront cost.

The economics:

CostAmount
Access list: per address entry2,400 gas
Access list: per storage slot entry1,900 gas
Cold CALL/BALANCE (without access list)2,600 gas
Cold SLOAD (without access list)2,100 gas
Warm access (after pre-warming)100 gas

When access lists save gas β€” the math:

Per address:   save (2,600 - 100) = 2,500 cold penalty, pay 2,400 entry = net save 100 gas βœ“
Per slot:      save (2,100 - 100) = 2,000 cold penalty, pay 1,900 entry = net save 100 gas βœ“

The savings are modest per item (100 gas), but they compound across complex transactions. A multi-hop DEX swap touching 3 contracts with 9 storage slots saves ~1,200 gas.

When access lists DON’T help:

  • Simple transfers β€” only 1-2 cold accesses, overhead may exceed savings
  • Dynamic routing β€” you don’t know which slots will be accessed until runtime
  • Already-warm slots β€” accessing a contract you’ve already called wastes the entry cost

How to generate access lists:

# Use eth_createAccessList RPC to auto-detect which addresses/slots a tx touches
cast access-list \
  --rpc-url $RPC_URL \
  --from 0xYourAddress \
  0xRouterAddress \
  "swap(address,uint256,uint256)" \
  0xTokenA 1000000 0

# Returns: list of addresses + slots the transaction will access
# Add this to your transaction for gas savings

Real DeFi impact:

In a multi-hop Uniswap V3 swap touching 3 pools:

  • Without access list: 3 cold CALL + ~9 cold SLOAD = 3Γ—2,600 + 9Γ—2,100 = 26,700 gas in cold penalties
  • With access list: 3Γ—2,400 + 9Γ—1,900 = 24,300 gas upfront, all accesses warm = ~1,200 gas during execution = 25,500 gas total
  • Savings: ~1,200 gas β€” modest, but MEV bots compete on margins this small

πŸ”— DeFi Pattern Connection

Where cold/warm access matters most:

  1. DEX aggregators (1inch, Paraswap) β€” Route through multiple pools. Each pool is a new address (cold). Aggregators use access lists to pre-warm pools on the route.
  2. Liquidation bots β€” Read health factors (cold SLOAD), call liquidate (cold CALL), swap collateral (cold CALL). Access lists are critical for staying competitive on gas.
  3. Storage-heavy protocols (Aave V3) β€” Multiple storage reads per operation. Aave packs related data in fewer slots to minimize cold reads.

πŸ’Ό Job Market Context

Interview question:

β€œHow do cold and warm storage accesses affect gas costs?”

What to say:

β€œSince EIP-2929 (Berlin upgrade), the EVM maintains an access set per transaction. The first read of any storage slot costs 2,100 gas (cold), subsequent reads cost 100 gas (warm). Same pattern for external calls β€” first call to an address costs 2,600 gas. This means the order you access storage matters: reading the same slot twice costs 2,200 gas total, not 4,200. You can also use EIP-2930 access lists to pre-warm slots, which is valuable for multi-pool DEX swaps and liquidation bots.”

Interview Red Flags:

  • 🚩 β€œSLOAD always costs 200 gas” β€” Outdated (pre-Berlin pricing)
  • 🚩 Not knowing about access lists β€” Critical optimization tool
  • 🚩 β€œGas costs are the same for every storage read” β€” Cold/warm distinction is fundamental

πŸ’‘ Concept: EIP-1559 β€” Base Fee Market

Why this matters: EIP-1559 fundamentally changed how Ethereum prices gas. Understanding it matters for MEV strategy, gas estimation, transaction ordering, and L2 fee models.

Introduced in EIP-1559, activated with the London upgrade (August 2021)

The model:

Before EIP-1559, gas pricing was a first-price auction: users bid gas prices, miners picked the highest bids. This led to overpaying, gas price volatility, and poor UX.

EIP-1559 split the gas price into two components:

Total gas price = base fee + priority fee (tip)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ BASE FEE (burned)                                β”‚
β”‚ - Set by the protocol, not the user              β”‚
β”‚ - Adjusts based on block fullness                β”‚
β”‚ - If block > 50% full β†’ base fee increases       β”‚
β”‚ - If block < 50% full β†’ base fee decreases       β”‚
β”‚ - Max change: Β±12.5% per block                   β”‚
β”‚ - Burned (removed from supply) β€” not paid        β”‚
β”‚   to validators                                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PRIORITY FEE / TIP (paid to validator)           β”‚
β”‚ - Set by the user                                β”‚
β”‚ - Incentivizes validators to include your tx     β”‚
β”‚ - During congestion, higher tip = faster          β”‚
β”‚   inclusion                                      β”‚
β”‚ - During calm periods, 1-2 gwei is sufficient    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why DeFi developers care:

  1. Gas estimation: block.basefee is available in Solidity β€” protocols can read the current base fee for gas-aware logic
  2. MEV: Searchers set high priority fees to get their bundles included. Understanding base fee vs. tip is essential for MEV strategies
  3. L2 fee models: L2s adapt EIP-1559 for their own fee markets (Arbitrum ArbGas, Optimism L1 data fee + L2 execution fee)
  4. Protocol design: Some protocols adjust fees based on gas conditions (e.g., oracle update frequency)

DeFi-relevant Solidity globals:

block.basefee    // Current block's base fee (EIP-1559)
block.blobbasefee // Current block's blob base fee (EIP-4844)
tx.gasprice      // Actual gas price of the transaction (base + tip)

πŸ’» Quick Try:

contract BaseFeeReader {
    /// @dev Returns the current base fee and the effective priority fee
    function feeInfo() external view returns (uint256 baseFee, uint256 priorityFee) {
        baseFee = block.basefee;
        // tx.gasprice = baseFee + priorityFee, so:
        priorityFee = tx.gasprice - block.basefee;
    }
}

Deploy and call feeInfo(). On a local Foundry/Hardhat chain, baseFee starts at a default value and priorityFee reflects your gas price setting. On mainnet, you’d see the real fluctuating base fee.

πŸ’Ό Job Market Context

Interview question:

β€œHow does EIP-1559 affect MEV strategies?”

What to say:

β€œEIP-1559 separated the gas price into base fee (burned, set by protocol) and priority fee (paid to validators, set by user). For MEV, the base fee is a floor cost you can’t avoid β€” it determines whether an arbitrage is profitable. The priority fee is how you bid for inclusion. Flashbots bypasses the public mempool entirely, but understanding base fee dynamics helps you predict profitability windows and set appropriate tips.”


πŸ’‘ Concept: EIP-3529 β€” Gas Refund Changes

Why this matters: EIP-3529 killed the gas token pattern and changed how SSTORE refunds work. If you’ve ever seen CHI or GST2 tokens mentioned in old DeFi code, this is why they’re dead.

Introduced in EIP-3529, activated with the London upgrade (August 2021)

What changed:

Before EIP-3529:

  • Clearing a storage slot (nonzero β†’ zero) refunded 15,000 gas
  • SELFDESTRUCT refunded 24,000 gas
  • Refunds could offset up to 50% of total transaction gas

After EIP-3529:

  • Clearing a storage slot refunds only 4,800 gas
  • SELFDESTRUCT refund removed entirely
  • Refunds capped at 20% of total transaction gas (down from 50%)

The gas token exploit (now dead):

// Before EIP-3529: Gas tokens exploited the refund mechanism
contract GasToken {
    // During low gas prices: write to many storage slots (cheap)
    function mint(uint256 amount) external {
        for (uint256 i = 0; i < amount; i++) {
            assembly { sstore(add(i, 0x100), 1) }  // Write nonzero
        }
    }

    // During high gas prices: clear those slots (get refunds!)
    function burn(uint256 amount) external {
        for (uint256 i = 0; i < amount; i++) {
            assembly { sstore(add(i, 0x100), 0) }  // Clear β†’ refund
        }
        // Each clear refunded 15,000 gas β€” effectively "stored" cheap gas
        // for use during expensive periods. Arbitrage on gas prices!
    }
}
// CHI (1inch) and GST2 (Gas Station Network) used this pattern.
// EIP-3529 reduced refunds to 4,800 gas, making gas tokens unprofitable.

Impact on DeFi:

  • Any protocol that relied on SELFDESTRUCT gas refunds for economic models is broken
  • Storage cleanup patterns still get some refund (4,800 gas), but it’s not a significant optimization target anymore
  • The 20% refund cap means you can’t use gas refunds to subsidize large transactions

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWhat were gas tokens and why don’t they work anymore?”

    • Good answer: β€œGas tokens exploited SSTORE refunds by storing data cheaply and clearing it during high gas periods. EIP-3529 reduced refunds from 15,000 to 4,800 gas and capped total refunds at 20% of transaction gas.”
    • Great answer: Adds that the 20% cap means you can’t use gas refunds to subsidize large transactions, and that SELFDESTRUCT refunds were removed entirely β€” breaking any economic model that relied on contract destruction for gas recovery.
  2. β€œHow does SSTORE gas work for writing the same value?”

    • Good answer: β€œWriting the same value that’s already in the slot costs only 100 gas (warm access, no state change). The EVM detects no-op writes and charges minimally.”
    • Great answer: Adds the optimization insight β€” Uniswap V2’s reentrancy guard uses 1β†’2β†’1 instead of 0β†’1β†’0 because non-zero-to-non-zero writes (5,000 gas) are cheaper than zero-to-non-zero (20,000 gas), and the partial refund for clearing is now too small to offset the initial cost.

Interview Red Flags:

  • 🚩 Designing token economics that rely on gas refunds β€” the 20% cap makes this unreliable
  • 🚩 Not knowing the SSTORE cost state machine (zeroβ†’nonzero, nonzeroβ†’nonzero, nonzeroβ†’zero, same value)
  • 🚩 β€œSELFDESTRUCT gives a gas refund” β€” hasn’t been true since London upgrade (2021)

Pro tip: Understanding the SSTORE state machine is a recurring theme across all of Part 4 (EVM deep dive). The cost differences between create (20,000), update (5,000), and reset (with 4,800 refund) directly shape how production protocols design their storage layouts.


πŸ’‘ Concept: Contract Size Limits (EIP-170)

Why this matters: If you’re building a full-featured DeFi protocol, you will hit the 24 KiB contract size limit. Knowing the strategies to work around it is essential practical knowledge.

Introduced in EIP-170, activated with the Spurious Dragon upgrade (November 2016)

The limit: Deployed contract bytecode cannot exceed 24,576 bytes (24 KiB). Attempting to deploy a larger contract reverts with an out-of-gas error.

Why DeFi protocols hit this:

Complex protocols (Aave, Uniswap, Compound) have many functions, modifiers, and internal logic. With Solidity’s inline expansion of internal functions, a contract can easily exceed 24 KiB.

Strategies to stay under the limit:

StrategyDescriptionTradeoff
Optimizeroptimizer = true, runs = 200 in foundry.tomlReduces bytecode but increases compile time
via_irvia_ir = true in foundry.toml β€” uses the Yul IR optimizerMore aggressive optimization, slower compilation
LibrariesExtract logic into library contracts with using forAdds DELEGATECALL overhead per call
Split contractsDivide into core + periphery contractsAdds deployment and integration complexity
Diamond patternEIP-2535 β€” modular facets behind a single proxyComplex but powerful for large protocols
Custom errorsReplace require(cond, "long string") with custom errorsSaves ~200 bytes per error message
Remove unused codeDead code still compiles into bytecodeFree β€” always do this first

Real DeFi examples:

  • Aave V3: Split into Pool.sol (core) + PoolConfigurator.sol + L2Pool.sol β€” each under 24 KiB
  • Uniswap V3: NonfungiblePositionManager.sol required careful optimization to stay under the limit
  • Compound V3: Uses the β€œComet” architecture with a single streamlined contract
# foundry.toml β€” common settings for large DeFi contracts
[profile.default]
optimizer = true
optimizer_runs = 200     # Lower = smaller bytecode, higher = cheaper runtime
via_ir = true           # Yul IR optimizer β€” often saves 10-20% bytecode
evm_version = "cancun"  # PUSH0 saves ~1 byte per zero-push

πŸ’Ό Job Market Context

Interview question: β€œYour contract is 26 KiB and won’t deploy. What do you do?”

What to say: β€œFirst, enable the optimizer with via_ir = true and lower optimizer_runs β€” this often saves 10-20% bytecode. Second, replace string revert messages with custom errors. Third, check for dead code. If it’s still too large, extract read-only view functions into a separate β€˜Lens’ contract, or split business logic into a core + periphery pattern. For very large protocols, the Diamond pattern (EIP-2535) provides modular facets behind a single proxy address. I’d also check if any internal functions should be external libraries instead.”


πŸ’‘ Concept: CREATE vs CREATE2 vs CREATE3

Why this matters: Deterministic contract deployment is critical DeFi infrastructure. Uniswap uses it for pool deployment, Safe for wallet creation, and understanding it is essential for the SELFDESTRUCT metamorphic attack explanation later in this module.

The three deployment methods:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CREATE (opcode 0xF0)                                      β”‚
β”‚ address = keccak256(sender, nonce)                        β”‚
β”‚                                                           β”‚
β”‚ - Address depends on deployer's nonce (tx count)          β”‚
β”‚ - Non-deterministic: deploying the same code from         β”‚
β”‚   different nonces gives different addresses              β”‚
β”‚ - Standard deployment method                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CREATE2 (opcode 0xF5, EIP-1014, Constantinople 2019)     β”‚
β”‚ address = keccak256(0xff, sender, salt, keccak256(code))  β”‚
β”‚                                                           β”‚
β”‚ - Address is DETERMINISTIC β€” depends on:                  β”‚
β”‚   1. The deployer address (sender)                        β”‚
β”‚   2. A user-chosen salt (bytes32)                         β”‚
β”‚   3. The init code hash                                   β”‚
β”‚ - Same inputs β†’ same address, regardless of nonce         β”‚
β”‚ - Enables counterfactual addresses (know the address      β”‚
β”‚   before deployment)                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CREATE3 (not an opcode β€” a pattern)                       β”‚
β”‚ address = keccak256(0xff, deployer, salt, PROXY_HASH)     β”‚
β”‚                                                           β”‚
β”‚ - Deploys a minimal proxy via CREATE2, then the proxy     β”‚
β”‚   deploys the actual contract via CREATE                  β”‚
β”‚ - Address depends ONLY on deployer + salt (not init code) β”‚
β”‚ - Same address across chains even if constructor args     β”‚
β”‚   differ (chain-specific config)                          β”‚
β”‚ - Used by: Axelar, LayerZero for cross-chain deployments  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

CREATE2 in DeFi β€” the key pattern:

// How Uniswap V2 deploys pair contracts deterministically
function createPair(address tokenA, address tokenB) external returns (address pair) {
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));

    // CREATE2: address is deterministic based on tokens
    pair = address(new UniswapV2Pair{salt: salt}());

    // Anyone can compute the pair address WITHOUT calling the factory:
    // address pair = address(uint160(uint256(keccak256(abi.encodePacked(
    //     hex"ff",
    //     factory,
    //     keccak256(abi.encodePacked(token0, token1)),
    //     INIT_CODE_HASH
    // )))));
}

Why counterfactual addresses matter:

// Routers can compute pair addresses off-chain without storage reads
function getAmountsOut(uint256 amountIn, address[] calldata path)
    external view returns (uint256[] memory)
{
    for (uint256 i = 0; i < path.length - 1; i++) {
        // No SLOAD needed! Compute pair address from tokens:
        address pair = computePairAddress(path[i], path[i + 1]);
        // This saves ~2,100 gas (cold SLOAD) per hop
        (uint256 reserveIn, uint256 reserveOut) = getReserves(pair);
        amounts[i + 1] = getAmountOut(amountIn, reserveIn, reserveOut);
    }
}

πŸ’» Quick Try:

Verify CREATE2 address computation yourself:

contract CREATE2Demo {
    event Deployed(address addr);

    function deploy(bytes32 salt) external returns (address) {
        // Deploy a minimal contract via CREATE2
        SimpleChild child = new SimpleChild{salt: salt}();
        emit Deployed(address(child));
        return address(child);
    }

    function predict(bytes32 salt) external view returns (address) {
        // Compute the address WITHOUT deploying
        return address(uint160(uint256(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),           // deployer
            salt,                    // user-chosen salt
            keccak256(type(SimpleChild).creationCode)  // init code hash
        )))));
    }
}

contract SimpleChild {
    uint256 public value = 42;
}

Call predict(0x01), then call deploy(0x01). The addresses match β€” deterministic, no storage reads needed. This is the core of Uniswap’s pool address computation.

Safe (Gnosis Safe) wallet deployment:

CREATE2 enables counterfactual wallets β€” you can send funds to a Safe address before the Safe is even deployed. The address is computed from the owners + threshold + salt. When the user is ready, they deploy the Safe at the pre-computed address and the funds are already there.

The metamorphic contract risk (now dead):

CREATE2 address depends on init code hash. If you can SELFDESTRUCT a contract and redeploy different code at the same address, you get a metamorphic contract. EIP-6780 killed this β€” see SELFDESTRUCT Changes below.

πŸ” Deep dive: Module 7 (Deployment) covers CREATE2 deployment scripts and cross-chain deployment patterns in detail. This section provides the conceptual foundation.

πŸ’Ό Job Market Context

Interview question: β€œWhat’s CREATE2 and why does Uniswap use it?”

What to say: β€œCREATE2 gives deterministic contract addresses based on the deployer, a salt, and the init code hash β€” unlike CREATE where the address depends on the nonce. Uniswap uses it so any contract can compute a pair’s address off-chain by hashing the two token addresses, without needing a storage read. This saves ~2,100 gas per pool lookup in multi-hop swaps. Safe uses it for counterfactual wallets β€” you know the wallet address before deployment so you can send funds to it first. The newer CREATE3 pattern makes addresses independent of init code, which is useful for cross-chain deployments where constructor args differ per chain.”


πŸ’‘ Concept: Precompile Landscape

Why this matters: Precompiles are native EVM functions at fixed addresses, much cheaper than equivalent Solidity. You’ve used ecrecover (address 0x01) every time you verify an ERC-2612 permit signature.

The precompile addresses:

AddressNameGasDeFi Usage
0x01ecrecover3,000ERC-2612 permit, EIP-712 signatures, meta-transactions
0x02SHA-25660 + 12/wordBitcoin SPV proofs (rare in DeFi)
0x03RIPEMD-160600 + 120/wordBitcoin address derivation (rare)
0x04Identity (memcpy)15 + 3/wordCompiler optimization (transparent)
0x05modexpVariableRSA verification, large-number math
0x06ecAdd (BN254)150zkSNARK verification (Tornado Cash, zkSync)
0x07ecMul (BN254)6,000zkSNARK verification
0x08ecPairing (BN254)34,000 + per-pairzkSNARK verification
0x09blake2fVariableZcash interop (rare)
0x0apoint evaluation50,000EIP-4844 blob verification
0x0b-0x13BLS12-381VariableValidator signatures (see above)

The ones that matter for DeFi:

  1. ecrecover (0x01) β€” Used in every permit() call, every EIP-712 typed data signature, every meta-transaction. You’ve been using this indirectly through ECDSA.recover() from OpenZeppelin.

  2. BN254 pairing (0x06-0x08) β€” The foundation of zkSNARK verification on Ethereum. Tornado Cash, zkSync’s proof verification, and privacy protocols all depend on these. Note: this is a different curve from BLS12-381.

  3. BLS12-381 (0x0b-0x13) β€” New in Pectra. Enables on-chain validator signature verification. See the BLS section above.

Key distinction: BN254 (alt-bn128) is for zkSNARKs. BLS12-381 is for signature aggregation. Different curves, different use cases. Confusing them is a common interview mistake.


πŸ’‘ Dencun Upgrade β€” EIP-1153 & EIP-4844

πŸ’‘ Concept: Transient Storage Deep Dive (EIP-1153)

Why this matters: You’ve used transient in Solidity. Now understand what the EVM actually does. Uniswap V4’s entire architectureβ€”the flash accounting that lets you batch swaps, add liquidity, and pay only net balancesβ€”depends on transient storage behaving exactly right across CALL boundaries.

πŸ”— Connection to Module 1: Remember the TransientGuard exercise? You used the transient keyword and raw tstore/tload assembly. Now we’re diving into how EIP-1153 actually works at the EVM levelβ€”the opcodes, gas costs, and why it’s revolutionary for DeFi.

Introduced in EIP-1153, activated with the Dencun upgrade (March 2024)

The model:

Transient storage is a key-value store (32-byte keys β†’ 32-byte values) that:

  • Is scoped per contract, per transaction (same scope as regular storage, but transaction lifetime)
  • Gets wiped clean when the transaction endsβ€”values are never written to disk
  • Persists across external calls within the same transaction (unlike memory, which is per-call-frame)
  • Costs ~100 gas for both TSTORE and TLOAD (vs ~100 for warm SLOAD, but ~2,100-20,000 for SSTORE)
  • Reverts correctlyβ€”if a call reverts, transient storage changes in that call frame are also reverted

πŸ“Š The critical distinction: Transient storage sits between memory (per-call-frame, byte-addressed) and storage (permanent, slot-addressed). It’s slot-addressed like storage but temporary like memory. The key difference from memory is that it survives across CALL, DELEGATECALL, and STATICCALL boundaries within the same transaction.

πŸ” Deep Dive: Transient Storage Memory Layout

Visual comparison of the three storage types:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       CALLDATA                              β”‚
β”‚  - Byte-addressed, read-only input to a call               β”‚
β”‚  - Per call frame (each call has its own calldata)         β”‚
β”‚  - ~3 gas per 32 bytes (CALLDATALOAD)                      β”‚
β”‚  - Cheaper than memory for read-only access                β”‚
β”‚  - In DeFi: function args, encoded swap paths, proofs      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       RETURNDATA                            β”‚
β”‚  - Byte-addressed, output from the last external call      β”‚
β”‚  - Overwritten on each new CALL/STATICCALL/DELEGATECALL    β”‚
β”‚  - ~3 gas per 32 bytes (RETURNDATACOPY)                    β”‚
β”‚  - In DeFi: decoded return values, revert reasons          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         MEMORY                              β”‚
β”‚  - Byte-addressed (0x00, 0x01, 0x02, ...)                  β”‚
β”‚  - Per call frame (isolated to each function call)         β”‚
β”‚  - Wiped when call returns                                 β”‚
β”‚  - ~3 gas per word access                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓ External call (CALL/DELEGATECALL) ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    New memory context                       β”‚
β”‚  - Previous memory is inaccessible                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   TRANSIENT STORAGE                         β”‚
β”‚  - Slot-addressed (slot 0, slot 1, slot 2, ...)           β”‚
β”‚  - Per contract, per transaction                           β”‚
β”‚  - Persists across all calls in same transaction          β”‚
β”‚  - Wiped when transaction ends                            β”‚
β”‚  - ~100 gas per TLOAD/TSTORE                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓ External call (CALL/DELEGATECALL) ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   TRANSIENT STORAGE                         β”‚
β”‚  - SAME transient storage accessible! ✨                   β”‚
β”‚  - This is the key difference from memory                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      STORAGE                                β”‚
β”‚  - Slot-addressed (slot 0, slot 1, slot 2, ...)           β”‚
β”‚  - Per contract, permanent on-chain                        β”‚
β”‚  - Persists across transactions                            β”‚
β”‚  - First access: ~2,100 gas (cold) β€” see EIP-2929 below   β”‚
β”‚  - Subsequent: ~100 gas (warm)                             β”‚
│  - Writing zero→nonzero: ~20,000 gas                       │
│  - Writing nonzero→nonzero: ~5,000 gas                     │
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step-by-step example: Transient storage across calls

contract Parent {
    function execute() external {
        // Transaction starts - transient storage is empty
        assembly { tstore(0, 100) }  // Write 100 to slot 0

        Child child = new Child();
        child.readTransient();  // Child CANNOT see Parent's transient storage
                                // (different contract = different transient storage)

        this.callback();  // External call to self - CAN see transient storage
    }

    // Note: `view` is valid here β€” tload is a read-only opcode (like sload).
    // The compiler treats transient storage reads the same as storage reads
    // for function mutability purposes.
    function callback() external view returns (uint256) {
        uint256 value;
        assembly { value := tload(0) }  // Reads 100 ✨
        return value;
    }
}

Gas cost breakdown - actual numbers:

OperationCold AccessWarm AccessNotes
SLOAD (storage read)2,100 gas100 gasFirst access in tx is β€œcold” (EIP-2929)
SSTORE (zero→nonzero)20,000 gas20,000 gasAdds new data to state (cold/warm affects slot access, not write cost)
SSTORE (nonzero→nonzero)5,000 gas5,000 gasModifies existing data (+2,100 cold surcharge on first access)
SSTORE (nonzero→zero)5,000 gas5,000 gasRemoves data (gets partial refund — EIP-3529)
TLOAD100 gas100 gasAlways same cost ✨
TSTORE100 gas100 gasAlways same cost ✨
MLOAD/MSTORE (memory)~3 gas~3 gasCheapest but doesn’t persist

Note: SSTORE costs shown are the base write cost. If the storage slot hasn’t been accessed yet in the transaction (cold), EIP-2929 adds a 2,100 gas cold access surcharge on top. Once the slot is warm, subsequent SSTOREs to the same slot pay only the base cost. See EIP-2929 section for the full cold/warm model.

Real cost comparison for reentrancy guard:

// Classic storage guard (OpenZeppelin ReentrancyGuard pattern)
contract StorageGuard {
    uint256 private _locked = 1;  // 20,000 gas deployment cost

    modifier nonReentrant() {
        require(_locked == 1);     // SLOAD: 2,100 gas (cold first time)
        _locked = 2;               // SSTORE: 5,000 gas (nonzero→nonzero)
        _;
        _locked = 1;               // SSTORE: 5,000 gas (nonzero→nonzero)
    }
    // Total: ~12,100 gas first call, ~10,100 gas subsequent calls
}

// Transient storage guard
contract TransientGuard {
    bool transient _locked;        // 0 gas deployment cost ✨

    modifier nonReentrant() {
        require(!_locked);         // TLOAD: 100 gas
        _locked = true;            // TSTORE: 100 gas
        _;
        _locked = false;           // TSTORE: 100 gas
    }
    // Total: ~300 gas (40x cheaper!) ✨
}

Why this matters for DeFi:

In a Uniswap V4 swap that touches 5 pools in a single transaction:

  • With storage locks: 5 Γ— 12,100 = 60,500 gas just for reentrancy protection
  • With transient locks: 5 Γ— 300 = 1,500 gas for the same protection
  • Savings: 59,000 gas per multi-pool swap (enough to do 590+ more TLOAD operations!)

DeFi use cases beyond reentrancy locks:

  1. Flash accounting (Uniswap V4): Track balance deltas across multiple operations in a single transaction, settling the net difference at the end. The PoolManager uses transient storage to accumulate what each caller owes or is owed, then enforces that everything balances to zero before the transaction completes.

  2. Temporary approvals: ERC-20 approvals that last only for the current transactionβ€”approve, use, and automatically revoke, all without touching persistent storage.

  3. Callback validation: A contract can set a transient flag before making an external call that expects a callback, then verify in the callback that it was legitimately triggered by the calling contract.

πŸ’» Quick Try:

Test transient storage in Remix (requires Solidity 0.8.24+, set EVM version to cancun):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract TransientDemo {
    uint256 transient counter;  // Lives only during transaction

    // Note: `view` is valid β€” reading transient storage (tload) is treated
    // like reading regular storage (sload) for mutability purposes.
    function demonstrateTransient() external view returns (uint256, uint256) {
        // Read current value (will be 0 on first call in tx)
        uint256 before = counter;

        // In a real non-view function, you could: counter++;
        // But it would reset to 0 in the next transaction

        return (before, 0);  // Always returns (0, 0) in separate txs
    }

    function demonstratePersistence() external returns (uint256, uint256) {
        uint256 before = counter;
        counter++;  // Increment
        uint256 after = counter;

        // Call yourself - transient storage persists across calls!
        this.checkPersistence();

        return (before, after);  // Returns (0, 1) first time, (0, 1) every time
    }

    function checkPersistence() external view returns (uint256) {
        return counter;  // Can read the value set by caller! ✨
    }
}

Try calling demonstratePersistence() twice. Notice that counter is always 0 at the start of each transaction.

πŸŽ“ Intermediate Example: Building a Simple Flash Accounting System

Before diving into Uniswap V4’s complex implementation, let’s build a minimal flash accounting example:

// A simple "borrow and settle" pattern using transient storage
contract SimpleFlashAccount {
    mapping(address => uint256) public balances;

    // Track debt in transient storage
    int256 transient debt;
    bool transient locked;

    modifier withLock() {
        require(!locked, "Locked");
        locked = true;
        debt = 0;  // Reset debt tracker
        _;
        require(debt == 0, "Must settle all debt");  // Enforce settlement
        locked = false;
    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function flashBorrow(uint256 amount) external withLock {
        // "Borrow" tokens (just accounting, not actual transfer)
        debt -= int256(amount);  // Owe the contract

        // In real usage, caller would do swaps, arbitrage, etc.
        // For demo, just settle the debt immediately
        flashRepay(amount);

        // withLock modifier ensures debt == 0 before finishing
    }

    function flashRepay(uint256 amount) public {
        debt += int256(amount);  // Pay back the debt
    }
}

How this connects to Uniswap V4:

Uniswap V4’s PoolManager does exactly this, but for hundreds of pools:

  • unlock() opens a flash accounting session (calls back via unlockCallback)
  • Swaps, adds liquidity, removes liquidity all update transient deltas
  • settle() enforces that you’ve paid what you owe (or received what you’re owed)
  • All within ~300 gas for the unlock mechanism ✨

⚠️ Common pitfallβ€”new reentrancy vectors: Because TSTORE costs only ~100 gas, it can execute within the 2,300 gas stipend that transfer() and send() forward. A contract receiving ETH via transfer() can now execute TSTORE (something impossible with SSTORE). This creates new reentrancy attack surfaces in contracts that assumed 2,300 gas was β€œsafe.” This is one reason transfer() and send() are deprecated β€” Solidity 0.8.31 emits compiler warnings, and they’ll be removed entirely in 0.9.0.

πŸ” Deep dive: ChainSecurity - TSTORE Low Gas Reentrancy demonstrates the attack with code examples. Their GitHub repo provides exploit POCs.

The attack in code:

// VULNERABLE: This vault uses a transient-storage-based reentrancy guard,
// but sends ETH via transfer() BEFORE updating state.
contract VulnerableVault {
    uint256 transient _locked;

    modifier nonReentrant() {
        require(_locked == 0, "locked");
        _locked = 1;
        _;
        _locked = 0;
    }

    mapping(address => uint256) public balances;

    function withdraw() external nonReentrant {
        uint256 bal = balances[msg.sender];
        // Sends ETH via transfer() β€” 2,300 gas stipend
        payable(msg.sender).transfer(bal);
        balances[msg.sender] = 0;  // State update AFTER transfer
    }
}

// ATTACKER: Pre-Cancun, transfer()'s 2,300 gas stipend was too little
// for SSTORE (~5,000+ gas), so reentrancy via transfer() was "impossible."
// Post-Cancun, TSTORE costs only ~100 gas β€” well within the 2,300 budget.
contract Attacker {
    VulnerableVault vault;
    uint256 transient _attackCount;  // TSTORE fits in 2,300 gas!

    receive() external payable {
        // This executes within transfer()'s 2,300 gas stipend.
        // Pre-Cancun: SSTORE here would exceed gas limit β†’ safe.
        // Post-Cancun: TSTORE costs ~100 gas β†’ attack is possible.
        if (_attackCount < 3) {
            _attackCount += 1;      // ~100 gas (TSTORE)
            vault.withdraw();       // Re-enters! Guard uses transient storage
                                    // but the SAME transient slot is already 1
                                    // Wait β€” the guard checks _locked == 0...
        }
    }
}
// KEY INSIGHT: The guard actually blocks this specific attack because _locked
// is still 1 during re-entry. The REAL danger is contracts that DON'T use
// a reentrancy guard but relied on transfer()'s gas limit as implicit protection.
// Post-Cancun, transfer()/send() are NO LONGER safe assumptions for reentrancy
// prevention. Always use explicit guards + checks-effects-interactions.

Bottom line: The transient reentrancy guard itself is fine β€” it’s contracts that relied on transfer()’s gas limit instead of a guard that are now vulnerable. Any contract that assumed β€œ2,300 gas isn’t enough to do anything dangerous” is broken post-Cancun.

πŸ—οΈ Real usage:

Read Uniswap V4’s PoolManager.solβ€”the entire protocol is built on transient storage tracking deltas. You’ll see this pattern in Part 3.

πŸ“– Code Reading Strategy for Uniswap V4 PoolManager:

When you open PoolManager.sol, follow this path to understand the flash accounting:

  1. Start at the top: Find the transient storage declarations

    // Look for transient state in PoolManager and related contracts:
    // Currency deltas tracked per-caller in transient storage
    // NonzeroDeltaCount tracks how many currencies have outstanding deltas
    
  2. Understand the unlock mechanism: Search for function unlock()

    • Notice how it uses a callback pattern: IUnlockCallback(msg.sender).unlockCallback(...)
    • The caller executes all operations inside the callback
    • _nonzeroDeltaCount tracks how many currencies still have unsettled deltas
  3. Follow a swap flow: Search for function swap()

    • See how it calls _accountPoolBalanceDelta() to update transient deltas
    • Notice: No actual token transfers happen yet!
  4. Understand settlement: Search for function settle()

    • This is where actual token transfers occur
    • It reduces the debt tracked in _currencyDelta
    • If debt > 0 after all operations, transaction reverts
  5. The key insight:

    • A user can swap Pool A β†’ Pool B β†’ Pool C in one transaction
    • Each swap updates transient deltas (cheap!)
    • Only the NET difference is transferred at the end (one transfer, not three!)

Why this is revolutionary:

  • Before V4: Swap Aβ†’B = transfer. Swap Bβ†’C = transfer. Two transfers, two SSTORE operations.
  • After V4: Swap Aβ†’Bβ†’C = three TSTORE operations, ONE transfer at the end. ~50,000 gas saved per multi-hop swap.

πŸ” Deep dive: Dedaub - Transient Storage Impact Study analyzes real-world usage patterns. Hacken - Uniswap V4 Transient Storage Security covers security considerations in production flash accounting.

πŸ’Ό Job Market Context: Transient Storage

Interview question you WILL be asked:

β€œWhat’s the difference between transient storage and memory?”

What to say (30-second answer):

β€œMemory is byte-addressed and isolated per call frameβ€”when you make an external call, the callee can’t access your memory. Transient storage is slot-addressed like regular storage, but it persists across external calls within the same transaction and gets wiped when the transaction ends. This makes it perfect for flash accounting patterns like Uniswap V4, where you want to track deltas across multiple pools and settle the net at the end. Gas-wise, both TLOAD and TSTORE cost ~100 gas regardless of warm/cold state, versus storage which ranges from 2,100 to 20,000 gas depending on the operation.”

Follow-up question:

β€œWhen would you use transient storage instead of memory or regular storage?”

What to say:

β€œUse transient storage when you need to share state across external calls within a single transaction. Classic examples: reentrancy guards (~40x cheaper than storage guards), flash accounting in AMMs, temporary approvals, or callback validation. Don’t use it if the data needs to persist across transactionsβ€”that’s what regular storage is for. And don’t use it if you only need data within a single function scopeβ€”memory is cheaper at ~3 gas per access.”

Interview Red Flags:

  • 🚩 β€œTransient storage is like memory but cheaper” β€” No! It’s more expensive than memory (~100 vs ~3 gas)
  • 🚩 β€œYou can use transient storage to avoid storage costs” β€” Only if data doesn’t need to persist across transactions
  • 🚩 β€œTSTORE is always cheaper than SSTORE” β€” True, but irrelevant if you need persistence

What production DeFi engineers know:

  1. Reentrancy guards: If your protocol will be deployed post-Cancun (March 2024), use transient guards
  2. Flash accounting: Essential for any multi-step operation (swaps, liquidity management, flash loans)
  3. The 2,300 gas pitfall: TSTORE works within transfer()/send() stipendβ€”creates new reentrancy vectors
  4. Testing: Foundry’s vm.transient* cheats for testing transient storage behavior

Pro tip: Flash accounting is THE architectural pattern to understand for DEX/AMM roles. If you can whiteboard how Uniswap V4’s PoolManager tracks deltas in transient storage and enforces settlement, you’ll demonstrate systems-level thinking that separates senior candidates from mid-level ones.


πŸ’‘ Concept: Proto-Danksharding (EIP-4844)

Why this matters: If you’re building on L2 (Arbitrum, Optimism, Base, Polygon zkEVM), your users’ transaction costs dropped 90-95% after Dencun. Understanding blob transactions explains why.

Introduced in EIP-4844, activated with the Dencun upgrade (March 2024)

What changed:

EIP-4844 introduced β€œblob transactions”—a new transaction type (Type 3) that carries large data blobs (128 KiB / 131,072 bytes each) at significantly lower cost than calldata. The blobs are available temporarily (roughly 18 days) and then pruned from the consensus layer.

πŸ“Š The impact on L2 DeFi:

Before Dencun, L2s posted transaction data to L1 as expensive calldata (~16 gas/byte). After Dencun, they post to cheap blob space (~1 gas/byte or less, depending on demand).

πŸ” Deep Dive: Blob Fee Market Math

The blob fee formula:

Blobs use an independent fee market from regular gas. The blob base fee adjusts based on cumulative excess blob gas:

blob_base_fee = MIN_BLOB_BASE_FEE Γ— e^(excess_blob_gas / BLOB_BASE_FEE_UPDATE_FRACTION)

Where:
- Each blob = 131,072 blob gas
- Target: 3 blobs per block = 393,216 blob gas
- Maximum: 6 blobs per block = 786,432 blob gas
- excess_blob_gas accumulates across blocks:
    excess(block_n) = max(0, excess(block_n-1) + blob_gas_used - 393,216)
- BLOB_BASE_FEE_UPDATE_FRACTION = 3,338,477
- MIN_BLOB_BASE_FEE = 1 wei

Step-by-step calculation:

  1. Block has 3 blobs (target): excess_blob_gas unchanged β†’ fee stays the same
  2. Block has 6 blobs (max): excess_blob_gas increases by 393,216 β†’ fee multiplies by e^(393,216/3,338,477) β‰ˆ 1.125 (~12.5% increase per max block)
  3. Block has 0 blobs: excess_blob_gas decreases by up to 393,216 β†’ fee drops
  4. After ~8.5 consecutive max blocks: excess accumulates enough for fee to roughly triple (e^1 β‰ˆ 2.718)

Concrete numerical verification:

Let’s trace the blob base fee through a sequence of full blocks to see the exponential in action:

Starting state: excess_blob_gas = 0, blob_base_fee = 1 wei (minimum)

Block 1: 6 blobs (max) β†’ excess += (6 - 3) Γ— 131,072 = +393,216
  excess = 393,216
  fee = 1 Γ— e^(393,216 / 3,338,477) = 1 Γ— e^0.1178 β‰ˆ 1.125 wei

Block 2: 6 blobs again β†’ excess += 393,216
  excess = 786,432
  fee = 1 Γ— e^(786,432 / 3,338,477) = 1 Γ— e^0.2355 β‰ˆ 1.266 wei

Block 5: still max β†’ excess = 1,966,080
  fee = 1 Γ— e^0.589 β‰ˆ 1.80 wei

Block 9: still max β†’ excess = 3,539,000
  fee = 1 Γ— e^1.06 β‰ˆ 2.89 wei  (roughly tripled from minimum)

Block 20: still max β†’ excess = 7,864,320
  fee = 1 Γ— e^2.36 β‰ˆ 10.5 wei  (10x from minimum)

The key insight: it takes ~20 consecutive max-capacity blocks (about 4 minutes at 12s/block) to reach just 10x the minimum fee. The system is designed to stay cheap under normal usage. Only sustained, extreme demand drives fees up β€” and a single empty block starts bringing them back down.

In plain terms: e^(excess / fraction) means the fee grows exponentially β€” slowly at first, then accelerating. The large denominator (3,338,477) is a dampening factor that keeps the growth gentle.

Why this matters:

The fee adjusts gradually β€” it takes many consecutive full blocks to drive fees up significantly. In practice, blob demand rarely sustains max capacity for long, so blob fees stay very low most of the time.

Real cost comparison with actual protocols:

ProtocolOperationBefore Dencun (Calldata)After Dencun (Blobs)Your Cost
Aave on BaseSupply USDC~$0.50~$0.0198% cheaper ✨
Uniswap on ArbitrumSwap ETHβ†’USDC~$1.20~$0.0397.5% cheaper ✨
GMX on ArbitrumOpen position~$2.00~$0.0597.5% cheaper ✨
Velodrome on OptimismAdd liquidity~$0.80~$0.0297.5% cheaper ✨

(Costs as of post-Dencun 2024, at ~$3,000 ETH and normal L1 activity)

Concrete math example:

L2 posts a batch of 1,000 transactions:

  • Average transaction data: 200 bytes
  • Total data: 200,000 bytes

Before Dencun (calldata):

Cost = 200,000 bytes Γ— 16 gas/byte = 3,200,000 gas
At 20 gwei L1 gas price and $3,000 ETH:
= 3,200,000 Γ— 20 Γ— 10^-9 Γ— $3,000
= $192 per batch
= $0.192 per transaction

After Dencun (blobs):

Blob size: 128 KB = 131,072 bytes
Blobs needed: 200,000 / 131,072 β‰ˆ 2 blobs

Two separate costs (blobs have their OWN fee market):

1. Blob fee (priced in blob gas, NOT regular gas):
   Blob gas = 2 blobs Γ— 131,072 = 262,144 blob gas
   At minimum blob price (~1 wei per blob gas):
   = 262,144 wei β‰ˆ $0.0000008 (essentially free)

2. L1 transaction overhead (regular gas for the Type 3 tx):
   ~50,000 gas for tx base + versioned hash calldata
   At 20 gwei and $3,000 ETH:
   = 50,000 Γ— 20 Γ— 10^-9 Γ— $3,000 = $3.00

Total β‰ˆ $3.00 per batch = $0.003 per transaction

Savings: ~98% reduction ($192 β†’ ~$3)

The blob data itself is nearly free β€” the remaining cost is just the L1 transaction overhead. During blob fee spikes (high demand), the blob portion increases, but typical post-Dencun costs match the real-world figures in the table above.

πŸ’» Quick Try:

EIP-4844 is infrastructure-level (L2 sequencers use it to post data to L1), not application-level. You won’t write blob transaction code in your DeFi contracts. But you CAN read the blob base fee on-chain:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Read blob base fee β€” available in contracts targeting Cancun+
contract BlobFeeReader {
    /// @dev block.blobbasefee returns the current blob base fee (EIP-7516)
    function currentBlobBaseFee() external view returns (uint256) {
        return block.blobbasefee;
    }

    /// @dev Compare blob fee to regular gas price
    function feeComparison() external view returns (
        uint256 blobBaseFee,
        uint256 regularGasPrice,
        uint256 ratio
    ) {
        blobBaseFee = block.blobbasefee;
        regularGasPrice = tx.gasprice;
        ratio = regularGasPrice > 0 ? blobBaseFee / regularGasPrice : 0;
    }
}

Deploy in Remix (set EVM to cancun) and call currentBlobBaseFee(). In a local environment it returns 1 (minimum). On mainnet, it fluctuates based on blob demand.

Explore further:

  1. Etherscan Dencun Upgrade β€” first Dencun block, March 13, 2024. Look for Type 3 blob transactions.
  2. L2Beat Blobs β€” real-time blob usage by L2s, fee market dynamics.
  3. Read blob data: Use eth_getBlob RPC if your node supports it (within 18-day window).

For application developers: Your L2 DeFi contract doesn’t interact with blobs directly. The impact is on user economics: design for higher volume, smaller transactions.

From a protocol developer’s perspective:

  • L2 DeFi became dramatically cheaper, accelerating adoption
  • block.blobbasefee and blobhash() are now available in Solidity (though you’ll rarely use them directly in application contracts)
  • Understanding the blob fee market matters if you’re building infrastructure-level tooling (sequencers, data availability layers)

πŸ” Deep dive: The blob fee market uses a separate fee mechanism from regular gas. Read EIP-4844 blob fee market dynamics to understand how blob pricing adjusts based on demand.

πŸ’Ό Job Market Context: EIP-4844 & L2 DeFi

Interview question you WILL be asked:

β€œWhy did L2 transaction costs drop 90%+ after the Dencun upgrade?”

What to say (30-second answer):

β€œBefore Dencun, L2 rollups posted transaction data to L1 as calldata, which costs ~16 gas per byte. EIP-4844 introduced blob transactionsβ€”a new transaction type that carries up to ~128 KB of data per blob at ~1 gas/byte or less. Blobs use a separate fee market from regular gas, targeting 3 blobs per block with a max of 6. Since L2s were the primary users and adoption was gradual, blob fees stayed near-zero, dropping L2 costs by 90-97%. The blobs are available for ~18 days then pruned, which is fine since L2 nodes already have the data.”

Follow-up question:

β€œDoes EIP-4844 affect how you build DeFi protocols on L2?”

What to say:

β€œNot directly for application contracts. EIP-4844 is an L1 infrastructure changeβ€”the L2 sequencer uses blobs to post data to L1, but your DeFi contract on the L2 doesn’t interact with blobs. The impact is user acquisition: cheaper transactions mean more users can afford to use your protocol. For example, a $0.02 Aave supply on Base is viable for small amounts, whereas $0.50 wasn’t. Your protocol should be designed for higher volume, smaller transactions post-Dencun.”

Interview Red Flags:

  • 🚩 β€œEIP-4844 is full Danksharding” β€” No! It’s proto-Danksharding. Full danksharding will shard blob data across validators.
  • 🚩 β€œBlobs are stored on-chain forever” β€” No! Blobs are pruned after ~18 days. L2 nodes keep the data.
  • 🚩 β€œMy DeFi contract needs to handle blobs” β€” No! Blobs are for L2β†’L1 data posting, not application contracts.

What production DeFi engineers know:

  1. L2 selection matters: Post-Dencun, Base, Optimism, Arbitrum became equally cheap. Choose based on liquidity, ecosystem, not cost.
  2. Blob fee spikes: During congestion, blob fees can spike (like March 2024 inscriptions). Your L2 costs are tied to blob fee volatility.
  3. The 18-day window: If you’re building infra (block explorers, analytics), you need to archive blob data within 18 days.
  4. Future scaling: EIP-4844 is step 1. Full danksharding will increase from 6 max blobs per block to potentially 64+, further reducing costs.

Pro tip: When interviewing for L2-focused teams, frame EIP-4844 as a protocol design lever: β€œPost-Dencun, I’d design for higher frequency, smaller transactions because the L1 data cost bottleneck is largely gone.” This shows you think about infrastructure economics, not just smart contract logic.

MEV implications of blobs:

EIP-4844 affects MEV economics in subtle ways:

  • L2 sequencer MEV: Cheaper L2 transactions mean more transaction volume, which means more MEV opportunities for L2 sequencers. This is why shared sequencer designs and L2 MEV protection (Flashbots Protect on L2) are becoming critical
  • Cross-domain MEV: With blobs, L2s batch data to L1 faster and cheaper. This tightens the window for cross-L1/L2 arbitrage β€” searchers must be faster
  • L1 builder dynamics: Blob transactions compete for inclusion alongside regular transactions. Builders must optimize for both fee markets simultaneously, adding complexity to block building algorithms

πŸ’‘ Concept: PUSH0 (EIP-3855, Shanghai) and MCOPY (EIP-5656, Cancun)

Behind-the-scenes optimizations that make your compiled contracts smaller and cheaper:

Note: PUSH0 was activated in the Shanghai upgrade (April 2023), predating Dencun. MCOPY was activated in Dencun (March 2024). Both are covered here because they affect post-Dencun compiler output.

PUSH0 (EIP-3855): A new opcode that pushes the value 0 onto the stack. Previously, pushing zero required PUSH1 0x00 (2 bytes). PUSH0 is a single byte. This saves gas and reduces bytecode size. The Solidity compiler uses it automatically when targeting Shanghai or later.

MCOPY (EIP-5656): Efficient memory-to-memory copy. Previously, copying memory required loading and storing word by word, or using identity precompile tricks. MCOPY does it in a single opcode. The compiler can use this for struct copying, array slicing, and similar operations.

πŸ” Deep Dive: Bytecode Before & After

PUSH0 example - initializing variables:

function example() external pure returns (uint256) {
    uint256 x = 0;
    return x;
}

Before PUSH0 (EVM < Shanghai):

PUSH1 0x00    // 0x60 0x00 (2 bytes, 3 gas)
PUSH1 0x00    // 0x60 0x00 (2 bytes, 3 gas)
RETURN        // 0xf3 (1 byte)

After PUSH0 (EVM >= Shanghai):

PUSH0         // 0x5f (1 byte, 2 gas)
PUSH0         // 0x5f (1 byte, 2 gas)
RETURN        // 0xf3 (1 byte)

Savings:

  • Bytecode size: 2 bytes smaller (4 bytes β†’ 2 bytes for two pushes)
  • Gas cost: 2 gas cheaper (6 gas β†’ 4 gas for two pushes)
  • Deployment cost: 2 bytes Γ— 200 gas/byte = 400 gas saved on deployment

Real impact on a typical contract:

A contract that initializes 20 variables to zero:

  • Before: 20 Γ— 2 bytes = 40 bytes, 20 Γ— 3 gas = 60 gas
  • After: 20 Γ— 1 byte = 20 bytes, 20 Γ— 2 gas = 40 gas
  • Deployment savings: 20 bytes Γ— 200 gas/byte = 4,000 gas
  • Runtime savings: 20 gas per function call

MCOPY example - copying structs:

struct Position {
    uint256 amount;
    uint256 timestamp;
    address owner;
}

function copyPosition(Position memory pos) internal pure returns (Position memory) {
    return pos;  // Copies the struct in memory
}

Before MCOPY (EVM < Cancun):

// Load and store word by word (3 words for the struct)
MLOAD offset        // Load word 1
MSTORE dest        // Store word 1
MLOAD offset+32    // Load word 2
MSTORE dest+32     // Store word 2
MLOAD offset+64    // Load word 3
MSTORE dest+64     // Store word 3

// Total: 6 operations Γ— ~3-6 gas = ~18-36 gas

After MCOPY (EVM >= Cancun):

MCOPY dest offset 96    // Copy 96 bytes (3 words) in one operation

// Total: ~3 gas per word + base cost = ~9-12 gas

Savings:

  • Gas cost: ~50% cheaper for typical struct copies
  • Bytecode size: Smaller (1 opcode vs 6 opcodes)

Real impact in DeFi:

Uniswap V4 pools copy position structs frequently during swaps:

  • Before: ~30 gas per position copy
  • After: ~12 gas per position copy
  • On a 5-hop swap (5 position copies): 90 gas saved

What you need to know: You won’t write code that explicitly uses these opcodes, but they make your compiled contracts smaller and cheaper. Make sure your compiler’s EVM target is set to cancun or later in your Foundry config:

# foundry.toml
[profile.default]
evm_version = "cancun"  # Enables PUSH0, MCOPY, and transient storage

πŸ’Ό Job Market Context: PUSH0 & MCOPY

Interview question:

β€œWhat are some gas optimizations from recent EVM upgrades?”

What to say (30-second answer):

β€œPUSH0 from Shanghai (EIP-3855) saves 1 byte and 1 gas every time you push zero to the stackβ€”common in variable initialization and padding. MCOPY from Cancun (EIP-5656) makes memory copies ~50% cheaper by replacing word-by-word MLOAD/MSTORE loops with a single operation. These are automatic optimizations when you set your compiler’s EVM target to cancun or later in foundry.toml. For a typical DeFi contract, PUSH0 saves ~5-10 KB of bytecode and hundreds of gas across all zero-pushes, while MCOPY optimizes struct copying in AMM swaps and lending protocols. The compiler handles theseβ€”you don’t write them explicitly.”

Follow-up question:

β€œShould I manually optimize my code to use PUSH0 and MCOPY?”

What to say:

β€œNo, the Solidity compiler handles these automatically when targeting the right EVM version. Trying to manually optimize at the opcode level is an anti-patternβ€”it makes code harder to read and maintain for minimal gain. Focus on high-level optimizations like reducing storage operations, using memory efficiently, and batching transactions. Set evm_version = \"cancun\" in your config and let the compiler do its job. The only time you’d write assembly with these opcodes is if you’re building compiler tooling or doing very specialized low-level work.”

Interview Red Flags:

  • 🚩 β€œI manually use PUSH0 in my code” β€” The compiler does this automatically
  • 🚩 β€œMCOPY makes all operations faster” β€” Only memory-to-memory copies, not storage or other operations
  • 🚩 β€œSetting EVM version to cancun might break my Solidity code” β€” Source code is backwards compatible. However, if deploying to a chain that hasn’t activated Cancun, the bytecode will fail (new opcodes aren’t available). Always match your EVM target to the deployment chain.

What production DeFi engineers know:

  1. Always set evm_version = "cancun" in foundry.toml for post-Dencun deployments
  2. Bytecode size matters: PUSH0 helps stay under the 24KB contract size limit
  3. Pre-Shanghai deployments: If deploying to a chain that hasn’t upgraded, use paris or earlier
  4. Gas profiling: Use forge snapshot to measure actual gas savings, not assumptions
  5. The 80/20 rule: These opcodes give ~5-10% savings. Storage optimization gives 50%+ savings. Focus on the latter.

Pro tip: If asked about gas optimization in interviews, mention PUSH0/MCOPY as β€œfree wins from the compiler” then pivot to the high-impact stuff: reducing SSTORE operations, batching with transient storage, minimizing cold storage reads. Teams want engineers who know where the real gas costs are.


πŸ’‘ Concept: SELFDESTRUCT Changes (EIP-6780)

Why this matters: Some older upgrade patterns are now permanently broken. If you encounter legacy code that relies on SELFDESTRUCT for upgradability, it won’t work post-Dencun.

Changed in EIP-6780, activated with Dencun (March 2024)

What changed:

Post-Dencun, SELFDESTRUCT only deletes the contract if called in the same transaction that created it. In all other cases, it sends the contract’s ETH to the target address but the contract code and storage remain.

This effectively neuters SELFDESTRUCT as a code deletion mechanism.

DeFi implications:

PatternStatusExplanation
Metamorphic contracts❌ DeadDeploy β†’ SELFDESTRUCT β†’ redeploy at same address with different code no longer works
Old proxy patterns❌ BrokenSome relied on SELFDESTRUCT + CREATE2 for upgradability
Contract immutabilityβœ… GoodContracts can no longer be unexpectedly removed, making blockchain state more predictable

πŸ” Historical Context: Why SELFDESTRUCT Was Neutered

The metamorphic contract exploit pattern:

Before EIP-6780, attackers could:

  1. Deploy a benign contract at address A using CREATE2 (deterministic address)

    // Looks safe!
    contract Benign {
        function withdraw(address token) external {
            IERC20(token).transfer(msg.sender, IERC20(token).balanceOf(address(this)));
        }
    }
    
  2. Get the contract whitelisted by a DAO or protocol

  3. SELFDESTRUCT the contract, removing all code from address A

  4. Redeploy DIFFERENT code at the same address A using CREATE2

    // Same address, malicious code!
    contract Malicious {
        function withdraw(address token) external {
            IERC20(token).transfer(ATTACKER, IERC20(token).balanceOf(address(this)));
        }
    }
    
  5. Exploit: The DAO/protocol thinks address A is still the benign contract, but it’s now malicious!

Real attack: Tornado Cash governance (2023)

An attacker used metamorphic contracts to:

  • Deploy a proposal contract with benign code
  • Get it approved by governance vote
  • SELFDESTRUCT + redeploy with malicious code
  • Drain governance funds

Post-EIP-6780: This attack is impossible

SELFDESTRUCT now only deletes code if called in the same transaction as deployment. The redeploy attack requires two transactions (deploy β†’ selfdestruct β†’ redeploy), so the code persists.

⚑ Common pitfall: If you’re reading older DeFi code (pre-2024) and see SELFDESTRUCT used for upgrade patterns, be aware that pattern is now obsolete. Modern upgradeable contracts use UUPS or Transparent Proxy patterns (covered in Module 6).

πŸ” Deep dive: Dedaub - Removal of SELFDESTRUCT explains security benefits. Vibranium Audits - EIP-6780 Objectives covers how metamorphic contracts were exploited in governance attacks.

πŸ’Ό Job Market Context: SELFDESTRUCT Changes

Interview question:

β€œI noticed your ERC-20 contract has a kill() function using SELFDESTRUCT. Is that still safe?”

What to say (This is a red flag test!):

β€œActually, SELFDESTRUCT behavior changed with EIP-6780 in the Dencun upgrade (March 2024). It no longer deletes contract code unless called in the same transaction as deployment. The kill() function will send ETH to the target address but the contract code and storage will remain. If the goal is to disable the contract, we should use a paused state variable instead. Using SELFDESTRUCT post-Dencun suggests the codebase hasn’t been updated for recent EVM changes, which is a red flag.”

Interview Red Flags:

  • 🚩 Any contract using SELFDESTRUCT for upgradability (broken post-Dencun)
  • 🚩 Contracts that rely on SELFDESTRUCT freeing up storage (no longer true)
  • 🚩 Documentation mentioning CREATE2 + SELFDESTRUCT for redeployment (metamorphic pattern dead)

What production DeFi engineers know:

  1. Pause, don’t destroy: Use OpenZeppelin’s Pausable pattern instead of SELFDESTRUCT
  2. Upgradability: Use UUPS or Transparent Proxy (Module 6), not metamorphic contracts
  3. The one exception: Factory contracts that deploy+test+destroy in a single transaction (rare)
  4. Historical code: Pre-2024 contracts may have SELFDESTRUCTβ€”understand it won’t work as originally intended

Pro tip: Knowing the Tornado Cash metamorphic governance exploit in detail is a strong auditor signal. If you can explain the deploy β†’ whitelist β†’ selfdestruct β†’ redeploy attack chain and why EIP-6780 killed it, you demonstrate both historical awareness and security mindset.


🎯 Build Exercise: FlashAccounting

Workspace: workspace/src/part1/module2/exercise1-flash-accounting/ β€” starter file: FlashAccounting.sol, tests: FlashAccounting.t.sol

Build a β€œflash accounting” pattern using transient storage:

  1. Create a FlashAccounting contract that uses transient storage to track balance deltas
  2. Implement lock() / unlock() / settle() functions:
    • lock() opens a session (sets a transient flag)
    • During a locked session, operations accumulate deltas in transient storage
    • settle() verifies all deltas net to zero (or the caller has paid the difference)
    • unlock() clears the session
  3. Write a test that executes multiple token swaps within a single locked session, settling only the net difference
  4. Test reentrancy: verify that if an operation reverts during the locked session, the transient storage deltas are correctly reverted

🎯 Goal: This pattern is the foundation of Uniswap V4’s architecture. Building it now means you’ll instantly recognize it when reading V4 source code in Part 3.


⚠️ Common Mistakes: Dencun Recap

Transient Storage:

  1. ❌ Using transient storage for cross-transaction state β†’ It resets every transaction! Use regular storage.
  2. ❌ Assuming TSTORE is cheaper than memory β†’ Memory is ~3 gas, TSTORE is ~100 gas. Use TSTORE when you need cross-call persistence.
  3. ❌ Forgetting the 2,300 gas reentrancy vector β†’ transfer() and send() now allow TSTORE, creating new attack surfaces.
  4. ❌ Not testing transient storage reverts β†’ If a call reverts, transient changes revert too. Test this behavior.

EIP-4844:

  1. ❌ Saying β€œfull danksharding is live” β†’ It’s proto-danksharding. Full danksharding comes later.
  2. ❌ Thinking your DeFi contract needs blob logic β†’ Blobs are L1 infrastructure. Your L2 contract doesn’t interact with them.
  3. ❌ Assuming blob fees are always cheap β†’ During congestion (inscriptions, etc.), blob fees can spike.

PUSH0 & MCOPY:

  1. ❌ Not setting evm_version = "cancun" in foundry.toml β†’ You’ll miss out on these optimizations.
  2. ❌ Manually optimizing for PUSH0 β†’ The compiler does this automatically. Focus on logic, not opcode-level tricks.

SELFDESTRUCT:

  1. ❌ Using SELFDESTRUCT for upgradability β†’ Broken post-Dencun. Use proxy patterns (Module 6).
  2. ❌ Relying on SELFDESTRUCT for contract removal β†’ Code persists unless called in same transaction as deployment.
  3. ❌ Trusting pre-2024 code with SELFDESTRUCT β†’ Understand it won’t work as originally intended.

πŸ“‹ Summary: Foundational Concepts & Dencun Upgrade

βœ“ Covered (Foundational):

  • EIP-2929 cold/warm access model β€” why first storage read costs 2,100 gas vs 100 gas, access lists
  • EIP-1559 base fee market β€” base fee + priority fee, MEV implications
  • EIP-3529 gas refund reduction β€” death of gas tokens (CHI, GST2)
  • Contract size limits (EIP-170) β€” the 24 KiB limit and strategies to work around it
  • CREATE vs CREATE2 vs CREATE3 β€” deterministic deployment, counterfactual addresses
  • Precompile landscape β€” ecrecover, BN254 (zkSNARKs), BLS12-381 (signatures)

βœ“ Covered (Dencun):

  • Transient storage mechanics (EIP-1153) β€” how it differs from memory and storage, gas costs, flash accounting
  • Flash accounting pattern β€” Uniswap V4’s core innovation with code reading strategy
  • Proto-Danksharding (EIP-4844) β€” why L2s became 90-97% cheaper, blob fee market math
  • PUSH0 & MCOPY β€” bytecode comparisons and gas savings
  • SELFDESTRUCT changes (EIP-6780) β€” metamorphic contracts are dead, historical context

Next: EIP-7702 (EOA code delegation) and the Pectra upgrade


πŸ’‘ Pectra Upgrade β€” EIP-7702 and Beyond

πŸ’‘ Concept: EIP-7702 β€” EOA Code Delegation

Why this matters: EIP-7702 bridges the gap between the 200+ million existing EOAs and modern account abstraction. Users don’t need to migrate to smart accountsβ€”their EOAs can temporarily become smart accounts. This is the biggest UX shift in Ethereum since EIP-1559.

Introduced in EIP-7702, activated with the Pectra upgrade (May 2025)

What it does:

EIP-7702 allows Externally Owned Accounts (EOAs) to temporarily delegate to smart contract code. A new transaction type (Type 4) includes an authorization_listβ€”a list of (chain_id, contract_address, nonce, signature) tuples. When processed, the EOA’s code is temporarily set to a delegation designator pointing to the specified contract. For the duration of the transaction, calls to the EOA execute the delegated contract’s code.

Key properties:

  • The EOA retains its private keyβ€”the owner can always revoke the delegation
  • The delegation persists across transactions (until explicitly changed or revoked)
  • Multiple EOAs can delegate to the same contract implementation
  • The EOA’s storage is used (like DELEGATECALL semantics), not the implementation’s

Why DeFi engineers care:

EIP-7702 means EOAs can:

  • βœ… Batch transactions: Execute multiple operations in a single transaction
  • βœ… Use paymasters: Have someone else pay gas fees (covered in Module 4)
  • βœ… Implement custom validation: Use multisig, passkeys, session keys, etc.
  • βœ… All without creating a new smart account

Example flow:

  1. Alice (EOA) signs an authorization to delegate to a BatchExecutor contract
  2. Alice submits a Type 4 transaction with the authorization
  3. For that transaction, Alice’s EOA acts like a smart account with batching capabilities
  4. Alice can batch: approve USDC β†’ swap on Uniswap β†’ stake in Aave, all atomically ✨

πŸ” Deep Dive: Delegation Designator Format

How the EVM knows an EOA has delegated:

When a Type 4 transaction is processed, the EVM sets the EOA’s code to a special delegation designator:

Delegation Designator Format (23 bytes):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  0xef  β”‚  0x0100  β”‚  address (20 bytes)           β”‚
β”‚ magic  β”‚ version  β”‚  delegated contract address   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example:
0xef0100 1234567890123456789012345678901234567890
β”‚       β”‚
β”‚       └─ Points to BatchExecutor contract
└─ Identifies this as a delegation

Step-by-step: What happens during a call

// Scenario: Alice's EOA (0xAA...AA) delegates to BatchExecutor (0xBB...BB)

// 1. Alice signs authorization:
authorization = {
    chain_id: 1,
    address: 0xBB...BB,  // BatchExecutor
    nonce: 0,
    signature: sign(hash(chain_id, address, nonce), alice_private_key)
}

// 2. Alice submits Type 4 transaction with authorization_list = [authorization]

// 3. EVM processes transaction:
//    - Verifies signature against Alice's EOA
//    - Sets code at 0xAA...AA to: 0xef0100BB...BB
//    - Now when anyone calls 0xAA...AA, it DELEGATECALLs to 0xBB...BB

// 4. Someone calls alice.execute([call1, call2]):
//    β†’ EVM sees code = 0xef0100BB...BB
//    β†’ EVM does: DELEGATECALL to 0xBB...BB with calldata = execute([call1, call2])
//    β†’ BatchExecutor.execute() runs in context of Alice's EOA
//    β†’ msg.sender = Alice's EOA, storage = Alice's storage

Key insight: DELEGATECALL semantics

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Alice's EOA (0xAA...AA)                 β”‚
β”‚  Code: 0xef0100BB...BB (delegation designator)  β”‚
β”‚  Storage: Alice's storage (ETH, tokens, etc.)   β”‚
β”‚                                                 β”‚
β”‚  When called, it DELEGATECALLs to:             β”‚
β”‚         ↓                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚  BatchExecutor (0xBB...BB)      β”‚           β”‚
β”‚  β”‚  - Code executes in Alice's     β”‚           β”‚
β”‚  β”‚    storage context               β”‚           β”‚
β”‚  β”‚  - msg.sender = original caller β”‚           β”‚
β”‚  β”‚  - address(this) = 0xAA...AA    β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’» Quick Try:

Simulate EIP-7702 delegation using DELEGATECALL (since Foundry’s Type 4 support is evolving):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract BatchExecutor {
    struct Call {
        address target;
        bytes data;
    }

    function execute(Call[] calldata calls) external returns (bytes[] memory) {
        bytes[] memory results = new bytes[](calls.length);
        for (uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory result) = calls[i].target.call(calls[i].data);
            require(success, "Call failed");
            results[i] = result;
        }
        return results;
    }
}

// Simulate an EOA delegating to BatchExecutor
contract SimulatedEOA {
    // Pretend this EOA has delegated to BatchExecutor via EIP-7702

    function simulateDelegation(address batchExecutor, bytes calldata data)
        external
        returns (bytes memory)
    {
        // This is what the EVM does when it sees the delegation designator
        (bool success, bytes memory result) = batchExecutor.delegatecall(data);
        require(success, "Delegation failed");
        return result;
    }
}

Try batching: approve ERC20 + swap on Uniswap, all in one call!

πŸŽ“ Intermediate Example: Batch Executor with Security

Before jumping to production account abstraction, here’s a practical batch executor:

contract SecureBatchExecutor {
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }

    // Only the EOA that delegated can execute (in delegated context)
    modifier onlyDelegator() {
        // In EIP-7702, address(this) = the EOA that delegated
        // msg.sender = external caller
        // We want to ensure only the EOA owner can trigger execution
        require(msg.sender == address(this), "Only delegator");
        _;
    }

    function execute(Call[] calldata calls)
        external
        payable
        onlyDelegator
        returns (bytes[] memory)
    {
        bytes[] memory results = new bytes[](calls.length);

        for (uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory result) = calls[i].target.call{
                value: calls[i].value
            }(calls[i].data);

            require(success, "Call failed");
            results[i] = result;
        }

        return results;
    }
}

Security consideration:

// ❌ INSECURE: Anyone can call this and execute as the EOA!
function badExecute(Call[] calldata calls) external {
    for (uint256 i = 0; i < calls.length; i++) {
        calls[i].target.call(calls[i].data);
    }
}

// βœ… SECURE: Only the EOA owner (via msg.sender == address(this))
function goodExecute(Call[] calldata calls) external {
    require(msg.sender == address(this), "Only delegator");
    // ...
}

πŸ” Deep dive: EIP-7702 is closely related to ERC-4337 (Module 4). The difference: ERC-4337 requires deploying a new smart account, while EIP-7702 upgrades existing EOAs. Read Vitalik’s post on EIP-7702 for the full account abstraction roadmap.

Security considerations:

  • msg.sender vs tx.origin: When an EIP-7702-delegated EOA calls your contract, msg.sender is the EOA address (as expected). But tx.origin is also the EOA. Be careful with tx.origin checksβ€”they can’t distinguish between direct EOA calls and delegated calls.
  • Delegation revocation: A user can always sign a new authorization pointing to a different contract (or to zero address to revoke delegation). Your DeFi protocol shouldn’t assume delegation is permanent.

⚑ Common pitfall: Some contracts use tx.origin checks for authentication (e.g., β€œonly allow if tx.origin == owner”). These patterns break with EIP-7702 because delegated calls have the same tx.origin as direct calls. Avoid tx.origin-based authentication.

πŸ” Deep dive: QuickNode - EIP-7702 Implementation Guide provides hands-on Foundry examples. Biconomy - Comprehensive EIP-7702 Guide covers app integration. Gelato - Account Abstraction from ERC-4337 to EIP-7702 explains how EIP-7702 compares to ERC-4337.

πŸ“– Code Reading Strategy for EIP-7702 Delegation Targets:

Real delegation targets are what EOAs point to via EIP-7702. Study them to understand production security patterns:

  1. Start with the interface β€” Look for execute(Call[]) or executeBatch(). Every delegation target exposes a batch execution entry point.
  2. Find the auth check β€” Search for msg.sender == address(this). This is the critical guard: in delegated context, address(this) is the EOA, so only the EOA owner can trigger execution.
  3. Check for module support β€” Modern targets (Rhinestone, Biconomy) support pluggable validators and executors. Look for isValidSignature() and module registry patterns.
  4. Look at fallback handling β€” What happens if someone calls an unknown function on the delegated EOA? Good targets have a secure fallback() that either reverts or routes to modules.
  5. Test files first β€” As always, start with the test suite. Search for test_batch, test_unauthorized, test_delegatecall to see what security properties are verified.

Recommended study order:

Don’t get stuck on: Module installation/uninstallation flows or ERC-4337 validateUserOp() specifics β€” those are Module 4 topics. Focus on the batch execution path and auth model.

πŸ’Ό Job Market Context: EIP-7702

Interview question you WILL be asked:

β€œHow does EIP-7702 differ from ERC-4337 for account abstraction?”

What to say (30-second answer):

β€œERC-4337 requires deploying a new smart account contractβ€”the user creates a dedicated account abstraction wallet separate from their EOA. EIP-7702 lets existing EOAs temporarily delegate to smart contract code without deploying anything new. The EOA’s code is set to a delegation designator (0xef0100 + address), and calls to the EOA DELEGATECALL to the implementation. Key difference: EIP-7702 is reversible and works with existing wallets, while ERC-4337 requires user migration to a new address. Both enable batching, paymasters, and custom validation, but EIP-7702 reduces onboarding friction.”

Follow-up question:

β€œYour DeFi protocol has a function that checks tx.origin == owner for admin access. What happens with EIP-7702?”

What to say (This is a red flag test!):

β€œThat’s a security vulnerability. With EIP-7702, when an EOA delegates to a batch executor, tx.origin is still the EOA address even though the code executing is from the delegated contract. An attacker could trick the owner into batching malicious calls alongside legitimate ones, bypassing the tx.origin check. The fix is to use msg.sender instead of tx.origin, or implement a proper access control pattern like OpenZeppelin’s Ownable. Using tx.origin for auth is already an antipattern, and EIP-7702 makes it actively exploitable.”

Interview Red Flags:

  • 🚩 tx.origin for authentication (broken by EIP-7702 delegation)
  • 🚩 Assuming code at an address is immutable (delegation can change behavior)
  • 🚩 No validation of delegation designator (if your protocol interacts with EOAs, expect some might be delegated)

What production DeFi engineers know:

  1. Never use tx.origin: Always use msg.sender for authentication
  2. Delegation is persistent: Once set, the delegation stays until explicitly changed
  3. Users can revoke: Sign a new authorization pointing to address(0)
  4. Testing: Foundry support for Type 4 txs is evolvingβ€”simulate with DELEGATECALL for now
  5. UX opportunity: EIP-7702 enables β€œtry before you migrate” for AAβ€”users can test batching with their existing EOA before committing to a full ERC-4337 smart account

Common interview scenario:

β€œA user with an EIP-7702-delegated EOA calls your lending protocol’s borrow() function. What security considerations apply?”

What to say:

β€œFrom the lending protocol’s perspective, the call looks normal: msg.sender is the EOA, the protocol can check balances, approvals work as expected. But we need to be aware that the user might be batching multiple operationsβ€”for example, borrow + swap + repay in one transaction. Our reentrancy guards must work correctly, and we shouldn’t assume the call is β€˜simple’. Also, if we emit events with msg.sender, they’ll correctly show the EOA address, not the delegated contract. The key is that EIP-7702 is transparent to most protocolsβ€”the EOA still owns the assets, still approves tokens, still is the msg.sender.”

Pro tip: EIP-7702 and ERC-4337 are converging β€” wallets like Ambire and Rhinestone already support both paths. If you can articulate how a protocol should handle both delegated EOAs (7702) and smart accounts (4337) transparently, you show the kind of forward-thinking AA expertise teams are actively hiring for.


πŸ’‘ Concept: Other Pectra EIPs

EIP-7623 β€” Increased calldata cost (EIP-7623):

Transactions that predominantly post data (rather than executing computation) pay higher calldata fees. This affects:

  • L2 data posting (though most L2s now use blobs from EIP-4844)
  • Any protocol that uses heavy calldata (e.g., posting Merkle proofs, batch data)

EIP-2537 β€” BLS12-381 precompile (EIP-2537):

Native BLS signature verification becomes available as a precompile. EIP-2537 defines 9 separate precompile operations at addresses 0x0b through 0x13:

AddressOperationGas Cost
0x0bG1ADD~500
0x0cG1MUL~12,000
0x0dG1MSM (multi-scalar multiplication)Variable
0x0eG2ADD~800
0x0fG2MUL~45,000
0x10G2MSMVariable
0x11PAIRING~43,000 + per-pair
0x12MAP_FP_TO_G1~5,500
0x13MAP_FP2_TO_G2~75,000

Useful for:

  • Threshold signatures
  • Validator-adjacent logic (e.g., liquid staking protocols)
  • Any system that needs efficient pairing-based cryptography (privacy protocols, zkSNARKs)

πŸŽ“ Concrete Example: Liquid Staking Validator Verification

The problem:

Lido/Rocket Pool needs to verify that validators are correctly attesting to Beacon Chain blocks. Validators sign attestations using BLS12-381 signatures. Before EIP-2537, verifying these on-chain was prohibitively expensive (~1M+ gas).

With BLS12-381 precompile:

contract ValidatorRegistry {
    // BLS12-381 precompile addresses (EIP-2537, activated in Pectra)
    // Note: Signature verification requires the PAIRING precompile.
    // This is a conceptual simplification β€” real BLS verification
    // involves multiple precompile calls (G1MUL + PAIRING).
    address constant BLS_PAIRING = address(0x11);

    struct ValidatorAttestation {
        bytes48 publicKey;      // BLS public key (G1 point)
        bytes32 messageHash;    // Hash of attested data
        bytes96 signature;      // BLS signature (G2 point)
    }

    function verifyAttestation(ValidatorAttestation calldata attestation)
        public
        view
        returns (bool)
    {
        // Prepare input for BLS verify precompile
        bytes memory input = abi.encodePacked(
            attestation.publicKey,
            attestation.messageHash,
            attestation.signature
        );

        // Call BLS12-381 pairing precompile
        (bool success, bytes memory output) = BLS_PAIRING.staticcall(input);

        require(success, "BLS verification failed");
        return abi.decode(output, (bool));

        // Gas cost: ~5,000-10,000 gas vs ~1M+ without precompile ✨
    }

    function verifyMultipleAttestations(ValidatorAttestation[] calldata attestations)
        external
        view
        returns (bool)
    {
        for (uint256 i = 0; i < attestations.length; i++) {
            if (!verifyAttestation(attestations[i])) {
                return false;
            }
        }
        return true;
    }
}

Real use case: Lido’s Distributed Validator Technology (DVT)

// Simplified DVT oracle contract
contract LidoDVTOracle {
    struct ConsensusReport {
        uint256 beaconChainEpoch;
        uint256 totalValidators;
        uint256 totalBalance;
        ValidatorAttestation[] signatures;  // From multiple operators
    }

    function submitConsensusReport(ConsensusReport calldata report)
        external
    {
        // Verify all operator signatures (threshold: 5 of 7 must sign)
        uint256 validSigs = 0;
        for (uint256 i = 0; i < report.signatures.length; i++) {
            if (verifyAttestation(report.signatures[i])) {
                validSigs++;
            }
        }

        require(validSigs >= 5, "Insufficient consensus");

        // Update Lido's accounting based on verified report
        _updateValidatorBalances(report.totalBalance);

        // Gas cost: ~50,000 gas vs ~7M+ without precompile
        // Makes on-chain oracle consensus practical ✨
    }
}

Why this matters for DeFi:

Before BLS precompile:

  • Liquid staking protocols relied on off-chain signature verification
  • Trusted oracle committees (centralization risk)
  • Users couldn’t verify validator attestations on-chain

After BLS precompile:

  • On-chain verification of validator signatures
  • Decentralized oracle consensus (multiple operators sign, verify on-chain)
  • Users can independently verify staking rewards are accurate

Gas comparison:

OperationWithout PrecompileWith BLS PrecompileSavings
Single BLS signature verification~1,000,000 gas~8,000 gas99.2% ✨
5-of-7 threshold verification~7,000,000 gas~40,000 gas99.4% ✨
Batch verify 100 attestationsWould revert (OOG)~800,000 gasEnables new use cases ✨

πŸ’Ό Job Market Context: BLS12-381 Precompile

Interview question:

β€œWhat’s the BLS12-381 precompile and why does it matter for DeFi?”

What to say (30-second answer):

β€œBLS12-381 is an elliptic curve used for signature aggregation and pairing-based cryptography. EIP-2537 adds it as a precompile, reducing BLS signature verification from ~1 million gas to ~8,000 gasβ€”a 99%+ reduction. This enables on-chain validator consensus for liquid staking protocols like Lido. Before the precompile, protocols had to verify signatures off-chain using trusted oracles, which is a centralization risk. Now they can verify multiple validator attestations on-chain, enabling truly decentralized oracle consensus. The gas savings also unlock threshold signatures and privacy-preserving protocols that weren’t viable before.”

Follow-up question:

β€œIs BLS12-381 the same curve used for zkSNARKs?”

What to say (This is a knowledge test!):

β€œNo, that’s a common misconception. Most zkSNARKs in production use BN254 (also called alt-bn128), which Ethereum already has precompiles for (EIP-196, EIP-197). BLS12-381 is optimized for signature aggregationβ€”it lets you combine multiple signatures into one, which is why Ethereum 2.0 validators use it. Some newer zkSNARK systems do use BLS12-381, but the primary use case in Ethereum is validator signatures and threshold cryptography, not zero-knowledge proofs.”

Interview Red Flags:

  • 🚩 β€œBLS12-381 is for zkSNARKs” β€” No! It’s primarily for signature aggregation
  • 🚩 β€œAll pairing-based crypto is the same” β€” Different curves have different security/performance tradeoffs
  • 🚩 β€œThe precompile makes all cryptography cheap” β€” Only BLS12-381 operations. ECDSA (standard Ethereum signatures) uses secp256k1

What production DeFi engineers know:

  1. Liquid staking oracles: Lido, Rocket Pool, and others can now do on-chain validator consensus
  2. Threshold signatures: N-of-M multisigs without multiple on-chain transactions
  3. Signature aggregation: Combine signatures from multiple validators/oracles into one verification
  4. The 99% rule: BLS operations went from ~1M gas (unusable) to ~8K gas (practical)
  5. Cross-chain messaging: Bridges can aggregate validator signatures for cheaper verification

Pro tip: Liquid staking is the largest DeFi sector by TVL. If you’re targeting Lido, Rocket Pool, or EigenLayer roles, being able to explain how BLS signature verification enables decentralized oracle consensus shows you understand the trust assumptions that underpin the entire staking ecosystem.


🎯 Build Exercise: EIP7702Delegate

Workspace: workspace/src/part1/module2/exercise2-eip7702-delegate/ β€” starter file: EIP7702Delegate.sol, tests: EIP7702Delegate.t.sol

  1. Research EIP-7702 delegation designator formatβ€”understand how the EVM determines whether an address has delegated code
  2. Write a simple delegation target contract:
    contract BatchExecutor {
        function execute(Call[] calldata calls) external {
            // Execute multiple calls
        }
    }
    
  3. Write tests that simulate EIP-7702 behavior using DELEGATECALL (since Foundry’s Type 4 transaction support is still evolving):
    • Simulate an EOA delegating to your BatchExecutor
    • Test batched operations: approve + swap + stake
    • Verify msg.sender behavior
  4. Security exercise: Write a test that shows how tx.origin checks can be bypassed with EIP-7702 delegation

🎯 Goal: Understand the mechanics well enough to reason about how EIP-7702 interacts with DeFi protocols. When a user interacts with your lending protocol through an EIP-7702-delegated EOA, what are the security implications?


⚠️ Common Mistakes: Pectra Recap

EIP-7702:

  1. ❌ Using tx.origin for authentication β†’ Broken by EIP-7702 delegation. Always use msg.sender.
  2. ❌ Assuming EOA code is immutable β†’ Post-7702, EOAs can have delegated code. Check for delegation designator if needed.
  3. ❌ Confusing EIP-7702 with ERC-4337 β†’ 7702 = EOA delegation. 4337 = new smart account. Different approaches to AA.
  4. ❌ Not validating delegation in batch executors β†’ Add require(msg.sender == address(this)) to prevent unauthorized execution.
  5. ❌ Assuming delegation is one-time β†’ Delegation persists across transactions until explicitly revoked.

BLS12-381:

  1. ❌ Saying β€œBLS is for zkSNARKs” β†’ BLS12-381 is for signature aggregation. zkSNARKs often use BN254 (alt-bn128).
  2. ❌ Not understanding the gas savings β†’ 99%+ reduction (1M gas β†’ 8K gas). Enables on-chain validator consensus for liquid staking.

πŸ“‹ Summary: Pectra Upgrade

βœ“ Covered:

  • EIP-7702 β€” EOA code delegation, delegation designator format, DELEGATECALL semantics
  • Type 4 transactions β€” authorization lists and how the EVM processes them
  • Security implications β€” tx.origin antipattern, delegation revocation, batch executor security
  • Other Pectra EIPs β€” increased calldata costs, BLS12-381 precompile with liquid staking example

Key takeaway: EIP-7702 brings account abstraction to existing EOAs without migration. Combined with ERC-4337 (Module 4), this creates a comprehensive AA ecosystem. The tx.origin antipattern becomes actively exploitable with EIP-7702β€”always use msg.sender for authentication.


πŸ“š Looking Ahead

πŸ’‘ Concept: EOF β€” EVM Object Format

Why this matters (awareness level): EOF is the next major structural change to the EVM, targeted for the Osaka/Fusaka upgrade. While not yet live, DeFi developers at top teams should know what it is and why it matters.

What EOF changes:

EOF introduces a new container format for EVM bytecode that separates code from data, replaces dynamic jumps with static control flow, and adds new sections for metadata.

Current bytecode: Raw bytes, code and data mixed
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ opcodes + data + constructor args (flat) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

EOF container: Structured sections
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Header  β”‚  Types   β”‚   Code   β”‚  Data  β”‚
β”‚ (magic + β”‚ (functionβ”‚ (validatedβ”‚(static β”‚
β”‚ version) β”‚  sigs)   β”‚  opcodes)β”‚  data) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key changes:

  • Static jumps only β€” JUMP and JUMPI replaced by RJUMP, RJUMPI, RJUMPV (relative jumps). No more JUMPDEST scanning.
  • Code/data separation β€” Bytecode analysis becomes simpler and safer. No more ambiguity about whether bytes are code or data.
  • Stack validation β€” The EVM validates stack heights at deploy time, catching errors that currently only surface at runtime.
  • New calling convention β€” CALLF/RETF for internal function calls, reducing stack manipulation overhead.

Why DeFi developers should care:

  • Compiler changes: Solidity will eventually target EOF containers, potentially changing gas profiles
  • Bytecode analysis: Tools that analyze deployed bytecode (decompilers, security scanners) will need updates
  • Backwards compatible: Legacy (non-EOF) contracts continue to work. EOF is opt-in via the new container format

What you DON’T need to do right now: Nothing. EOF is not yet live. When it ships, the Solidity compiler will handle the transition. Keep an eye on Solidity release notes for EOF compilation support.

πŸ” Deep dive: EIP-3540 (EOF v1), ipsilon/eof β€” the EOF specification and reference implementation.


Backward references (← concepts from earlier modules):

Module 2 ConceptBuilds onWhere
Transient storage (EIP-1153)transient keyword, tstore/tload assemblyΒ§1 β€” Transient Storage
Flash accounting gas savingsunchecked blocks, mulDiv precisionΒ§1 β€” Checked Arithmetic
Delegation designator formatCustom types (UDVTs), type safetyΒ§1 β€” User-Defined Value Types

Forward references (β†’ concepts you’ll use later):

Module 2 ConceptUsed inWhere
Transient storageTemporary approvals, flash loansΒ§3 β€” Token Approvals
EIP-7702 delegationAccount abstraction architecture, paymastersΒ§4 β€” Account Abstraction
SELFDESTRUCT neuteredWhy proxy patterns are the only upgrade pathΒ§6 β€” Proxy Patterns
Gas profiling (PUSH0/MCOPY)Forge snapshot, gas optimization workflowsΒ§5 β€” Foundry
CREATE2 deterministic deploymentDeployment scripts, cross-chain deploymentsΒ§7 β€” Deployment
Cold/warm access (EIP-2929)Gas optimization in vault operations, DEX routingPart 2 β€” AMMs
Contract size limits (EIP-170)Diamond pattern, proxy splittingΒ§6 β€” Proxy Patterns

Part 2 connections:

Module 2 ConceptPart 2 ModuleHow it connects
Transient storage + flash accountingM2 β€” AMMsUniswap V4’s entire architecture is built on transient storage deltas
EIP-4844 blob economicsM2–M9All L2 DeFi is 90-97% cheaper post-Dencun β€” affects protocol design assumptions
Transient storageM5 β€” Flash LoansFlash loan settlement patterns use the same lock β†’ operate β†’ settle flow
BLS12-381 precompileM7 β€” Vaults & YieldOn-chain validator consensus for liquid staking protocols (Lido, Rocket Pool)
EIP-7702 + tx.originM8 β€” DeFi SecurityNew attack surfaces from delegated EOAs, tx.origin exploits
SELFDESTRUCT changesM8 β€” DeFi SecurityMetamorphic contract attacks are dead β€” historical context for audit work

πŸ“– Production Study Order

Read these files in order to build progressive understanding of Module 2’s concepts in production code:

#FileWhyLines
1OZ ReentrancyGuardTransient.solSimplest transient storage usage β€” compare to classic ReentrancyGuard~30
2V4 Transient state declarationsSee NonzeroDeltaCount transient and mapping(...) transient β€” how V4 declares transient stateTop ~50
3V4 swap() β†’ _accountPoolBalanceDelta()Follow how swaps update transient deltas without moving tokens~100
4V4 settle() and take()Where actual token transfers happen β€” the settlement phase~60
5Lido AccountingOracle.solValidator reporting β€” context for BLS precompile use cases~200
6Rhinestone ModuleKitEIP-7702 compatible account modules β€” delegation target patterns~150
7Alchemy LightAccount.solProduction ERC-4337 account that works with EIP-7702 delegation~200

Reading strategy: Files 1–4 cover transient storage from simple β†’ complex. File 5 gives BLS context. Files 6–7 show real EIP-7702 delegation targets β€” study how they validate msg.sender and handle batch execution.


πŸ“š Resources

EIP-1153 β€” Transient Storage

EIP-4844 β€” Proto-Danksharding

SELFDESTRUCT Changes

EIP-7702 β€” EOA Code Delegation

Other EIPs

Foundational EVM EIPs

Future EVM

Tooling & Pectra Support


Navigation: ← Module 1: Solidity Modern | Module 3: Token Approvals β†’