EVM and Solidity
In one line
Section titled “In one line”The EVM (Ethereum Virtual Machine) is a deterministic stack machine that every node runs identically. Solidity is the language most contracts are written in. A transaction is a signed request that makes the EVM run one contract function and pay for every instruction in gas.
Why it exists
Section titled “Why it exists”Bitcoin (2009) could only move coins. Ethereum (2015) put a general-purpose virtual machine on the same kind of replicated ledger, so anyone can deploy a program that every node executes. Two problems follow from “strangers run your code”:
- Runaway code. A loop that never ends would stall every node. The fix is gas: every instruction has a price, the sender sets a budget, and execution stops when the budget runs out.
- Fee chaos. Fees were a blind auction until EIP-1559 (proposed 2019, live since 2021) split the fee into a protocol-set base fee and a tip.
Later changes worth knowing: Solidity 0.8 made arithmetic revert on overflow by default. EIP-1153 added transient storage that is cleared at the end of the transaction. EIP-7702 (Pectra upgrade, May 2025) lets a normal wallet delegate to contract code.
How it works
Section titled “How it works”One transaction, end to end
Section titled “One transaction, end to end”- Sign. The wallet or signer builds the transaction and signs it with the private key. Fields: from, to, value, data (calldata: 4-byte selector plus ABI-encoded arguments), nonce, gas limit,
maxFeePerGasandmaxPriorityFeePerGas(EIP-1559), chainId, signature (v, r, s). - Send. The signed bytes go to an RPC node, the API endpoint. The node checks the signature, the nonce and that the sender can pay.
- Gossip. The node passes the transaction into the public mempool. Anyone can watch it there before it lands.
- Bid. Block builders pick transactions and order them, mostly by fee.
- Seal. The transaction lands in a block. Ethereum produces one block every 12 seconds.
- Execute. Every node runs the call through the EVM. The transaction pays an intrinsic cost of 21,000 gas before any code runs, then gas per opcode. If anything reverts, all state changes roll back and the gas spent is still paid.
- Wait. The block becomes final about 13 minutes later. Until then a reorg can drop it. See Chain mechanics.
Accounts
Section titled “Accounts”- EOA (externally owned account): controlled by a private key, has a nonce, no code.
- Contract account: code plus storage. It acts only when called.
- Since EIP-7702 (Pectra, May 2025) an EOA can delegate to contract code, so “EOA means no code” is no longer safe.
Transaction fields and the nonce
Section titled “Transaction fields and the nonce”The nonce gives ordering and replay protection per account. It must increase by exactly one each time, so a gap blocks every later transaction from that sender. The chainId binds the signature to one chain.
The function to run is picked by the selector: the first 4 bytes of the Keccak-256 hash of the function signature. For transfer(address,uint256) that is 0xa9059cbb. The arguments follow, ABI-encoded in 32-byte words.
Gas and fees
Section titled “Gas and fees”- Every opcode (a single EVM instruction) costs gas. Storage writes cost the most.
- Fee paid = gas used × (base fee + tip). The protocol sets the base fee per block and moves it by at most 12.5% from one block to the next, depending on how full the previous block was.
maxFeePerGascaps what the sender will pay in total per gas unit.maxPriorityFeePerGasis the tip. - All transactions in a block must fit under the block gas limit. An unbounded loop over an array that users can grow can hit that limit and lock a function forever.
Who is calling
Section titled “Who is calling”msg.senderis the immediate caller.tx.originis the EOA that started the transaction.- Never authorise on
tx.origin. A malicious contract that the owner calls can then act as the owner.
Functions, modifiers, errors, events
Section titled “Functions, modifiers, errors, events”- Mutability:
viewreads state,pureneither reads nor writes,payableaccepts ETH. - Special functions:
receive()runs on plain ETH transfers,fallback()runs for unknown selectors. - Modifiers wrap functions with a check:
onlyOwner,nonReentrant,whenNotPaused. - Errors:
requireandrevert CustomError()roll back the whole call. - Events are logs. They are cheap, contracts cannot read them, and they are what monitoring watches:
Upgraded,RoleGranted,OwnershipTransferred.
Arithmetic
Section titled “Arithmetic”Solidity 0.8 and later reverts on overflow, except inside unchecked {} and in assembly. There are no floats: everything is an integer scaled by decimals (ETH 18, USDC 6). Division rounds down. Who the rounding favours is a security decision.
Storage is public
Section titled “Storage is public”A contract’s storage is a key-value store of 32-byte slots, numbered from 0 in the order variables are declared. Everything in storage is readable by anyone, including variables marked private. private only stops other contracts from reading it through Solidity.
Compare
Section titled “Compare”Account types
Section titled “Account types”| Type | Controlled by | Has code | Can start a transaction | Note |
|---|---|---|---|---|
| EOA | A private key | No | Yes | The classic wallet |
| Contract account | Its own code | Yes | No, it only reacts to calls | Has its own storage and balance |
| EOA with EIP-7702 delegation | A private key, running delegated code | A pointer to a contract | Yes | Breaks checks that assume an EOA has no code |
| Contract wallet (Safe, ERC-4337 smart account) | Code that checks owners’ signatures | Yes | No, someone must submit for it | Signatures are checked through ERC-1271 |
Call types
Section titled “Call types”| Opcode | Whose code runs | Whose storage it touches | msg.sender seen by the code |
Use |
|---|---|---|---|---|
call |
Target’s | Target’s | The calling contract | Ordinary calls and ETH transfers |
delegatecall |
Target’s | The caller’s | The original caller | Proxies and libraries. The most dangerous opcode |
staticcall |
Target’s | None, state changes are not allowed | The calling contract | view and pure calls |
Low-level calls return (bool, bytes) and do not revert on failure. Check the bool.
Data locations
Section titled “Data locations”| Location | Lifetime | Cost | Note |
|---|---|---|---|
storage |
Permanent | Expensive | 32-byte slots, public to everyone |
memory |
One call | Cheap | Scratch space |
calldata |
One call | Cheapest | Read-only input |
| transient (EIP-1153) | One transaction | Cheap | Cleared at the end of the transaction; used for cheap reentrancy locks |
Function visibility
Section titled “Function visibility”| Visibility | Callable from outside | Callable from inside | Callable by derived contracts |
|---|---|---|---|
external |
Yes | Only through this.f() |
Only through this.f() |
public |
Yes | Yes | Yes |
internal |
No | Yes | Yes |
private |
No | Yes | No |
Anything external or public is reachable by anyone on the internet unless a modifier or a require stops them.
What breaks
Section titled “What breaks”The commands use cast from Foundry and assume --rpc-url or ETH_RPC_URL is set.
| Symptom | Cause | Check |
|---|---|---|
| Transactions from one address stop landing | Nonce gap, or an earlier transaction is stuck with a low fee | cast nonce $ADDR against cast nonce $ADDR --block pending |
| Transaction reverted and you need to know where | A require or custom error fired somewhere in the call tree |
cast run $TX_HASH replays it and prints the trace |
| “Out of gas” on a function that used to work | A loop over an array that has grown | cast receipt $TX_HASH gasUsed, then read the loop bounds |
| Unknown calldata in a transaction someone asks you to sign | You cannot tell what it does from the hex | cast 4byte-calldata $CALLDATA or cast decode-calldata "f(address,uint256)" $CALLDATA |
A “secret” stored in a private variable leaked |
Storage is public | cast storage $CONTRACT 0 reads slot 0 |
| A low-level call failed without anyone noticing | The returned bool was ignored | Slither detector unchecked-lowlevel |
| An owner-only function was called by a stranger’s contract | Authorisation on tx.origin |
Slither detector tx-origin |
| A check for “caller is an EOA” let a contract through | EIP-7702 delegation: the EOA now runs code | cast code $ADDR returns 0xef0100 followed by the delegate address |
The classic bug: reentrancy
Section titled “The classic bug: reentrancy”The contract sends ETH first and zeroes the balance after. Sending ETH runs the receiver’s code, which calls withdraw() again while the balance is still full.
// Reentrantfunction withdraw() external { uint amt = bal[msg.sender]; (bool ok,) = msg.sender.call{value: amt}(""); require(ok); bal[msg.sender] = 0; // too late}// Checks, effects, interactions, plus a guardfunction withdraw() external nonReentrant { uint amt = bal[msg.sender]; // checks bal[msg.sender] = 0; // effects (bool ok,) = msg.sender.call{value: amt}(""); require(ok); // interactions}The runtime sequence, the variants and the real cases are in Contract attacks.
Keywords
Section titled “Keywords”EVM, Solidity, bytecode, opcode, EOA, contract account, EIP-7702, Pectra, transaction, calldata, ABI, function selector, Keccak-256, nonce, chainId, gas, gas limit, base fee, priority fee, maxFeePerGas, maxPriorityFeePerGas, EIP-1559, block gas limit, storage, memory, calldata, transient storage, EIP-1153, storage slot, msg.sender, tx.origin, call, delegatecall, staticcall, external, public, internal, private, view, pure, payable, receive, fallback, modifier, onlyOwner, nonReentrant, whenNotPaused, require, revert, custom error, event, unchecked, overflow, decimals, wei, checks-effects-interactions, cast, Foundry.
Sources
Section titled “Sources”- ethereum.org: Ethereum Virtual Machine
- ethereum.org: Opcodes
- ethereum.org: Accounts
- ethereum.org: Transactions
- ethereum.org: Gas and fees
- ethereum.org: Blocks
- EIP-1559: Fee market change
- EIP-1153: Transient storage opcodes
- EIP-7702: Set Code for EOAs
- Solidity docs: Contracts (visibility, modifiers, events, errors)
- Solidity docs: Layout of state variables in storage
- Solidity docs: Contract ABI specification
- Solidity docs: Security considerations
- Solidity docs: 0.8.0 breaking changes
- Foundry: cast reference
- Slither: detectors