Section E · Reference

Practice Interview Questions

28 questions across 8 sections. Drill out loud. Tick the practice tracker as you go. Toggle drill mode to hide answers.

1. Solidity & EVM

Q1. Walk through what happens, step by step, when a user calls transfer(address,uint256) on an ERC-20.

A: (1) Wallet sends a tx to the token's address; (2) EVM dispatches via the 4-byte selector to the function; (3) the function checks balance[msg.sender] >= amount; (4) updates balance[msg.sender] -= amount and balance[to] += amount; (5) emits a Transfer event; (6) returns true (or doesn't, for USDT). The two SSTOREs dominate gas cost (~30k zero-to-nonzero or ~5k both nonzero). The wallet sees the receipt and the new balance via JSON-RPC.

Q2. What is transient storage and when would you use it?

A: Transient storage (EIP-1153, Cancun) gives you TLOAD/TSTORE — a per-transaction key-value store that resets when the tx ends. ~100 gas per op. Use cases: reentrancy guards (much cheaper than the SSTORE-based ReentrancyGuard), per-tx caches, callback state passing. In Solidity 0.8.24+, declare with the transient data location.

Q3. Explain custom errors vs require strings. What's in the revert data?

A: require("msg") reverts with Error(string) — a 4-byte selector + ABI-encoded string. revert CustomError(x, y) reverts with the error's 4-byte selector + ABI-encoded args. Custom errors are cheaper at deploy and call time, self-document, and provide structured failure data. The selector is bytes4(keccak256("CustomError(uint256,uint256)")). Off-chain consumers can decode via cast 4byte-decode or via the contract ABI.

Q4. What's the difference between calldata, memory, and storage?

A: Calldata is the read-only transaction payload (cheap, ~3 gas/word read). Memory is the function-scoped scratch space (cheap reads/writes, quadratic expansion past ~700 bytes). Storage is the contract's persistent state (SLOAD 2100 cold / 100 warm; SSTORE 20k zero→non-zero, 5k otherwise). Heuristic: prefer calldata for read-only inputs; cache storage to local stack variables inside loops; never use memory when calldata works.

Q5. Where does mapping(address => uint256) bal declared at storage slot 3 actually live?

A: The mapping itself doesn't store anything at slot 3 — slot 3 is reserved but empty. bal[user] lives at keccak256(abi.encode(user, uint256(3))). There is no length, no enumeration; you can only read values whose keys you already know. To enumerate, you must index events off-chain.

2. Mechanism design

Q6. Whiteboard a minimal lending market. What's the state and what are the invariants?

A: Mapping id => Market; mapping id => user => Position. Market holds totalSupplyAssets/Shares, totalBorrowAssets/Shares, lastUpdate. Position holds supplyShares, borrowShares, collateral. Invariants: (1) totalSupplyAssets >= totalBorrowAssets, (2) every position is healthy after every tx, (3) sum of position shares equals total shares, (4) interest accumulators are monotonic, (5) one user cannot modify another's position except via liquidation. Transitions: supply, withdraw, supplyCollateral, withdrawCollateral, borrow, repay, liquidate, accrueInterest.

Q7. Defend isolated markets vs cross-collateral. When is each right?

A: Isolated markets give strong risk isolation — bad debt in one cannot touch another. They allow permissionless market creation because the protocol does not have to curate every pair. Cost: lower capital efficiency (same collateral cannot back two borrows). Cross-collateral aggregates a user's position into one account; better UX and capital efficiency but a single toxic listing can damage the whole pool. Pick isolated for permissionless / minimal cores (Morpho Blue, Euler v2). Pick cross-collateral for curated, brand-driven UX where governance is comfortable picking listings (Aave v3, Compound).

Q8. Walk through a kinked IRM curve. Why a kink at 80% rather than linear?

A: Two slopes: gentle from 0 to U* (e.g., 90%), steep from U* to 100%. Below the kink the rate climbs slowly to encourage utilization. Above, the rate climbs sharply to: (1) compensate suppliers for the risk of being illiquid; (2) discourage borrowers from pushing utilization to 100% where withdrawals fail; (3) pull in fresh supply to lower utilization. A purely linear curve does not produce the urgency near 100% — suppliers earn the same marginal rate at 91% as at 30%, so they don't show up just when they're needed.

