Overview
Curated: · Written: · Reviewed:
Smart-contract security protects invariants across adversarial composition
Interviewers use smart-contract security as a proxy for two things: whether you can reason about state machines that anyone can call, and whether you think in invariants instead of bug checklists. The strong answer in any security interview is a named invariant plus an attack that breaks it plus a mitigation with its own trade-off. The weak answer is a list: "reentrancy, overflow, access control, use OpenZeppelin." If you cannot say which invariant a pattern threatens and who can trigger it, you sound like you memorized a blog post.
Why this domain is different
Public code holds transferable value, adversaries can inspect and simulate it before acting, transactions compose many protocols atomically, and a finalized exploit may be hard to reverse. Security is therefore not a list of syntax warnings. It is evidence that critical economic, authorization and accounting invariants survive every reachable sequence of calls, actors, upgrades, oracle states and chain conditions within an explicit threat model.
Start with assets, powers and trust boundaries. Inventory tokens, native value, debt, collateral, governance rights, upgrade and pause authority, oracle inputs, bridges, callbacks, signatures, keepers and off-chain services. For each privileged or user action, state preconditions, postconditions and conservation rules. Identify who can censor, reorder, delay, manipulate prices, compromise keys, deploy adversarial contracts or exploit temporary liquidity. A design cannot be secured if its intended economic behavior is undefined.
What interviewers probe here: "Walk me through how you'd threat-model this vault." They want the inventory above, in order, not a jump straight to reentrancy. Weak answer: naming tools (Slither, Echidna) before naming a single asset or trust boundary. Likely follow-up: "What's the maximum credible loss if the oracle fails?" — have a number or a bound, not a shrug.
Access control and trust boundaries
Access control is the highest-leverage boundary. Every mint, burn, sweep, upgrade, pause, parameter, oracle and role-management path needs explicit authority. Check authorization at the actual state-changing entry point, not only in a trusted UI. Avoid tx.origin; an intermediary can preserve a victim as origin. Apply least privilege, separate routine and emergency roles, use two-step transfers, multisignature and delay where appropriate, and design recovery before administrators are lost or compromised.
Initialization is authorization. Proxy constructors do not initialize proxy storage, so exposed initializers and reinitializers can grant ownership to the first attacker who calls them. Initialize atomically at deployment, lock implementation instances when appropriate, version initialization, validate inheritance order and test an already-initialized live state. Upgrade authority can replace all future logic; storage compatibility, bytecode review, delay, event monitoring and emergency rollback or migration are security controls.
What interviewers probe: "Who can call this function, and what exactly can they do with that power?" and "What happens if the admin key is compromised?" The strong answer names the role, the blast radius, and the delay or timelock that bounds the damage. Weak answer: "we use OpenZeppelin's Ownable" with no discussion of what the owner can do or how the key is held. Follow-up to expect: "Why is tx.origin unsafe but msg.sender fine?" — be ready to trace a phishing-indirect-call scenario in two sentences.
Reentrancy, in all four shapes
Reentrancy is the canonical interview probe, and the trap is answering with only the classic Ether-withdrawal pattern. Reentrancy occurs when control leaves a contract before its invariant is restored and an adversarial callee returns through any reachable entry point. The four shapes interviewers expect you to distinguish:
- Same-function — the textbook case: the balance is zeroed after the transfer, so the callback withdraws again.
- Cross-function — the callback enters a different function that reads the same not-yet-updated state (e.g.
withdrawreentersbalanceOf-backed logic). - Cross-contract — two contracts share state through a third (a vault and its accounting token), and reentering one corrupts the other's view.
- Read-only — no state is changed in the victim, but a view function exposes an inconsistent intermediate value (typically a pool's spot price mid-operation) that another protocol reads and acts on. Curve-style pool exploits used exactly this.
Use checks-effects-interactions where valid, pull payments, narrow guards and state-machine design, then test malicious callbacks across the whole call graph. Note the honest caveat: a reentrancy guard on one function does not protect its siblings, and guards do not compose across contracts.
What interviewers probe: "Explain reentrancy without the bank analogy" and "can reentrancy happen without transferring Ether?" (Yes — ERC-777 hooks, ERC-721 onERC721Received, any callback.) Weak answer: reciting checks-effects-interactions without being able to say which invariant it protects (accounting is restored before control leaves). Follow-up: "Why is read-only reentrancy dangerous if nothing is written?" — because the reader is a different protocol whose own invariant depends on your intermediate state.
External calls and Ether-handling edge cases
External calls return untrusted behavior and data. A low-level call succeeding means only that it did not revert at the EVM boundary. Tokens may return false, return no bytes, take fees, rebase, invoke hooks or block addresses. Validate the target, interface, return encoding and the actual balance delta your invariant requires. Limit arbitrary delegatecall because it executes foreign bytecode with the caller's storage and authority. Never let attacker-controlled return or revert bytes drive privileged logic without validation.
Ether specifically has edge cases interviewers like: transfer and send forward only 2300 gas, which fails against any recipient with logic (and post-EIP-150 gas-cost changes made that limit fragile); plain call forwards all gas and reentrancy risk with it; a constructor with no payable keyword rejects value; and selfdestruct can force value into a contract that assumes it holds none, breaking equality assumptions. Prefer call with an explicit return-value check, or pull payments, over transfer.
What interviewers probe: "call vs transfer — which and why?" and "what does a successful low-level call actually prove?" Weak answer: "transfer is safer" without the 2300-gas caveat. Follow-up: "How do you integrate a fee-on-transfer token into a vault?" — measure balance deltas, not return values.
Arithmetic, rounding and precision
Solidity 0.8 checks ordinary overflow, but unchecked blocks and narrowing casts can wrap or truncate. Integer division loses precision; mismatched token and oracle decimals can scale values by orders of magnitude; rounding direction can transfer value; repeated small operations can accumulate dust or extract it. State units in names and types where possible, normalize once with bounds, order multiplication before division deliberately, and test extrema plus adversarial sequences.
What interviewers probe: "Give me a rounding bug that extracts value." Two strong answers:
- Repeated rounding extraction: a withdraw path that rounds in the user's favor each call. Withdraw 3 units ten times at a rounding error of 0.4 per call and you receive 30 units while the ledger decrements less than 30 — the vault bleeds the accumulated remainder.
- First-depositor (inflation) attack, traced: a vault mints shares as
shares = assets * totalSupply / totalAssets. Attacker deposits 1 wei of the asset when supply is 0, minting 1 wei of shares (the floor of 1×1/1). Attacker then donates, say, 100 tokens to the vault directly. NowtotalAssets = 100.000000001e18-scale againsttotalSupply = 1 wei, so each share is backed by an enormous asset amount. A victim deposits 100 tokens and receives100e18 * 1 / 100.000000001e18≈ 0 shares (floored), leaving their deposit redeemable only by the attacker's share. The donation only inflates the ratio because the attacker's deposit came first — donating before any deposit exists does nothing, since the first depositor's mint is proportional to assets at that moment. Mitigations: mint a minimum virtual supply at deployment, round shares up for depositors, and cap the donation effect with a virtual-offset decimal scheme.
Weak answer: "Solidity 0.8 handles overflow" as if that closed the category.
Oracles, economic manipulation and ordering
Oracles are authenticated but fallible inputs. Validate feed identity, sign and range, decimals, update timestamp, round completeness and liveness. A spot price from a shallow pool can be moved within one transaction, especially with flash liquidity. Use an independently governed feed or a sufficiently long manipulation-resistant observation appropriate to the asset, and specify circuit breakers and fallback behavior. Multiple sources do not help when they share one upstream market or administration.
Flash loans are an amplifier, not a root cause: they provide temporary capital that makes an existing oracle, governance, accounting or liquidity assumption exploitable atomically. Assume adversaries can borrow large amounts for one transaction, call every public entry point in a chosen order, and restore the loan before completion. Test invariants under extreme but valid balances instead of banning one funding mechanism.
Ordering is adversarial. Public transactions can be copied, inserted before, placed after, censored or replaced. Slippage limits, deadlines, nonces, commit-reveal, batch auctions or private submission bound particular risks, each with availability and trust trade-offs. Block timestamps and prevrandao are consensus inputs with bounded uses, not unbiased secret randomness for high-value games.
What interviewers probe: "Design a lending protocol's price feed" — the strong answer names the asset's liquidity, the observation window, the deviation bound, and what happens when the feed goes stale (pause? conservative fallback? who decides?). Weak answer: "use Chainlink" with no staleness check or fallback. Follow-up: "Walk me through a flash-loan attack on your design" — they want you to compose borrow → manipulate → extract → repay in one transaction and show which invariant holds anyway.
Randomness, timestamps and denial of service
For unpredictability that carries value, use a verifiable randomness mechanism with request binding and callback validation, and freeze the eligible set before the result. block.timestamp is fine for coarse time windows (it's bounded by a few seconds of miner influence per consensus rules) but not for lotteries; block.number and prevrandao are observable or biasable by a proposer who can choose among outcomes.
Denial of service often emerges from unbounded work or mandatory push interactions. A user-growing array can make a loop exceed block gas; one recipient that always reverts can block a batch; forced native value can violate equality assumptions; external dependencies can pause. Prefer pull claims, bounded pagination, idempotent progress markers and failure isolation. Model what happens when an oracle, bridge, token, administrator or recipient is unavailable indefinitely.
What interviewers probe: "Why is block.timestamp acceptable here but not there?" — the answer is the value at stake versus the proposer's bias window. Weak answer: "never use block variables," which is false and signals you haven't thought about the threshold.
Signatures and replay
Signed messages require canonical typed domains (EIP-712), chain and contract binding, nonce and expiry, correct signer recovery and atomic consumption. A valid signature is not current authorization: roles, ownership and balances may have changed since signing. Contract wallets may use a validation interface (ERC-1271) rather than ECDSA. Avoid signature malleability and replay across Chains, deployments, functions or forks, and never use a signature byte string as the sole business identity.
What interviewers probe: "Design a gasless permit flow" and then "replay it on a fork." Weak answer: hashing the message without a domain separator — the canonical cross-application replay bug.
Business logic, testing and evidence
Business-logic vulnerabilities are protocol-specific: donation changes share price, rounding favors repeated withdrawals, initial depositors manipulate ratios, fee updates strand positions, governance votes use borrowable power, liquidation incentives fail under congestion, or an emergency pause traps users forever. Define solvency, conservation, monotonicity, bounded loss, role separation and liveness invariants, then attack those assumptions with stateful fuzzing and economic simulation.
Defense combines methods: static analyzers identify patterns; unit and integration tests exercise known behavior; fuzz and invariant tests explore inputs and sequences; differential tests compare an independent model; fork tests cover actual integrations; formal tools prove specified properties in a model; manual review finds intent, economic and compositional flaws. An audit is time-bounded evidence for a commit and configuration, not permanent certification or a substitute for ownership.
Deployment and operation complete the system: pin compiler and dependencies, reproduce bytecode, verify target chain, addresses, constructor and initializer data, roles and storage layout, and monitor first use. Watch privileged events, implementation slots, oracle freshness, abnormal balance changes, revert rates and invariant health. Prepare pausing scopes, communication, evidence preservation, key rotation, patched upgrade, migration, compensation and postmortem procedures. Pause powers should reduce harm without silently granting administrators confiscation or indefinite lockup.
The professional standard is explicit residual risk: which components can fail, maximum credible loss, user exit conditions, governance latency, accepted finality and what remains unsafe after every mitigation. Weak answer at the senior level: "we got audited, so it's secure." Strong answer: "the audit covered these invariants at this commit; these risks remain and here's the bound."
