Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Part 2 β€” Module 2: AMMs from First Principles

Difficulty: Advanced

Estimated reading time: ~75 minutes | Exercises: ~2.5-3 hours


πŸ“š Table of Contents

The Constant Product Formula

Reading Uniswap V2

Concentrated Liquidity (Uniswap V3 Concepts)

Uniswap V4 β€” Singleton Architecture and Flash Accounting

Uniswap V4 Hooks

Beyond Uniswap and Advanced AMM Topics


πŸ’‘ The Constant Product Formula

Why this matters: AMMs are the foundation of decentralized finance. Lending protocols need them for liquidations. Aggregators route through them. Yield strategies compose on top of them. Intent systems like UniswapX exist to improve on them. If you’re going to build your own protocols, you need to understand AMMs deeply β€” not just the interface, but the math, the design trade-offs, and the evolution from V2’s elegant simplicity through V3’s concentrated liquidity to V4’s programmable hooks.

Real impact: Uniswap V3 processes $1.5+ trillion in annual volume (as of 2024). The entire DeFi ecosystem β€” $50B+ TVL across lending, derivatives, yield (Q4 2024) β€” depends on AMM liquidity for price discovery and liquidations.

This module is 12 days because you’re building one from scratch, then studying three generations of production AMM code, plus exploring alternative AMM designs and the advanced topics (MEV, aggregators, LP management) that every protocol builder needs.

Deep dive: Uniswap V2 Whitepaper, V3 Whitepaper, V4 Whitepaper

πŸ’‘ Concept: The Math

Why this matters: Every AMM begins with a single equation: x Β· y = k

Where x is the reserve of token A, y is the reserve of token B, and k is a constant that only changes when liquidity is added or removed. This equation defines a hyperbolic curve β€” every valid state of the pool sits on this curve.

Why this formula works:

The constant product creates a price that changes proportionally to how much of the reserves you consume. Small trades barely move the price. Large trades move it significantly. The pool can never be fully drained of either token (the curve approaches but never touches the axes).

Price from reserves:

The spot price of token A in terms of token B is simply y / x. This falls directly out of the curve β€” the slope of the tangent at any point gives the instantaneous exchange rate.

Calculating swap output:

When a trader sends dx of token A to the pool, they receive dy of token B. The invariant must hold:

(x + dx) Β· (y - dy) = k

Solving for dy:

dy = y Β· dx / (x + dx)

This is the output amount formula. Notice it’s nonlinear β€” as dx increases, dy increases at a decreasing rate. This is price impact (also called slippage, though technically slippage refers to price movement between submission and execution).

Fees:

In practice, a fee is deducted from the input before computing the swap. With a 0.3% fee (introduced by Uniswap V1):

dx_effective = dx Β· (1 - 0.003)
dy = y Β· dx_effective / (x + dx_effective)

The fee stays in the pool, increasing k over time. This is how LPs earn β€” the pool’s reserves grow from accumulated fees.

Used by: Uniswap V2, SushiSwap (V2 fork), PancakeSwap (V2 fork), and hundreds of other AMMs use this exact formula.

πŸ” Deep Dive: Visualizing the Constant Product Curve

The curve x Β· y = k looks like this:

  Token B
  (reserve1)
    β”‚
 2000 ─ β•²
    β”‚   β•²
 1500 ─    β•²
    β”‚      β•²
 1000 ─●───────╲────────── Pool starts here (1000, 1000), k = 1,000,000
    β”‚         β•²
  750 ─          β•²
    β”‚            β•²
  500 ─              ╲──── After buying 500 token A β†’ pool has (500, 2000)
    β”‚                 β•²       Trader got 1000 B for 500 A? NO! Let's calculate...
  250 ─                    β•²
    β”‚                       β•²
    └───┬───┬───┬───┬───┬───┬── Token A (reserve0)
      250 500 750 1000 1500 2000

Let’s trace a real swap on this curve:

Pool starts: x = 1000 ETH, y = 1000 USDC, k = 1,000,000

Trader sells 100 ETH to the pool (no fee for simplicity):

New x = 1000 + 100 = 1100 ETH
New y = k / x = 1,000,000 / 1100 = 909.09 USDC
Output  = 1000 - 909.09 = 90.91 USDC

Key insight: The spot price was 1.0 USDC/ETH, but the trader got 90.91 USDC for 100 ETH β€” an effective price of 0.909 USDC/ETH. That’s ~9% price impact for consuming 10% of the reserves.

Price impact by trade size (starting from 1:1 pool):

Trade size     β”‚ Output    β”‚ Effective price β”‚ Price impact
(% of reserve) β”‚           β”‚                 β”‚
───────────────┼───────────┼─────────────────┼─────────────
1%   (10 ETH)  β”‚ 9.90 USDC β”‚ 0.990 USDC/ETH  β”‚ ~1.0%
5%   (50 ETH)  β”‚ 47.62     β”‚ 0.952           β”‚ ~4.8%
10% (100 ETH)  β”‚ 90.91     β”‚ 0.909           β”‚ ~9.1%
25% (250 ETH)  β”‚ 200.00    β”‚ 0.800           β”‚ ~20%
50% (500 ETH)  β”‚ 333.33    β”‚ 0.667           β”‚ ~33%

The takeaway: Price impact is NOT linear. It accelerates as you consume more of the reserves. This is why large trades need to be split across multiple DEXes (see AMM Aggregators later in this module).

πŸ’» Quick Try:

Verify the constant product formula with this Foundry test:

// In Foundry console or a quick test
function test_ConstantProduct() public pure {
    uint256 x = 1000e18; // 1000 ETH
    uint256 y = 1000e18; // 1000 USDC
    uint256 k = x * y;

    // Trader sells 100 ETH
    uint256 dx = 100e18;
    uint256 dy = (y * dx) / (x + dx); // output formula

    // Verify: k should be maintained
    uint256 newK = (x + dx) * (y - dy);
    assert(newK >= k); // Equal without fees, > k with fees

    // Verify: output is ~90.91 USDC (with 18 decimals)
    assert(dy > 90e18 && dy < 91e18);
}

Deploy and verify the price impact matches the table above. Then add the 0.3% fee and see how it changes the output.

Impermanent loss:

Why this matters: When the price of token A rises relative to token B, arbitrageurs buy A from the pool (cheap) and sell it on external markets. This re-balances the pool but means LPs end up with less A and more B than if they had just held. The difference between β€œhold” and β€œLP” value is impermanent loss.

It’s called β€œimpermanent” because it reverses if the price returns to the original ratio β€” but in practice, for volatile pairs, it’s very real.

The formula for impermanent loss given a price change ratio r:

IL = 2·√r / (1 + r) - 1

For a 2x price move: ~5.7% loss. For a 5x price move: ~25.5% loss. LPs need fee income to exceed IL to be profitable.

Real impact: During the May 2021 crypto crash, many ETH/USDC LPs on Uniswap V2 experienced 20-30% impermanent loss as ETH dropped from $4,000 to $1,700. Fee income over the same period was only ~5-8%, resulting in net losses compared to simply holding.

πŸ” Deep Dive: Impermanent Loss Step-by-Step

Setup: You deposit 1 ETH + 1000 USDC into a pool (ETH price = $1000). Your share is 10% of the pool.

Pool:    10 ETH + 10,000 USDC       k = 100,000
Your LP: 10% of pool = 1 ETH + 1,000 USDC = $2,000 total
HODL:    1 ETH + 1,000 USDC = $2,000

ETH price doubles to $2000. Arbitrageurs buy cheap ETH from the pool until the pool price matches:

New pool reserves (k must stay 100,000):
  price = y/x = 2000 β†’ y = 2000x
  x Β· 2000x = 100,000 β†’ x = √50 β‰ˆ 7.071 ETH
  y = 100,000 / 7.071 β‰ˆ 14,142 USDC

Your 10% LP share:
  0.7071 ETH ($1,414.21) + 1,414.21 USDC = $2,828.43

If you had just held:
  1 ETH ($2,000) + 1,000 USDC = $3,000

Impermanent Loss = $2,828.43 / $3,000 - 1 = -5.72%

Verify with the formula:

r = 2 (price doubled)
IL = 2·√2 / (1 + 2) - 1 = 2.828 / 3 - 1 = -0.0572 = -5.72% βœ“

IL at various price changes:

Price change β”‚ IL      β”‚ In dollar terms ($2000 initial)
─────────────┼─────────┼─────────────────────────────────
1.25x        β”‚ -0.6%   β”‚ LP: $2,236 vs HODL: $2,250 β†’ $14 lost
1.5x         β”‚ -2.0%   β”‚ LP: $2,449 vs HODL: $2,500 β†’ $51 lost
2x           β”‚ -5.7%   β”‚ LP: $2,828 vs HODL: $3,000 β†’ $172 lost
3x           β”‚ -13.4%  β”‚ LP: $3,464 vs HODL: $4,000 β†’ $536 lost
5x           β”‚ -25.5%  β”‚ LP: $4,472 vs HODL: $6,000 β†’ $1,528 lost
0.5x (drop)  β”‚ -5.7%   β”‚ LP: $1,414 vs HODL: $1,500 β†’ $86 lost

Why LPs accept this: Fee income. If the ETH/USDC pool earns 30% APR in fees, the LP is profitable as long as the price doesn’t move more than ~5x in a year. For stablecoin pairs (minimal price movement), fee income almost always exceeds IL.

The mental model: By LP-ing, you’re continuously selling the winning token and buying the losing one. You’re essentially selling volatility β€” profitable when fees > IL, unprofitable when the price moves too far.

Deep dive: Pintail’s IL calculator, Bancor IL research

πŸ” Deep Dive: Beyond IL β€” The LVR Framework

Why this matters: Impermanent loss is the classic way to measure LP costs, but it only captures the loss at the moment of withdrawal. The DeFi research community has moved to LVR (Loss-Versus-Rebalancing) as the more accurate framework β€” and it’s increasingly expected knowledge in interviews at serious DeFi teams.

The core insight:

IL compares β€œLP position” vs β€œholding.” But that’s not the right comparison for a professional market maker. The right comparison is: β€œLP position” vs β€œa portfolio that continuously rebalances to the same token ratio at market prices.”

IL perspective (snapshot):
  "I deposited at price X, now the price is Y, I lost Z% vs holding"
  β†’ Only matters at withdrawal. Reversible if price returns.

LVR perspective (continuous):
  "Every time the price moves on Binance, an arbitrageur trades against
   my AMM position at a stale price. The difference between the stale
   AMM price and the true market price is value extracted from me."
  β†’ Accumulates continuously. NEVER reverses. Scales with volatility.

Why LVR is more useful than IL:

  1. IL can be zero while LPs are losing money. If the price moves to 2x and back to 1x, IL = 0. But LVR accumulated the entire time β€” arbers profited on the way up AND on the way down.
  2. LVR explains WHY passive LPing loses. The cost is real-time extraction by informed traders (mostly CEX-DEX arbitrageurs), not just an abstract β€œthe price moved.”
  3. LVR informs protocol design. Dynamic fee mechanisms (like V4 hooks that increase fees during volatility) are designed to offset LVR, not IL.

The formula (for full-range CPMM):

LVR / V β‰ˆ σ² / 8   (annualized, as fraction of pool value)

Where:
  Οƒ = asset volatility (annualized)
  V = pool value

LVR scales with the square of volatility β€” which is why volatile pairs are so much more expensive to LP. A 2x increase in volatility β†’ 4x increase in LVR.

The practical takeaway for protocol builders:

Fees must exceed LVR, not just IL, for LPs to profit. When evaluating whether a pool can sustain liquidity, estimate LVR from historical volatility and compare against fee income. This is what Arrakis, Gamma, and other LP managers actually optimize for.

Deep dive: Milionis et al. β€œAutomated Market Making and Loss-Versus-Rebalancing” (2022), a16z LVR explainer, Tim Roughgarden’s LVR lecture

πŸ”— DeFi Pattern Connection

