Solidity¶
What experienced Solidity engineers forget before an interview, grouped by subtopic.
Language semantics¶
Visibility and mutability¶
externalfunctions are callable internally only asthis.f()— a real external call;publicworks both ways.privateisn't secret: anyone can read any storage slot off-chain witheth_getStorageAt.- External calls to
view/purefunctions useSTATICCALL(EIP-214), so a state change inside them reverts at runtime — except libraryviewfunctions, called withDELEGATECALLand not enforced.
Inheritance and linearization¶
- Bases are listed most base-like first:
contract D is B, C(bothis A) linearizes to D → C → B → A (C3, like Python's MRO). supercalls the next contract in the linearization, not the declared parent: in D,super.f()inside C calls B even though C only inherits A.- Overridable functions need
virtual; overriding needsoverride, andoverride(B, C)when several bases define it; implementing a single interface's function needs nooverridesince 0.8.8.
Modifiers¶
_marks where the function body runs in a modifier; areturnin the body only leaves the body, so code after_still runs after the return.- Modifiers apply left to right;
_may run several times, and if it never runs the body is skipped and return values stay at their defaults. - Legacy codegen inlines modifier code at every use — move the logic into an internal function to shrink bytecode; via-IR emits modifiers as functions.
Libraries and user-defined types¶
internallibrary functions are inlined into the caller (aJUMP);public/externalones run throughDELEGATECALLinto a separately deployed, linked library.- Libraries have no state variables and can't receive ETH;
using L for Tattaches their functions to a type, andglobal(user-defined types only) applies it in every file. - User-defined value types (
type Price is uint256;, 0.8.8) are zero-cost wrappers that stop mixing units;using {add as +} for Price globalbinds operators.
Arithmetic and casts¶
- Overflow and underflow revert since 0.8 with
Panic(0x11);unchecked { }opts out for proven-safe math. - Explicit downcasts truncate silently, even in 0.8:
uint8(uint256(300)) == 44— useSafeCast. int↔uintconversions reinterpret bits:uint256(int256(-1)) == type(uint256).max.- Division truncates, so multiply before dividing; OpenZeppelin's
Math.mulDivkeeps a 512-bit intermediate so the product can't overflow.
Data and storage¶
Data locations¶
storageis persistent state;memoryis per call;calldatais read-only input — the cheapest data location for external array and struct parameters.- Storage → memory assignment copies; memory → memory copies only the reference; a local
storagevariable is a pointer into state.
Storage layout¶
- Variables under 32 bytes pack into shared slots in declaration order; structs and arrays always start a new slot.
- A
mappingvalue lives atkeccak256(key . slot); a dynamic array stores its length at the slot and elements fromkeccak256(slot). string/bytesup to 31 bytes sit in the slot itself withlength * 2in the lowest byte; longer ones storelength * 2 + 1there and data fromkeccak256(slot).- With inheritance, slots follow the C3 linearization, most base-like contract first — reordering bases shifts every slot.
Transient and relocated storage¶
transientstate variables (0.8.28, value types only) compile toTSTORE/TLOAD(EIP-1153): 100 gas, cleared at the end of the transaction — see ethereum.md.- They can't have initializers, and they persist across calls within one transaction — a lock or flag must be reset explicitly, or a later call in a batch sees it.
Mapping and delete gotchas¶
- Mappings have no length and can't be iterated; every key "exists" with a zero value — keep an
EnumerableSet/EnumerableMapto enumerate. deleteon a struct skips its mappings: the entries survive and reappear if the struct is reused.delete arr[i]zeroes the element without shrinking the array; swap with the last element andpop()to remove it.deleteon a dynamic storage array clears every element — its gas grows with the length.
Calls and ABI¶
Function selectors and fallback¶
- Calldata starts with a 4-byte selector (first 4 bytes of
keccak256("fn(types)")) followed by ABI-encoded arguments; selectors can collide, a risk in proxy dispatchers. - No matching selector →
fallback(); plain ETH with empty calldata →receive()(or a payable fallback); a contract with neither rejects plain ETH.
call, delegatecall, staticcall¶
callruns in the callee's context;delegatecallruns the callee's code on the caller's storage,msg.sender, andmsg.value;staticcallforbids state changes.- The 63/64 rule (EIP-150): a call forwards at most 63/64 of remaining gas, keeping 1/64 for the caller.
delegatecallkeepsmsg.value, so a payable multicall that delegatecalls itself credits one payment many times (the 2021 SushiSwap MISO bug); the same happens readingmsg.valuein a loop.
Sending ETH¶
transferreverts andsendreturnsfalseon failure; both forward only a 2,300-gas stipend — too little for most smart wallets, and brittle when opcode costs change (EIP-1884 broke receivers in 2019).- Both are deprecated since 0.8.31, ahead of removal in 0.9; use
(bool ok, ) = to.call{value: amount}(""), checkok, and guard against reentrancy. - ETH can arrive without running your code — another contract's
selfdestruct, fee recipient or withdrawal credits — soaddress(this).balancecan exceed internal accounting; never use it in a strict equality.
ABI encoding pitfalls¶
- A low-level call to an address with no code succeeds — check
code.lengthfirst when you expect a contract (precompiles have no code either); high-level calls revert instead. abi.encodePackedof several dynamic values can collide (("a","bc")vs("ab","c")) — hashabi.encodeoutput for signatures.abi.encodeCall(0.8.11) type-checks the function and arguments;abi.encodeWithSignaturesilently accepts a typo in the signature string.
Event encoding¶
- An indexed
string,bytes, array, or struct is stored as its keccak256 hash — the value can't be recovered from the log. anonymousevents drop the signature topic, allowing 4 indexed parameters, but can't be filtered by event name.- Non-indexed parameters are ABI-encoded into the log data; contracts can't read logs back — see ethereum.md.
Security¶
Reentrancy¶
- An external call re-enters before state updates: single-function, cross-function, cross-contract, or read-only (a view function returns pre-update state to another protocol).
- Fix with Checks-Effects-Interactions plus a reentrancy guard; OpenZeppelin's
ReentrancyGuardTransientkeeps the lock in transient storage. - Hooks are external calls too: ERC-721/1155
safeTransferreceivers, ERC-777 hooks, and ETH sent to a contract.
Access control¶
- Missing modifiers and unprotected initializers let anyone call admin functions.
- Authenticate with
msg.sender, nevertx.origin— a malicious contract the owner calls can pass atx.origincheck. msg.sender == tx.originno longer proves there is no code in the path: since EIP-7702 (Pectra, May 2025) a delegated EOA runs code and makes several calls per transaction.code.length == 0doesn't prove an EOA either: it's 0 for a contract still in its constructor, and a 7702-delegated EOA has code.
Admin keys and governance¶
Ownable2Stepmakes the new owner accept the transfer, so a typo in the address can't brick ownership.- Route admin actions through a timelock controlled by a multisig, giving users a window to exit before a change lands.
- Flash-loan governance attacks borrow voting power for one transaction (Beanstalk, April 2022, ~$182M lost); snapshot voting power at proposal creation (
ERC20Votescheckpoints) and add a voting delay.
Signature replay and malleability¶
- Signed messages without a nonce, chainId, and deadline can be replayed on another chain or later (EIP-712 domains bind the chain and contract).
- ECDSA
scan be flipped (n − s) into a second valid signature — enforce low-s(EIP-2) and never use a signature as a unique ID. - Raw
ecrecoverreturnsaddress(0)on a bad signature, which matches an unset signer — use OpenZeppelin'sECDSA, which reverts and enforces low-s. - Smart-contract wallets (Safe, ERC-4337 accounts) sign via ERC-1271
isValidSignature; OpenZeppelin'sSignatureCheckertries ECDSA, then ERC-1271, and ERC-6492 wraps signatures from wallets not yet deployed.
Denial of service¶
- Unbounded loops over user-growable arrays eventually exceed the gas limit — paginate or cap the size.
- Pushing ETH to many receivers fails if one reverts (a contract without
receive) — switch to pull payments (a withdraw function). - A malicious callee can return huge data that Solidity copies into memory even when discarded — a returnbomb; cap the copy with an assembly
call(as inExcessivelySafeCall).
Randomness and block data¶
block.prevrandao(0.8.18, replaceddifficulty; EIP-4399) is known to the proposer, who can bias it by withholding a block — use Chainlink VRF or commit-reveal.blockhash(n)returns 0 for blocks older than 256; EIP-2935 (Pectra) serves 8,191 hashes from a system contract, but the opcode is unchanged.- Anything derived from block data can be computed by an attacker's contract in the same transaction, which reverts when the outcome is bad.
Integration risks¶
Token integration quirks¶
- Non-standard ERC-20s (USDT) return nothing or
falsefromtransfer— useSafeERC20. - Fee-on-transfer and rebasing tokens break "amount sent = amount received" assumptions — measure balance deltas.
- Changing a non-zero allowance lets the spender front-run the change and spend old + new.
- USDT's
approvereverts unless the current allowance is 0 —SafeERC20.forceApproveresets it first. - Blocklists (USDC, USDT) make any transfer to or from a flagged address revert.
Oracle integration¶
- Never price assets off a DEX spot price — manipulation and TWAPs in ethereum.md.
- Chainlink
latestRoundData: check staleness (updatedAtagainst the feed's heartbeat) andanswer > 0; on L2s also check the sequencer uptime feed. - Feeds have their own decimals (USD pairs use 8, ETH pairs 18) — scale before combining with token amounts.
Front-running and slippage¶
- Public-mempool transactions can be front-run or sandwiched: the attacker trades right before and after the victim's swap.
- Swap and liquidity functions take a
minAmountOutand adeadlinechosen off-chain by the user; a minimum computed on-chain from the current spot price protects nothing. - Use commit-reveal for bids, auctions, and games where seeing a pending transaction gives an edge; MEV mechanics in ethereum.md.
Rounding and precision¶
- Round in the protocol's favor: ERC-4626 rounds shares down on deposit and up on withdraw.
- First-depositor inflation attack: the attacker mints 1 wei of shares and donates assets, so the next depositor's shares round down to 0 — mitigate with virtual shares (OpenZeppelin's decimals offset) or dead shares.
- Tokens mix decimals (USDC 6, WETH 18) — normalize before math and never compare raw amounts across tokens.
Gas optimization¶
Gas optimization patterns¶
SSTOREcost compares the slot's value at transaction start, now, and new (EIP-2200): a clean slot (now == start) costs 20,000 gas from zero or 2,900 from non-zero; a dirty slot or a no-op costs 100; +2,100 if cold (EIP-2929) — see ethereum.md.- Pack small variables that are read and written together into one slot.
constant/immutablevalues are embedded in bytecode — noSLOAD.- Cache storage reads in local variables inside loops; use
calldataparameters. - Custom errors are cheaper than revert strings; the compiler already skips overflow checks on simple loop-counter increments, so
unchecked { ++i; }is no longer needed there.
Code size limit¶
- Deployed code is capped at 24,576 bytes (EIP-170) and initcode at 49,152 (EIP-3860, Shanghai).
- Workarounds: move logic into external libraries, split into several contracts, lower optimizer
runs, or use the Diamond pattern.
Clones and factories¶
- An EIP-1167 minimal proxy is 45 bytes of runtime code that
delegatecalls a fixed implementation, so deploying many instances (vaults, wallets, pools) costs a fraction of a full deployment. - Clones can't be upgraded and run no constructor: they need an initializer, and each call pays an extra
DELEGATECALL. CREATE2sets the address tokeccak256(0xff ++ deployer ++ salt ++ keccak256(initcode))[12:], known before deployment (counterfactual wallets).- Derive the salt from the caller and the init parameters, or a front-runner can take the predicted address with other arguments.
Upgradeability¶
Proxy patterns¶
| Pattern | Upgrade logic lives in | Note |
|---|---|---|
| Transparent | proxy (admin-only path) | admin can't call the implementation |
| UUPS (ERC-1822) | implementation | cheaper proxy; an implementation without upgrade code bricks it |
| Beacon | shared beacon contract | upgrades many proxies at once |
| Diamond (EIP-2535) | proxy routing to many facets | per-selector implementations |
- Every pattern keeps storage in the proxy and
delegatecalls the implementation.
Proxy storage and initializers¶
- The implementation address sits at the pseudo-random EIP-1967 slot, away from the implementation's own variables.
- New versions only append storage variables, never reorder — or use ERC-7201 namespaced storage.
- Constructors don't run through a proxy: use initializers, and call
_disableInitializers()in the implementation's constructor. immutablevalues live in the implementation's bytecode, so every proxy shares the values set at implementation deployment.
Upgrade safety¶
- A proxy function whose selector matches an implementation function shadows it — Transparent proxies route by caller, UUPS proxies have no functions of their own.
- An uninitialized UUPS implementation let anyone take ownership and upgrade it to a
selfdestructcontract, bricking every proxy (OpenZeppelin advisory, 2021); since EIP-6780 the code survives. - Metamorphic contracts redeployed different code at one address via
CREATE2+SELFDESTRUCT; EIP-6780 (Cancun) ended the trick. - OpenZeppelin's Upgrades plugins (Foundry, Hardhat) diff storage layouts and flag unsafe patterns before an upgrade.
Errors and reverts¶
Error types¶
require(cond, "msg")andrevert("msg")encodeError(string)(selector0x08c379a0).assert, overflow, division by zero, and out-of-bounds access raisePanic(uint256)with codes0x01,0x11,0x12,0x32.- Before 0.8,
asserthit theINVALIDopcode and burned all remaining gas; now it reverts like any other error. - Custom errors (0.8.4) encode a selector plus arguments;
require(cond, MyError())accepts them since 0.8.26 (via-IR) / 0.8.27 (legacy).
try/catch limits¶
try/catchworks only on external calls andnewcontract creation, not on internal calls.- Catches only reverts inside the callee: undecodable return data or a target with no code reverts the caller.
- Clauses:
catch Error(string memory),catch Panic(uint256), andcatch (bytes memory)for everything else — a custom error can't be caught by name. - The callee can burn 63/64 of the gas (EIP-150), leaving too little for the
catchblock.
Revert data bubbling¶
- A low-level
callreturns(bool success, bytes memory data): a callee revert givessuccess == falseinstead of bubbling up — always check it. - Re-throw the callee's error unchanged with assembly:
revert(add(data, 32), mload(data)). - OpenZeppelin's
Address.functionCalldoes both.
Compiler and tooling¶
Compiler pipelines¶
- via-IR compiles through Yul: stronger cross-function optimization and fewer "stack too deep" errors (it moves variables to memory), but slower builds; legacy is still the default.
- via-IR changes some semantics: each contract initializes its state variables right before its own constructor, and modifiers reset return variables at every
_. - Optimizer
runs(default 200) is the expected number of executions per opcode: lower favors deployment cost, higher favors call cost.
EVM version targeting¶
evmVersionpicks the target hard fork; the default moved toshanghaiin 0.8.20 (emitsPUSH0, EIP-3855),cancunin 0.8.25,praguein 0.8.30, andosakain 0.8.31.- Code with an opcode the target chain lacks fails to deploy —
PUSH0broke L2 deployments in 2023; setevmVersionto the oldest chain you target. - Pin the pragma (
pragma solidity 0.8.37;) in contracts you deploy; a floating^0.8.0suits libraries.
Testing with Foundry¶
- Tests are Solidity contracts; cheatcodes (
vm.prank,vm.warp,vm.deal,vm.expectRevert) set the caller, time, balances, and expected failures. - Fuzz tests take random arguments (256 runs by default);
bound()constrains them. - Invariant tests call random sequences of handler functions and check properties after each (total supply = sum of balances).
- Fork tests (
--fork-url) run against live chain state.
Static analysis and formal verification¶
- Slither flags known bug patterns (reentrancy, shadowing, uninitialized storage) in seconds.
- Symbolic and formal tools (Halmos, Certora, the built-in SMTChecker) prove properties for all inputs, not just sampled ones.
Inline assembly¶
Memory layout¶
0x00–0x3fis scratch space for hashing,0x40holds the free memory pointer,0x60is the zero slot; allocation starts at0x80— hence bytecode starting6080604052.- Memory is never freed within a call: Solidity only bumps the free memory pointer.
Assembly gotchas¶
- No overflow or bounds checks, and values narrower than 256 bits may carry dirty upper bits — mask them.
- Mark blocks
assembly ("memory-safe")(0.8.13) so via-IR can still move variables to memory. returnandrevertin inline assembly end the whole call, not just the current function.- Storage variables expose
.slotand.offset; calldata arrays expose.offsetand.length.