Section B · Technical Core

Applied Patterns

The repertoire of architectural patterns a senior DEX engineer reaches for. None of these is exotic; all of them show up in interview design rounds.

Singleton vs factory

Two ways to deploy "N pools":

Factory (v2, v3)Singleton (v4, Balancer Vault)
Pool storageOne contract per poolOne contract; all pool state in mappings
Deploy cost per pool~3-5M gas (full contract deploy)~50k gas (just a mapping write)
Cross-pool composabilityHard — token transfers required between callsEasy — flash accounting, net deltas
Storage cost per swapPer-pool slot0 readOne central state + per-pool sub-slot
Upgrade storyPool immutable; redeploy whole factory generationSame — the singleton itself is immutable
Hooks?No (or external)Native

The singleton pattern won in v4 because (a) deployments on L1 are expensive and (b) flash accounting unlocks new mechanism designs (MEV-internalizing hooks, in-protocol batching). Be ready to explain both sides in a design round.

The hooks pattern

Hooks externalize lifecycle moments to a user-deployed contract. The core pool calls hook functions at well-defined points; the hook returns delta values that can adjust the operation.

// Sketch — minimal v4-style hook contract
interface IHooks {
    function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata data)
        external returns (bytes4 selector, BeforeSwapDelta delta, uint24 lpFeeOverride);
    function afterSwap(address sender, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata data)
        external returns (bytes4 selector, int128 hookDelta);
    // ... add/remove liquidity, initialize, donate
}

// Example — a dynamic-fee hook
contract DynamicFeeHook is IHooks {
    function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
        uint24 fee = _computeFee(_volatility());          // higher fee when vol is high
        return (IHooks.beforeSwap.selector, EMPTY_DELTA, fee);
    }
}

Encoding the hook's permissions into its address (lower bits of the address indicate which hooks it implements) is a v4-specific innovation — saves a storage read per swap.

Hook security

Hooks are trust amplifiers. A malicious or buggy hook can drain a pool. Pools advertise their attached hook; integrators must vet it before using. Some teams curate "approved" hook lists.

The callback pattern

Most modern AMMs are "pull-based" via callback. The pool gives you the output token first, then calls you back. Your callback must repay the input. If it doesn't, the post-call balance check reverts.

// Uniswap v3 swap callback contract
interface IUniswapV3SwapCallback {
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external;
}

contract MyRouter is IUniswapV3SwapCallback {
    function uniswapV3SwapCallback(int256 a0, int256 a1, bytes calldata data) external {
        // verify caller is a real pool
        (address payer, address token0, address token1, uint24 fee) = abi.decode(data, (address, address, address, uint24));
        require(msg.sender == _computePoolAddress(token0, token1, fee), "BAD CALLER");

        if (a0 > 0) IERC20(token0).transferFrom(payer, msg.sender, uint256(a0));
        if (a1 > 0) IERC20(token1).transferFrom(payer, msg.sender, uint256(a1));
    }
}

Three things you must check in every callback handler:

  1. Caller authentication. Anyone can call your callback — check msg.sender is a legitimate pool.
  2. Authorization carry-over. Use a transient "unlocked" flag to ensure the callback only fires during your initiated swap.
  3. Reentrancy. The pool is calling you mid-state-mutation. Don't re-enter the pool.

Router / core separation

Routers do five things the core doesn't:

  1. Multi-hop path resolution
  2. Slippage protection (the core does no slippage check)
  3. Deadline enforcement
  4. Token unwrapping (ETH/WETH)
  5. Permit / signed-approval forwarding

This separation is a load-bearing design choice:

  • Core stays small. Less code, easier to audit, immutable.
  • Routers evolve. v3 has shipped multiple routers; v4 will ship even more.
  • Custom routers compose. Aggregators (1inch, 0x, ParaSwap) build their own routers that talk directly to pool cores.

In an interview, "where would you put this logic — core or periphery?" is a frequent forking question. Default: anything mutable, anything UX-sugar, anything chain-specific → periphery. The invariant lives in core, and only the invariant.

Position manager NFTs

v3 wraps LP positions in NonfungiblePositionManager (ERC-721). Each token represents (pool, tickLower, tickUpper, liquidity) plus collected-fee accounting.

Why NFTs not ERC-20?

  • Positions are non-fungible by construction — same range and same pool yes, but different deposit times earn different fees.
  • NFT semantics give a clean approval/transfer story.
  • Composability — other protocols accept the NFT as collateral, wrap it in vaults, etc.