Where the constant product formula matters beyond AMMs:

  1. Lending liquidations (Module 4): Liquidation bots swap collateral through AMMs β€” price impact from the constant product formula determines whether liquidation is profitable
  2. Oracle design (Module 3): TWAP oracles built on AMM prices inherit the constant product curve’s properties β€” large trades cause large price movements that accumulate in TWAP
  3. Stablecoin pegs (Module 6): Curve’s StableSwap modifies the constant product formula for near-1:1 assets β€” understanding xΒ·y=k is prerequisite for understanding Curve’s hybrid invariant

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWhat is impermanent loss and when does it matter?”

    Answer
    • Good answer: β€œIL is the difference between LP value and holding value. It’s caused by arbitrageurs rebalancing the pool after external price changes.”
    • Great answer: β€œLPs are implicitly short volatility β€” they sell the appreciating token and buy the depreciating one as the pool rebalances. IL = 2√r/(1+r) - 1 where r is the price ratio change. For a 2x move, that’s ~5.7%. But IL is just a snapshot β€” the more accurate framework is LVR (Loss-Versus-Rebalancing), which measures the continuous cost of CEX-DEX arbitrageurs trading against stale AMM prices. LVR scales with σ² (volatility squared), which is why volatile pairs are so much more expensive to LP. For stablecoin pairs, both IL and LVR are near zero, making fee income almost pure profit. The key question is always: do fees exceed LVR? For most volatile pairs on V3, the answer is barely β€” especially with JIT liquidity extracting 5-10% of fee revenue.”

Interview Red Flags:

  • 🚩 Saying β€œimpermanent loss isn’t real” β€” it is real, and LVR makes it even more concrete
  • 🚩 Only knowing IL but not LVR β€” shows outdated understanding of LP economics
  • 🚩 Not understanding that LPs are selling volatility (short gamma)

Pro tip: In interviews, mention LVR by name and cite the Milionis et al. paper β€” it shows you follow DeFi research, not just Twitter summaries.


🎯 Build Exercise: Minimal Constant Product Pool

Workspace: workspace/src/part2/module2/exercise1-constant-product/ β€” starter file: ConstantProductPool.sol, tests: ConstantProductPool.t.sol

Build a ConstantProductPool.sol with these features:

Core state:

  • reserve0, reserve1 β€” current token reserves
  • totalSupply of LP tokens (use a simple internal accounting, or inherit ERC-20 (OZ implementation))
  • token0, token1 β€” the two ERC-20 token addresses
  • FEE_NUMERATOR = 3, FEE_DENOMINATOR = 1000 β€” 0.3% fee

Functions to implement:

1. addLiquidity(uint256 amount0, uint256 amount1) β†’ uint256 liquidity

First deposit: LP tokens minted = √(amount0 Β· amount1) (geometric mean). Burn a MINIMUM_LIQUIDITY (1000 wei) to the zero address to prevent the pool from ever being fully drained (this is a critical anti-manipulation measure β€” read the Uniswap V2 whitepaper section 3.4 on this).

Why this matters: Without minimum liquidity lock, an attacker can donate tiny amounts to manipulate the LP token price to extreme values, then exploit protocols that use LP tokens as collateral. Analysis by Haseeb Qureshi.

Subsequent deposits: LP tokens minted proportionally to the smaller ratio:

liquidity = min(amount0 Β· totalSupply / reserve0, amount1 Β· totalSupply / reserve1)

This incentivizes depositors to add liquidity at the current ratio. If they deviate, they get fewer LP tokens (the excess is effectively donated to existing LPs).

Common pitfall: Not checking both ratios. If you only check one token’s ratio, an attacker can donate the other token to manipulate the LP token price. Always use min() of both ratios.

2. removeLiquidity(uint256 liquidity) β†’ (uint256 amount0, uint256 amount1)

Burns LP tokens, returns proportional share of both reserves:

amount0 = liquidity Β· reserve0 / totalSupply
amount1 = liquidity Β· reserve1 / totalSupply

3. swap(address tokenIn, uint256 amountIn) β†’ uint256 amountOut

Apply fee, compute output using constant product formula, transfer tokens. Update reserves.

Critical: use the balance-before-after pattern from Module 1 if you want to support fee-on-transfer tokens. For this exercise, you can start without it and add it as an extension.

4. getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) β†’ uint256

Pure function implementing the swap math. This is the formula from above with fees applied.

Used by: Uniswap V2 Router uses this exact function to compute multi-hop paths.

Security considerations to implement:

  • Reentrancy guard on swap and liquidity functions (OpenZeppelin ReentrancyGuard)
  • Minimum liquidity lock on first deposit
  • Reserve synchronization β€” update reserves from actual balances after every operation
  • Zero-amount checks β€” revert on zero deposits or zero output swaps
  • K invariant check β€” after every swap, verify that k_new >= k_old (fees should only increase k)

Real impact: Early AMM forks that skipped reentrancy guards were drained via flash loan attacks. Example: Warp Finance exploit ($8M, December 2020) β€” reentrancy during LP token deposit allowed attacker to manipulate oracle price.

Test suite:

Write comprehensive Foundry tests covering:

  • Add initial liquidity, verify LP token minting and MINIMUM_LIQUIDITY lock
  • Add subsequent liquidity at correct ratio, verify proportional minting
  • Add liquidity at incorrect ratio, verify the depositor gets fewer LP tokens
  • Swap token0 for token1, verify output matches formula
  • Swap with fee, verify fee stays in pool (k increases)
  • Remove liquidity, verify proportional share returned
  • Large swap (high price impact), verify output is sublinear
  • Multiple sequential swaps, verify price moves in expected direction
  • Sandwich scenario: large swap moves price, second swap at worse rate, then reverse
  • Edge case: attempt to drain pool, verify it reverts or returns near-zero

Common pitfall: Testing only with equal reserve ratios. Real pools drift over time as prices change. Test with imbalanced reserves (e.g., 1000:5000 ratio) to catch ratio-dependent bugs.

Extension exercises:

  • Add a getSpotPrice() view function
  • Add a getAmountIn() function (given desired output, compute required input)
  • Add events: Swap, Mint, Burn (match Uniswap V2’s event signatures)
  • Implement a simple TWAP (time-weighted average price) oracle: store cumulative price and timestamp on each swap, expose a function to compute average price over a period

πŸ“‹ Key Takeaways: The Constant Product Formula

After this section, you should be able to:

  • Derive the swap output formula from x Β· y = k and calculate the exact output for a given input amount including fees
  • Explain why price impact is nonlinear (not proportional to trade size) and sketch the curve showing acceleration at larger trade sizes
  • Walk through an impermanent loss scenario step by step: initial deposit β†’ price change β†’ compare hold vs LP β†’ calculate the IL percentage using the formula 2√r / (1+r) - 1
  • Explain how fees grow k over time and why this partially offsets impermanent loss for LPs
Check your understanding
  • Swap output formula: From x * y = k, after adding dx (minus fee): dy = y * dx_fee / (x + dx_fee). The output is always less than proportional because you’re buying against the curve β€” the further you push reserves, the worse the rate gets.
  • Nonlinear price impact: Small trades move the price minimally, but large trades accelerate along the hyperbolic curve. A trade using 10% of reserves gets a much worse average price than ten trades of 1%, because each unit purchased raises the marginal cost of the next unit.
  • Impermanent loss calculation: When the price ratio changes by factor r, the LP’s position is worth 2*sqrt(r) / (1+r) of what holding would be worth. For a 2x price move, IL is about 5.7%. IL is β€œimpermanent” because it reverses if the price returns, but LVR (the realized cost to arbitrageurs) is permanent.
  • Fees offsetting IL: Every swap pays a fee (e.g., 0.3%) that increases k, growing the pool’s total value. If cumulative fee revenue exceeds the impermanent loss, LPs are net profitable. High-volume, low-volatility pairs tend to be profitable; low-volume, high-volatility pairs often are not.

πŸ’‘ Reading Uniswap V2

Why V2 Matters

Why this matters: Even though V3 and V4 exist, Uniswap V2’s codebase is the Rosetta Stone of DeFi. It’s clean, well-documented, and every concept maps directly to what you just built. Most AMM forks in DeFi (SushiSwap, PancakeSwap, hundreds of others) are V2 forks.

Real impact: SushiSwap forked Uniswap V2 in September 2020, currently holds $300M+ TVL. Understanding V2 deeply means you can audit and reason about a huge swath of deployed DeFi.

Deep dive: Uniswap V2 Core contracts (May 2020 deployment), V2 technical overview


πŸ“– Read: UniswapV2Pair.sol

Source: github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol

Read the entire contract. Map every function to your own implementation. Focus on:

mint() β€” Adding liquidity (line 110)

  • How it uses _mintFee() to collect protocol fees before computing LP tokens
  • The MINIMUM_LIQUIDITY lock (exactly what you implemented)
  • How it reads balances directly from IERC20(token0).balanceOf(address(this)) rather than relying on amount parameters β€” this is the β€œpull” pattern that makes V2 composable

Why this matters: The balance-reading pattern means you can send tokens first, then call mint(). This enables flash mints and complex atomic transactions. UniswapX uses this pattern.

burn() β€” Removing liquidity (line 134)

  • The same balance-reading pattern
  • How it sends tokens back using _safeTransfer (their own SafeERC20 equivalent)

swap() β€” The swap function (line 159)

This is the most important function to understand deeply.

  • The β€œoptimistic transfer” pattern: tokens are sent to the recipient first, then the invariant is checked. This is what enables flash swaps β€” you can receive tokens, use them, and return them (or the equivalent) in the same transaction.
  • The require(balance0Adjusted * balance1Adjusted >= reserve0 * reserve1 * 1000**2) check β€” this is the k-invariant with fees factored in
  • The callback to IUniswapV2Callee β€” this is the flash swap mechanism

Real impact: Flash swaps enabled the entire flash loan arbitrage ecosystem. Furucombo aggregates flash swaps from multiple DEXes, DeFi Saver uses them for debt refinancing. Without this pattern, these protocols wouldn’t exist.

Common pitfall: Forgetting to implement the callback when using flash swaps. The pool calls your contract’s uniswapV2Call() function β€” if it doesn’t exist or doesn’t return tokens, the transaction reverts with β€œK”.

_update() β€” Reserve and oracle updates (line 73)

  • Cumulative price accumulators: price0CumulativeLast and price1CumulativeLast
  • How TWAP oracles work: the price is accumulated over time, and external contracts can compute the time-weighted average by reading the cumulative value at two different timestamps
  • The use of UQ112.112 fixed-point numbers for precision

Used by: MakerDAO’s OSM oracle, Reflexer RAI, Liquity LUSD all use Uniswap V2 TWAP for price feeds.

Deep dive: Uniswap V2 Oracle guide, TWAP manipulation risks.

_mintFee() β€” Protocol fee logic (line 88)

  • If fees are on, the protocol takes 1/6th of LP fee growth (0.05% of the 0.3% swap fee)
  • The clever math: instead of tracking fees directly, it compares √k growth between fee checkpoints

πŸ“– Read: UniswapV2Factory.sol

Source: github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol

Focus on:

  • createPair() β€” how CREATE2 is used for deterministic addresses
  • Why deterministic addresses matter: the Router can compute pair addresses without on-chain lookups (saves gas)
  • The feeTo address for protocol fee collection

Why this matters: CREATE2 determinism means you can compute a pair address off-chain before it exists. Uniswap V2 Router uses this to avoid SLOAD for address lookups. V3 and V4 both adopted this pattern.


πŸ“– Read: UniswapV2Router02.sol

Source: github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol

This is the user-facing contract. Note how it:

  • Wraps ETH to WETH transparently (swapExactETHForTokens)
  • Computes optimal liquidity amounts for adding liquidity
  • Handles multi-hop swaps by chaining pair-to-pair transfers
  • Enforces slippage protection via amountOutMin parameters
  • Has deadline parameters to prevent stale transactions from executing

Common pitfall: Not setting amountOutMin properly. Setting it to 0 means accepting any price β€” frontrunners will sandwich your trade for maximum slippage. Always compute expected output and use a reasonable slippage tolerance (e.g., 0.5-1% for volatile pairs).

Real impact: MEV-Boost searchers extract $500M+ annually from sandwich attacks on poorly configured trades. Flashbots Protect RPC helps mitigate this.


🎯 Build Exercise: V2 Extensions

Workspace:

  • Tests: V2Extensions.t.sol (test-only exercise β€” implements FlashSwapConsumer and SimpleRouter inline)

The challenge: Three exercises covering V2’s advanced patterns β€” flash swaps, multi-hop routing, and TWAP oracles.

What you’ll practice:

  • Flash swap callback implementation and fee repayment
  • Multi-hop routing through multiple pools
  • TWAP oracle mechanics and price accumulator math

Exercise 1: Flash swap. Using your own pool or a V2 fork, implement a flash swap consumer contract. Borrow tokens, β€œuse” them (e.g., check arbitrage conditions), then return them with fee. Write tests verifying the flash swap callback works and that failing to return tokens reverts.

// Example flash swap consumer
contract FlashSwapConsumer is IUniswapV2Callee {
    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external override {
        // Verify caller is a legitimate pair
        require(sender == address(this));

        // Do something with borrowed tokens
        // ...

        // Return tokens + 0.3% fee (must satisfy k-invariant: amount * 1000 / 997)
        uint amountToRepay = amount0 > 0 ? amount0 * 1000 / 997 : amount1 * 1000 / 997;
        IERC20(token).transfer(msg.sender, amountToRepay);
    }
}