Q9. How would you add fixed-rate loans on top of an existing variable-rate lending core?

A: Two main approaches. (1) Maturity-bucketed overlay: suppliers commit to a tenor (e.g., 30 days); a separate market matches borrowers at a fixed rate; on maturity, principal+interest settles. Used by Notional, Pendle PTs. (2) Perpetual fixed-rate via IRS: leave the variable-rate core, add an interest-rate-swap contract where borrowers pay fixed + receive floating; net result: synthetic fixed rate. Cleaner integration but introduces oracle/funding mechanism. Both are periphery changes — the variable-rate core remains untouched.

3. Oracles

Q10. Compare push (Chainlink) and pull (Pyth) oracles. Which would you pick for a new isolated lending market on a brand-new L2 and why?

A: Push: simple read at use time; freshness limited by heartbeat / deviation. Pull: consumer posts a signed update from a relay; can get sub-second freshness but adds UX cost (caller pays the update fee). For a new L2 where Chainlink may not yet have a feed for the asset, Pyth often has coverage earlier. For mature pairs (ETH/USD on Base), Chainlink is operationally simpler. Hybrid pattern: Pyth primary, Chainlink cross-check; or vice versa. The "right" answer depends on the asset's volatility and which feed exists with credible publishers — but in interviews always frame the answer as a function of (asset, chain, availability) rather than picking blindly.

Q11. What can go wrong with a Uniswap v3 TWAP?

A: (1) Observation cardinality too low — TWAP reverts on long lookbacks. (2) Pool too shallow — manipulator can move price for a window cheaper than the win. (3) Wrong window — too short lets attackers manipulate cheaply; too long lags real price in fast markets. (4) Sign rounding: Solidity's truncating division biases TWAP up for negative ticks. (5) Wrong pool — if multiple fee-tier pools exist for the pair, pick the deepest. (6) Inverted pair — token0/token1 ordering matters. (7) On L2s during a sequencer outage, the TWAP can be stuck at the pre-outage price.

Q12. Describe a hardened oracle adapter for a high-TVL lending market.

A: Primary feed (e.g., Chainlink push). Staleness check (max age based on asset volatility). Positivity check. L2 sequencer feed + grace period if on an L2. Cross-check against a second source (TWAP or secondary push). On disagreement > threshold, fall back to secondary or freeze. Hard min/max sanity bounds to catch catastrophic feed corruption. All readable via a single price() view that reverts on any failure mode. The market is bound to this adapter immutably; if it goes bad, the market gracefully freezes — no rug.

4. Liquidations

Q13. Walk through what happens when a position becomes liquidatable on a typical isolated market.

A: Oracle updates; off-chain liquidator bots detect new price; recompute every position's health factor; identify newly-unhealthy positions; submit liquidation tx (usually via private mempool / Flashbots). Tx flow: accrueInterest first, then check unhealthy, then transfer repaid loan tokens from liquidator to market, deduct from position debt, transfer seized collateral to liquidator (typically debtRepaid × (1 + LI) worth at oracle price). If collateral is exhausted with residual debt, socialize the loss: write down totalSupplyAssets by the bad-debt amount.

Q14. Why might you use a Dutch auction instead of a fixed-LI partial liquidation?

A: A fixed LI is a one-size-fits-all incentive. In high-vol moments, it can be too small (no one liquidates); in stable moments, too large (over-payment to liquidators). Dutch auctions price-discover: the discount starts at zero (or small) and increases until someone bids. The winning searcher pays exactly the LI the market clears at. Resists priority-gas auction dynamics. Cost: complexity; auctions take time to settle, which is bad in fast cascades. Used by Liquity v2 redemptions, some Aave-derivatives, and recent isolated-market designs.

Q15. What is a pre-liquidation and what problem does it solve?