struct Position {
    uint96  nonce;
    address operator;
    uint80  poolId;          // packed pool reference
    int24   tickLower;
    int24   tickUpper;
    uint128 liquidity;
    uint256 feeGrowthInside0LastX128;
    uint256 feeGrowthInside1LastX128;
    uint128 tokensOwed0;
    uint128 tokensOwed1;
}
mapping(uint256 => Position) private _positions;

Permit and Permit2

EIP-2612 permit: a token-side signed approval. Avoids the two-tx "approve then transferFrom" UX. Token must implement it.

Permit2: a single canonical allowance manager deployed at the same address across all chains (0x000000000022D473030F116dDEE9F6B43aC78BA3). Users approve Permit2 once for a token; then sign per-spender, per-amount, per-deadline permits.

EIP-2612 permitPermit2
Token must implement?Yes — most don'tNo — wraps any ERC-20
Approval statePer token, per spenderPer token to Permit2 (once); then sigs per spender
Replay protectionNonce on tokenNonce on Permit2
Batch?NoYes — PermitBatch struct

Modern DEX routers default to Permit2 because most real tokens (USDC, WETH at launch) didn't ship EIP-2612.

ERC-4626 vault adapters that wrap AMM positions

ERC-4626 is the standard vault interface (deposit, withdraw, mint, redeem, totalAssets). Wrapping an AMM position in a 4626 vault makes it integrable with money markets and yield aggregators.

Patterns:

  • Single-LP 4626 — vault holds one v3 position. Auto-compounds fees. Shares are fungible. Examples: Gamma, Arrakis, Steer.
  • Strategy 4626 — vault dynamically rebalances between multiple ranges/pools.
  • Wrapper 4626 — turn a Curve LP token into a 4626 with auto-claim.
4626 inflation attack

The classic ERC-4626 vulnerability: a first attacker deposits 1 wei to get 1 share, donates a million tokens to the vault, then subsequent depositors round to zero shares. Mitigation: virtual shares + virtual assets (OpenZeppelin's default). Know this — it shows up in interviews.

Intent-based architecture

The latest design movement. Instead of users signing transactions ("call this router with this calldata"), they sign intents ("I'll trade X for at least Y by deadline Z"). Off-chain solvers compete to fulfill the intent.

Notable systems:

  • CoW Protocol — batch auctions; solvers compete on a uniform clearing price.
  • UniswapX — Dutch auction over fillers. Cross-chain extension via 7683.
  • 1inch Fusion — resolver-based filling with private mempool option.
  • 0x v2 / Settler — meta-aggregation with calldata compression.

The smart-contract layer is small: an order-verifier, a callback-to-filler, a settle step. The complexity lives off-chain. You should know:

  • EIP-712 typed signatures for the order format.
  • Nonce schemes (per-user counter vs per-order salt vs partial fills).
  • Filler authentication — pre-approved set vs open competition.
  • Reactor / settler contracts — the on-chain handler that pulls user tokens via Permit2, calls filler, verifies output, pays out.

JIT liquidity

"Just-in-time" liquidity: an MEV searcher sees a large swap in the mempool, adds concentrated liquidity directly around the current price right before the swap, captures the fee, and removes immediately after.

Why it matters:

  • JIT compresses fees that should have gone to long-term LPs.
  • It's not strictly evil — the user gets slightly better execution (more depth at-price) — but it cannibalizes passive LP returns.
  • v4 hooks can implement JIT-resistance: e.g. require a min lockup, or skim a portion of fees if liquidity was added recently.

Interview question: "design a hook that resists JIT but preserves market depth." One answer: a fee surcharge on liquidity additions made in the last N blocks, refunded if it stays past M blocks.

MEV-resistant pricing

Categories of defense:

  • Batch auctions — clear all orders at one price per block. Sandwich attacks become structurally impossible. CoW is the canonical example.
  • Rate-limited oracles — TWAPs over N seconds resist single-block manipulation.
  • Commit-reveal — too slow for swaps, used in NFT mints.
  • Threshold encryption / shielded mempools — orders are encrypted until inclusion. Examples: Shutter, Aztec.
  • Private flow — Flashbots private, MEV-Share, MEV-Blocker. Send transactions through searchers who don't frontrun.
Senior signal

"How would you defend an AMM against sandwich attacks?" is a near-certain interview question. A strong answer enumerates 3-4 categories above, picks one, and discusses tradeoffs. A weak answer says "use a slippage limit."