AMM execution, liquidity shape, and LP economics from V1 to V4.
A swap is not matching you with another trader. It is a trade with a pool that already holds two tokens.
When you sell ETH for UNI, you put ETH into the pool and the pool sends UNI back. The contract decides the amount from three things: how much is already in the pool, what fee applies, and how far your trade moves the pool price.
Use the demo as a small lab:
fee, depth, and impact explain why the Buy amount changes.One evolution thread: V1 proves that a pool can quote by itself, V2 makes token pairs reusable, V3 puts liquidity near active prices, and V4 lets pools run custom logic around the swap.
The page uses one normalized ETH/UNI pool as a controlled experiment. The numbers below are shared by the swap UI, comparison table, and version curves.
The pool is intentionally small and the order is intentionally large. That makes price impact visible, so it is easier to see what each version changes.
Tiny glossary:
reserve: token inventory already sitting in the pool.fee: the input amount kept by the pool before pricing the trade.impact: how far your own trade pushes the execution price away from spot.active depth: liquidity that is actually available around the current price.LP: liquidity provider; someone who deposits both tokens and earns fees while taking inventory risk.V3/V4 rows use an illustrative segmented-depth model. V4 also applies a hook-like fee adjustment across the order. The historical contract snippets below keep their original fee constants; this demo normalizes fees so the version comparison is easier to read.
| Version | Scenario assumption | Fee | Active depth | Output UNI | Avg (UNI/ETH) | Impact |
|---|---|---|---|---|---|---|
| V1 | full-range CPMM | 0.05% | 1x | 16.6597 | 0.833 | 16.701% |
| V2 | pair primitive (same path) | 0.05% | 1x | 16.6597 | 0.833 | 16.701% |
| V3 | tick-segment concentrated liquidity | 0.05% | 1.53x | 18.3499 | 0.9175 | 8.25% |
| V4 | tick-segment + hook fee policy | 0.035% | 1.53x | 18.3524 | 0.9176 | 8.238% |
Read this table as the map before the details.
| Version | Pool model | LP style | Extensibility | Operational complexity |
|---|---|---|---|---|
| V1 | full-range x·y=k | passive full-range | low | low |
| V2 | pair-based x·y=k | passive full-range | medium | low |
| V3 | concentrated by ticks | active range mgmt | medium | high |
| V4 | concentrated + hooks | active + programmable | very high | very high |
V1 answers the first question: can a pool quote a trade by itself?
Yes. The contract looks at two reserves, applies the fee to the input, and solves the next point on the curve. There is no order book and no separate market maker.
Its invariant is:
In the running case, V1 is the baseline: sell ETH, receive UNI, and let the reserve curve determine the output.
For ETH input:
Fees are applied on input first, so effective input is smaller than raw input.
The mechanism worked, but the architecture was still narrow. V2 keeps the same pricing idea and turns the pair into a reusable building block.
V1's core pricing function is the whole AMM idea in a few lines: discount the input by the fee, multiply by output reserves, then divide by the new input side.
input_amount_with_fee: uint256 = input_amount * 997
numerator: uint256 = input_amount_with_fee * output_reserve
denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
return numerator / denominatorThe 997 / 1000 term is the historical V1 fee treatment. Bigger input increases the denominator, so each additional ETH gets worse execution.
V2 is easier to understand as an architecture upgrade, not a new curve.
The quote still comes from reserves. The important change is that token-token pairs become a standard primitive that wallets, routers, and other contracts can compose.
It keeps CPMM at pair level:
So in the same running case, output stays close to V1. The math path is similar; the surrounding system becomes much easier to build on.
Liquidity still spans the full theoretical range. Simple UX, but low capital efficiency.
That bottleneck leads to V3: put more depth near the prices where flow actually trades.
V2's pair contract does not ask an oracle for the price. After tokens move, it checks that the fee-adjusted balances still satisfy the constant-product rule.
uint amount0In = balance0 > _reserve0 - amount0Out
? balance0 - (_reserve0 - amount0Out)
: 0;
uint amount1In = balance1 > _reserve1 - amount1Out
? balance1 - (_reserve1 - amount1Out)
: 0;
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(
balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2),
'UniswapV2: K'
);That last K check is the guardrail: after accounting for fees, the pool must not be left with less value than the invariant allows.
V3 solves the capital-efficiency problem.
Instead of spreading liquidity across every possible price, LPs choose tick ranges. A swap near the active range can meet more local depth, so the same input can produce less impact.
In the running case, concentrating liquidity around the current price increases active depth and reduces impact.
LPs move from passive exposure to active range management. Tick crossing and rebalancing turn microstructure into an operational problem.
This sets up V4: once liquidity shape is configurable, pool policy becomes the next thing to configure.
V3 swap execution walks through initialized ticks. Each step computes how much input can be consumed before the next tick, then updates price and liquidity if the tick boundary is crossed.
This excerpt is trimmed to show the control flow; the full function also handles price limits, oracle writes, and protocol fee accounting.
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
(step.tickNext, step.initialized) =
tickBitmap.nextInitializedTickWithinOneWord(state.tick, tickSpacing, zeroForOne);
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) =
SwapMath.computeSwapStep(
state.sqrtPriceX96,
step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
);
if (state.sqrtPriceX96 == step.sqrtPriceNextX96 && step.initialized) {
int128 liquidityNet = ticks.cross(/* tick accounting omitted */);
if (zeroForOne) liquidityNet = -liquidityNet;
state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
}
}This is why V3 is not just "same curve, more liquidity." The active liquidity can change as the swap moves across ticks.
V4 keeps V3’s concentrated-liquidity base and makes pool behavior programmable.
The curve is no longer the whole story. A pool can run hook logic around swap and liquidity actions, so execution quality also depends on policy code.
Two additions define it:
In the running case, realized output depends on both curve state and a hook-like fee policy.
Protocol teams can express differentiated behavior natively, instead of rebuilding custom layers outside the AMM.
Evaluation shifts from "is the curve sound?" to "is the curve plus policy code sound?"
V4's pool manager makes the hook layer explicit. A swap can be modified before execution, executed by the pool, then adjusted after execution.
This excerpt is trimmed to show the before-swap / pool-swap / after-swap sequence.
(amountToSwap, beforeSwapDelta, lpFeeOverride) =
key.hooks.beforeSwap(key, params, hookData);
swapDelta = _swap(pool, id, Pool.SwapParams({
tickSpacing: key.tickSpacing,
zeroForOne: params.zeroForOne,
amountSpecified: amountToSwap,
sqrtPriceLimitX96: params.sqrtPriceLimitX96,
lpFeeOverride: lpFeeOverride
}), params.zeroForOne ? key.currency0 : key.currency1);
(swapDelta, hookDelta) =
key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta);So V4 does not replace the concentrated-liquidity engine. It wraps that engine with programmable policy points.
At this point, you already have the core swap model: pool inventory plus fee plus price movement. The remaining sections are the parts LPs and protocol designers care about.
Execution quality is governed by order size / active liquidity, not notional size alone.
CPMM spreads depth across the full range; concentrated models place depth around selected ticks.
| Model | Liquidity shape |
|---|---|
| CPMM (x·y=k) | full-range hyperbola, uniform across prices |
| StableSwap | near-peg flatter zone, tails approach CPMM |
| CLMM (ticks) | liquidity allocated by tick intervals |
Use net LP return ≈ fee income − LVR − operating costs.
Hooks make behavior programmable and shift risk from pure curve math to policy logic and implementation quality.
| Hook pattern | Primary objective | Main risk |
|---|---|---|
| dynamic_fee | adapt spread to volatility regime | wrong regime detection harms flow |
| limit_order_like | conditional execution around trigger price | execution starvation / stale trigger logic |
| oracle_aware | robust parameter updates with external signals | oracle lag or manipulation surface |