Exercise 2: Multi-hop routing. Create two pools (A/B and B/C) and implement a simple router that executes an A→C swap through both pools. Compute the optimal path off-chain and verify the output matches.

Exercise 3: TWAP oracle consumer. Deploy a pool, execute swaps at known prices, advance time with vm.warp(), and read the TWAP. Verify the oracle returns the time-weighted average.

Common pitfall: Not accounting for price accumulator overflow. V2 uses uint256 for cumulative prices which can overflow. You must compute the difference modulo 2^256. Example implementation.

🎯 Goal: Internalize the three V2 patterns that appear everywhere in DeFi: flash swaps (the foundation for flash loans and arbitrage), multi-hop routing (every aggregator does this), and TWAP oracles (the precursor to V3’s improved oracle design).

Run:

forge test --match-path "test/part2/module2/exercise1b-v2-extensions/*"

πŸ“– How to Study Uniswap V2:

  1. Read tests first β€” See how mint(), burn(), swap() are called in practice
  2. Read getAmountOut() in UniswapV2Library.sol β€” This is just dy = yΒ·dx/(x+dx) with fees. Match it to the formula you implemented
  3. Read swap() β€” Understand optimistic transfer + k-check pattern. Trace the flash swap callback
  4. Read mint() and burn() β€” Match to your own addLiquidity/removeLiquidity
  5. Read _update() β€” TWAP oracle mechanics with cumulative price accumulators

Don’t get stuck on: _mintFee() on first pass β€” it uses a clever √k growth comparison that’s elegant but not essential for initial understanding.


πŸŽ“ Intermediate Example: From V2 to V3

Before diving into V3’s concentrated liquidity, notice the key limitation of V2:

V2 Pool: 10 ETH + 20,000 USDC (ETH at $2,000)

Liquidity is spread from price 0 β†’ ∞
At the current price of $2,000, only a tiny fraction is "active"

If all the liquidity were concentrated between $1,800-$2,200:
  β†’ Same dollar amount provides ~20x more effective depth
  β†’ Trades in that range get ~20x less slippage
  β†’ LPs earn ~20x more fees per dollar

This is exactly what V3 does β€” but it adds complexity:
  β†’ LPs must choose their range
  β†’ Positions go out of range (stop earning)
  β†’ Each position is unique β†’ NFTs instead of fungible LP tokens
  β†’ The swap loop must cross tick boundaries

V3 trades simplicity for capital efficiency. Keep this tradeoff in mind as you read the next part of this module.

πŸ“‹ Key Takeaways: Reading Uniswap V2

After this section, you should be able to:

  • Trace a V2 swap() call end-to-end: optimistic transfer β†’ balance read β†’ k-invariant check, and explain why V2 reads balances instead of trusting transfer amounts
  • Explain the V2 flash swap mechanism: how IUniswapV2Callee.uniswapV2Call enables atomic arbitrage without upfront capital, and why the k-check at the end makes it safe
  • Describe how V2’s TWAP oracle works: cumulative price accumulators in _update(), why they’re manipulation-resistant over time, and how to compute a TWAP from two snapshots
  • Calculate a V2 pair address off-chain using CREATE2 (Factory address + token pair + init code hash) without querying the chain
Check your understanding
  • V2 swap flow: The pool optimistically transfers output tokens first, then reads its own balances to verify the k-invariant holds. Reading balanceOf instead of trusting transfer amounts makes V2 composable β€” you can send tokens via any mechanism (direct transfer, flash swap, router) and the pool just checks the math.
  • Flash swaps: V2’s swap() sends output tokens before checking k. If you request tokens and provide a callback address, uniswapV2Call fires with the borrowed tokens. You must return enough tokens to satisfy k by the end of the callback. The k-check at the end makes this safe β€” revert if not repaid.
  • V2 TWAP oracle: Each swap updates price0CumulativeLast += price * timeElapsed. To compute a TWAP, read the cumulative value at two different times and divide the difference by the time interval. Because it accumulates over many blocks, a single-block manipulation has negligible effect on a 30-minute TWAP.
  • CREATE2 pair address: address = keccak256(0xff, factory, keccak256(token0, token1), initCodeHash)[12:]. Tokens are sorted so (A,B) and (B,A) produce the same address. This lets routers and aggregators compute pair addresses without any on-chain calls.

πŸ’‘ Concentrated Liquidity (Uniswap V3 Concepts)

πŸ’‘ Concept: The Problem V3 Solves

Why this matters: In V2, liquidity is spread uniformly across the entire price range from 0 to infinity. For a stablecoin pair like DAI/USDC, the price almost always stays between 0.99 and 1.01 β€” meaning ~99.5% of LP capital is sitting idle at extreme price ranges that never get traded. This is massively capital-inefficient.

V3 lets LPs choose a specific price range for their liquidity. Capital between 0.99–1.01 instead of 0β€“βˆž means the same dollar amount provides ~2000x more effective liquidity.

Real impact: Uniswap V3 launched May 2021, holds $4B+ TVL (as of Q4 2024) with significantly less capital than V2’s peak. The USDC/ETH 0.05% pool on V3 provides equivalent liquidity to V2’s pool with ~10x less capital.

Deep dive: Uniswap V3 Whitepaper, V3 Math Primer


πŸ’‘ Concept: Core V3 Concepts

Ticks:

V3 divides the price space into discrete points called ticks. Each tick i corresponds to a price:

price(i) = 1.0001^i

This means each tick represents a 0.01% price increment (1 basis point). Ticks range from -887272 to 887272, covering prices from effectively 0 to infinity.

Not every tick can be used for position boundaries β€” tick spacing limits where positions can start and end. Tick spacing depends on the fee tier:

  • 0.01% fee β†’ tick spacing 1
  • 0.05% fee β†’ tick spacing 10
  • 0.3% fee β†’ tick spacing 60
  • 1% fee β†’ tick spacing 200

Why this matters: Tick spacing controls gas costs (fewer initialized ticks = lower gas) and prevents position fragmentation. V3 fee tier guide.

Positions:

An LP position is defined by: (lowerTick, upperTick, liquidity). The position is β€œactive” (earning fees) only when the current price is within the tick range. When the price moves outside the range, the position becomes entirely denominated in one token and stops earning fees.

Real impact: During volatile markets, many V3 LPs see their positions go out of range and stop earning fees entirely. On average, 60% of V3 liquidity is out of range at any given time. Active management is required.

sqrtPriceX96:

V3 stores prices as √P Β· 2^96 β€” the square root of the price in Q96 fixed-point format. Two reasons:

  1. The key AMM math formulas involve √P directly, so storing it avoids repeated square root operations
  2. Q96 fixed-point gives 96 bits of fractional precision without floating-point, which Solidity doesn’t support

To convert sqrtPriceX96 to a human-readable price:

price = (sqrtPriceX96 / 2^96)^2

Deep dive: TickMath.sol library handles all tick ↔ sqrtPriceX96 conversions, SqrtPriceMath.sol computes token amounts.

πŸ” Deep Dive: Ticks, Prices, and sqrtPriceX96 Visually

How ticks map to prices:

Tick:    ...  -20000    0     20000    40000    60000   ...
Price:   ... 0.1353   1.0    7.389    54.60    403.4   ...
                       ↑
                    tick 0 = price 1.0

Every tick is a 0.01% (1 basis point) step. The relationship is exponential:

  • Tick 0 β†’ price 1.0
  • Tick 10000 β†’ price 1.0001^10000 β‰ˆ 2.718 (β‰ˆ e!)
  • Tick -10000 β†’ price 1.0001^(-10000) β‰ˆ 0.368

Why square root? A visual intuition:

The V3 swap formulas need √P everywhere. Instead of computing √(1.0001^i) every time, V3 stores √P directly and scales it by 2^96 for fixed-point precision:

                        sqrtPriceX96
Price        √Price     = √Price Γ— 2^96           (2^96 = 79,228,162,514,264,337,593,543,950,336)
───────────────────────────────────────────────────────────────────────────
$1.00        1.0         79,228,162,514,264,337,593,543,950,336
$2,000       44.72       3,543,191,142,285,914,205,922,034,944
$100,000     316.23      25,054,144,837,504,793,118,641,380,156
$0.001       0.0316      2,505,414,483,750,479,311,864,138,816

Note: These are for ETH/USDC where price = USDC per ETH
      token0 = ETH (lower address), token1 = USDC

Reading sqrtPriceX96 in practice:

Given: sqrtPriceX96 = 3,543,191,142,285,914,205,922  (from slot0)

Step 1: Divide by 2^96
  √P = 3,543,191,142,285,914,205,922 / 79,228,162,514,264,337,593
  √P β‰ˆ 44.72

Step 2: Square to get price
  P = 44.72Β² β‰ˆ 2,000

β†’ ETH is trading at ~$2,000 USDC

Tick spacing visually β€” what LPs can actually use:

0.3% fee pool (tick spacing = 60):

Tick:   ... -120   -60    0     60    120   180   240  ...
Price:  ... 0.988  0.994  1.0   1.006 1.012 1.018 1.024 ...
                   ↑                   ↑
               Position A: 0.994 β€” 1.012 (ticks -60 to 120)
                          ↑       ↑
                    Position B: 1.0 β€” 1.006 (ticks 0 to 60) ← narrower, more concentrated

Position B has same capital in a tighter range β†’ earns MORE fees per dollar
but goes out of range faster during price movements.

πŸ’» Quick Try:

Play with tick-to-price conversions in Foundry:

function test_TicksAndPrices() public pure {
    // tick 0 = price 1.0 β†’ sqrtPriceX96 = 2^96
    uint160 sqrtPriceAtTick0 = uint160(1 << 96); // = 79228162514264337593543950336

    // For ETH at $2000 USDC, compute sqrtPriceX96:
    // √2000 β‰ˆ 44.72
    // sqrtPriceX96 β‰ˆ 44.72 Γ— 2^96
    // In practice, use TickMath.getSqrtRatioAtTick()

    // Verify: tick 23027 β‰ˆ $10 (1.0001^23027 β‰ˆ 10)
    // tick 46054 β‰ˆ $100
    // tick 69081 β‰ˆ $1000
    // Each doubling of price β‰ˆ +6931 ticks
}

Try computing: if ETH is at tick 86,841 relative to USDC, what’s the approximate price? (Answer: 1.0001^86841 β‰ˆ $5,900 β€” note: each +23,027 ticks β‰ˆ 10Γ— price, so 4 Γ— 23,027 = 92,108 would be ~$10,000)

The swap loop:

In V2, a swap is one formula evaluation. In V3, a swap may cross multiple tick boundaries, each changing the active liquidity. The swap loop:

  1. Compute how much of the swap can be filled within the current tick range
  2. If the swap isn’t fully filled, cross the tick boundary β€” activate/deactivate liquidity from positions at that tick
  3. Repeat until the swap is filled or the price limit is reached

Between any two initialized ticks, the math is identical to V2’s constant product β€” just with L (liquidity) potentially different in each range.

Common pitfall: Assuming V3 swaps are always more gas-efficient than V2. For swaps that cross many ticks (e.g., 10+ tick crossings), V3 can be more expensive. Gas comparison analysis.

Liquidity (L):

In V3, L represents the depth of liquidity at the current price. It relates to token amounts via:

Ξ”token0 = L Β· (1/√P_lower - 1/√P_upper)
Ξ”token1 = L Β· (√P_upper - √P_lower)

These formulas are why √P is stored directly β€” they simplify beautifully.

πŸ” Deep Dive: V3 Liquidity Math Step-by-Step

Setup: An LP wants to provide liquidity for ETH/USDC between $1,800 and $2,200 (current price = $2,000). To keep the math readable, we’ll use abstract price units (not token-decimals-adjusted). The key is understanding the formulas and ratios, not the raw numbers.

Given:
  P_current = 2000,  √P_current = 44.72
  P_lower   = 1800,  √P_lower   = 42.43
  P_upper   = 2200,  √P_upper   = 46.90
  L = 1,000,000 (abstract units β€” see note below)

Token amounts needed (price is WITHIN range):

  Ξ”token0 (ETH)  = L Β· (1/√P_current - 1/√P_upper)
                  = 1,000,000 Β· (1/44.72 - 1/46.90)
                  = 1,000,000 Β· (0.02236 - 0.02132)
                  = 1,000,000 Β· 0.00104
                  = 1,040

  Ξ”token1 (USDC) = L Β· (√P_current - √P_lower)
                  = 1,000,000 Β· (44.72 - 42.43)
                  = 1,000,000 Β· 2.29
                  = 2,290,000

  Ratio check: 2,290,000 / 1,040 β‰ˆ $2,202 per ETH βœ“ (close to current price, as expected)

