Part 2 β Module 2: AMMs from First Principles
Difficulty: Advanced
Estimated reading time: ~65 minutes | Exercises: ~4-5 hours
π Table of Contents
The Constant Product Formula
Reading Uniswap V2
Concentrated Liquidity (V3)
Build Exercise: Simplified Concentrated Liquidity Pool
V4 β Singleton Architecture and Flash Accounting
V4 Hooks
- The 10 Hook Functions
- Hook Capabilities
- Read: Hook Examples
- Hook Security Considerations
- Build: A Simple Hook
Beyond Uniswap and Advanced AMM Topics
- AMMs vs Order Books (CLOBs)
- Curve StableSwap
- Balancer Weighted Pools
- Trader Joe Liquidity Book
- ve(3,3) DEXes (Velodrome / Aerodrome)
- MEV & Sandwich Attacks
- JIT (Just-In-Time) Liquidity
- AMM Aggregators & Routing
- LP Management Strategies
π‘ 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 (2024). The entire DeFi ecosystem β $50B+ TVL across lending, derivatives, yield β 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
πΌ Job Market Context
What DeFi teams expect you to know:
- βWhat is impermanent loss and when does it matter?β
- 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) - 1where 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.
π 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:
- 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.
- 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.β
- 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:
- Lending liquidations (Module 4): Liquidation bots swap collateral through AMMs β price impact from the constant product formula determines whether liquidation is profitable
- 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
- Stablecoin pegs (Module 6): Curveβs StableSwap modifies the constant product formula for near-1:1 assets β understanding
xΒ·y=kis prerequisite for understanding Curveβs hybrid invariant
π― 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 reservestotalSupplyof LP tokens (use a simple internal accounting, or inherit ERC-20 (OZ implementation))token0,token1β the two ERC-20 token addressesFEE_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
π Summary: The Constant Product Formula
β Covered:
- Constant product formula (
x Β· y = k) and swap output calculation - Price impact β nonlinear, accelerates with trade size
- Fee mechanics β fees stay in pool, increasing
k - Impermanent loss β formula, step-by-step walkthrough, dollar impact at various price changes
- Built a minimal constant product pool from scratch
Next: Read production V2 code and map it to your implementation.
π‘ 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_LIQUIDITYlock (exactly what you implemented) - How it reads balances directly from
IERC20(token0).balanceOf(address(this))rather than relying onamountparameters β 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:
price0CumulativeLastandprice1CumulativeLast - 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.112fixed-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
βkgrowth between fee checkpoints
π Read: UniswapV2Factory.sol
Source: github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol
Focus on:
createPair()β howCREATE2is used for deterministic addresses- Why deterministic addresses matter: the Router can compute pair addresses without on-chain lookups (saves gas)
- The
feeToaddress 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
SLOADfor 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
amountOutMinparameters - Has deadline parameters to prevent stale transactions from executing
Common pitfall: Not setting
amountOutMinproperly. 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.
Exercises
Workspace: workspace/test/part2/module2/exercise1b-v2-extensions/ β test-only exercise: V2Extensions.t.sol (implements FlashSwapConsumer and SimpleRouter inline, then runs tests for flash swaps, multi-hop routing, and TWAP)
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
uint amountToRepay = amount0 > 0 ? amount0 * 1003 / 1000 : amount1 * 1003 / 1000;
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.
π How to Study Uniswap V2:
- Read tests first β See how
mint(),burn(),swap()are called in practice - Read
getAmountOut()in UniswapV2Library.sol β This is justdy = yΒ·dx/(x+dx)with fees. Match it to the formula you implemented - Read
swap()β Understand optimistic transfer + k-check pattern. Trace the flash swap callback - Read
mint()andburn()β Match to your own addLiquidity/removeLiquidity - 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.
π Summary: Reading Uniswap V2
β Covered:
- Read V2 Pair, Factory, and Router contracts
- Understood
mint()/burn()/swap()β balance-reading pattern, optimistic transfers, k-invariant check - Flash swap mechanism via
IUniswapV2Calleecallback - TWAP oracle accumulators in
_update() - Protocol fee logic in
_mintFee() - CREATE2 deterministic addresses in Factory
- Exercises: flash swap consumer, multi-hop routing, TWAP oracle consumer
Next: Concentrated liquidity β how V3 achieves 2000x capital efficiency.
π‘ 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, currently holds $4B+ TVL 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:
- The key AMM math formulas involve
βPdirectly, so storing it avoids repeated square root operations - 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:
- Compute how much of the swap can be filled within the current tick range
- If the swap isnβt fully filled, cross the tick boundary β activate/deactivate liquidity from positions at that tick
- 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,
Lis auint128representing β(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):
UniswapV3Pool.solβ the pool itself (swap, mint, burn, collect)UniswapV3Factory.solβ pool deployment with fee tiers
Focus areas in UniswapV3Pool:
swap()β the main swap loop. Trace thewhileloop step by step. UnderstandcomputeSwapStep(), tick crossing, and howstate.liquiditychanges at tick boundaries.mint()β how positions are created, how tick bitmaps track initialized ticks_updatePosition()β fee growth accounting per positionslot0β the packed storage slot holdingsqrtPriceX96,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 sqrtPriceX96SqrtPriceMath.solβ token amount calculations given liquidity and price rangesSwapMath.solβ compute swap steps within a single tick rangeTickBitmap.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.
Exercises
Workspace: workspace/src/part2/module2/exercise2-v3-position/ β starter file: V3PositionCalculator.sol, tests: V3PositionCalculator.t.sol
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.
πΌ Job Market Context
What DeFi teams expect you to know:
- βExplain how Uniswap V3βs concentrated liquidity works and why it matters.β
- 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:
- Start with the V3 Development Book β Build a simplified V3 alongside reading production code
- Read
SqrtPriceMath.solFIRST β Pure math functions. Focus on inputs/outputs, not the bit manipulation - Read
SwapMath.computeSwapStep()β One step of the swap loop, the core unit of work - Read the
swap()while loop in UniswapV3Pool.sol β Now you see how steps compose into a full swap - Read
Tick.solandTickBitmap.solLAST β 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).
π Summary: Concentrated Liquidity (V3)
β Covered:
- Ticks (
price = 1.0001^i), tick spacing, and fee tiers - Positions as
(tickLower, tickUpper, liquidity)β active only when price is in range sqrtPriceX96β why storeβP Γ 2^96, how to convert to human-readable price- V3 liquidity math (
Ξtoken0,Ξtoken1) with worked numerical example - The swap loop β crossing tick boundaries, active liquidity changes
- LP tokens β NFTs (each position is unique)
- Fee accounting with
feeGrowthGlobaland per-tick tracking - Read V3 Pool, Factory, and key libraries (TickMath, SqrtPriceMath, SwapMath)
- Exercises: tick math, position value calculator, swap simulation
Next: Build your own simplified CLAMM to internalize the swap loop.
π― 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
SqrtPriceMathformulas) - 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.
π Summary: Simplified CLAMM Challenge
π― Learning goals:
- Build a simplified CLAMM with
addLiquidity,swap(with tick-crossing loop),removeLiquidity - Internalize V3βs core insight: between any two initialized ticks, the pool behaves like V2 with liquidity
L - Implement fee accrual per position (only while in range)
- Test tick crossings, overlapping positions, out-of-range behavior, IL comparison
Next: V4βs singleton architecture β one contract to rule all pools.
π 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.
π‘ Uniswap V4 β Singleton Architecture and Flash Accounting
π‘ 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 November 2024, 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:
- The caller βunlocksβ the PoolManager
- The caller can perform multiple operations (swaps, liquidity changes) across any pools
- The PoolManager tracks net balance changes (βdeltasβ) in transient storage
- At the end, the caller must settle all deltas to zero β either by transferring tokens or using ERC-6909 claim tokens
- 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()ortransferFrom()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. Studyunlock(),swap(),modifyLiquidity(), and the delta accounting systemPool.sol(library) β the actual pool math, used by PoolManager. Note how itβs a library, not a contract β keeping the PoolManager modularPoolKeyβ 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 NFTsV4Router.sol/ Universal Router β the entry point for swaps
Common pitfall: Trying to call
swap()directly on PoolManager. You must go through theunlock()pattern β your contract implementsunlockCallback()which then callsswap(). Example router implementation.
Exercises
Workspace: workspace/src/part2/module2/exercise3-dynamic-fee/ β starter file: DynamicFeeHook.sol, tests: DynamicFeeHook.t.sol
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:
- βWalk me through Uniswap V4βs flash accounting. How does it save gas?β
- 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:
- Read
PoolManager.unlock()andIUnlockCallbackβ Understand the interaction pattern before anything else - Read the delta accounting β How deltas are tracked, settled, and validated
- Read a simple hook (FullRange or SwapCounter) β See the full hook lifecycle before complex hooks
- Read
Pool.sol(library) β V3βs math adapted for V4βs singleton, familiar territory - Read
PositionManager.solin v4-periphery β How the user-facing contract interacts with PoolManager
π Summary: V4 Singleton & Flash Accounting
β Covered:
- Singleton pattern β all pools in one PoolManager contract
- Flash accounting β delta tracking with transient storage, settle-at-end pattern
unlock()β callback β operations βsettle()/take()flow- Native ETH support and ERC-6909 claim tokens
- Read PoolManager, Pool.sol library, PoolKey, BalanceDelta
- Exercises: unlock pattern tracing, multi-hop delta analysis, local V4 deployment
Next: V4 hooks β the extension mechanism that makes AMMs programmable.
π‘ 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/afterAddLiquiditybeforeRemoveLiquidity/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 (July 2024) β hook didnβt verify
msg.senderwas 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.
π― Build Exercise: A Simple Hook
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
BaseHookfrom v4-periphery - Set the correct hook address bits (use the
Hookslibrary to mine an address with the right flags) - Implement
beforeSwapto 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
πΌ Job Market Context
What DeFi teams expect you to know:
- βHow do V4 hooks work and what are the security considerations?β
- 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.
π DeFi Pattern Connection
Where V4 hooks are being used in production:
- MEV protection: Sorellaβs Angstrom uses hooks to batch-settle swaps at uniform clearing prices, eliminating sandwich attacks
- Lending integration: Hooks that auto-deposit idle LP assets into lending protocols between swaps β earning additional yield on liquidity
- Custom oracles: GeomeanOracle hook provides TWAP with better properties than V2/V3βs built-in oracle
- 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.
π Summary: V4 Hooks
β Covered:
- V4 hook system β 10 lifecycle functions, address-encoded permissions
- Hook capabilities: dynamic fees, custom accounting, access control, oracle integration
- Read production hooks: limit order, TWAMM, dynamic fee, full-range
- Hook security: access control (
msg.sender == poolManager), gas griefing, reentrancy, trust model, immutability - Built: dynamic fee hook and swap counter hook
- Real exploits: Cork Protocol ($400k from missing access control)
Next: Alternative AMM designs and advanced ecosystem topics.
π 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.
| Dimension | AMM | Order Book (CLOB) |
|---|---|---|
| Liquidity provision | Passive (deposit and earn) | Active (post/cancel orders) |
| Infrastructure | Fully on-chain, permissionless | Needs off-chain matching engine |
| Price discovery | Derived from reserve ratios | Explicit from order flow |
| LP risk | Impermanent loss / LVR | No IL (makers choose their prices) |
| Gas efficiency | One swap() call | Multiple order operations |
| Long-tail assets | Anyone can create a pool | Low liquidity = wide spreads |
| MEV exposure | Sandwich attacks, JIT | Front-running, quote stuffing |
| Capital efficiency | V2: poor, V3/V4: good | High (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β
Athat 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, 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
Aparameter.
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:
- veToken locking β Users lock the DEX token (VELO/AERO) for up to 4 years, receiving veNFTs with voting power
- Gauge voting β veToken holders vote on which liquidity pools receive token emissions (incentives)
- Bribes β Protocols bribe veToken holders to vote for their poolβs emissions, creating a marketplace for liquidity
- 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 (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:
| Mechanism | How it works | Trade-off |
|---|---|---|
amountOutMin (slippage protection) | Revert if output is below threshold | Tight = safe but may fail; loose = executes but loses value |
| Flashbots Protect | Submit tx privately to block builders, skip public mempool | Depends on builder honesty; slightly slower inclusion |
| MEV Blocker | OFA (Order Flow Auction) β searchers bid for your order flow, you get a rebate | New, less battle-tested |
| Private mempools / OFAs | Route through private channels (CoW Protocol, 1inch Fusion) | Requires trust in the operator; may have slower execution |
| Batch auctions | CoW Protocol batches trades and solves off-chain for uniform clearing price | No frontrunning possible, but introduces latency |
| V4 hooks | Custom hooks can implement MEV protection (e.g., Sorellaβs Angstrom) | Application-level; requires hook trust |
Common pitfall: Relying solely on
amountOutMinfor MEV protection. A tightamountOutMinprevents 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:
- βHow would you protect a protocolβs liquidation swaps from sandwich attacks?β
- Good answer: βUse slippage protection with
amountOutMinand submit through Flashbots Protect.β - Great answer: βLayer multiple defenses: (1) Flashbots Protect or MEV Blocker for private submission, (2) Set
amountOutMinbased 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.β
- Good answer: βUse slippage protection with
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:
beforeAddLiquidityhook: 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:
| Aggregator | Approach | Key Innovation |
|---|---|---|
| 1inch | Pathfinder algorithm, limit orders, Fusion mode (MEV-protected) | Largest market share; Fusion uses Dutch auctions for MEV protection |
| CoW Protocol | Batch auctions with coincidence of wants (CoWs) | Peer-to-peer matching eliminates AMM fees when possible; MEV-proof by design |
| Paraswap | Multi-path routing with gas optimization | Augustus Router V6 supports complex multi-hop, multi-DEX routes |
| 0x / Matcha | Professional market maker integration | Combines 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):
- Never hardcode a single DEX β use aggregator APIs or on-chain aggregator contracts
- Consider intent-based systems for large or predictable swaps (less MEV, better execution)
- 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:
| Strategy | Range Width | Rebalance Frequency | Best For |
|---|---|---|---|
| Wide range (Β±50%) | Passive, rarely out of range | Never/rarely | Low-maintenance, lower yield |
| Medium range (Β±10%) | Monthly rebalance | Monthly | Balance of yield and effort |
| Tight range (Β±2%) | Daily rebalance | Daily | Max yield, high gas costs |
| Single-sided (above/below price) | Limit-order-like behavior | On trigger | Targeted entry/exit points |
| Full range (V2-equivalent) | Never out of range | Never | Simplicity, composability |
LP management protocols:
These protocols manage V3/V4 positions for you, abstracting away range selection and rebalancing:
| Protocol | Approach | Key Feature |
|---|---|---|
| Arrakis (PALM) | Algorithmic rebalancing vaults | Market-making strategies; used by protocols for their own token liquidity |
| Gamma | Active management vaults | Multiple strategies per pool; wide protocol integrations |
| Bunni | V4 hooks-based LP management | Native V4 integration; βLiquidity-as-a-Serviceβ |
| Maverick | AMM with built-in LP modes | Directional 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:
- Collect fees
- Swap to correct ratio
- Add liquidity at current range
- 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
π Summary: Beyond Uniswap & Advanced AMM Topics
β Covered:
- AMMs vs Order Books β tradeoffs, when each wins, the convergence toward hybrid systems
- Curve StableSwap β hybrid invariant, amplification parameter, stablecoin dominance
- Balancer weighted pools β N-token pools, LBPs, Vault architecture (inspiration for V4)
- Trader Joe Liquidity Book β bins vs ticks, a different approach to concentrated liquidity
- ve(3,3) DEXes β Velodrome/Aerodrome, vote-escrowed tokenomics, bribe markets for liquidity
- MEV & sandwich attacks β types, CEX-DEX arbitrage (primary LP cost), cost tables, protection mechanisms
- JIT liquidity β economics, impact on passive LPs, V4 countermeasures
- AMM aggregators β 1inch, CoW Protocol, Paraswap, intent-based architectures (UniswapX)
- LP management β strategy spectrum, pool profitability analysis, Arrakis/Gamma/Bunni/Maverick
Internalized patterns: The constant product formula is everywhere (V3 reduces to it within each tick range). Price impact is nonlinear by design. LVR (not just IL) is the real cost of LPing β it scales with volatility squared and never reverses. CEX-DEX arbitrage is the dominant force in AMM markets (majority of V3 volume is toxic flow). V3 concentrated liquidity trades capital efficiency for complexity. V4 hooks are the future of AMM innovation (extend shared liquidity, donβt fork). Flash accounting + transient storage is a reusable pattern (not just V4). MEV is not optional knowledge (sandwich, frontrunning, JIT). Never hardcode a single liquidity source (use aggregators). AMMs and order books are converging toward intent-based systems. LP management is now a professional activity (Arrakis, Gamma, Bunni, Volume/TVL, LVR, toxic flow share).
π Cross-Module Concept Links
β Backward References (Part 1 + Module 1)
| Source | Concept | How It Connects |
|---|---|---|
| Part 1 Module 1 | ERC-4626 share math / mulDiv | LP token minting uses the same shares-proportional-to-deposit pattern; Math.sqrt in V2 parallels vault share math |
| Part 1 Module 1 | Unchecked arithmetic | V2/V3 use unchecked blocks for gas-optimized tick and fee math where overflow is intentional |
| Part 1 Module 2 | Transient storage | V4 flash accounting uses TSTORE/TLOAD for delta tracking β 20Γ cheaper than SSTORE |
| Part 1 Module 3 | Permit2 | Universal token approvals for V4 PositionManager; aggregator integrations use Permit2 for gasless approvals |
| Part 1 Module 5 | Fork testing | Essential for testing AMM integrations against real mainnet liquidity and verifying swap routing |
| Part 1 Module 5 | Invariant / fuzz testing | Property-based testing for AMM invariants: x * y >= k, tick math boundaries, fee accumulation monotonicity |
| Part 1 Module 6 | Immutable core + periphery | V2/V3/V4 all use immutable core contracts with upgradeable periphery routers β the canonical DeFi proxy pattern |
| Module 1 | SafeERC20 / balance-before-after | V2 implements its own _safeTransfer; mint()/burn() read balances directly β the foundation of composability |
| Module 1 | Fee-on-transfer tokens | V2βs _update() syncs reserves from actual balances; V3/V4 donβt natively support fee-on-transfer |
| Module 1 | WETH wrapping | All AMM routers wrap/unwrap ETH; V4 supports native ETH pairs directly |
| Module 1 | Token decimals handling | Price display and tick math must account for differing decimals between token0/token1 |
β Forward References (Modules 3β9)
| Target | Concept | How AMM Knowledge Applies |
|---|---|---|
| Module 3 (Oracles) | TWAP oracles | Built on AMM price accumulators; oracle manipulation via concentrated liquidity price impact |
| Module 4 (Lending) | Liquidation swaps | Route through AMMs; LP tokens as collateral; CEX-DEX arb informs liquidation MEV |
| Module 5 (Flash Loans) | Flash swaps / flash accounting | V2 flash swaps and V4 flash accounting are specialized flash loan patterns |
| Module 6 (Stablecoins) | Curve StableSwap | AMM design optimized for peg maintenance; AMM-based depegging detection signals |
| Module 7 (Yield) | LP fee income | Yield source from trading fees; auto-compounding vaults; LVR framework for LP strategy evaluation |
| Module 8 (DeFi Security) | Protocol fee switches | V2 feeTo, V3 factory owner, V4 hook governance; ve(3,3) gauge voting and bribe markets |
| Module 9 (Integration) | Full-stack capstone | Combining AMM + lending + oracles + yield in a production-grade protocol |
π Production Study Order
Study these codebases in order β each builds on the previous oneβs patterns:
| # | Repository | Why Study This | Key Files |
|---|---|---|---|
| 1 | Uniswap V2 Pair | The foundational AMM β mint(), burn(), swap() in ~250 lines. Understand constant product, LP share math (Math.sqrt), and the TWAP price accumulator | UniswapV2Pair.sol (mint, burn, swap, _update), UniswapV2Factory.sol |
| 2 | Uniswap V2 Router02 | User-facing routing β multi-hop swaps, slippage protection, deadline enforcement, WETH wrapping. Separation of core (immutable) from periphery (upgradeable) | UniswapV2Router02.sol (swapExactTokensForTokens, addLiquidity), UniswapV2Library.sol (getAmountOut) |
| 3 | Uniswap V3 Pool | Concentrated liquidity β ticks, positions, fee accumulation per-position. Understand how swap() traverses ticks and how liquidity is tracked per-range | UniswapV3Pool.sol (swap, mint, burn), Position.sol, Tick.sol |
| 4 | Uniswap V3 TickMath + SqrtPriceMath | Core AMM math β getSqrtRatioAtTick() (log-space conversion), getAmount0Delta/getAmount1Delta (liquidity-to-amount conversion). The mathematical foundation of concentrated liquidity | libraries/TickMath.sol, libraries/SqrtPriceMath.sol |
| 5 | Uniswap V4 PoolManager | Singleton architecture β all pools in one contract, flash accounting via transient storage, unlock() β callback β settle() pattern | src/PoolManager.sol (swap, modifyLiquidity, unlock), src/libraries/Pool.sol |
| 6 | Uniswap V4 Hooks | Hook interface and lifecycle β beforeSwap/afterSwap, fee overrides, custom curves via NoOp. Address-based permission encoding | src/libraries/Hooks.sol, src/interfaces/IHooks.sol, src/PoolManager.sol (hook calls) |
| 7 | Curve StableSwap | StableSwap invariant β amplification parameter A, multi-asset pools, Newtonβs method for get_y(). The dominant AMM design for pegged assets | SwapTemplateBase.vy (exchange, get_dy, _get_D, _get_y) |
| 8 | Balancer V2 Vault | Multi-pool singleton β all tokens held in one vault contract, internal balances, batch swaps. Predecessor to V4βs singleton pattern | Vault.sol (swap, batchSwap), PoolBalances.sol, FlashLoans.sol |
Reading strategy: Start with V2 Pair (1) β itβs the simplest production AMM and every later design builds on it. Then the Router (2) to see the user-facing layer and core/periphery separation. Move to V3 Pool (3) for concentrated liquidity β trace one swap() call through tick traversal. Study the math libraries (4) separately with small number examples. V4 PoolManager (5) shows the singleton + flash accounting evolution; compare with Balancer V2βs earlier singleton (8). Read Hooks (6) to understand the extensibility model. Finally, Curveβs StableSwap (7) shows an entirely different invariant optimized for pegged assets β compare the A parameterβs effect with constant product.
π Resources
Essential reading:
- Uniswap V2 Whitepaper
- Uniswap V3 Whitepaper
- Uniswap V4 Whitepaper
- Uniswap V3 Math Primer (Parts 1 & 2)
- UniswapX Whitepaper β intent-based swap architecture
Source code:
- Uniswap V2 Core (deployed May 2020)
- Uniswap V2 Periphery
- Uniswap V3 Core (deployed May 2021, archived)
- Uniswap V4 Core (deployed November 2024)
- Uniswap V4 Periphery
- Awesome Uniswap Hooks
- Curve StableSwap contracts
- Balancer V2 Vault
Deep dives:
- Concentrated liquidity math
- V3 ticks deep dive
- V4 architecture and security
- Uniswap V4 hooks documentation
- Curve StableSwap whitepaper
- Curve v2 Tricrypto whitepaper
- Balancer V2 Whitepaper
- Trader Joe V2 Liquidity Book Whitepaper
LVR & LP economics:
- Milionis et al. βAutomated Market Making and Loss-Versus-Rebalancingβ (2022) β the foundational LVR paper
- a16z LVR explainer β accessible summary
- Tim Roughgardenβs LVR lecture β video walkthrough
- CrocSwap LP profitability framework
- Revert Finance β real-time LP position profitability tracker
AMM design & market structure:
ve(3,3) & alternative DEX models:
- Andre Cronjeβs ve(3,3) design β original design post
- Velodrome documentation
- Aerodrome documentation
MEV & market microstructure:
- Flashbots documentation β MEV protection, Flashbots Protect, MEV-Boost
- Flashbots MEV explorer β live MEV extraction data
- Paradigm MEV research β foundational MEV paper
- MEV Blocker β order flow auction MEV protection
- CoW Protocol documentation β batch auctions, CoWs, MEV-proof swaps
- Intent-based architectures β Paradigm overview
LP management & JIT liquidity:
- Arrakis documentation β algorithmic LP management
- Gamma strategies β active LP management vaults
- Bunni V2 design β V4 hooks-based LP management
- Maverick AMM docs β directional liquidity and built-in LP modes
- JIT liquidity analysis β Uniswapβs own research on JIT impact
- 0x JIT impact study β quantitative JIT analysis
Aggregators:
- 1inch API documentation β pathfinder routing, Fusion mode
- Paraswap documentation β Augustus Router, multi-path routing
Interactive learning:
Security and exploits:
- Warp Finance postmortem β reentrancy in LP deposit ($8M)
- Cork Protocol exploit analysis β hook access control ($400k)
Analytics:
- Uniswap metrics dashboard β live V2/V3/V4 volume and TVL
- Curve pool analytics β stablecoin pool slippage comparison
- JIT liquidity Dune dashboard
Navigation: β Module 1: Token Mechanics | Module 3: Oracles β