Coding Fundamentals
The tooling, idioms, and mental models you need to walk into a live Solidity round and start writing immediately.
Foundry workflow
If you ever switch between Hardhat and Foundry in interviews, switch to Foundry now. Every senior DEX team runs on Foundry.
# From zero
curl -L https://foundry.paradigm.xyz | bash
foundryup
# New project
forge init mini-amm
cd mini-amm
# Compile
forge build
# Test (everything)
forge test -vv
# Test (one fn, max verbosity)
forge test --mt test_Swap_RoundsInProtocolFavor -vvvv
# Fuzz with explicit runs
forge test --fuzz-runs 100000
# Invariant
forge test --mc PoolInvariants
# Gas snapshot
forge snapshot
forge snapshot --diff # vs committed baseline
# Coverage
forge coverage --report lcov
# Mainnet fork
forge test --fork-url $RPC_URL --fork-block-number 19_000_000
# Inspect
forge inspect Pool storage-layout
forge inspect Pool bytecode
forge inspect Pool abiThings mid-level engineers don't know about Foundry but seniors use:
vm.snapshotGasLastCall()for per-line gas measurement.vm.cool(address)to artificially cool storage between assertions.vm.txGasPrice,vm.fee— control EIP-1559 base fee in tests.vm.signCompact— clean EIP-712 signing in fuzz.vm.expectEmit(true, true, true, true)— assert events bit-exact.vm.makePersistent(addr)— survive cross-chain forks.
Solidity coding style for live interviews
What graders look for in a 45-minute live round, in order of priority:
- Correctness first. A working naive solution beats an elegant broken one.
- State the invariant out loud before writing the function. "I'll maintain that `x · y ≥ k` post-swap…"
- Inputs validated.
require/custom error on zero amounts, on insufficient liquidity. - Rounding direction explicit. Add a comment.
- Storage reads cached. One pattern: read into locals at function top, write back at the end.
- Events emitted for any state change.
- One test case — show you'd test it.
- "What I'd change in production" — list 2-3 things you'd add but won't fit in time.
// Idiomatic shape for a swap function
function swap(uint256 amountIn, uint256 minOut, address to)
external
nonReentrant
returns (uint256 amountOut)
{
if (amountIn == 0) revert ZeroIn();
(uint256 r0, uint256 r1) = (reserve0, reserve1); // single SLOAD-pair
// -- compute --
uint256 amountInWithFee = amountIn * FEE_NUM;
amountOut = (amountInWithFee * r1) / (r0 * FEE_DEN + amountInWithFee);
if (amountOut < minOut) revert Slippage(amountOut, minOut);
// -- effects --
reserve0 = uint128(r0 + amountIn);
reserve1 = uint128(r1 - amountOut);
// -- interactions --
token0.safeTransferFrom(msg.sender, address(this), amountIn);
token1.safeTransfer(to, amountOut);
emit Swap(msg.sender, amountIn, amountOut, to);
}EVM mental model
The minimum that lets you reason about any opcode question in real time:
- Stack-based VM. 256-bit words. Max 1024 stack depth.
- Memory: linear, zero-initialized, paid by-the-word, quadratic expansion.
- Storage: 256-bit slots, indexed by 256-bit keys. SLOAD/SSTORE cost dominates.
- Transient storage: same shape, tx-scoped, cheap.
- Calldata: read-only, externally provided, near-free to read.
- Code: read via EXTCODE*. Immutables embedded in deployed bytecode.
- Logs: bloom-indexed by topics; topics are SSTORE-cheap but data is byte-priced.
- Gas refunds: capped at gas/5 (London). SSTORE-to-zero is the main refund source.
Fixed-point math libraries
Know which library does what:
| Library | Best for |
|---|---|
| FullMath (Uniswap) | mulDiv — multiply two uint256s, divide, no overflow. Use everywhere. |
| FixedPoint96 / FixedPoint128 | Q64.96 and Q128.128 constants and helpers. v3-native. |
| TickMath | Tick ↔ sqrtPriceX96 conversion. |
| SafeCast | Downcasts that revert on overflow. uint256.toUint128(). |
| PRBMath (UD60x18 / SD59x18) | Float-like signed/unsigned 18-decimal. Has exp, ln, pow. |
| Solady FixedPointMathLib | The Yul-optimized successor. Fast sqrt, mulWad, divWad, expWad. |
| ABDKMath64x64 | The original; still used in older Curve contracts. |
| OZ Math | Average, mulDiv, ceildiv. Conservative. |
For a new DEX project today: Solady's library + Uniswap's FullMath/TickMath where v3-compat matters.
Newton's method in Solidity
The pattern that solves StableSwap, sqrt, and several v3 helpers. The shape:
function newton(uint256 x) internal pure returns (uint256 y) {
y = _initialGuess(x);
for (uint256 i; i < MAX_ITER; ++i) {
uint256 yNext = _step(y, x); // y - f(y)/f'(y)
if (yNext == y) break;
// Optional: clamp / detect oscillation
if (yNext > y) { if (yNext - y <= 1) { y = yNext; break; } }
else { if (y - yNext <= 1) { y = yNext; break; } }
y = yNext;
}
require(i < MAX_ITER, "NEWTON_NO_CONVERGE");
}Three things to get right:
- Initial guess. Bad guess → many iterations. For StableSwap D, use the sum-of-reserves. For sqrt, use a bit-width-based starting point.
- Convergence test. Don't use equality — use "delta ≤ 1." Floating-point oscillation in integer math is real.
- Iteration cap. Always have a max. Always revert if not converged. Never silently return a wrong value.
Binary search in Solidity
Used for tick lookup, position-list searches, and oracle observation queries.
function findIndex(uint32[] storage arr, uint32 target) internal view returns (uint256) {
uint256 lo = 0;
uint256 hi = arr.length;
while (lo < hi) {
uint256 mid = (lo + hi) >> 1;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}Be careful with cyclic arrays (v3 oracle observations). The classic v3 observe binary search wraps around observationIndex — get the modular indexing right.
ABI encoding / decoding tricks
Patterns that show up:
// Forward arbitrary calldata to another contract
function forward(address target, bytes calldata data) external returns (bytes memory) {
(bool ok, bytes memory ret) = target.call(data);
require(ok, "FORWARD");
return ret;
}
// Decode struct from calldata without copying to memory
struct SwapParams { address tokenIn; address tokenOut; uint24 fee; uint256 amountIn; uint256 minOut; address to; }
function swap(SwapParams calldata p) external returns (uint256) {
// p is read directly from calldata; no memory copy
return _swap(p.tokenIn, p.tokenOut, p.fee, p.amountIn, p.minOut, p.to);
}
// Pack two addresses + a uint24 into 32 bytes for a callback `data` payload
bytes memory data = abi.encodePacked(payer, tokenIn, tokenOut, fee); // 20 + 20 + 20 + 3 = 63 bytes
// — note: encodePacked is shorter but lossy for variable-length types. Use abi.encode for structs.Yul-level shortcuts (for hot paths):
// Read a calldata uint256 at byte offset N without abi.decode
assembly {
let value := calldataload(N)
}
// Encode and call in one shot, no memory expansion beyond what's needed
assembly {
let p := mload(0x40)
mstore(p, selector)
mstore(add(p, 0x04), arg0)
let ok := call(gas(), target, 0, p, 0x24, 0, 0)
if iszero(ok) { revert(0, 0) }
}The "guess the gas" exercise
Interviewer shows a function. "How much gas does this call cost?" The exercise tests your reading speed of the EVM.
The mental subroutine:
- Base tx: 21,000
- Calldata bytes: nonzero = 16, zero = 4. A 100-byte calldata payload is ~800-1600.
- Each cold SLOAD: +2,100. Each warm: +100.
- Each cold SSTORE (new): +22,100. Cold modify: +5,000.
- Each external call: +2,600 cold, +100 warm. Token transfers add their own logic cost on top (~30-60k typically).
- Memory: roughly +3 per word, +1 per ~512 words for expansion.
- Keccak: +30 + 6/word.
- The rest (ADD, MUL, etc): ~3-5 each. Ignore unless in a tight loop.
You're not expected to be exact. Be within 20-30%. The reasoning is what's graded.
A v2 swap: ~80-100k. A v3 swap (single tick): ~110-130k. A v3 swap that crosses a tick: ~140-180k. An ERC-20 transfer between EOAs: 21-65k depending on token.