Coding Fundamentals
The Foundry toolchain, Solidity idioms for live-coding rounds, and the EVM mental models you should hold without reaching for docs.
Foundry — forge / cast / anvil / chisel
Modern Solidity work is Foundry-first. You should know each tool by reflex.
| Tool | What it does | You use it for |
|---|---|---|
forge | Build, test, snapshot, deploy | Compilation, test runs, deployments, gas snapshots |
cast | RPC client and calldata utilities | Call mainnet contracts, decode calldata, derive selectors |
anvil | Local EVM node | Local fork of mainnet, time travel, impersonation |
chisel | REPL | One-off Solidity expressions, quick math checks |
# Forge
forge build
forge test -vvv
forge test --match-contract Liquidate --fuzz-runs 5000
forge snapshot # gas snapshot diff vs main
forge inspect MarketCore storage # storage layout
forge coverage --report summary
# Cast
cast call $MORPHO_ADDR "totalSupplyAssets(bytes32)(uint256)" $MARKET_ID
cast balance $USER
cast 4byte 0xa9059cbb # transfer(address,uint256)
cast --abi-decode "f(uint256)" 0x...
# Anvil
anvil --fork-url $MAINNET_RPC --fork-block-number 19000000
cast rpc anvil_impersonateAccount $WHALE
cast send $TOKEN "transfer(address,uint256)" $ME 1e18 --from $WHALE --unlocked
# Chisel
chisel # REPL
> uint256 a = 5e18 * 1e18 / 1e18; a # 5_000000000000000000
If they give you a Foundry project and ask you to fix a bug, the first three things you do: forge build to confirm it compiles, forge test to see the existing tests, forge test --match-test <the failing one> -vvv to read the trace. Talk through what you're doing. Interviewers love seeing the loop.
Solidity coding style for interviews
Some style choices read as "this person ships." Some read as "this person learned in 2020 and stopped." Use the modern set:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24; // enables transient storage, etc.
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @title IsolatedMarket
/// @notice Single-collateral lending market.
/// @author you
contract IsolatedMarket {
using SafeERC20 for IERC20;
/* ============ Errors ============ */
error Unauthorized();
error ZeroAssets();
error InsufficientCollateral(uint256 requested, uint256 max);
/* ============ Events ============ */
event Supply(address indexed user, uint256 assets, uint256 shares);
event Borrow(address indexed user, uint256 assets, uint256 shares);
/* ============ Immutables ============ */
IERC20 public immutable LOAN;
IERC20 public immutable COLLATERAL;
address public immutable ORACLE;
uint256 public immutable LLTV;
/* ============ Storage ============ */
uint128 public totalSupplyAssets;
uint128 public totalSupplyShares;
uint128 public totalBorrowAssets;
uint128 public totalBorrowShares;
uint128 public lastUpdate;
mapping(address => Position) public position;
/* ============ Constructor ============ */
constructor(IERC20 loan, IERC20 collateral, address oracle, uint256 lltv) {
LOAN = loan;
COLLATERAL = collateral;
ORACLE = oracle;
LLTV = lltv;
lastUpdate = uint128(block.timestamp);
}
/* ============ External ============ */
function supply(uint256 assets, address onBehalf) external returns (uint256 shares) {
if (assets == 0) revert ZeroAssets();
_accrue();
shares = _toSharesDown(assets, totalSupplyAssets, totalSupplyShares);
totalSupplyAssets += uint128(assets);
totalSupplyShares += uint128(shares);
position[onBehalf].supplyShares += shares;
LOAN.safeTransferFrom(msg.sender, address(this), assets);
emit Supply(onBehalf, assets, shares);
}
}
Style markers a senior would notice:
- Pragma
^0.8.24or newer (transient storage, etc.). - Named imports —
import {SafeERC20} from "..."notimport "...". - NatSpec on the contract and on external functions.
- Custom errors, not strings.
- Events for every state change.
- Immutables in UPPER_SNAKE_CASE by convention (consistent with Solady / Solmate).
- Section banners for navigation.
- Internal functions prefixed with
_. - Round in protocol favor by default (
_toSharesDown,_toAssetsUp). - Cache storage to memory inside loops.
Mental model: storage slots
You should be able to look at any contract and predict forge inspect storage.
contract Demo {
address owner; // slot 0, bytes 0-19
uint64 nonce; // slot 0, bytes 20-27 (packed)
uint32 flags; // slot 0, bytes 28-31 (packed)
uint256 total; // slot 1
mapping(address => uint256) bal; // virtual, base slot 2
uint256[] queue; // length at slot 3, elements at keccak256(3) + i
bytes data; // similar to dynamic array
}
Mapping value lookup:
// bal[user] lives at:
bytes32 slot = keccak256(abi.encode(user, uint256(2)));
uint256 value = uint256(vm.load(address(demo), slot));
Nested mapping:
// mapping(a => mapping(b => uint256)) at base slot N
// inner[a] lives at keccak256(abi.encode(a, N))
// inner[a][b] lives at keccak256(abi.encode(b, keccak256(abi.encode(a, N))))
Mental model: calldata layout
Calldata is a contiguous byte array starting with a 4-byte selector, followed by ABI-encoded arguments. Each "head" is 32 bytes; dynamic types use offset + tail.
# cast 4byte resolves selectors
$ cast 4byte 0xa9059cbb
transfer(address,uint256)
# decode a full transfer call
$ cast --abi-decode "transfer(address,uint256)" 0xa9059cbb000000000000000000000000aaaa...0000000000000000000000000000000000000000000000000de0b6b3a7640000
0xaaaa...
1000000000000000000
# encode for sending
$ cast calldata "transfer(address,uint256)" 0xaaaa... 1000000000000000000
Hot tip: in any interview where they show you a transaction trace, walk through the calldata selector and arguments. Demonstrates fluency.
Mental model: gas estimation
Rules of thumb that survive most interview questions:
- SSTORE: 20,000 zero→non-zero; 5,000 non-zero→non-zero; 2,900 non-zero→zero refund.
- SLOAD: 2,100 cold; 100 warm.
- External call: 2,600 cold address; 100 warm; plus gas forwarded.
- Memory: linear up to ~700 bytes; quadratic past that.
- KECCAK256: 30 + 6/word.
- 21,000: base transaction cost.
- 4 / 16: gas per calldata byte (zero / nonzero).
From these, you can estimate any function: count cold/warm SLOADs, SSTOREs, external calls, and add. Always within ±20%.
forge test --gas-report # function-level gas
forge snapshot # write a .gas-snapshot file
forge snapshot --diff # compare to existing snapshot
DSA patterns common in DeFi interviews
Binary search
Often used to invert a monotone function: find the price at which a position becomes liquidatable, find the share count for a given asset, etc.
/// @notice Binary search for the largest x in [lo, hi] such that f(x) <= target.
function search(uint256 lo, uint256 hi, uint256 target, function(uint256) view returns (uint256) f)
internal view returns (uint256)
{
while (lo < hi) {
uint256 mid = (lo + hi + 1) / 2; // upper-bias to avoid infinite loop
if (f(mid) <= target) lo = mid;
else hi = mid - 1;
}
return lo;
}
Sorted doubly-linked list
Used by Liquity, original Morpho Optimizers, Aave for ordered queues of users by some metric (health factor, supply rate). Insertion is O(N) in the worst case, but with hints, it becomes O(1) average.
struct Node { address prev; address next; uint96 score; }
mapping(address => Node) public node;
address public head;
function insert(address user, uint96 score, address hint) external {
// Walk from hint to find correct position (caller supplies a near-correct one)
address cur = hint;
while (cur != address(0) && node[cur].score > score) cur = node[cur].prev;
while (node[cur].next != address(0) && node[node[cur].next].score < score) cur = node[cur].next;
// ... link user between cur and node[cur].next ...
}
Heap (priority queue)
Useful for "next liquidation candidate" if you maintain a heap of users ordered by health-factor. Less common on-chain (O(log N) cost per op multiplied by N users gets expensive); usually delegated off-chain.
Fenwick tree / segment tree
Rare on-chain because of gas cost, but you should recognize the pattern if the interviewer asks "how would you track aggregate stats efficiently?" — typically the answer is "off-chain via an indexer."
Inline assembly — when and why
Inline Yul assembly is a power tool. Reach for it when:
- You need to read/write a specific storage slot directly (e.g., upgradable storage patterns).
- You need to call a precompile (ecrecover, etc.) optimally.
- You need bit-level operations on packed structs.
- You're implementing transient storage primitives before solidity-native syntax matures.
Do not reach for it when:
- You can express the same thing in high-level Solidity safely.
- The gas saving is < 100 gas / call and the readability cost is significant.
- You're not 100% sure of stack/memory layout.
// Read an arbitrary storage slot — used in upgradeable patterns
function _readSlot(bytes32 slot) internal view returns (bytes32 v) {
assembly { v := sload(slot) }
}
// EFFICIENT mulDiv — Uniswap v3 / Solady style. Full 512-bit precision.
function mulDiv(uint256 a, uint256 b, uint256 d) internal pure returns (uint256 result) {
assembly {
let mm := mulmod(a, b, not(0))
let prod0 := mul(a, b)
let prod1 := sub(sub(mm, prod0), lt(mm, prod0))
if iszero(prod1) {
result := div(prod0, d)
}
// ...full 512-bit branch elided...
}
}
Reaching for inline assembly on a problem that doesn't call for it signals "I want to look smart." Reaching for the simple Solidity solution and explaining "I'd write it this way for clarity; if gas matters I'd benchmark and consider Yul, but my default is readable" signals "I ship."
Reusable snippets to memorize
Safe transfer
using SafeERC20 for IERC20;
IERC20(token).safeTransferFrom(from, to, amount);
IERC20(token).safeTransfer(to, amount);
IERC20(token).forceApprove(spender, amount); // OZ v5 — handles USDT-style
Permit + transferFrom in one call
try IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}
IERC20(token).safeTransferFrom(msg.sender, address(this), value);
Min / max / abs
function min(uint256 a, uint256 b) pure returns (uint256) { return a < b ? a : b; }
function max(uint256 a, uint256 b) pure returns (uint256) { return a > b ? a : b; }
function abs(int256 a) pure returns (uint256) { return a >= 0 ? uint256(a) : uint256(-a); }
Ceil division
function ceilDiv(uint256 a, uint256 b) pure returns (uint256) {
return a == 0 ? 0 : (a - 1) / b + 1;
}
Compounded interest
// linear approximation, suitable for short dt at moderate rates
function accrueLinear(uint256 total, uint256 rate, uint256 dt) pure returns (uint256) {
return total + total * rate * dt / 1e18;
}
// continuous compounding via Taylor — see WadRayMath or PRBMath for hardened versions