On-chain units: In production V3, L is a uint128 representing √(token0_amount Γ— token1_amount) in wei-scale units. A real position providing ~1 ETH + ~2,290 USDC in this range would have L β‰ˆ 1.54 Γ— 10^15. The formulas above use simplified numbers to show the math clearly β€” the ratios and relationships are identical.

What happens when price moves OUT of range:

If ETH rises to $2,500 (above upper bound):
  β†’ Position is 100% USDC, 0% ETH (LP sold all ETH on the way up)
  β†’ Stops earning fees

If ETH drops to $1,500 (below lower bound):
  β†’ Position is 100% ETH, 0% USDC (LP bought ETH all the way down)
  β†’ Stops earning fees

The key insight: A narrower range requires LESS capital for the same liquidity depth L. That’s capital efficiency β€” but the position goes out of range faster.

LP tokens β†’ NFTs:

In V2, all LPs in a pool share fungible LP tokens. In V3, every position is unique (different range, different liquidity), so positions are represented as NFTs (ERC-721). This has major implications for composability β€” you can’t just hold an ERC-20 LP token and deposit it into a farm; you need the NFT.

Real impact: This NFT design broke composability with yield aggregators. Arrakis, Gamma, and Uniswap’s own PCSM emerged to manage V3 positions and provide fungible vault tokens.

Fee accounting:

Fees in V3 are tracked per unit of liquidity within active ranges using feeGrowthGlobal and per-tick feeGrowthOutside values. The math for computing fees owed to a specific position involves subtracting the fee growth β€œbelow” and β€œabove” the position’s range from the global fee growth. This is elegant but complex β€” study it closely.

Deep dive: V3 fee math explanation, Position.sol library


πŸ“– Read: Key V3 Contracts

Core contracts (v3-core):

Focus areas in UniswapV3Pool:

  • swap() β€” the main swap loop. Trace the while loop step by step. Understand computeSwapStep(), tick crossing, and how state.liquidity changes at tick boundaries.
  • mint() β€” how positions are created, how tick bitmaps track initialized ticks
  • _updatePosition() β€” fee growth accounting per position
  • slot0 β€” the packed storage slot holding sqrtPriceX96, tick, observationIndex, and other frequently accessed data

Common pitfall: Not understanding tick bitmap navigation. V3 uses a clever bit-packing scheme where each word in the bitmap represents 256 ticks. TickBitmap.sol handles this β€” read it carefully.

Libraries:

  • TickMath.sol β€” conversions between ticks and sqrtPriceX96
  • SqrtPriceMath.sol β€” token amount calculations given liquidity and price ranges
  • SwapMath.sol β€” compute swap steps within a single tick range
  • TickBitmap.sol β€” efficient lookup of the next initialized tick

Used by: These libraries are extensively reused. PancakeSwap V3, Trader Joe V2.1, and many others fork or adapt V3’s math libraries.


🎯 Build Exercise: V3 Position Calculator

Workspace:

The challenge: Three exercises covering V3’s core math β€” tick conversions, position value calculations, and cross-tick swap simulation.

What you’ll practice:

  • Converting between ticks, prices, and sqrtPriceX96
  • Computing position amounts for the three cases (below, within, above range)
  • Tracing how liquidity changes when swaps cross tick boundaries

Exercise 1: Tick math implementation. Write Solidity functions that convert between ticks, prices, and sqrtPriceX96. Verify against TickMath.sol outputs using Foundry tests. This will cement the relationship between these representations.

Exercise 2: Position value calculator. Given a position’s (tickLower, tickUpper, liquidity) and the current sqrtPriceX96, compute how many of each token the position currently holds. Handle the three cases: price below range, price within range, price above range.

// Skeleton β€” implement the three cases
function getPositionAmounts(
    uint160 sqrtPriceX96,
    int24 tickLower,
    int24 tickUpper,
    uint128 liquidity
) public pure returns (uint256 amount0, uint256 amount1) {
    uint160 sqrtLower = TickMath.getSqrtRatioAtTick(tickLower);
    uint160 sqrtUpper = TickMath.getSqrtRatioAtTick(tickUpper);

    if (sqrtPriceX96 <= sqrtLower) {
        // Price BELOW range: position is 100% token0
        // amount0 = L · (1/√P_lower - 1/√P_upper)
        // TODO: implement using SqrtPriceMath
    } else if (sqrtPriceX96 >= sqrtUpper) {
        // Price ABOVE range: position is 100% token1
        // amount1 = L · (√P_upper - √P_lower)
        // TODO: implement using SqrtPriceMath
    } else {
        // Price WITHIN range: position holds both tokens
        // amount0 = L · (1/√P_current - 1/√P_upper)
        // amount1 = L · (√P_current - √P_lower)
        // TODO: implement using SqrtPriceMath
    }
}

Write tests that verify all three cases and check that amounts change continuously as price moves through the range boundaries.

Exercise 3: Simulate a swap across ticks. On paper or in a test, set up a pool with three positions at different ranges. Execute a large swap that crosses two tick boundaries. Trace the liquidity changes and verify the total output matches what V3 would produce.

🎯 Goal: Build working V3 math from scratch. If you can compute position values and trace cross-tick swaps, you can read Uniswap V3’s actual SqrtPriceMath and SwapMath libraries fluently.

Run:

forge test --match-path "test/part2/module2/exercise2-v3-position/*"

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œExplain how Uniswap V3’s concentrated liquidity works and why it matters.”

    Answer
    • Good answer: β€œLPs choose a price range. Within that range, their capital provides the same liquidity depth as a much larger V2 position. It’s more capital-efficient but requires active management.”
    • Great answer: β€œV3 divides the price space into ticks at 1 basis point intervals. Between any two initialized ticks, the pool behaves like a V2 pool with liquidity L. The swap loop crosses tick boundaries, adding/removing liquidity from positions. sqrtPriceX96 is stored as the square root to simplify the core math formulas. The tradeoff is that LPs now compete with JIT liquidity providers and need active management β€” which spawned Arrakis, Gamma, and eventually V4 hooks for native LP management.”

Interview Red Flags:

  • 🚩 Not knowing what sqrtPriceX96 is or why prices are stored as square roots
  • 🚩 Thinking V3 is always better than V2 (not true for high-volatility, low-volume pairs)
  • 🚩 Unaware that ~60% of V3 liquidity is out of range at any given time

Pro tip: Be ready to trace through V3’s swap loop (computeSwapStep β†’ tick crossing β†’ liquidity update). Teams want engineers who can debug at the source code level, not just explain concepts.

πŸ“– How to Study Uniswap V3:

  1. Start with the V3 Development Book β€” Build a simplified V3 alongside reading production code
  2. Read SqrtPriceMath.sol FIRST β€” Pure math functions. Focus on inputs/outputs, not the bit manipulation
  3. Read SwapMath.computeSwapStep() β€” One step of the swap loop, the core unit of work
  4. Read the swap() while loop in UniswapV3Pool.sol β€” Now you see how steps compose into a full swap
  5. Read Tick.sol and TickBitmap.sol LAST β€” Gas optimizations, important but not for first pass

Don’t get stuck on: FullMath.sol (it’s mulDiv for precision β€” you know this from Part 1), Oracle.sol (save for Module 3).

πŸ“‹ Key Takeaways: Concentrated Liquidity (V3)

After this section, you should be able to:

  • Convert between ticks and prices (price = 1.0001^i) and explain why V3 stores sqrtPriceX96 instead of the price itself (hint: the liquidity math formulas use √P directly)
  • Describe a V3 position as (tickLower, tickUpper, liquidity) and calculate how much of each token an LP must deposit given the current price
  • Walk through V3’s swap loop: what happens when price crosses an initialized tick (active liquidity changes), and why the pool behaves like V2 between any two adjacent ticks
  • Explain V3’s fee accounting: how feeGrowthGlobal accumulates, how per-tick feeGrowthOutside tracks fees above/below a tick, and how an LP’s uncollected fees are computed from these values
Check your understanding
  • Ticks and sqrtPriceX96: Each tick i maps to price = 1.0001^i (1 basis point per tick). V3 stores sqrtPriceX96 = sqrt(price) * 2^96 because the core liquidity formulas (L = dx * sqrt(P) and L = dy / sqrt(P)) use square root of price directly, avoiding an expensive on-chain square root operation.
  • V3 positions: A position is defined by (tickLower, tickUpper, liquidity). The token amounts required depend on where the current price sits relative to the range: below range = 100% token0, above range = 100% token1, in range = a mix of both determined by the liquidity math.
  • V3 swap loop and tick crossing: Between two initialized ticks, the pool behaves as a constant product pool with liquidity L. When price hits an initialized tick, the pool adds or removes the liquidity of positions that start/end at that tick, then continues with the updated L. This is why V3 swaps iterate through ticks.
  • V3 fee accounting: feeGrowthGlobal accumulates total fees per unit of liquidity. Each initialized tick stores feeGrowthOutside, representing fees accumulated on the β€œother side” of that tick. An LP’s uncollected fees = (feeGrowthInside - feeGrowthInsideLastX128) * liquidity, where feeGrowthInside is derived by subtracting the outside values from the global.

🎯 Build Exercise: Simplified Concentrated Liquidity Pool

What to Build

Note: This is a self-directed challenge β€” there is no workspace scaffold or pre-written test suite. Design the contract, write the tests, and iterate on your own. The Uniswap V3 Development Book is an excellent companion resource for this build.

You won’t replicate V3’s full complexity (the tick bitmap alone is a masterwork of gas optimization). Instead, build a simplified CLAMM (Concentrated Liquidity AMM) that captures the core mechanics:

Simplified design:

  • Use a small, fixed set of tick boundaries (e.g., ticks every 100 units) instead of V3’s full bitmap
  • Support 3–5 concurrent positions
  • Implement the swap loop that crosses ticks
  • Track fees per position

Contract: SimpleCLAMM.sol

State:

struct Position {
    int24 tickLower;
    int24 tickUpper;
    uint128 liquidity;
    uint256 feeGrowthInside0LastX128;
    uint256 feeGrowthInside1LastX128;
    uint128 tokensOwed0;
    uint128 tokensOwed1;
}

uint160 public sqrtPriceX96;
int24 public currentTick;
uint128 public liquidity;  // active liquidity at current tick
mapping(int24 => TickInfo) public ticks;
mapping(bytes32 => Position) public positions;

Functions:

1. addLiquidity(int24 tickLower, int24 tickUpper, uint128 amount)

  • Compute token0 and token1 amounts needed for the given liquidity at the current price
  • Update tick data (add/remove liquidity at boundaries)
  • If the position range includes the current tick, add to active liquidity

2. swap(bool zeroForOne, int256 amountSpecified)

  • Implement the swap loop:
    • Compute the next initialized tick in the swap direction
    • Compute how much of the swap fills within the current range (use SqrtPriceMath formulas)
    • If the swap crosses a tick, update active liquidity and continue
    • Accumulate fees in feeGrowthGlobal

3. removeLiquidity(int24 tickLower, int24 tickUpper, uint128 amount)

  • Reverse of addLiquidity
  • Compute and distribute accrued fees to the position

The key insight to internalize:

Between any two initialized ticks, the pool behaves exactly like a V2 pool with liquidity L. The CLAMM is essentially a linked list of V2 segments, each with potentially different depth. The swap loop walks through these segments.

Deep dive: Uniswap V3 Development Book β€” comprehensive guide to building a V3 clone from scratch.


Test Checklist

Write Foundry tests covering:

  • Create a single full-range position (equivalent to V2 behavior), verify swap outputs match your Constant Product Pool
  • Create two overlapping positions, verify liquidity adds at overlapping ticks
  • Execute a swap that crosses a tick boundary, verify liquidity changes correctly
  • Verify fee accrual: position earning fees only while in range
  • Out-of-range position: add liquidity above current price, verify it earns zero fees, verify it’s 100% token0
  • Impermanent loss test: add position, execute swaps that move price significantly, remove position, compare to holding

Common pitfall: Not testing tick crossings in both directions. A swap buying token0 (decreasing price) crosses ticks differently than a swap buying token1 (increasing price). Test both directions.

πŸ“‹ Key Takeaways: Simplified CLAMM Challenge

After this section, you should be able to:

  • Implement a simplified CLAMM with addLiquidity, swap (tick-crossing loop), and removeLiquidity that demonstrates V3’s core mechanic
  • Explain V3’s core insight in one sentence: between any two initialized ticks, the pool behaves exactly like a constant product pool with liquidity L
  • Implement per-position fee accrual that only accumulates while the position’s range includes the current price
  • Write tests covering tick crossings, overlapping positions, and out-of-range deposits
