Module 2: EVM-Level Changes
Difficulty: Intermediate
Estimated reading time: ~65 minutes | Exercises: ~4-5 hours
π Table of Contents
Foundational EVM Concepts
- EIP-2929: Cold/Warm Access Model
- EIP-1559: Base Fee Market
- EIP-3529: Gas Refund Changes & Death of Gas Tokens
- Contract Size Limits (EIP-170)
- CREATE vs CREATE2 vs CREATE3
- Precompile Landscape
Dencun Upgrade (March 2024)
- Transient Storage Deep Dive (EIP-1153)
- Proto-Danksharding (EIP-4844)
- PUSH0 & MCOPY
- SELFDESTRUCT Changes
- Build Exercise: FlashAccounting
Pectra Upgrade (May 2025)
- EIP-7702 β EOA Code Delegation
- EIP-7623 β Increased Calldata Cost
- EIP-2537 β BLS12-381 Precompile
- Build Exercise: EIP7702Delegate
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:
| Operation | Cold (first access) | Warm (subsequent) | Before EIP-2929 |
|---|---|---|---|
SLOAD | 2,100 gas | 100 gas | 800 gas (flat) |
CALL / STATICCALL | 2,600 gas | 100 gas | 700 gas (flat) |
BALANCE / EXTCODESIZE | 2,600 gas | 100 gas | 700 gas (flat) |
EXTCODECOPY | 2,600 gas | 100 gas | 700 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:
| Cost | Amount |
|---|---|
| Access list: per address entry | 2,400 gas |
| Access list: per storage slot entry | 1,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:
- 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.
- Liquidation bots β Read health factors (cold SLOAD), call liquidate (cold CALL), swap collateral (cold CALL). Access lists are critical for staying competitive on gas.
- 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:
- Gas estimation:
block.basefeeis available in Solidity β protocols can read the current base fee for gas-aware logic - MEV: Searchers set high priority fees to get their bundles included. Understanding base fee vs. tip is essential for MEV strategies
- L2 fee models: L2s adapt EIP-1559 for their own fee markets (Arbitrum ArbGas, Optimism L1 data fee + L2 execution fee)
- 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
SELFDESTRUCTrefunded 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
SELFDESTRUCTrefund 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:
-
β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.
-
β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:
| Strategy | Description | Tradeoff |
|---|---|---|
| Optimizer | optimizer = true, runs = 200 in foundry.toml | Reduces bytecode but increases compile time |
via_ir | via_ir = true in foundry.toml β uses the Yul IR optimizer | More aggressive optimization, slower compilation |
| Libraries | Extract logic into library contracts with using for | Adds DELEGATECALL overhead per call |
| Split contracts | Divide into core + periphery contracts | Adds deployment and integration complexity |
| Diamond pattern | EIP-2535 β modular facets behind a single proxy | Complex but powerful for large protocols |
| Custom errors | Replace require(cond, "long string") with custom errors | Saves ~200 bytes per error message |
| Remove unused code | Dead code still compiles into bytecode | Free β 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.solrequired 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:
| Address | Name | Gas | DeFi Usage |
|---|---|---|---|
0x01 | ecrecover | 3,000 | ERC-2612 permit, EIP-712 signatures, meta-transactions |
0x02 | SHA-256 | 60 + 12/word | Bitcoin SPV proofs (rare in DeFi) |
0x03 | RIPEMD-160 | 600 + 120/word | Bitcoin address derivation (rare) |
0x04 | Identity (memcpy) | 15 + 3/word | Compiler optimization (transparent) |
0x05 | modexp | Variable | RSA verification, large-number math |
0x06 | ecAdd (BN254) | 150 | zkSNARK verification (Tornado Cash, zkSync) |
0x07 | ecMul (BN254) | 6,000 | zkSNARK verification |
0x08 | ecPairing (BN254) | 34,000 + per-pair | zkSNARK verification |
0x09 | blake2f | Variable | Zcash interop (rare) |
0x0a | point evaluation | 50,000 | EIP-4844 blob verification |
0x0b-0x13 | BLS12-381 | Variable | Validator signatures (see above) |
The ones that matter for DeFi:
-
ecrecover (
0x01) β Used in everypermit()call, every EIP-712 typed data signature, every meta-transaction. Youβve been using this indirectly throughECDSA.recover()from OpenZeppelin. -
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. -
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
transientkeyword and rawtstore/tloadassembly. 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
TSTOREandTLOAD(vs ~100 for warmSLOAD, but ~2,100-20,000 forSSTORE) - 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:
| Operation | Cold Access | Warm Access | Notes |
|---|---|---|---|
SLOAD (storage read) | 2,100 gas | 100 gas | First access in tx is βcoldβ (EIP-2929) |
SSTORE (zeroβnonzero) | 20,000 gas | 20,000 gas | Adds new data to state (cold/warm affects slot access, not write cost) |
SSTORE (nonzeroβnonzero) | 5,000 gas | 5,000 gas | Modifies existing data (+2,100 cold surcharge on first access) |
SSTORE (nonzeroβzero) | 5,000 gas | 5,000 gas | Removes data (gets partial refund β EIP-3529) |
TLOAD | 100 gas | 100 gas | Always same cost β¨ |
TSTORE | 100 gas | 100 gas | Always same cost β¨ |
MLOAD/MSTORE (memory) | ~3 gas | ~3 gas | Cheapest 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:
-
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.
-
Temporary approvals: ERC-20 approvals that last only for the current transactionβapprove, use, and automatically revoke, all without touching persistent storage.
-
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 viaunlockCallback)- 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
TSTOREcosts only ~100 gas, it can execute within the 2,300 gas stipend thattransfer()andsend()forward. A contract receiving ETH viatransfer()can now executeTSTORE(something impossible withSSTORE). This creates new reentrancy attack surfaces in contracts that assumed 2,300 gas was βsafe.β This is one reasontransfer()andsend()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:
-
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 -
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
_nonzeroDeltaCounttracks how many currencies still have unsettled deltas
- Notice how it uses a callback pattern:
-
Follow a swap flow: Search for
function swap()- See how it calls
_accountPoolBalanceDelta()to update transient deltas - Notice: No actual token transfers happen yet!
- See how it calls
-
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
-
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:
- Reentrancy guards: If your protocol will be deployed post-Cancun (March 2024), use transient guards
- Flash accounting: Essential for any multi-step operation (swaps, liquidity management, flash loans)
- The 2,300 gas pitfall: TSTORE works within
transfer()/send()stipendβcreates new reentrancy vectors - 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:
- Block has 3 blobs (target): excess_blob_gas unchanged β fee stays the same
- 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)
- Block has 0 blobs: excess_blob_gas decreases by up to 393,216 β fee drops
- 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:
| Protocol | Operation | Before Dencun (Calldata) | After Dencun (Blobs) | Your Cost |
|---|---|---|---|---|
| Aave on Base | Supply USDC | ~$0.50 | ~$0.01 | 98% cheaper β¨ |
| Uniswap on Arbitrum | Swap ETHβUSDC | ~$1.20 | ~$0.03 | 97.5% cheaper β¨ |
| GMX on Arbitrum | Open position | ~$2.00 | ~$0.05 | 97.5% cheaper β¨ |
| Velodrome on Optimism | Add liquidity | ~$0.80 | ~$0.02 | 97.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:
- Etherscan Dencun Upgrade β first Dencun block, March 13, 2024. Look for Type 3 blob transactions.
- L2Beat Blobs β real-time blob usage by L2s, fee market dynamics.
- Read blob data: Use
eth_getBlobRPC 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.blobbasefeeandblobhash()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:
- L2 selection matters: Post-Dencun, Base, Optimism, Arbitrum became equally cheap. Choose based on liquidity, ecosystem, not cost.
- Blob fee spikes: During congestion, blob fees can spike (like March 2024 inscriptions). Your L2 costs are tied to blob fee volatility.
- The 18-day window: If youβre building infra (block explorers, analytics), you need to archive blob data within 18 days.
- 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
cancunmight 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:
- Always set
evm_version = "cancun"in foundry.toml for post-Dencun deployments - Bytecode size matters: PUSH0 helps stay under the 24KB contract size limit
- Pre-Shanghai deployments: If deploying to a chain that hasnβt upgraded, use
parisor earlier - Gas profiling: Use
forge snapshotto measure actual gas savings, not assumptions - 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:
| Pattern | Status | Explanation |
|---|---|---|
| Metamorphic contracts | β Dead | Deploy β SELFDESTRUCT β redeploy at same address with different code no longer works |
| Old proxy patterns | β Broken | Some relied on SELFDESTRUCT + CREATE2 for upgradability |
| Contract immutability | β Good | Contracts 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:
-
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))); } } -
Get the contract whitelisted by a DAO or protocol
-
SELFDESTRUCT the contract, removing all code from address A
-
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))); } } -
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
SELFDESTRUCTused 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
SELFDESTRUCTfor upgradability (broken post-Dencun) - π© Contracts that rely on
SELFDESTRUCTfreeing up storage (no longer true) - π© Documentation mentioning CREATE2 + SELFDESTRUCT for redeployment (metamorphic pattern dead)
What production DeFi engineers know:
- Pause, donβt destroy: Use OpenZeppelinβs
Pausablepattern instead of SELFDESTRUCT - Upgradability: Use UUPS or Transparent Proxy (Module 6), not metamorphic contracts
- The one exception: Factory contracts that deploy+test+destroy in a single transaction (rare)
- 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:
- Create a
FlashAccountingcontract that uses transient storage to track balance deltas - 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
- Write a test that executes multiple token swaps within a single locked session, settling only the net difference
- 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:
- β Using transient storage for cross-transaction state β It resets every transaction! Use regular storage.
- β Assuming TSTORE is cheaper than memory β Memory is ~3 gas, TSTORE is ~100 gas. Use TSTORE when you need cross-call persistence.
- β Forgetting the 2,300 gas reentrancy vector β
transfer()andsend()now allow TSTORE, creating new attack surfaces. - β Not testing transient storage reverts β If a call reverts, transient changes revert too. Test this behavior.
EIP-4844:
- β Saying βfull danksharding is liveβ β Itβs proto-danksharding. Full danksharding comes later.
- β Thinking your DeFi contract needs blob logic β Blobs are L1 infrastructure. Your L2 contract doesnβt interact with them.
- β Assuming blob fees are always cheap β During congestion (inscriptions, etc.), blob fees can spike.
PUSH0 & MCOPY:
- β Not setting
evm_version = "cancun"in foundry.toml β Youβll miss out on these optimizations. - β Manually optimizing for PUSH0 β The compiler does this automatically. Focus on logic, not opcode-level tricks.
SELFDESTRUCT:
- β Using SELFDESTRUCT for upgradability β Broken post-Dencun. Use proxy patterns (Module 6).
- β Relying on SELFDESTRUCT for contract removal β Code persists unless called in same transaction as deployment.
- β 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
DELEGATECALLsemantics), 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:
- Alice (EOA) signs an authorization to delegate to a BatchExecutor contract
- Alice submits a Type 4 transaction with the authorization
- For that transaction, Aliceβs EOA acts like a smart account with batching capabilities
- 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.sendervstx.origin: When an EIP-7702-delegated EOA calls your contract,msg.senderis the EOA address (as expected). Buttx.originis also the EOA. Be careful withtx.originchecksβ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.originchecks for authentication (e.g., βonly allow iftx.origin == ownerβ). These patterns break with EIP-7702 because delegated calls have the sametx.originas direct calls. Avoidtx.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:
- Start with the interface β Look for
execute(Call[])orexecuteBatch(). Every delegation target exposes a batch execution entry point. - 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. - Check for module support β Modern targets (Rhinestone, Biconomy) support pluggable validators and executors. Look for
isValidSignature()and module registry patterns. - 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. - Test files first β As always, start with the test suite. Search for
test_batch,test_unauthorized,test_delegatecallto see what security properties are verified.
Recommended study order:
- Alchemy LightAccount β cleanest minimal implementation
- Rhinestone ModuleKit β modular architecture with validators/executors
- Biconomy Nexus β production AA account with EIP-7702 support
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 == ownerfor 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.originfor 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:
- Never use
tx.origin: Always usemsg.senderfor authentication - Delegation is persistent: Once set, the delegation stays until explicitly changed
- Users can revoke: Sign a new authorization pointing to address(0)
- Testing: Foundry support for Type 4 txs is evolvingβsimulate with DELEGATECALL for now
- 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:
| Address | Operation | Gas Cost |
|---|---|---|
0x0b | G1ADD | ~500 |
0x0c | G1MUL | ~12,000 |
0x0d | G1MSM (multi-scalar multiplication) | Variable |
0x0e | G2ADD | ~800 |
0x0f | G2MUL | ~45,000 |
0x10 | G2MSM | Variable |
0x11 | PAIRING | ~43,000 + per-pair |
0x12 | MAP_FP_TO_G1 | ~5,500 |
0x13 | MAP_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:
| Operation | Without Precompile | With BLS Precompile | Savings |
|---|---|---|---|
| Single BLS signature verification | ~1,000,000 gas | ~8,000 gas | 99.2% β¨ |
| 5-of-7 threshold verification | ~7,000,000 gas | ~40,000 gas | 99.4% β¨ |
| Batch verify 100 attestations | Would revert (OOG) | ~800,000 gas | Enables 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:
- Liquid staking oracles: Lido, Rocket Pool, and others can now do on-chain validator consensus
- Threshold signatures: N-of-M multisigs without multiple on-chain transactions
- Signature aggregation: Combine signatures from multiple validators/oracles into one verification
- The 99% rule: BLS operations went from ~1M gas (unusable) to ~8K gas (practical)
- 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
- Research EIP-7702 delegation designator formatβunderstand how the EVM determines whether an address has delegated code
- Write a simple delegation target contract:
contract BatchExecutor { function execute(Call[] calldata calls) external { // Execute multiple calls } } - 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.senderbehavior
- Security exercise: Write a test that shows how
tx.originchecks 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:
- β Using
tx.originfor authentication β Broken by EIP-7702 delegation. Always usemsg.sender. - β Assuming EOA code is immutable β Post-7702, EOAs can have delegated code. Check for delegation designator if needed.
- β Confusing EIP-7702 with ERC-4337 β 7702 = EOA delegation. 4337 = new smart account. Different approaches to AA.
- β Not validating delegation in batch executors β Add
require(msg.sender == address(this))to prevent unauthorized execution. - β Assuming delegation is one-time β Delegation persists across transactions until explicitly revoked.
BLS12-381:
- β Saying βBLS is for zkSNARKsβ β BLS12-381 is for signature aggregation. zkSNARKs often use BN254 (alt-bn128).
- β 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.originantipattern, 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 β
JUMPandJUMPIreplaced byRJUMP,RJUMPI,RJUMPV(relative jumps). No moreJUMPDESTscanning. - 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/RETFfor 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.
π Cross-Module Concept Links
Backward references (β concepts from earlier modules):
| Module 2 Concept | Builds on | Where |
|---|---|---|
| Transient storage (EIP-1153) | transient keyword, tstore/tload assembly | Β§1 β Transient Storage |
| Flash accounting gas savings | unchecked blocks, mulDiv precision | Β§1 β Checked Arithmetic |
| Delegation designator format | Custom types (UDVTs), type safety | Β§1 β User-Defined Value Types |
Forward references (β concepts youβll use later):
| Module 2 Concept | Used in | Where |
|---|---|---|
| Transient storage | Temporary approvals, flash loans | Β§3 β Token Approvals |
| EIP-7702 delegation | Account abstraction architecture, paymasters | Β§4 β Account Abstraction |
| SELFDESTRUCT neutered | Why proxy patterns are the only upgrade path | Β§6 β Proxy Patterns |
| Gas profiling (PUSH0/MCOPY) | Forge snapshot, gas optimization workflows | Β§5 β Foundry |
| CREATE2 deterministic deployment | Deployment scripts, cross-chain deployments | Β§7 β Deployment |
| Cold/warm access (EIP-2929) | Gas optimization in vault operations, DEX routing | Part 2 β AMMs |
| Contract size limits (EIP-170) | Diamond pattern, proxy splitting | Β§6 β Proxy Patterns |
Part 2 connections:
| Module 2 Concept | Part 2 Module | How it connects |
|---|---|---|
| Transient storage + flash accounting | M2 β AMMs | Uniswap V4βs entire architecture is built on transient storage deltas |
| EIP-4844 blob economics | M2βM9 | All L2 DeFi is 90-97% cheaper post-Dencun β affects protocol design assumptions |
| Transient storage | M5 β Flash Loans | Flash loan settlement patterns use the same lock β operate β settle flow |
| BLS12-381 precompile | M7 β Vaults & Yield | On-chain validator consensus for liquid staking protocols (Lido, Rocket Pool) |
| EIP-7702 + tx.origin | M8 β DeFi Security | New attack surfaces from delegated EOAs, tx.origin exploits |
| SELFDESTRUCT changes | M8 β DeFi Security | Metamorphic 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:
| # | File | Why | Lines |
|---|---|---|---|
| 1 | OZ ReentrancyGuardTransient.sol | Simplest transient storage usage β compare to classic ReentrancyGuard | ~30 |
| 2 | V4 Transient state declarations | See NonzeroDeltaCount transient and mapping(...) transient β how V4 declares transient state | Top ~50 |
| 3 | V4 swap() β _accountPoolBalanceDelta() | Follow how swaps update transient deltas without moving tokens | ~100 |
| 4 | V4 settle() and take() | Where actual token transfers happen β the settlement phase | ~60 |
| 5 | Lido AccountingOracle.sol | Validator reporting β context for BLS precompile use cases | ~200 |
| 6 | Rhinestone ModuleKit | EIP-7702 compatible account modules β delegation target patterns | ~150 |
| 7 | Alchemy LightAccount.sol | Production 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-1153 specification β full technical spec
- Uniswap V4 PoolManager.sol β production flash accounting using transient storage
- go-ethereum PR #26003 β implementation discussion
EIP-4844 β Proto-Danksharding
- EIP-4844 specification β blob transactions and data availability
- Ethereum.org β Dencun upgrade β overview of all Dencun EIPs
- L2Beat β Blob Explorer β see real-time blob usage and costs
SELFDESTRUCT Changes
- EIP-6780 specification β SELFDESTRUCT behavior change
- Why SELFDESTRUCT was changed β Ethereum Magicians discussion
EIP-7702 β EOA Code Delegation
- EIP-7702 specification β full technical spec
- Vitalikβs account abstraction roadmap β context on how EIP-7702 fits into AA
- Ethereum.org β Pectra upgrade β overview of all Pectra EIPs
Other EIPs
- EIP-3855 (PUSH0) β single-byte zero push (Shanghai)
- EIP-5656 (MCOPY) β memory copy opcode (Cancun)
- EIP-7623 (Calldata cost) β increased calldata pricing (Pectra)
- EIP-2537 (BLS precompile) β BLS12-381 pairing operations (Pectra)
- EIP-2929 (Cold/Warm access) β access list gas pricing (Berlin)
- EIP-1559 (Base fee) β fee market reform (London)
- EIP-3529 (Gas refund reduction) β reduced SSTORE/SELFDESTRUCT refunds (London)
Foundational EVM EIPs
- EIP-170 (Contract size limit) β 24 KiB bytecode limit
- EIP-1014 (CREATE2) β deterministic contract deployment
- EIP-2930 (Access lists) β optional access list transaction type
Future EVM
- EIP-3540 (EOF v1) β EVM Object Format specification
- ipsilon/eof β EOF reference implementation
Tooling & Pectra Support
- Foundry EIP-7702 support β evolving Type 4 transaction support
- ethers.js v6 Type 4 transactions β account abstraction integration
Navigation: β Module 1: Solidity Modern | Module 3: Token Approvals β