EVM Deep Knowledge
Architecture
- Stack-based VM: 256-bit word size, 1024 stack depth max
- Memory model: storage (persistent/expensive), memory (temporary/cheap), stack (cheapest), calldata (read-only input)
- Account types: EOA (externally owned, has private key) vs Contract (has code + storage)
Transaction Lifecycle
- Creation: construct tx object (to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas)
- Signing: ECDSA signature with private key → v, r, s
- Broadcasting: submit to mempool via RPC node
- Mempool: pending transactions ordered by gas price (priority fee)
- Inclusion: validator selects tx for block, executes EVM bytecode
- Execution: opcodes run sequentially, gas consumed per operation
- Receipt: status (0=fail, 1=success), gasUsed, logs (events)
Gas Mechanics (EIP-1559)
- Base fee: protocol-set, burned. Adjusts per block based on utilization.
- Priority fee (tip): goes to validator. Incentivizes inclusion.
- Max fee: ceiling the sender will pay.
actualFee = min(maxFee, baseFee + priorityFee)
- Gas limit: max gas units for the tx. Unused gas refunded.
- Block gas limit: ~30M gas on Ethereum mainnet
Key Opcode Costs
| Opcode |
Gas |
Notes |
| ADD/SUB/MUL |
3 |
Arithmetic |
| DIV/MOD |
5 |
Division |
| SLOAD (cold) |
2,100 |
First read of a storage slot in tx |
| SLOAD (warm) |
100 |
Subsequent reads of same slot |
| SSTORE (cold, 0→nonzero) |
20,000 |
Most expensive common operation |
| SSTORE (cold, nonzero→nonzero) |
2,900 |
Modifying existing value |
| SSTORE (warm) |
100 |
Already accessed this tx |
| SSTORE refund (nonzero→0) |
4,800 |
Refund for clearing storage |
| CALL (cold) |
2,600 |
First call to an address |
| CALL (warm) |
100 |
Subsequent calls |
| CREATE |
32,000 |
Deploy new contract |
| CREATE2 |
32,000 |
Deterministic deploy |
| LOG0 |
375 |
Base event cost |
| LOG topic |
+375 each |
Per indexed parameter |
| LOG data |
+8/byte |
Event data |
| MLOAD/MSTORE |
3 |
Memory read/write |
| CALLDATALOAD |
3 |
Read calldata |
| PUSH0 |
2 |
EIP-3855, push zero |
EVM Execution Context
msg.sender — immediate caller (contract or EOA)
msg.value — ETH sent with call (in wei)
msg.data — full calldata (selector + encoded args)
msg.sig — first 4 bytes of calldata (function selector)
block.timestamp — current block timestamp (seconds since epoch)
block.number — current block number
block.basefee — current base fee (EIP-1559)
block.chainid — chain ID (1=mainnet, 8453=Base, 42161=Arbitrum)
gasleft() — remaining gas
tx.origin — original EOA sender (NEVER use for auth)
Storage Layout
- Each contract has 2^256 storage slots, each 32 bytes
- State variables assigned sequentially starting at slot 0
- Multiple variables packed into one slot if they fit (e.g., two uint128s)
- Mappings:
slot(mapping[key]) = keccak256(key . mappingSlot)
- Dynamic arrays: length at
slot, elements at keccak256(slot) + index
- Nested mappings:
keccak256(key2 . keccak256(key1 . slot))
Memory Layout
0x00-0x3f: Scratch space (used by Solidity for hashing)
0x40-0x5f: Free memory pointer (points to next available memory)
0x60-0x7f: Zero slot (used as initial value for dynamic memory arrays)
0x80+: Usable memory starts here
- Memory expands in 32-byte words; expansion cost is quadratic:
cost = 3*words + words²/512
Contract Creation
- Constructor: Runs once at deployment, not stored in runtime bytecode
- Bytecode:
creationCode + runtimeCode. Creation code runs constructor and returns runtime code.
- CREATE:
address = keccak256(deployer, nonce) — nonce-dependent, non-deterministic
- CREATE2:
address = keccak256(0xff, deployer, salt, keccak256(initCode)) — deterministic, same address on any chain with same deployer + salt + code
Important EVM Changes
- Shanghai (2023): PUSH0 opcode, beacon chain withdrawals
- Dencun (2024): EIP-4844 blob transactions (cheap L2 data), transient storage (EIP-1153), SELFDESTRUCT deprecated
- Pectra (2025): EIP-7702 (EOA code setting), increased blob throughput
ABI Encoding
- Function selector:
bytes4(keccak256("functionName(type1,type2)")) — first 4 bytes of calldata
- Arguments: ABI-encoded after the selector (32-byte aligned, dynamic types have offset + length)
abi.encode() — standard encoding, includes length prefixes
abi.encodePacked() — tight packing, no length prefixes, cheaper but collision-prone for hashing
abi.encodeWithSelector() — prepends selector, use for low-level calls
abi.encodeWithSignature("transfer(address,uint256)", to, amt) — human-readable
DELEGATECALL
Executes callee's code in the CALLER's context (storage, msg.sender, msg.value unchanged).
Contract A delegatecalls Contract B:
- Code: Contract B's
- Storage: Contract A's (slot 0 in B maps to slot 0 in A)
- msg.sender: original caller (not A)
- msg.value: original value
Critical: storage layout between proxy and implementation MUST match exactly. Mismatched layouts = storage corruption.
SELFDESTRUCT (Post-Dencun)
- EIP-6780 (Dencun 2024): SELFDESTRUCT only works as originally designed if called in the SAME TRANSACTION as contract creation
- Existing contracts: SELFDESTRUCT still sends ETH but does NOT delete code or storage anymore
- Do NOT design systems that rely on SELFDESTRUCT for cleanup — it's effectively deprecated
Transient Storage (EIP-1153 — Dencun 2024)
- New opcodes:
TSTORE / TLOAD — storage that resets to 0 after every transaction
- Cost: ~100 gas (like warm SLOAD) vs 20,000 gas for cold SSTORE
- Use cases: reentrancy locks (replace storage-based mutex), temporary state within complex tx, cross-contract flags
// Reentrancy guard using transient storage (much cheaper)
modifier nonReentrant() {
assembly { if tload(0) { revert(0, 0) } tstore(0, 1) }
_;
assembly { tstore(0, 0) }
}
Common Assembly Patterns
// Read storage slot directly
assembly { value := sload(slot) }
// Write storage slot
assembly { sstore(slot, value) }
// Get contract balance
assembly { bal := selfbalance() }
// Revert with custom error (gas efficient)
assembly {
mstore(0x00, 0xe0e54ced) // error selector
revert(0x1c, 0x04)
}
// Efficient address masking
assembly { addr := and(addr, 0xffffffffffffffffffffffffffffffffffffffff) }
1---2name: evm-deep-knowledge3description: EVM Deep Knowledge4---5# EVM Deep Knowledge67## Architecture8- Stack-based VM: 256-bit word size, 1024 stack depth max9- Memory model: storage (persistent/expensive), memory (temporary/cheap), stack (cheapest), calldata (read-only input)10- Account types: EOA (externally owned, has private key) vs Contract (has code + storage)1112## Transaction Lifecycle131. Creation: construct tx object (to, value, data, gasLimit, maxFeePerGas, maxPriorityFeePerGas)142. Signing: ECDSA signature with private key → v, r, s153. Broadcasting: submit to mempool via RPC node164. Mempool: pending transactions ordered by gas price (priority fee)175. Inclusion: validator selects tx for block, executes EVM bytecode186. Execution: opcodes run sequentially, gas consumed per operation197. Receipt: status (0=fail, 1=success), gasUsed, logs (events)2021## Gas Mechanics (EIP-1559)22- **Base fee**: protocol-set, burned. Adjusts per block based on utilization.23- **Priority fee (tip)**: goes to validator. Incentivizes inclusion.24- **Max fee**: ceiling the sender will pay. `actualFee = min(maxFee, baseFee + priorityFee)`25- **Gas limit**: max gas units for the tx. Unused gas refunded.26- **Block gas limit**: ~30M gas on Ethereum mainnet2728## Key Opcode Costs29| Opcode | Gas | Notes |30|--------|-----|-------|31| ADD/SUB/MUL | 3 | Arithmetic |32| DIV/MOD | 5 | Division |33| SLOAD (cold) | 2,100 | First read of a storage slot in tx |34| SLOAD (warm) | 100 | Subsequent reads of same slot |35| SSTORE (cold, 0→nonzero) | 20,000 | Most expensive common operation |36| SSTORE (cold, nonzero→nonzero) | 2,900 | Modifying existing value |37| SSTORE (warm) | 100 | Already accessed this tx |38| SSTORE refund (nonzero→0) | 4,800 | Refund for clearing storage |39| CALL (cold) | 2,600 | First call to an address |40| CALL (warm) | 100 | Subsequent calls |41| CREATE | 32,000 | Deploy new contract |42| CREATE2 | 32,000 | Deterministic deploy |43| LOG0 | 375 | Base event cost |44| LOG topic | +375 each | Per indexed parameter |45| LOG data | +8/byte | Event data |46| MLOAD/MSTORE | 3 | Memory read/write |47| CALLDATALOAD | 3 | Read calldata |48| PUSH0 | 2 | EIP-3855, push zero |4950## EVM Execution Context51```52msg.sender — immediate caller (contract or EOA)53msg.value — ETH sent with call (in wei)54msg.data — full calldata (selector + encoded args)55msg.sig — first 4 bytes of calldata (function selector)56block.timestamp — current block timestamp (seconds since epoch)57block.number — current block number58block.basefee — current base fee (EIP-1559)59block.chainid — chain ID (1=mainnet, 8453=Base, 42161=Arbitrum)60gasleft() — remaining gas61tx.origin — original EOA sender (NEVER use for auth)62```6364## Storage Layout65- Each contract has 2^256 storage slots, each 32 bytes66- State variables assigned sequentially starting at slot 067- Multiple variables packed into one slot if they fit (e.g., two uint128s)68- Mappings: `slot(mapping[key]) = keccak256(key . mappingSlot)`69- Dynamic arrays: length at `slot`, elements at `keccak256(slot) + index`70- Nested mappings: `keccak256(key2 . keccak256(key1 . slot))`7172## Memory Layout73- `0x00-0x3f`: Scratch space (used by Solidity for hashing)74- `0x40-0x5f`: Free memory pointer (points to next available memory)75- `0x60-0x7f`: Zero slot (used as initial value for dynamic memory arrays)76- `0x80+`: Usable memory starts here77- Memory expands in 32-byte words; expansion cost is quadratic: `cost = 3*words + words²/512`7879## Contract Creation80- **Constructor**: Runs once at deployment, not stored in runtime bytecode81- **Bytecode**: `creationCode + runtimeCode`. Creation code runs constructor and returns runtime code.82- **CREATE**: `address = keccak256(deployer, nonce)` — nonce-dependent, non-deterministic83- **CREATE2**: `address = keccak256(0xff, deployer, salt, keccak256(initCode))` — deterministic, same address on any chain with same deployer + salt + code8485## Important EVM Changes86- **Shanghai (2023)**: PUSH0 opcode, beacon chain withdrawals87- **Dencun (2024)**: EIP-4844 blob transactions (cheap L2 data), transient storage (EIP-1153), SELFDESTRUCT deprecated88- **Pectra (2025)**: EIP-7702 (EOA code setting), increased blob throughput8990## ABI Encoding91- Function selector: `bytes4(keccak256("functionName(type1,type2)"))` — first 4 bytes of calldata92- Arguments: ABI-encoded after the selector (32-byte aligned, dynamic types have offset + length)93- `abi.encode()` — standard encoding, includes length prefixes94- `abi.encodePacked()` — tight packing, no length prefixes, cheaper but collision-prone for hashing95- `abi.encodeWithSelector()` — prepends selector, use for low-level calls96- `abi.encodeWithSignature("transfer(address,uint256)", to, amt)` — human-readable9798## DELEGATECALL99Executes callee's code in the CALLER's context (storage, msg.sender, msg.value unchanged).100```101Contract A delegatecalls Contract B:102 - Code: Contract B's103 - Storage: Contract A's (slot 0 in B maps to slot 0 in A)104 - msg.sender: original caller (not A)105 - msg.value: original value106```107Critical: storage layout between proxy and implementation MUST match exactly. Mismatched layouts = storage corruption.108109## SELFDESTRUCT (Post-Dencun)110- EIP-6780 (Dencun 2024): SELFDESTRUCT only works as originally designed if called in the SAME TRANSACTION as contract creation111- Existing contracts: SELFDESTRUCT still sends ETH but does NOT delete code or storage anymore112- Do NOT design systems that rely on SELFDESTRUCT for cleanup — it's effectively deprecated113114## Transient Storage (EIP-1153 — Dencun 2024)115- New opcodes: `TSTORE` / `TLOAD` — storage that resets to 0 after every transaction116- Cost: ~100 gas (like warm SLOAD) vs 20,000 gas for cold SSTORE117- Use cases: reentrancy locks (replace storage-based mutex), temporary state within complex tx, cross-contract flags118```solidity119// Reentrancy guard using transient storage (much cheaper)120modifier nonReentrant() {121 assembly { if tload(0) { revert(0, 0) } tstore(0, 1) }122 _;123 assembly { tstore(0, 0) }124}125```126127## Common Assembly Patterns128```solidity129// Read storage slot directly130assembly { value := sload(slot) }131132// Write storage slot133assembly { sstore(slot, value) }134135// Get contract balance136assembly { bal := selfbalance() }137138// Revert with custom error (gas efficient)139assembly {140 mstore(0x00, 0xe0e54ced) // error selector141 revert(0x1c, 0x04)142}143144// Efficient address masking145assembly { addr := and(addr, 0xffffffffffffffffffffffffffffffffffffffff) }146```