Check your understanding
  • Simplified CLAMM implementation: The core mechanic is a swap loop that iterates through tick boundaries. At each step, compute how much of the swap can execute within the current tick range using constant product math with the active liquidity L, then cross the tick and update L by adding/removing liquidity from positions at that boundary.
  • V3’s core insight: Between any two initialized ticks, the pool is mathematically identical to a V2 constant product pool with liquidity L. The concentrated liquidity innovation is simply allowing different L values in different price ranges, composed by summing overlapping positions’ liquidity at each tick.
  • Per-position fee accrual: Fees only accumulate for a position while the current price is within its [tickLower, tickUpper] range. Out-of-range positions earn zero fees because no swaps execute against their liquidity. This is tracked via the feeGrowthInside mechanism described in V3’s fee accounting.
  • Testing tick crossings: Test swaps in both directions (price increasing and decreasing) across tick boundaries where positions start or end. Verify that liquidity changes correctly at each crossing and that fees are only attributed to in-range positions.

πŸ’‘ Uniswap V4 β€” Singleton Architecture and Flash Accounting

πŸŽ“ Intermediate Example: From V3 to V4

Before diving into V4, notice V3’s key architectural limitation:

V3 multi-hop swap: ETH β†’ USDC β†’ DAI (two pools)

                    Pool A (ETH/USDC)          Pool B (USDC/DAI)
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 User sends ETH ──→│ swap()       │──USDC──→  β”‚ swap()       │──DAI──→ User receives DAI
                    β”‚ (separate    β”‚ (real     β”‚ (separate    β”‚ (real
                    β”‚  contract)   β”‚  ERC-20   β”‚  contract)   β”‚  ERC-20
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  transfer)β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  transfer)

Token transfers: 3 (ETH in, USDC between pools, DAI out)
Gas cost: ~300k+ (each transfer = approve + transferFrom + balance updates)

What if all pools lived in the same contract?

V4 multi-hop swap: ETH β†’ USDC β†’ DAI (same PoolManager)

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚              PoolManager                 β”‚
 User sends ETH ──→│                                         │──DAI──→ User receives DAI
                    β”‚  Pool A: ETH delta: +1                  β”‚
                    β”‚          USDC delta: -2000               β”‚
                    β”‚  Pool B: USDC delta: +2000  ← cancels!  β”‚
                    β”‚          DAI delta: -1999                β”‚
                    β”‚                                         β”‚
                    β”‚  Net: ETH +1, DAI -1999 (only these move)β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Token transfers: 2 (ETH in, DAI out β€” USDC never moves!)
Gas cost: ~200k (20-30% cheaper, and scales better with more hops)

V4 trades the simplicity of independent pool contracts for a singleton that tracks IOUs. The USDC delta from Pool A cancels with Pool B β€” it’s just accounting. Combined with transient storage (TSTORE at 100 gas vs SSTORE at 2,100+), this makes complex multi-pool interactions dramatically cheaper.

πŸ’‘ Concept: Architectural Revolution

Why this matters: V4 is a fundamentally different architecture from V2/V3. The two key innovations make it significantly more gas-efficient and composable.

Real impact: V4 launched January 2025, pool creation costs dropped from ~5M gas (V3) to ~500 gas (V4) β€” a 10,000x reduction. Multi-hop swaps save 20-30% gas compared to V3.

1. Singleton Pattern (PoolManager)

In V2 and V3, every token pair gets its own deployed contract (created by the Factory). This means multi-hop swaps (A→B→C) require actual token transfers between pool contracts — expensive in gas.

V4 consolidates all pools into a single contract called PoolManager. Pools are just entries in a mapping, not separate contracts. Creating a new pool is a state update, not a contract deployment β€” approximately 99% cheaper in gas.

The key benefit: multi-hop swaps never move tokens between contracts. All accounting happens internally within the PoolManager. Only the final net token movements are settled at the end.

Used by: Balancer V2 pioneered this pattern with its Vault architecture (July 2021). V4 adopted and extended it with transient storage.

2. Flash Accounting (EIP-1153 Transient Storage)

V4 uses transient storage (which you studied in Part 1 Module 2) to implement β€œflash accounting.” During a transaction:

  1. The caller β€œunlocks” the PoolManager
  2. The caller can perform multiple operations (swaps, liquidity changes) across any pools
  3. The PoolManager tracks net balance changes (β€œdeltas”) in transient storage
  4. At the end, the caller must settle all deltas to zero β€” either by transferring tokens or using ERC-6909 claim tokens
  5. If deltas aren’t zero, the transaction reverts

This is essentially flash-loan-like behavior baked into the protocol’s core. You can swap Aβ†’B in one pool and Bβ†’C in another without ever transferring B β€” the PoolManager tracks that your B delta nets to zero.

Why this matters: Transient storage (TSTORE/TLOAD) costs ~100 gas vs ~2,100+ gas for SSTORE/SLOAD. Flash accounting enables complex multi-pool interactions at a fraction of V3’s cost.

Deep dive: V4 unlock pattern, Flash accounting explainer

3. Native ETH Support

Because flash accounting handles all token movements internally, V4 can support native ETH directly β€” no WETH wrapping needed. ETH transfers (msg.value) are cheaper than ERC-20 transfers, saving gas on the most common trading pairs.

Real impact: ETH swaps in V4 save ~15,000 gas compared to WETH swaps in V3 (no approve() or transferFrom() needed for ETH).

4. ERC-6909 Claim Tokens

Instead of withdrawing tokens from the PoolManager, users can receive ERC-6909 tokens representing claims on tokens held by the PoolManager. These claims can be burned in future interactions instead of doing full ERC-20 transfers. This is a lightweight multi-token standard (simpler than ERC-1155) optimized for gas.

Deep dive: EIP-6909 specification, V4 Claims implementation


πŸ“– Read: Key V4 Contracts

Source: github.com/Uniswap/v4-core

Focus on:

  • PoolManager.sol β€” the singleton. Study unlock(), swap(), modifyLiquidity(), and the delta accounting system
  • Pool.sol (library) β€” the actual pool math, used by PoolManager. Note how it’s a library, not a contract β€” keeping the PoolManager modular
  • PoolKey β€” the struct that identifies a pool: (currency0, currency1, fee, tickSpacing, hooks)
  • BalanceDelta β€” a packed int256 representing net token changes

Periphery (v4-periphery):

  • PositionManager.sol β€” the entry point for LPs, manages positions as ERC-721 NFTs
  • V4Router.sol / Universal Router β€” the entry point for swaps

Common pitfall: Trying to call swap() directly on PoolManager. You must go through the unlock() pattern β€” your contract implements unlockCallback() which then calls swap(). Example router implementation.


Exercises

Exercise 1: Study the unlock pattern. Trace through a simple swap: how does the caller interact with PoolManager? What’s the sequence of unlock() β†’ callback β†’ swap() β†’ settle() / take()? Draw the flow.

Exercise 2: Multi-hop with flash accounting. On paper, trace a three-pool multi-hop swap (A→B→C→D). Show how deltas accumulate and net to zero for intermediate tokens. Compare the token transfer count to V2/V3 equivalents.

Exercise 3: Deploy PoolManager locally. Fork mainnet or deploy V4 contracts to anvil. Create a pool, add liquidity, execute a swap. Observe the delta settlement pattern in practice.

# Fork mainnet to test V4
forge test --fork-url $MAINNET_RPC --match-contract V4Test

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWalk me through Uniswap V4’s flash accounting. How does it save gas?”

    Answer
    • Good answer: β€œV4 uses a singleton contract and transient storage. Instead of transferring tokens between pool contracts for multi-hop swaps, it tracks balance changes (deltas) and only settles the net at the end.”
    • Great answer: β€œV4’s PoolManager consolidates all pools into one contract. When a caller unlock()s the PoolManager, it can perform multiple operations β€” swaps across different pools, liquidity changes β€” and the PoolManager tracks net balance changes per token using TSTORE/TLOAD (100 gas vs 2,100+ for SSTORE). For a 3-hop swap Aβ†’Bβ†’Cβ†’D, only A and D move β€” B and C deltas cancel to zero internally. The caller settles by transferring tokens or using ERC-6909 claim tokens. This saves 20-30% gas and eliminates intermediate token transfers entirely.”

Interview Red Flags:

  • 🚩 Not understanding the unlock β†’ callback β†’ settle pattern
  • 🚩 Confusing V4’s flash accounting with flash loans (related concepts but different mechanisms)

Pro tip: Mention that Balancer V2 pioneered the singleton Vault pattern and V4 extended it with transient storage β€” shows you understand the design lineage.

πŸ“– How to Study Uniswap V4:

  1. Read PoolManager.unlock() and IUnlockCallback β€” Understand the interaction pattern before anything else
  2. Read the delta accounting β€” How deltas are tracked, settled, and validated
  3. Read a simple hook (FullRange or SwapCounter) β€” See the full hook lifecycle before complex hooks
  4. Read Pool.sol (library) β€” V3’s math adapted for V4’s singleton, familiar territory
  5. Read PositionManager.sol in v4-periphery β€” How the user-facing contract interacts with PoolManager

πŸ“‹ Key Takeaways: V4 Singleton & Flash Accounting

After this section, you should be able to:

  • Explain V4’s singleton architecture and why consolidating all pools into one PoolManager contract eliminates redundant ERC-20 transfers between pools during multi-hop swaps
  • Trace the flash accounting flow: unlock() β†’ callback β†’ swap/modify operations accumulate deltas in transient storage β†’ settle()/take() zero out all deltas before the callback returns
  • Describe how ERC-6909 claim tokens work as an alternative to ERC-20 transfers for frequent traders (keep balances inside PoolManager, avoid repeated approve/transferFrom overhead)
  • Compare V3’s multi-hop cost (N+1 token transfers for N hops) vs V4’s cost (always 2 transfers regardless of hops) and explain why transient storage makes this possible
Check your understanding
  • Singleton architecture: V4 deploys all pools inside a single PoolManager contract. Multi-hop swaps no longer require intermediate ERC-20 transfers between separate pool contracts β€” instead, each hop just updates internal deltas (accounting entries), and only the net token movements at the edges require real transfers.
  • Flash accounting flow: unlock() grants callback access. Inside the callback, swap/modify operations accumulate positive and negative deltas in transient storage (EIP-1153). Before the callback returns, the caller must zero all deltas via settle() (pay tokens in) and take() (receive tokens out). If any delta is non-zero, the transaction reverts.
  • ERC-6909 claim tokens: Frequent traders can keep token balances inside PoolManager as ERC-6909 claims instead of settling to ERC-20 each time. This avoids repeated approve/transferFrom overhead. Claims can be used to settle deltas in future swaps, reducing gas for high-frequency users.
  • V3 vs V4 multi-hop gas: V3 requires N+1 ERC-20 transfers for N hops (each intermediate token physically moves between pool contracts). V4 tracks all intermediate amounts as transient storage deltas within PoolManager, so only 2 real transfers occur (input token in, output token out) regardless of hop count.

πŸ’‘ Uniswap V4 Hooks

πŸ’‘ Concept: The Hook System

Why this matters: Hooks are external smart contracts that the PoolManager calls at specific points during pool operations. They are V4’s extension mechanism β€” the β€œapp store” for AMMs.

A pool is linked to a hook contract at initialization and cannot change it afterward. The hook address itself encodes which callbacks are enabled β€” specific bits in the address determine which hook functions the PoolManager will call. This is a gas optimization: the PoolManager checks the address bits rather than making external calls to query capabilities.

Real impact: Over 100+ production hooks deployed in V4’s first 3 months. Examples: Clanker hook (meme coin launching), Brahma hook (MEV protection), Full Range hook (V2-style behavior).

Deep dive: Hooks documentation, Awesome Uniswap Hooks list


The 10 Hook Functions

Hooks can intercept at these points:

Pool lifecycle:

  • beforeInitialize / afterInitialize β€” when a pool is created

Swaps:

  • beforeSwap / afterSwap β€” before and after swap execution

Liquidity modifications:

  • beforeAddLiquidity / afterAddLiquidity
  • beforeRemoveLiquidity / afterRemoveLiquidity

Donations:

  • beforeDonate / afterDonate β€” donations send fees directly to in-range LPs

Hook Capabilities

Dynamic fees: A hook can implement getFee() to return a custom fee for each swap. This enables strategies like: higher fees during volatile periods, lower fees for certain users, MEV-aware fee adjustment.