A: A pre-liquidation triggers above the LLTV threshold — e.g., at 95% of LLTV — and seizes a small fraction of collateral at a discounted LI. It opt-in: borrower elects to allow it. Solves the "borrower gets fully liquidated by a price gap that just barely crosses LLTV" problem: smaller, gentler, earlier intervention. Some designs route pre-liquidations to a curated keeper rather than the public mempool. Reduces MEV competition for the borrower; can be a credit-improvement signal.

Q16. How do you design liquidation incentives so they work in extreme markets?

A: First, recognize that constant-LI fixed liquidations are robust in calm markets and fragile in volatile ones. Mitigations: (a) make LI a function of how unhealthy the position is (worse health → higher incentive); (b) Dutch auction the incentive; (c) pre-liquidations to handle small over-shoots; (d) bad-debt insurance fund as a backstop; (e) accept that in extreme tails, some bad debt is inevitable and design clear socialization rules. The senior takeaway: a single-knob "LI = 8%" is a sign of immature design; modern designs adapt.

5. Gas & efficiency

Q17. A supply() function does 5 cold SLOADs and 3 SSTOREs. Estimate the gas.

A: ~5 × 2,100 = 10,500 for cold SLOADs; SSTOREs depend: if all three are zero→nonzero, 3 × 20,000 = 60,000; if all nonzero→nonzero, 3 × 5,000 = 15,000. Plus 21,000 base + calldata + external calls. Typical first-time supply: ~120-150k gas; subsequent supply (warm slots, no zero→nonzero): 60-80k. SSTORE is overwhelmingly the dominant term.

Q18. When does it pay to pack two uint128s into one slot?

A: When you write to both in the same function call. Both fields in one SSTORE = 5,000 gas; two separate SSTOREs = 10,000. If you only ever touch one, packing saves nothing on writes (an SSTORE on a packed slot still costs 5,000) and complicates the code. For reads, packing helps if you load both at once. Net: pack when fields are co-written in hot paths; don't pack purely "to look tight."

Q19. When should you reach for inline assembly?

A: When the high-level Solidity expression has a measurable, repeatable gas overhead and the assembly version is well-understood. Canonical cases: storage slot reads/writes for upgradeable patterns; 512-bit mulDiv (Uniswap/Solady); precompile dispatch. Don't reach for it for general "I think this loop is slow" — benchmark first, and prefer the Solidity refactor (e.g., calldata vs memory, caching SLOADs). Inline assembly is a maintenance tax and a bug surface.

6. Attack vectors

Q20. Explain the donation / share-inflation attack and three mitigations.

A: Attacker mints 1 share, donates a large balance directly to the vault, victim's deposit rounds down to 0 shares, attacker withdraws and walks away with a chunk of victim's deposit. Mitigations: (1) virtual shares (OpenZeppelin v5) — add a constant to numerator and denominator so the ratio cannot be inflated meaningfully; (2) dead shares — mint a chunk to address(0) on first deposit; (3) shares offset — make 1 wei of assets correspond to 1e6 shares so donating 1 wei doesn't shift the price ratio.

Q21. What is read-only reentrancy and how do you defend?

A: A function is mid-execution; it calls out to user-controlled code; that code calls a view function on this contract which returns stale (uncommitted) state. The view is read by another protocol as oracle — and that protocol gets fooled. Classic case: Curve / Balancer-style LP-pricing oracles that read pool state mid-callback. Defenses: (1) transient lock that blocks view reads during the callback window; (2) ensure all views remain consistent across the callback boundary (commit state before any external call); (3) clearly document which views are safe to use as oracles.

Q22. Walk through an oracle staleness exploit and how you'd design against it.

A: Price feed hasn't updated; real price has moved 10% down on the collateral. Attacker opens a max-LTV position at the stale (high) price, then waits for the oracle to update — the position is now underwater, but the attacker has already pocketed the loan-token value of the over-borrow. Defenses: (1) staleness check rejecting prices > MAX_AGE; (2) protocol pauses borrowing if oracle is stale; (3) cross-check with a TWAP — disagreement > threshold triggers fallback; (4) for L2s, sequencer-uptime grace period to catch outage-induced staleness.

Q23. Name three flavors of governance attack and how to defend each.

