# What is the EVM? Ethereum Virtual Machine Explained
The Ethereum Virtual Machine (EVM) is the execution engine that runs smart contracts on Ethereum and all EVM-compatible blockchains (Polygon, Arbitrum, BSC, etc.). Understanding the EVM is crucial for writing efficient, secure Solidity code.
In this deep dive, we'll explore how the EVM works under the hood, why it matters for developers, and how to optimize your contracts for better performance.
What is a Virtual Machine?
A virtual machine is a software emulation of a computer. Just like you can run Windows on a Mac using VMware, the EVM runs bytecode in a sandboxed environment, isolated from the host system.
The EVM is:
- Deterministic: Same input = same output, always
- Sandboxed: Cannot access network, filesystem, or other contracts without explicit calls
- Gas-metered: Every operation costs gas to prevent infinite loops
- Stack-based: Uses a stack for computation (vs register-based like x86)
EVM Architecture
The Stack Machine Model
The EVM is a stack-based virtual machine. All operations manipulate a stack of 256-bit values.
Example: Adding Two Numbers
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
Compiled to bytecode, this becomes:
PUSH1 0x00 // Push 0 to stack
CALLDATALOAD // Load 'a' from calldata
PUSH1 0x20 // Push 32 (0x20) to stack
CALLDATALOAD // Load 'b' from calldata
ADD // Pop two values, push sum
PUSH1 0x00
MSTORE // Store result in memory
PUSH1 0x20
PUSH1 0x00
RETURN // Return 32 bytes from memory
Stack evolution:
[5] // a = 5 pushed
[5, 3] // b = 3 pushed
[8] // ADD pops both, pushes 8
Memory Layout
The EVM has three data storage areas:
Cost Comparison:
Stack access: 3 gas
Memory read: 3 gas
Memory expand: quadratic cost
Storage write: 20,000 gas (new), 5,000 gas (update)
Storage read: 2,100 gas
Key EVM Opcodes
Opcodes are the assembly instructions the EVM executes. Solidity compiles to opcodes.
Stack Operations
PUSH1 0x42 // Push 1 byte (0x42) to stack
PUSH2 0x1234 // Push 2 bytes to stack
POP // Remove top item
DUP1 // Duplicate top item
SWAP1 // Swap top two items
Arithmetic
ADD // a + b
SUB // a - b
MUL // a * b
DIV // a / b (integer division)
MOD // a % b
EXP // a ** b (exponentiation)
Comparison & Logic
LT // Less than
GT // Greater than
EQ // Equal
ISZERO // Check if zero
AND // Bitwise AND
OR // Bitwise OR
XOR // Bitwise XOR
NOT // Bitwise NOT
Memory Operations
MLOAD // Load 32 bytes from memory
MSTORE // Store 32 bytes to memory
MSTORE8 // Store 1 byte to memory
Storage Operations
SLOAD // Load from persistent storage (2,100 gas)
SSTORE // Store to persistent storage (20,000 gas for new slot)
Call Operations
CALL // Call another contract
DELEGATECALL // Call with caller's context (used in proxies)
STATICCALL // Read-only call (cannot modify state)
CREATE // Deploy a new contract
CREATE2 // Deploy to deterministic address
Environment Information
ADDRESS // This contract's address
CALLER // msg.sender
CALLVALUE // msg.value (ETH sent)
CALLDATALOAD // Read from calldata
CALLDATASIZE // Size of calldata
TIMESTAMP // block.timestamp
NUMBER // block.number
GASPRICE // tx.gasprice
How Transactions Execute
When you send a transaction calling a smart contract:
- Set gas limit from transaction
- Load contract bytecode
- Prepare execution context (caller, value, calldata)
- Execute opcodes sequentially
- Deduct gas for each operation
- Modify state (storage, balance)
- Success: Changes committed, unused gas refunded
- Revert: Changes discarded, gas consumed
- Out of gas: Changes discarded, all gas consumed
Gas Costs Explained
Every opcode has a fixed gas cost. Gas prevents infinite loops and limits computation.
Common Operations:
ADD, SUB, MUL, DIV: 3-5 gas
SLOAD (storage read): 2,100 gas
SSTORE (new slot): 20,000 gas
SSTORE (update): 5,000 gas
LOG0: 375 gas + 8 gas per byte
CALL: 700 gas base + transfer cost
CREATE: 32,000 gas
Why is storage so expensive?
Storage is permanent and stored on every node. Writing to storage bloats the blockchain, so it's intentionally expensive to discourage overuse.
Gas Optimization Example:
// Bad: 3 SLOAD operations (6,300 gas)
function badSum() public view returns (uint) {
return myValue + myValue + myValue;
}
// Good: 1 SLOAD, use memory (2,100 gas)
function goodSum() public view returns (uint) {
uint val = myValue; // Cache storage read
return val + val + val;
}
Understanding Calldata, Memory, Storage
Calldata
- Read-only function arguments
- Cheapest to read (4 gas per non-zero byte, 16 gas per zero byte)
- Use for function parameters in external functions
function processArray(uint[] calldata data) external {
// 'data' lives in calldata, not copied to memory
}
Memory
- Temporary storage during function execution
- Wiped after function returns
- Use for temporary variables, dynamic arrays
function buildArray() public pure returns (uint[] memory) {
uint[] memory arr = new uint[](5);
arr[0] = 1;
return arr; // Exists only during execution
}
Storage
- Permanent blockchain storage
- Organized in 32-byte slots
- Each contract has its own storage
uint256 public myValue; // Slot 0
mapping(address => uint) public balances; // Slot 1+
EVM Bytecode Example
Let's see what Solidity compiles to:
contract SimpleStorage {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
Compile with solc --bin --asm SimpleStorage.sol:
Bytecode (partial):
PUSH1 0x80
PUSH1 0x40
MSTORE // Setup free memory pointer
CALLVALUE
DUP1
ISZERO
PUSH2 0x0F
JUMPI // Revert if ETH sent to non-payable function
...
CALLDATALOAD // Load function selector
PUSH4 0x3fa4f245 // setValue(uint256) selector
EQ
PUSH2 0x1E
JUMPI // Jump to setValue logic if selector matches
...
Why does this matter?
- Understanding bytecode helps you optimize gas usage
- You can audit contracts by reading bytecode
- Debugging reverts requires bytecode knowledge
EVM vs Other VMs
| Feature | EVM | WASM (Polkadot) | Move VM (Aptos) |
|---------|-----|-----------------|-----------------|
| Stack size | 1024 items | Dynamic | Register-based |
| Gas model | Per-opcode | Per-instruction | Per-bytecode |
| Determinism | Full | Full | Full |
| Language | Solidity, Vyper | Rust, C++ | Move |
| Upgradability | Proxy patterns | Native | Built-in |
EVM-Compatible Chains
The EVM's success led to many EVM-compatible chains:
- Layer 2s: Arbitrum, Optimism, zkSync, Polygon zkEVM
- Sidechains: Polygon PoS, Gnosis Chain
- Alternative L1s: BNB Chain, Avalanche C-Chain, Fantom
Why EVM compatibility?
- Reuse Solidity contracts without changes
- Same developer tools (Foundry, Hardhat, Remix)
- Leverage Ethereum's massive ecosystem
Optimizing for the EVM
1. Pack Storage Variables
// Bad: 3 storage slots
uint256 a; // Slot 0
uint128 b; // Slot 1
uint128 c; // Slot 2
// Good: 2 storage slots (b and c packed)
uint256 a; // Slot 0
uint128 b; // Slot 1 (first 128 bits)
uint128 c; // Slot 1 (last 128 bits)
2. Use immutable and constant
// Bad: SLOAD every time (2,100 gas)
address public owner;
// Good: Hardcoded into bytecode (3 gas)
address public immutable owner;
3. Minimize Storage Writes
// Bad: Update storage in loop
for (uint i = 0; i < 10; i++) {
total += i; // 10 SSTORE operations
}
// Good: Use memory, write once
uint temp = total;
for (uint i = 0; i < 10; i++) {
temp += i;
}
total = temp; // 1 SSTORE
4. Use Events Instead of Storage
For data you don't need to query on-chain, use events:
// Bad: Store every transaction (expensive)
Transaction[] public transactions;
// Good: Emit event (375 gas + data)
event TransactionMade(address indexed user, uint amount);
Why the EVM Matters for Developers
The Future: EVM Evolution
Upcoming improvements:
- EIP-4844 (Proto-Danksharding): Cheaper L2 data availability
- Verkle Trees: Reduce state storage size
- EVM Object Format (EOF): Better bytecode structure
- Account Abstraction (ERC-4337): Programmable accounts
Conclusion
The EVM is the beating heart of Ethereum and the broader EVM ecosystem. It's a simple stack machine with deterministic execution, metered by gas to prevent abuse.
As a Solidity developer, understanding the EVM helps you:
- Write cheaper contracts
- Debug faster
- Architect better
- Build confidently
Next steps:
- Explore bytecode with
forge inspect YourContract bytecode
- Use
forge test -vvvvto see EVM traces
- Learn assembly with
assembly { ... }blocks
The EVM is your runtime—master it, and you'll master Solidity. 💎