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 storage | One contract per pool | One contract; all pool state in mappings |
| Deploy cost per pool | ~3-5M gas (full contract deploy) | ~50k gas (just a mapping write) |
| Cross-pool composability | Hard — token transfers required between calls | Easy — flash accounting, net deltas |
| Storage cost per swap | Per-pool slot0 read | One central state + per-pool sub-slot |
| Upgrade story | Pool immutable; redeploy whole factory generation | Same — 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.
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:
- Caller authentication. Anyone can call your callback — check
msg.senderis a legitimate pool. - Authorization carry-over. Use a transient "unlocked" flag to ensure the callback only fires during your initiated swap.
- 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:
- Multi-hop path resolution
- Slippage protection (the core does no slippage check)
- Deadline enforcement
- Token unwrapping (ETH/WETH)
- 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 permit | Permit2 | |
|---|---|---|
| Token must implement? | Yes — most don't | No — wraps any ERC-20 |
| Approval state | Per token, per spender | Per token to Permit2 (once); then sigs per spender |
| Replay protection | Nonce on token | Nonce on Permit2 |
| Batch? | No | Yes — 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.
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.
"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."