A: (1) Flash governance — borrow gov tokens, vote, repay in same tx. Defense: vote weight from a past block snapshot. (2) Bribery markets — buy votes externally. Defense: vote-escrowed locks; minimize governance surface. (3) Timelock bypass — privileged function not behind the timelock. Defense: every privileged function gated by the same timelock; no special fast-paths. Bonus: parameter-griefing → allow-list of valid params + immutable per-market params. Multisig compromise → hardware-backed signers, geographic separation, threshold > 50%.

7. Design / take-home

Q24. Design pre-liquidations for an isolated lending core. State, invariants, transitions.

A: New struct PreLiqParams per position: enabled/disabled flag, pre-LLTV (between LLTV and a max), discount LI applied to pre-liq seizures, seize fraction. Transition: borrower opts in by signing a SignedAuthorization; liquidator passing a position's pre-LLTV but not full LLTV can call preLiquidate seizing up to seize-fraction × collateral at discount LI. Invariants: pre-LLTV < LLTV; pre-LI < full LI; seize fraction < close factor; borrower can always disable. Edge cases: post-pre-liquidation, position must still be healthy at full LLTV (or eligible for full liquidation). Audit-focused: ensure pre-liquidation cannot be used to grief a healthy position.

Q25. Design an oracle for an LST (e.g., wstETH) as collateral. What are you pricing?

A: Two-step. (1) Get the per-share value of stETH in ETH from a Lido-side rate accessor (or a hardened on-chain reading of the canonical exchange rate). (2) Convert ETH to the quote currency (typically USD) via Chainlink ETH/USD push feed. Compose: wstETHPrice_USD = wstETH_to_stETH_rate × stETH_to_ETH_rate × ETH_USD_price. The wstETH:stETH and stETH:ETH rates are not pure market prices — they're redemption-rate-like accumulators; price them off the staking protocol, not off a DEX (DEX prices can de-peg in stress, but for liquidation purposes the redemption rate is the conservative number). Add staleness checks on each component; if any is stale, revert.

Q26. Take-home: design a permissionless ERC-4626 vault that allocates across multiple isolated lending markets according to a curator's caps. Sketch the interface.

A: Vault tracks underlying (e.g., USDC) and shares. Constructor sets curator (address with allocation power), guardian (pause), owner (governance). State: supply caps per market (curator-set, increase-able instantly, decrease-able with timelock), allocation array. Functions: setSupplyCap(marketId, cap); reallocate(allocations[]) (curator only); deposit/withdraw/mint/redeem (standard 4626 with idle & rebalancing logic). On deposit, allocate to the market with most headroom (or per-curator policy). On withdraw, withdraw from markets in liquidation-priority order. Performance fee + management fee periodically mints shares to fee recipient. Pause flag for guardian.

8. Behavioral

Q27. Tell me about a time you found a bug in production code.

A: Use the SAR shape: Situation (what was the code, who used it), Action (how you found the bug — invariant test? code review? mainnet anomaly?), Result (what was the fix, what changed in process to prevent recurrence). The strongest version emphasizes the process change: "After we fixed it, we added an invariant test for that specific class of bug, and now our CI catches it pre-PR." Senior interviewers care about the systemic improvement, not the heroics.

Q28. You disagree with a senior teammate's design. They have more on-chain shipping experience than you. How do you handle it?

A: Honest answer: state your model crisply, ask theirs, look for the disagreement (often it's a hidden assumption about a constraint, not a real disagreement on logic). If after exploring it, you still disagree, push the disagreement to a decision: write up the trade-off, share with the team, take the L gracefully if the team goes their way. The cardinal sin is silent disagreement — you're a senior, your job is to surface the trade-off, not to acquiesce or to grumble. The interviewer is looking for: can you hold a position respectfully and update visibly.

Closing thought

Drill plan

First pass: read each question, attempt out loud, expand the answer. Mark the practice tracker. Second pass: drill mode on, go through with answers hidden. Aim to answer each in < 60 seconds, in your own words, with at least one concrete example. The questions are not exhaustive — the goal is to internalize the shape of the answer.