Custom accounting: Hooks can modify the token amounts involved in swaps. The beforeSwap return value can specify delta modifications, allowing the hook to effectively intercept and re-route part of the trade.

Access control: Hooks can implement KYC/AML checks, restricting who can swap or provide liquidity.

Oracle integration: A hook can maintain a custom oracle, updated on every swap β€” similar to V3’s built-in oracle but customizable.

Used by: EulerSwap hook implements volatility-adjusted fees, GeomeanOracle hook provides TWAP oracles with better properties than V2/V3.


πŸ“– Read: Hook Examples

Source: github.com/Uniswap/v4-periphery/tree/main/src/hooks (official examples) Source: github.com/fewwwww/awesome-uniswap-hooks (curated community list)

Study these hook patterns:

  • Limit order hook β€” converts a liquidity position into a limit order that executes when the price crosses a specific tick
  • TWAMM hook β€” time-weighted average market maker (execute large orders over time)
  • Dynamic fee hook β€” adjusts fees based on volatility or other on-chain signals
  • Full-range hook β€” enforces V2-style full-range liquidity for specific use cases

⚠️ Hook Security Considerations

Why this matters: Hooks introduce new attack surfaces that don’t exist in V2/V3.

Real impact: Cork Protocol exploit (February 2025) β€” hook didn’t verify msg.sender was the PoolManager, allowing direct calls to manipulate internal state. Loss: $400k.

Critical security patterns:

1. Access control β€” Hooks MUST verify that msg.sender is the legitimate PoolManager. Without this check, attackers can call hook functions directly and manipulate internal state.

// βœ… GOOD: Verify caller is PoolManager
modifier onlyPoolManager() {
    require(msg.sender == address(poolManager), "Not PoolManager");
    _;
}

function beforeSwap(...) external onlyPoolManager returns (...) {
    // Safe: only PoolManager can call
}

2. Gas griefing β€” A malicious or buggy hook with unbounded loops can make a pool permanently unusable by consuming all gas in swap transactions.

Common pitfall: Hooks that iterate over unbounded arrays. If a hook stores a list of all past swaps and loops over it in beforeSwap, an attacker can make thousands of tiny swaps to bloat the array until gas limits are hit.

3. Reentrancy β€” Hooks execute within the PoolManager’s context. If a hook makes external calls, it could re-enter the PoolManager.

// ❌ BAD: External call during hook execution
function afterSwap(...) external returns (...) {
    externalContract.doSomething(); // Could re-enter PoolManager
}

// βœ… GOOD: Use checks-effects-interactions pattern
function afterSwap(...) external returns (...) {
    // Update state first
    lastSwapTime = block.timestamp;

    // Then external calls (if absolutely necessary)
    // Better: avoid external calls entirely in hooks
}

4. Trust model β€” Users must trust the hook contract as much as they trust the pool itself. A malicious hook can front-run swaps, extract MEV, or drain liquidity.

5. Immutability β€” Once a pool is initialized with a hook, the hook cannot be changed. If the hook has a bug, the pool must be abandoned and a new one created.

Common pitfall: Not considering upgradability. If your hook needs to be upgradable, you must use a proxy pattern from the start. After pool initialization, you can’t change the hook address, but you can change the hook’s implementation if it’s behind a proxy.

πŸ”— DeFi Pattern Connection

Where V4 hooks are being used in production:

  1. MEV protection: Sorella’s Angstrom uses hooks to batch-settle swaps at uniform clearing prices, eliminating sandwich attacks
  2. Lending integration: Hooks that auto-deposit idle LP assets into lending protocols between swaps β€” earning additional yield on liquidity
  3. Custom oracles: GeomeanOracle hook provides TWAP with better properties than V2/V3’s built-in oracle
  4. LP management: Bunni uses hooks for native concentrated liquidity management without external vaults

The pattern: V4 hooks are the composability layer for AMM innovation. Instead of forking an AMM (fragmenting liquidity), you plug into shared liquidity with custom logic.

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow do V4 hooks work and what are the security considerations?”

    Answer
    • Good answer: β€œHooks are external contracts called at specific points during pool operations. The hook address encodes which callbacks are enabled through specific address bits.”
    • Great answer: β€œHooks intercept 10 lifecycle points: before/after initialize, swap, add/remove liquidity, and donate. The hook address itself determines which callbacks are active β€” specific bits in the address are checked by the PoolManager (gas optimization: bit checks vs external calls). Critical security: hooks MUST verify msg.sender == poolManager (Cork Protocol lost $400k from missing this check), avoid unbounded loops (gas griefing), and handle reentrancy carefully. Once a pool is initialized with a hook, it’s permanent β€” bugs mean abandoning the pool.”

Interview Red Flags:

  • 🚩 Not knowing that hooks are immutably linked to pools at initialization
  • 🚩 Thinking hooks can modify the pool’s core math (they intercept at lifecycle points, not replace the invariant)
  • 🚩 Not mentioning access control (msg.sender == poolManager) as a critical security pattern

Pro tip: Mention a specific production hook you’ve studied (Clanker, Bunni, or GeomeanOracle) β€” it shows you’ve gone beyond docs into actual codebases.


🎯 Build Exercise: A Simple Hook

Workspace:

The challenge: Build V4 hooks from scratch β€” a dynamic fee hook based on volatility, a minimal swap counter, and study a production hook.

What you’ll practice:

  • Extending BaseHook and configuring hook permissions
  • Mining hook addresses with correct flag bits
  • Implementing beforeSwap for dynamic fee adjustment

Exercise 1: Dynamic fee hook. Build a hook that adjusts the swap fee based on recent volatility. Track the last N swap prices, compute a simple volatility metric, and return a higher fee when volatility is elevated. This teaches you the full hook development cycle:

  • Extend BaseHook from v4-periphery
  • Set the correct hook address bits (use the Hooks library to mine an address with the right flags)
  • Implement beforeSwap to adjust fees
  • Deploy and test with a real PoolManager
// Example: Mining a hook address with correct flags
// Hook address must have specific bits set to indicate which callbacks are enabled
contract VolatilityHook is BaseHook {
    using Hooks for IHooks;

    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: false,
            afterInitialize: false,
            beforeAddLiquidity: false,
            afterAddLiquidity: false,
            beforeRemoveLiquidity: false,
            afterRemoveLiquidity: false,
            beforeSwap: true,        // We need beforeSwap to adjust fees
            afterSwap: false,
            beforeDonate: false,
            afterDonate: false
        });
    }

    function beforeSwap(...) external override returns (...) {
        // Calculate volatility and return dynamic fee
    }
}

Exercise 2: Swap counter hook. Build a minimal hook that simply counts the number of swaps on a pool. This is the β€œhello world” of hooks β€” it gets you through the setup and deployment mechanics without complex logic.

Exercise 3: Read an existing production hook. Pick one from the awesome-uniswap-hooks list (Clanker, EulerSwap, or the Full Range hook from Uniswap themselves). Read the source, understand what lifecycle points it hooks into and why.

Deep dive: Hook development guide, Hook security best practices

🎯 Goal: Complete the full hook development cycle β€” from extending BaseHook to deploying with the right address bits. After this exercise, you can build and test any V4 hook.

Run:

forge test --match-path "test/part2/module2/exercise3-dynamic-fee/*"

πŸ“‹ Key Takeaways: V4 Hooks

After this section, you should be able to:

  • List V4’s 10 hook lifecycle functions and explain how the hook’s address encodes which callbacks are active (specific address bits = enabled hooks)
  • Design a V4 hook for a given use case (dynamic fees, TWAMM, limit orders) by choosing the right lifecycle points and implementing the callback logic
  • Identify the critical security requirements for hooks: msg.sender == poolManager access control, gas limits to prevent griefing, reentrancy protection, and why hook immutability matters (pool can’t change its hook)
  • Analyze the Cork Protocol exploit ($400k) and explain what access control check was missing
Check your understanding
  • 10 hook lifecycle functions: before/afterInitialize, before/afterSwap, before/afterAddLiquidity, before/afterRemoveLiquidity, before/afterDonate. The hook’s address encodes active callbacks via specific bits β€” the PoolManager checks these bits to decide which hooks to call, saving gas versus querying the hook contract.
  • Hook design process: Choose lifecycle points that match your use case (e.g., beforeSwap for dynamic fees, afterSwap for TWAMM execution, beforeAddLiquidity for access control). Implement only the needed callbacks and set the corresponding address bits. The hook address must be mined to have the correct bit pattern.
  • Hook security requirements: Hooks must verify msg.sender == address(poolManager) to prevent direct calls. Gas limits prevent hooks from griefing swappers. Reentrancy guards protect state. Hook immutability (pool cannot change its hook after initialization) ensures users know what code governs their pool.
  • Cork Protocol exploit: The hook’s callback function was missing the msg.sender == poolManager check, allowing anyone to call the hook directly and manipulate its state outside of the normal pool operation flow, resulting in $400k loss.

πŸ’‘ Beyond Uniswap and Advanced AMM Topics

AMMs vs Order Books (CLOBs)

Why this matters: Before exploring alternative AMM designs, it’s worth asking the fundamental question: why use an AMM at all? Traditional finance uses order books (Central Limit Order Books β€” CLOBs), where makers post limit orders and takers fill them. Understanding the tradeoffs is essential for protocol design decisions and a common interview question.

DimensionAMMOrder Book (CLOB)
Liquidity provisionPassive (deposit and earn)Active (post/cancel orders)
InfrastructureFully on-chain, permissionlessNeeds off-chain matching engine
Price discoveryDerived from reserve ratiosExplicit from order flow
LP riskImpermanent loss / LVRNo IL (makers choose their prices)
Gas efficiencyOne swap() callMultiple order operations
Long-tail assetsAnyone can create a poolLow liquidity = wide spreads
MEV exposureSandwich attacks, JITFront-running, quote stuffing
Capital efficiencyV2: poor, V3/V4: goodHigh (makers deploy exactly where they want)

When AMMs win:

  • Long-tail / new tokens β€” permissionless pool creation bootstraps liquidity from zero
  • Composability β€” other contracts can swap atomically (liquidations, flash loans, yield harvesting)
  • Simplicity β€” no off-chain infrastructure needed
  • Passive investors β€” people who want yield without active market making

When CLOBs win:

  • High-volume majors (ETH/USDC) β€” professional market makers provide tighter spreads
  • Derivatives markets β€” options/perps need order book precision
  • Low-latency environments β€” L2s and app-chains with fast sequencers

The convergence: The line is blurring. V4 hooks enable limit-order-like behavior in AMMs. UniswapX and CoW Protocol use solver-based architectures that combine AMM liquidity with off-chain quotes. dYdX moved to a CLOB on its own app-chain. The future likely involves hybrid systems where intent-based architectures route between AMMs and CLOBs for optimal execution.

Deep dive: Paradigm β€” β€œOrder Book vs AMM” (2021), Hasu β€” β€œWhy AMMs will keep winning”, dYdX CLOB design


Beyond Uniswap: Other AMM Designs (Awareness)

This module focuses on Uniswap because it’s the Rosetta Stone of AMMs β€” V2’s constant product, V3’s concentrated liquidity, and V4’s hooks represent the core design space. But other AMM architectures are important to know about. The overviews below give you enough context to recognize them in the wild, evaluate protocol design decisions, and know when to reach for a specific AMM type. Some of these topics reappear in later modules: Curve StableSwap in Module 6 (Stablecoins), MEV in Part 3 Module 5, and LP management patterns in Module 7 (Vaults & Yield).

Curve StableSwap

Why this matters: Curve is the dominant AMM for assets that should trade near 1:1 (stablecoins, wrapped/staked ETH variants). Its invariant is a hybrid between constant-product (x Β· y = k) and constant-sum (x + y = k):

  • Constant-sum gives zero slippage but can be fully drained of one token
  • Constant-product can’t be drained but gives increasing slippage
  • Curve blends them via an β€œamplification parameter” A that controls how close to constant-sum the curve behaves near the equilibrium point

When prices are near 1:1, Curve pools offer far lower slippage than Uniswap. When prices deviate significantly, the curve reverts to constant-product behavior for safety.

Real impact: Curve’s 3pool (USDC/USDT/DAI) holds $1B+ TVL (as of Q4 2024), enables stablecoin swaps with <0.01% slippage for trades up to $10M.

Why this matters for DeFi builders: If your protocol involves stablecoin swaps (liquidations paying in USDC to receive DAI, for example), Curve pools will likely offer better execution than Uniswap V2/V3 for those pairs. Understanding the StableSwap invariant also helps you reason about stablecoin depegging mechanics (Module 6).

Deep dive: StableSwap whitepaper, Curve v2 (Tricrypto) whitepaper β€” extends StableSwap to volatile assets with dynamic A parameter.


