Section C · Coding

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.

ToolWhat it doesYou use it for
forgeBuild, test, snapshot, deployCompilation, test runs, deployments, gas snapshots
castRPC client and calldata utilitiesCall mainnet contracts, decode calldata, derive selectors
anvilLocal EVM nodeLocal fork of mainnet, time travel, impersonation
chiselREPLOne-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
In a live round

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.24 or newer (transient storage, etc.).
  • Named importsimport {SafeERC20} from "..." not import "...".
  • 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...
    }
}
Interview red flag

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