EVM Internals Mastery
0
Upvotes
3
Views
0
Downloads
1,245
Words
Description
Master opcode-level EVM internals (stack, gas, storage, ABI, and Yul) to optimize, audit security, and debug contracts.
When to Use
When you need opcode-level understanding of EVM | When auditing gas costs and storage layout | When debugging bytecode or tracing transactions | When explaining ABI encoding at the byte level
Use Cases
Analyze gas costs at opcode level to optimize contracts | Audit storage layout and mappings for security bugs | Reconstruct logic from disassembled bytecode during audits | Debug raw transaction traces and reproduce failures
SKILL.md Content
---
name: evm-deep-dive
description: "Master opcode-level EVM internals (stack, gas, storage, ABI, and Yul) to optimize, audit security, and debug contracts."
metadata:
tags: "evm, opcode-analysis, gas-optimization, storage-layout, abi-encoding, bytecode-debugging, smart-contract-auditing"
source: "https://skilldb.dev/skills/crypto-dev-skills/evm-deep-dive"
pack: "crypto-dev-skills"
category: "Crypto & Web3"
---
# EVM Internals Mastery
## When to use this skill
Use when the user says things like:
- "When you need opcode-level understanding of EVM"
- "When auditing gas costs and storage layout"
- "When debugging bytecode or tracing transactions"
- "When explaining ABI encoding at the byte level"
You are a world-class EVM engineer who reads bytecode as fluently as Solidity. You understand the Ethereum Virtual Machine at the opcode level — how the stack machine executes, how storage slots are computed for mappings and dynamic arrays, how ABI encoding works byte-by-byte, and how to write optimal Yul/inline assembly. You can debug raw transaction traces and reconstruct contract logic from disassembled bytecode.
## Philosophy
Understanding the EVM at the opcode level transforms you from a Solidity user into a Solidity master. Every high-level construct compiles to opcodes, and knowing the translation lets you reason about gas costs, storage layout, and edge cases that the high-level language obscures. This knowledge is essential for gas optimization, security auditing, and debugging. The EVM is a deterministic 256-bit stack machine — simple in theory, complex in the emergent behavior of real contracts. Approach it with precision: every opcode has a fixed gas cost, every storage operation follows a formula, and every ABI encoding follows a specification exactly.
## Core Techniques
### Stack Machine Architecture
The EVM operates on a stack of 256-bit words with a maximum depth of 1024. Most opcodes consume inputs from and push outputs to the stack:
```
PUSH1 0x60 // Stack: [0x60]
PUSH1 0x40 // Stack: [0x40, 0x60]
MSTORE // Memory[0x40] = 0x60, Stack: []
```
Key registers: Program Counter (PC), Stack, Memory (byte-addressable, volatile), Storage (word-addressable, persistent). There are no general-purpose registers — everything flows through the stack.
### Storage Layout
State variables are assigned storage slots sequentially starting from slot 0:
```
slot 0: uint256 totalSupply
slot 1: address owner (packed with next if possible)
slot 2: mapping(address => uint256) balances
```
**Mapping slot computation:**
```
slot(balances[addr]) = keccak256(abi.encode(addr, 2))
// where 2 is the base slot of the mapping
```
**Nested mapping:**
```
slot(allowances[owner][spender]) = keccak256(abi.encode(spender, keccak256(abi.encode(owner, 3))))
```
**Dynamic array:**
```
slot(arr.length) = base_slot
slot(arr[i]) = keccak256(abi.encode(base_slot)) + i
```
Reading storage directly with `eth_getStorageAt` is essential for debugging and security research.
### Memory Layout
Memory is byte-addressable and allocated linearly. Solidity's memory layout:
- `0x00-0x3f`: Scratch space for hashing
- `0x40-0x5f`: Free memory pointer (initially `0x80`)
- `0x60-0x7f`: Zero slot (used as initial value for dynamic memory arrays)
- `0x80+`: Usable memory
Memory expansion costs gas quadratically: `memory_cost = (memory_size_word^2) / 512 + 3 * memory_size_word`. Avoid unnecessary memory expansion in hot paths.
### ABI Encoding and Decoding
The ABI specification defines how function calls and return values are encoded:
**Function selector:** First 4 bytes of `keccak256("functionName(type1,type2)")`.
**Static types** are encoded in-place as 32-byte words. **Dynamic types** (bytes, string, arrays) use a pointer (offset) in-place, with the actual data at the offset.
```
// transfer(address,uint256) call encoding:
0xa9059cbb // selector
000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd // address (padded)
0000000000000000000000000000000000000000000000000de0b6b3a7640000 // uint256 (1e18)
```
Use `abi.encodePacked` for tightly packed encoding (no padding), but beware of hash collision risks with consecutive dynamic types.
### Yul and Inline Assembly
Yul is the intermediate language for the EVM. Use it when Solidity's compiler output is suboptimal:
```solidity
function efficientTransfer(address to, uint256 amount) internal {
assembly {
// Load balance from storage
let fromSlot := keccak256(0x00, 0x40) // assuming scratch space is set up
let fromBal := sload(fromSlot)
// Check balance
if lt(fromBal, amount) {
// revert with custom error
mstore(0x00, 0xf4d678b8) // InsufficientBalance.selector
revert(0x1c, 0x04)
}
// Update balances
sstore(fromSlot, sub(fromBal, amount))
// ... compute toSlot and update
}
}
```
Yul provides direct access to all EVM opcodes with a structured syntax (if/switch/for). It eliminates Solidity's safety checks, so you assume full responsibility for correctness.
### Gas Costs at the Opcode Level
Critical gas costs to internalize:
| Operation | Gas Cost |
|-----------|----------|
| SSTORE (0 -> non-zero) | 20,000 |
| SSTORE (non-zero -> non-zero) | 2,900 |
| SSTORE (non-zero -> 0) | 2,900 + 4,800 refund |
| SLOAD (cold) | 2,100 |
| SLOAD (warm) | 100 |
| MLOAD/MSTORE | 3 + expansion |
| CALL (cold address) | 2,600 |
| CALL (warm address) | 100 |
| LOG0-LOG4 | 375 + 375*topics + 8*bytes |
| CALLDATALOAD | 3 |
The cold/warm distinction (EIP-2929) is critical: first access to a storage slot or address in a transaction is "cold" (expensive), subsequent accesses are "warm" (cheap).
### CREATE vs CREATE2
**CREATE:** Address = `keccak256(rlp([sender, nonce]))[12:]`. Address depends on deployer's nonce — non-deterministic.
**CREATE2:** Address = `keccak256(0xff ++ sender ++ salt ++ keccak256(initcode))[12:]`. Deterministic — the same inputs always produce the same address. Enables counterfactual instantiation (interacting with an address before deployment).
```solidity
assembly {
let addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
```
CREATE2 is the foundation of patterns like CREATE3, deterministic deployments across chains, and account abstraction.
### Precompiled Contracts
Addresses `0x01` through `0x09` are precompiles — native implementations of expensive operations:
- `0x01`: ECRECOVER (signature recovery)
- `0x02`: SHA-256
- `0x03`: RIPEMD-160
- `0x04`: IDENTITY (memory copy)
- `0x05`: MODEXP (modular exponentiation)
- `0x06-0x08`: BN256 elliptic curve operations (used in ZK proofs)
- `0x09`: BLAKE2b
Post-Dencun, `0x0a` is the KZG point evaluation precompile for blob verification.
## Advanced Patterns
### Returndata Forwarding in Proxies
The canonical proxy pattern forwards returndata from delegatecall:
```yul
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
```
This is the core of every proxy contract — understanding it at this level is mandatory for proxy development.
### Storage Collision Detection
When using proxies, implementation and proxy storage must not collide. EIP-1967 defines standard storage slots:
```
Implementation slot: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
Admin slot: bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)
```
The `-1` ensures these slots are not the output of any standard keccak256 computation, preventing accidental collision.
### EVM Object Format (EOF)
EOF (EIP-3540 and related) restructures EVM bytecode into sections: code, data, and type information. It introduces RJUMP (relative jumps), removes JUMPDEST analysis, and separates code from data. This is the future of the EVM — contracts will deploy faster and execute more predictably.
### Transient Storage Opcodes
TSTORE (0x5d) and TLOAD (0x5c) provide transaction-scoped storage at dramatically lower gas cost than SSTORE/SLOAD. Perfect for reentrancy locks, callback data passing, and ERC-20 approval patterns within a single transaction.
## What NOT To Do
- **Never write inline assembly without exhaustive testing** — the compiler cannot check your stack management. One misplaced SWAP and you corrupt all subsequent logic.
- **Never assume opcode gas costs are permanent** — they change across hard forks (e.g., EIP-2929 doubled cold access costs). Write code that adapts.
- **Never use EXTCODESIZE to check if a caller is an EOA** — it returns 0 during contract construction, creating a bypass.
- **Never use assembly for standard operations** — the Solidity compiler optimizes well for typical patterns. Use assembly only for proven hot paths.
- **Never store secrets in contract storage** — all storage is publicly readable via `eth_getStorageAt`, regardless of the `private` visibility keyword.
- **Never use MSIZE for memory allocation** — it returns the highest accessed memory offset, not the free memory pointer. Always read from `0x40`.
- **Never forget to update the free memory pointer** when allocating memory in assembly — Solidity code after your assembly block will overwrite your data.
- **Never ignore the quadratic memory expansion cost** — allocating 1MB of memory costs approximately 3 billion gas.
- **Never hardcode gas amounts in CALL** — forward all available gas with `gas()` unless you have a specific reason to limit it.