Balancer Weighted Pools

Why this matters: Balancer generalizes the constant product formula to N tokens with arbitrary weights. The invariant:

∏(Bi^Wi) = k     (product of each balance raised to its weight)

A pool with 80% ETH / 20% USDC behaves like a self-rebalancing portfolio β€” the pool naturally maintains the target ratio as prices change. This enables:

  • Index-fund-like pools (e.g., 33% ETH, 33% BTC, 33% stables)
  • Liquidity bootstrapping pools (LBPs) where weights shift over time for token launches

Real impact: Balancer V2 Vault pioneered the singleton architecture that Uniswap V4 adopted. Its consolidated liquidity also provides zero-fee flash loans β€” which you’ll use in Module 5.

Deep dive: Balancer V2 Whitepaper, Balancer V3 announcement (builds on V2 Vault with hooks similar to Uniswap V4).

πŸ’» Quick Try: Spot the Difference

Compare how different invariants handle a stablecoin swap. In a quick Foundry test or on paper:

Pool: 1,000,000 USDC + 1,000,000 DAI (both $1)
Swap: 10,000 USDC β†’ DAI

Constant Product (Uniswap):
  dy = 1,000,000 Β· 10,000 / (1,000,000 + 10,000) = 9,900.99 DAI
  Slippage: ~1% ($99 lost)

Constant Sum (x + y = k, theoretical):
  dy = 10,000 DAI exactly
  Slippage: 0% (but pool can be fully drained!)

StableSwap (Curve, A=100):
  dy β‰ˆ 9,999.4 DAI
  Slippage: ~0.006% ($0.60 lost)  ← 165x better than constant product

This is why Curve dominates stablecoin trading. The amplification parameter A controls how close to constant-sum the curve behaves near equilibrium.


Trader Joe Liquidity Book (Bins vs Ticks)

Why this matters: Trader Joe V2 (dominant on Avalanche, growing on Arbitrum) takes a different approach to concentrated liquidity: instead of V3’s continuous ticks, it uses discrete bins. Each bin has a fixed price and holds only one token type.

V3 (ticks): Continuous curve, position spans a range, math uses √P
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ”‚  ← liquidity is continuous
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               $1,800                        $2,200

LB (bins):   Discrete buckets, each at a single price
                β”Œβ”€β”€β”β”Œβ”€β”€β”β”Œβ”€β”€β”β”Œβ”€β”€β”β”Œβ”€β”€β”β”Œβ”€β”€β”β”Œβ”€β”€β”
                β”‚  β”‚β”‚  β”‚β”‚β–ˆβ–ˆβ”‚β”‚β–ˆβ–ˆβ”‚β”‚β–ˆβ–ˆβ”‚β”‚  β”‚β”‚  β”‚  ← liquidity in discrete bins
                β””β”€β”€β”˜β””β”€β”€β”˜β””β”€β”€β”˜β””β”€β”€β”˜β””β”€β”€β”˜β””β”€β”€β”˜β””β”€β”€β”˜
              $1,800  $1,900  $2,000  $2,100  $2,200

Key differences:

  • Simpler math β€” no square root operations, each bin is a constant-sum pool
  • Fungible LP tokens per bin β€” unlike V3’s unique NFT positions
  • Zero slippage within a bin β€” trades within a single bin execute at the bin’s exact price
  • Variable bin width β€” bin step parameter controls price granularity (similar to tick spacing)

Deep dive: Trader Joe V2 Whitepaper, Liquidity Book contracts


ve(3,3) DEXes (Velodrome / Aerodrome)

Why this matters: Velodrome (Optimism) and Aerodrome (Base) are the highest-TVL DEXes on their respective L2s, using a model called ve(3,3) β€” vote-escrowed tokenomics combined with game theory (the β€œ3,3” from OlympusDAO). This model fundamentally changes how DEX liquidity is bootstrapped and incentivized.

How it works:

  1. veToken locking β€” Users lock the DEX token (VELO/AERO) for up to 4 years, receiving veNFTs with voting power
  2. Gauge voting β€” veToken holders vote on which liquidity pools receive token emissions (incentives)
  3. Bribes β€” Protocols bribe veToken holders to vote for their pool’s emissions, creating a marketplace for liquidity
  4. Fee sharing β€” veToken voters earn 100% of the trading fees from pools they voted for

Why this matters for protocol builders:

If you’re launching a token and need DEX liquidity, ve(3,3) DEXes are a primary venue. Instead of paying for liquidity mining directly, you bribe veToken holders β€” often cheaper and more sustainable. Understanding this model is essential for token launch strategy and liquidity management.

Real impact: Aerodrome on Base holds $1.5B+ TVL (as of Q4 2024), making it one of the largest DEXes on any L2. The ve(3,3) model creates a flywheel: more TVL β†’ more fees β†’ more bribes β†’ more emissions β†’ more TVL.

Deep dive: Andre Cronje’s original ve(3,3) design, Velodrome documentation, Aerodrome documentation


Advanced AMM Topics

These topics sit at the intersection of AMM mechanics, market microstructure, and protocol design. Understanding them is essential for building protocols that interact with AMMs β€” and for interview success.


⚠️ MEV & Sandwich Attacks

Why this matters: Every AMM swap is a public transaction that sits in the mempool before execution. MEV (Maximal Extractable Value) searchers monitor the mempool and exploit the ordering of transactions for profit. If you’re building any protocol that swaps through an AMM, MEV is your adversary.

Real impact: Flashbots data shows MEV extraction on Ethereum exceeds $600M+ cumulative since 2020. On average, ~$1-3M is extracted daily through sandwich attacks alone.

Types of MEV in AMMs:

1. Frontrunning

A searcher sees your pending swap (e.g., buy ETH for 10,000 USDC), submits the same trade with higher gas to execute before you. They profit from the price movement your trade causes.

Mempool:  [Your tx: buy ETH with 10,000 USDC, slippage 1%]

Searcher sequence:
1. Frontrun:  Buy ETH with 50,000 USDC   β†’ price moves up
2. Your tx:   Executes at worse price     β†’ you pay more
3. Backrun:   Searcher sells ETH          β†’ pockets the difference

2. Sandwich Attacks

The most common AMM MEV. The searcher wraps your trade with a frontrun and a backrun in the same block:

Block ordering (manipulated by searcher):

β”Œβ”€ Tx 1: Searcher buys ETH    (moves price UP)
β”‚  Pool: 1000 ETH / 2,000,000 USDC β†’ 950 ETH / 2,105,263 USDC
β”‚
β”œβ”€ Tx 2: YOUR swap buys ETH   (at WORSE price, moves price UP more)
β”‚  Pool: 950 β†’ 940 ETH        (you get fewer ETH than expected)
β”‚
└─ Tx 3: Searcher sells ETH   (at the inflated price)
   Searcher profit: the difference minus gas costs

How much do sandwiches cost users?

Your trade size  β”‚ Typical sandwich loss β”‚ As % of trade
─────────────────┼───────────────────────┼──────────────
$1,000           β”‚ $1-5                  β”‚ 0.1-0.5%
$10,000          β”‚ $20-100               β”‚ 0.2-1.0%
$100,000         β”‚ $500-5,000            β”‚ 0.5-5.0%
$1,000,000+      β”‚ $5,000-50,000+        β”‚ 0.5-5.0%+

Losses scale super-linearly because larger trades have more price impact to exploit.

3. Arbitrage (Non-harmful MEV)

When prices differ between AMMs (e.g., ETH is $2000 on Uniswap, $2010 on Sushi), arbitrageurs buy on the cheap venue and sell on the expensive one. This is beneficial β€” it keeps prices aligned across markets. But it comes at the cost of LP impermanent loss.

CEX-DEX arbitrage β€” the #1 source of LP losses:

The most important form of arbitrage to understand is CEX-DEX arb: when ETH moves from $2,000 to $2,010 on Binance, arbitrageurs immediately buy ETH from the on-chain AMM at the stale $2,000 price and sell on Binance at $2,010. This happens within seconds of every price movement.

Binance: ETH price moves $2,000 β†’ $2,010

β”Œβ”€ Arber buys ETH on Uniswap at ~$2,000  (stale AMM price)
β”‚  β†’ Pool moves to ~$2,010
└─ Arber sells ETH on Binance at $2,010
   β†’ Profit: ~$10 per ETH minus gas

Who pays? The LPs. They sold ETH at $2,000 when it was worth $2,010.
This is "toxic flow" β€” trades from informed participants who know
the AMM price is stale. It happens on EVERY price movement.

This is the mechanism behind impermanent loss and the real-time cost that LVR measures. CEX-DEX arb accounts for ~60-80% of Uniswap V3 volume on major pairs β€” the majority of trades LPs serve are from arbitrageurs, not retail users. This is why passive LPing at tight ranges is often unprofitable despite high fee APRs: most of the volume generating those fees is toxic flow that extracts more value than the fees pay.

Deep dive: Milionis et al. β€œAutomated Market Making and Arbitrage Profits” (2023), Thiccythot’s toxic flow analysis

4. Just-In-Time (JIT) Liquidity

Covered in detail below. A specialized form of MEV where searchers add and remove concentrated liquidity around large trades.

Protection Mechanisms:

MechanismHow it worksTrade-off
amountOutMin (slippage protection)Revert if output is below thresholdTight = safe but may fail; loose = executes but loses value
Flashbots ProtectSubmit tx privately to block builders, skip public mempoolDepends on builder honesty; slightly slower inclusion
MEV BlockerOFA (Order Flow Auction) β€” searchers bid for your order flow, you get a rebateNew, less battle-tested
Private mempools / OFAsRoute through private channels (CoW Protocol, 1inch Fusion)Requires trust in the operator; may have slower execution
Batch auctionsCoW Protocol batches trades and solves off-chain for uniform clearing priceNo frontrunning possible, but introduces latency
V4 hooksCustom hooks can implement MEV protection (e.g., Sorella’s Angstrom)Application-level; requires hook trust

Common pitfall: Relying solely on amountOutMin for MEV protection. A tight amountOutMin prevents sandwiches but can cause reverts during volatile periods. Best practice: use private submission channels (Flashbots Protect) AND reasonable slippage settings.

For protocol builders:

If your protocol executes AMM swaps (liquidations, rebalancing, yield harvesting), you MUST consider MEV:

  • Liquidation bots will be sandwiched if they swap through public AMMs naively
  • Yield strategies that harvest and swap reward tokens are prime sandwich targets
  • Rebalancing operations on predictable schedules can be frontrun

Solutions: use private mempools, implement internal buffers, randomize execution timing, or use auction-based swap mechanisms.

Deep dive: Flashbots documentation, MEV-Boost architecture, Paradigm MEV research, CoW Protocol documentation

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow would you protect a protocol’s liquidation swaps from sandwich attacks?”

    Answer
    • Good answer: β€œUse slippage protection with amountOutMin and submit through Flashbots Protect.”
    • Great answer: β€œLayer multiple defenses: (1) Flashbots Protect or MEV Blocker for private submission, (2) Set amountOutMin based on a reliable oracle price (Chainlink, not the AMM’s spot price β€” that’s circular), (3) Route through an aggregator like 1inch Fusion or CoW Protocol for large liquidations, (4) If the protocol has predictable rebalancing schedules, randomize timing. For maximum protection, use intent-based systems where solvers compete to fill the swap.”

Interview Red Flags:

  • 🚩 Not mentioning MEV/sandwich attacks when discussing AMM integrations
  • 🚩 Hardcoding a single DEX for protocol swaps instead of using aggregators
  • 🚩 Setting amountOutMin = 0 (β€œaccepting any price”) β€” invitation for sandwich attacks

Pro tip: In architecture discussions, proactively bring up MEV protection before being asked β€” it signals you think about adversarial conditions, not just happy paths.


JIT (Just-In-Time) Liquidity

Why this matters: JIT liquidity is a V3-specific MEV strategy that fundamentally changes the economics of concentrated liquidity provision. Understanding it is critical for anyone building on top of V3/V4 pools.

How it works:

A JIT liquidity provider monitors the mempool for large pending swaps. When they spot one, they:

Block ordering:

β”Œβ”€ Tx 1: JIT provider ADDS concentrated liquidity
β”‚         in an extremely tight range around the current price
β”‚         (e.g., just 1 tick wide)
β”‚
β”œβ”€ Tx 2: LARGE SWAP executes
β”‚         The JIT liquidity captures most of the fees
β”‚         because it dominates the liquidity at the active price
β”‚
└─ Tx 3: JIT provider REMOVES liquidity + collects fees
         All in the same block β€” near-zero impermanent loss risk

Why it works economically:

