Solidity basics
TL;DR: The minimum mental model: state lives in storage slots, functions are dispatched by 4-byte selectors, visibility is enforced at compile time only, modifiers wrap bodies, and
storagevsmemoryvscalldatadecides cost and aliasing. Read bytecode when source lies.
What it is
Solidity is a statically typed, contract-oriented language that compiles to EVM bytecode. A contract is a struct of state variables plus functions; the deployed bytecode begins with a dispatcher that reads the first 4 bytes of calldata (keccak256("funcSig(types)")[:4]) and jumps to the corresponding function. Knowing the data location rules (where a variable lives at runtime) and the storage layout (slot assignment) is what separates “I can write a contract” from “I can find bugs in one.”
Preconditions / where it applies
- Reading audit targets and writing PoCs
- Reasoning about gas, reverts, and storage collisions in proxies
- Decompiling unverified contracts when only bytecode is on-chain
Technique
State variable layout. Variables declared at contract scope land in storage, packed into 32-byte slots in declaration order. Booleans, small uints, and addresses are packed together; mappings and dynamic arrays use a derived slot:
mapping(k => v)value at slotkeccak256(abi.encode(k, p))wherepis the mapping’s declared slot.T[] dynlength at slotp, data atkeccak256(p) + i.structfields packed contiguously starting at the struct’s slot.
1
forge inspect MyContract storageLayout | jq .
Visibility. public (auto-getter), external (cheaper for external callers, can’t be called internally without this.), internal (subclass + this contract), private (this contract only — not secret, on-chain readable). Visibility is compile-time; nothing stops anyone from reading storage with eth_getStorageAt.
Function selectors + dispatch.
bytes4 sel = bytes4(keccak256("transfer(address,uint256)")); // 0xa9059cbb
The dispatcher is just a sequence of EQ/JUMPI against these constants. Selector collisions (cheap to find for 4-byte space) can be weaponised when a proxy forwards calldata to a logic contract.
Modifiers. Syntax sugar for wrapping a function body. Place _; where the body runs. Used for onlyOwner, nonReentrant, whenNotPaused. Reentrant or order-dependent logic in modifiers is a common bug.
Data locations. Reference types (string, bytes, arrays, structs) need a location:
storage— persistent contract state (writes cost SSTORE gas)memory— temporary, allocated per-callcalldata— read-only, cheapest, only valid forexternalparams
Aliasing trap: assigning a storage pointer copies the reference; assigning a memory variable copies the data.
unchecked, try/catch, low-level calls.
(bool ok, bytes memory ret) = target.call{value: 0, gas: 50_000}(data);
require(ok, string(ret));
call returns success bool — do not ignore it. delegatecall runs target code in caller’s context (proxies); staticcall forbids state writes.
Reading bytecode. cast disassemble or Dedaub / Panoramix decompile unverified contracts. cast 4byte 0xa9059cbb resolves a selector via the OpenChain registry.
Detection and defence
Not a defence topic on its own — but the patterns to watch for in code review:
- Public state with sensitive values (assume world-readable)
- Modifiers with state writes or external calls (re-entry surface)
- Missing visibility (
function foo()defaults to public in old code) - Implicit storage pointers in functions returning structs
unchecked { ... }blocks (see integer-overflow-solidity)
Related: smart-contracts-overview, ethereum-blockchain, foundry-toolkit, remix-tool.
References
- Solidity docs — language reference
- Solidity by Example — annotated patterns
- EVM Codes — opcode + gas reference
- OpenChain 4byte directory — selector lookup
- Solidity storage layout — slot rules