Section C · Coding

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 abi

Things 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:

  1. Correctness first. A working naive solution beats an elegant broken one.
  2. State the invariant out loud before writing the function. "I'll maintain that `x · y ≥ k` post-swap…"
  3. Inputs validated. require/custom error on zero amounts, on insufficient liquidity.
  4. Rounding direction explicit. Add a comment.
  5. Storage reads cached. One pattern: read into locals at function top, write back at the end.
  6. Events emitted for any state change.
  7. One test case — show you'd test it.
  8. "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:

LibraryBest for
FullMath (Uniswap)mulDiv — multiply two uint256s, divide, no overflow. Use everywhere.
FixedPoint96 / FixedPoint128Q64.96 and Q128.128 constants and helpers. v3-native.
TickMathTick ↔ sqrtPriceX96 conversion.
SafeCastDowncasts that revert on overflow. uint256.toUint128().
PRBMath (UD60x18 / SD59x18)Float-like signed/unsigned 18-decimal. Has exp, ln, pow.
Solady FixedPointMathLibThe Yul-optimized successor. Fast sqrt, mulWad, divWad, expWad.
ABDKMath64x64The original; still used in older Curve contracts.
OZ MathAverage, 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:

  1. Initial guess. Bad guess → many iterations. For StableSwap D, use the sum-of-reserves. For sqrt, use a bit-width-based starting point.
  2. Convergence test. Don't use equality — use "delta ≤ 1." Floating-point oscillation in integer math is real.
  3. 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:

  1. Base tx: 21,000
  2. Calldata bytes: nonzero = 16, zero = 4. A 100-byte calldata payload is ~800-1600.
  3. Each cold SLOAD: +2,100. Each warm: +100.
  4. Each cold SSTORE (new): +22,100. Cold modify: +5,000.
  5. Each external call: +2,600 cold, +100 warm. Token transfers add their own logic cost on top (~30-60k typically).
  6. Memory: roughly +3 per word, +1 per ~512 words for expansion.
  7. Keccak: +30 + 6/word.
  8. 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.

Calibration

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.