Normal LP (wide range, holds for weeks):
  - Capital: $100,000 across ticks -1000 to +1000
  - Active capital at current tick: ~$500 (0.5%)
  - Earns fees proportional to $500
  - Exposed to IL over weeks

JIT LP (1-tick range, holds for 1 block):
  - Capital: $100,000 concentrated in 1 tick
  - Active capital at current tick: ~$100,000 (100%)
  - Earns fees proportional to $100,000
  - IL risk β‰ˆ 0 (removed same block)

The JIT provider earns ~200x more fees per dollar of capital, but only for a single block. They extract most of the fee revenue from a large trade, leaving passive LPs with a smaller share.

Impact on passive LPs:

JIT liquidity dilutes passive LPs’ fee income. When a large trade comes in, the JIT provider’s concentrated liquidity captures 80-95% of the fees, even though they had zero capital in the pool moments before.

Real impact: Research by 0x Labs found JIT liquidity providers captured up to 80% of fees on some large V3 trades. Sorella’s analysis showed JIT accounts for ~5-10% of total V3 fee revenue.

V4’s response to JIT:

V4 hooks enable countermeasures:

  • beforeAddLiquidity hook: Reject liquidity additions that look like JIT (e.g., same-block add+remove patterns)
  • Time-weighted fee sharing: Hook distributes fees proportional to time liquidity was active, not just amount
  • Minimum liquidity duration: Hook enforces that liquidity must stay active for N blocks before collecting fees

Common pitfall: Assuming JIT liquidity is always harmful. It actually provides better execution for large traders (more liquidity at the active price). The debate is about fair fee distribution between active and passive LPs.

For protocol builders: If your protocol manages V3 LP positions (vault strategies, LP managers), understand that your passive positions compete with JIT providers. This affects yield projections and should inform whether you target high-volume pools (where JIT is most active) or long-tail pools (where JIT is less common).

Deep dive: Uniswap JIT analysis, JIT liquidity dataset on Dune


AMM Aggregators & Routing

Why this matters: No single AMM pool has the best price for every trade. A $100K ETH→USDC swap might get better execution by splitting: 60% through Uniswap V3 (0.05% pool), 30% through Curve, 10% through Balancer. Aggregators solve this routing problem.

How aggregators work:

User wants: Swap 100 ETH β†’ USDC

Aggregator scans:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Uniswap V3 (0.05%): 100 ETH β†’ 199,800 USDC               β”‚
β”‚ Uniswap V3 (0.30%): 100 ETH β†’ 199,200 USDC               β”‚
β”‚ Uniswap V2:         100 ETH β†’ 198,500 USDC               β”‚
β”‚ Curve:               100 ETH β†’ 199,600 USDC               β”‚
β”‚ Sushi:               100 ETH β†’ 198,800 USDC               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Optimal route (found by solver):
  60 ETH β†’ Uni V3 0.05%  = 119,920 USDC
  30 ETH β†’ Curve          =  59,910 USDC
  10 ETH β†’ Uni V3 0.30%  =  19,960 USDC
  Total:                   199,790 USDC  ← BETTER than any single pool

Major aggregators:

AggregatorApproachKey Innovation
1inchPathfinder algorithm, limit orders, Fusion mode (MEV-protected)Largest market share; Fusion uses Dutch auctions for MEV protection
CoW ProtocolBatch auctions with coincidence of wants (CoWs)Peer-to-peer matching eliminates AMM fees when possible; MEV-proof by design
ParaswapMulti-path routing with gas optimizationAugustus Router V6 supports complex multi-hop, multi-DEX routes
0x / MatchaProfessional market maker integrationCombines AMM liquidity with off-chain RFQ quotes from market makers

Coincidence of Wants (CoWs):

CoW Protocol’s key insight: if Alice wants to sell ETH for USDC, and Bob wants to sell USDC for ETH, they can trade directly β€” no AMM needed. No fees, no price impact, no MEV.

Without CoW:
  Alice β†’ AMM (0.3% fee + price impact) β†’ Bob's trade also hits AMM

With CoW:
  Alice ←→ Bob   (direct swap at market price, 0 fee, 0 slippage)
  Remainder β†’ AMM (only the unmatched portion touches the AMM)

Intent-based architectures:

The latest evolution: users express what they want (swap X for Y), not how (which DEX, which route). Solvers compete to fill the intent with the best execution.

  • UniswapX: Dutch auction for swap intents; fillers compete to provide best price
  • CoW Protocol: Batch-level solving with CoW matching
  • Across+: Cross-chain intent settlement

Common pitfall: Building a protocol that hardcodes a single AMM for swaps. Always integrate through an aggregator or allow configurable swap routes. Liquidity shifts between AMMs constantly.

For protocol builders:

If your protocol needs to execute swaps (liquidations, rebalancing, treasury management):

  1. Never hardcode a single DEX β€” use aggregator APIs or on-chain aggregator contracts
  2. Consider intent-based systems for large or predictable swaps (less MEV, better execution)
  3. Test with realistic routing β€” fork mainnet and compare single-pool vs aggregated execution

Deep dive: 1inch API docs, CoW Protocol docs, UniswapX whitepaper, Intent-based architectures overview


LP Management Strategies

Why this matters: In V3/V4, passive LP-ing (deposit and forget) is often unprofitable due to impermanent loss and JIT liquidity diluting fees. Active management has become essential β€” and it’s created an entire sub-industry of LP management protocols.

The problem: passive V3 LP-ing is hard

V2 LP lifecycle:        V3 LP lifecycle:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Deposit      β”‚        β”‚ Choose range                      β”‚
β”‚ Hold forever β”‚        β”‚ Monitor price vs range             β”‚
β”‚ Collect fees β”‚        β”‚ Price drifts out of range?         β”‚
β”‚ Withdraw     β”‚        β”‚  β†’ Stop earning fees               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚  β†’ Decide: wait or rebalance?      β”‚
                        β”‚ Rebalance = close + reopen position β”‚
                        β”‚  β†’ Pay gas + swap fees              β”‚
                        β”‚  β†’ Realize IL                       β”‚
                        β”‚  β†’ Compete with JIT liquidity       β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Strategy spectrum:

StrategyRange WidthRebalance FrequencyBest For
Wide range (Β±50%)Passive, rarely out of rangeNever/rarelyLow-maintenance, lower yield
Medium range (Β±10%)Monthly rebalanceMonthlyBalance of yield and effort
Tight range (Β±2%)Daily rebalanceDailyMax yield, high gas costs
Single-sided (above/below price)Limit-order-like behaviorOn triggerTargeted entry/exit points
Full range (V2-equivalent)Never out of rangeNeverSimplicity, composability

LP management protocols:

These protocols manage V3/V4 positions for you, abstracting away range selection and rebalancing:

ProtocolApproachKey Feature
Arrakis (PALM)Algorithmic rebalancing vaultsMarket-making strategies; used by protocols for their own token liquidity
GammaActive management vaultsMultiple strategies per pool; wide protocol integrations
BunniV4 hooks-based LP managementNative V4 integration; β€œLiquidity-as-a-Service”
MaverickAMM with built-in LP modesDirectional LPing (bet on price direction while earning fees)

Evaluating pool profitability β€” how to decide whether to LP:

Before deploying capital as an LP, you need to estimate whether fees will outpace losses. Here are the key metrics:

1. Fee APR = (24h Volume Γ— Fee Tier Γ— 365) / TVL

   Example: ETH/USDC 0.05% pool
   Volume: $200M/day, TVL: $300M
   Fee APR = ($200M Γ— 0.0005 Γ— 365) / $300M = 12.2%

2. Estimated LVR cost β‰ˆ σ² / 8
   (annualized, as % of position value, for full-range V2-style CPMM)

   ETH annualized volatility: ~80%
   LVR β‰ˆ 0.80Β² / 8 = 8%

3. Net LP return β‰ˆ Fee APR - LVR cost - Gas costs
   β‰ˆ 12.2% - 8% - gas β†’ marginally positive before gas, but tight.
   Concentrated ranges boost fee capture but also amplify LVR exposure.

4. Volume/TVL ratio β€” the single most useful metric
   > 0.5: High fee generation, likely profitable
   0.1-0.5: Moderate, depends on volatility
   < 0.1: Low fees relative to capital, likely unprofitable

Toxic flow share β€” the percentage of volume coming from informed traders (arbitrageurs) vs retail:

  • High toxic flow (>60%): LPs are mostly serving arbers at stale prices β†’ likely unprofitable
  • Low toxic flow (<40%): Pool serves mostly retail β†’ fees more likely to exceed LVR
  • Stablecoin pairs: Very low toxic flow β†’ almost always profitable for LPs

Deep dive: CrocSwap LP profitability framework, Revert Finance analytics β€” real-time LP position profitability tracker

The compounding problem:

V3 fees don’t auto-compound (they accumulate as uncollected tokens, not as additional liquidity). Manual compounding requires:

  1. Collect fees
  2. Swap to correct ratio
  3. Add liquidity at current range
  4. Pay gas for all three transactions

LP management protocols automate this, but take a performance fee (typically 10-20% of earned fees).

Common pitfall: Ignoring gas costs when evaluating LP strategies. A tight-range strategy earning 50% APR but requiring daily $20 rebalances on mainnet needs $7,300/year in gas alone. On an $10,000 position, that’s 73% of the gross yield eaten by gas. L2 deployment changes this calculus entirely.

For protocol builders:

If your protocol uses LP tokens as collateral or manages liquidity:

  • Vault tokens from Arrakis/Gamma are ERC-20s that represent managed V3 positions β€” much more composable than raw V3 NFTs
  • Consider Maverick for protocols needing directional liquidity (e.g., token launches, price pegs)
  • V4 hooks enable native LP management without external protocols β€” Bunni’s approach is worth studying

Deep dive: Arrakis documentation, Gamma strategies overview, Maverick AMM docs, Bunni V2 design

πŸ“‹ Key Takeaways: Beyond Uniswap & Advanced AMM Topics

After this section, you should be able to:

  • Compare AMMs vs CLOBs across 5 dimensions (liquidity provision, infrastructure, price discovery, LP risk, MEV exposure) and explain why DeFi is converging toward intent-based hybrid systems (UniswapX, CoW Protocol)
  • Explain Curve’s StableSwap invariant at a high level: how the amplification parameter A blends between constant-product and constant-sum behavior, and why this gives near-zero slippage for stablecoin swaps
  • Define LVR (Loss-Versus-Rebalancing) and explain why it’s a more accurate measure of LP cost than impermanent loss: LVR scales with volatility squared, never reverses, and represents the profit that arbitrageurs extract from stale pool prices
  • Describe a sandwich attack end-to-end (frontrun β†’ victim swap β†’ backrun) and name 3 protection mechanisms (private mempools, MEV-aware slippage, batch auctions)
  • Evaluate an LP position’s profitability using key metrics: volume/TVL ratio, fee APR vs LVR, toxic flow share, and explain why active LP management (Arrakis, Gamma, Bunni) outperforms passive positions in V3
Check your understanding
  • AMMs vs CLOBs: AMMs win for long-tail assets (permissionless pools), composability (atomic on-chain swaps), and passive LPs. CLOBs win for high-volume majors (tighter spreads from professional MMs), derivatives (order precision), and low-latency L2s. The industry is converging toward intent-based hybrids like UniswapX where users sign intent off-chain and solvers compete to fill optimally.
  • Curve StableSwap: The amplification parameter A blends between constant-product (A=0, infinite slippage tolerance) and constant-sum (A=infinity, zero slippage but can deplete). High A values create a flat zone near the peg where swaps have near-zero slippage, ideal for correlated assets like stablecoin pairs.
  • LVR (Loss-Versus-Rebalancing): LVR measures the cost LPs pay to arbitrageurs who correct stale pool prices after market moves. Unlike IL, LVR scales with volatility squared, never reverses (even if price returns), and represents real value extraction. LVR is the economically correct metric for LP cost analysis.
  • LP profitability evaluation: Volume/TVL ratio determines fee revenue. Fee APR must exceed LVR for profitability. Toxic flow share measures what percentage of volume is from arbitrageurs (extractive) vs organic traders (revenue-generating). Active managers (Arrakis, Gamma, Bunni) outperform passive V3 positions by dynamically rebalancing ranges to maximize fee capture while minimizing LVR exposure.


πŸ“š Resources

Essential reading:

Source code:

Deep dives:

LVR & LP economics:

AMM design & market structure:

ve(3,3) & alternative DEX models:

MEV & market microstructure:

LP management & JIT liquidity:

Aggregators:

Interactive learning:

Security and exploits:

Analytics:


Navigation: ← Module 1: Token Mechanics | Module 3: Oracles β†’