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:

  • Version buttons change the AMM design.
  • The Sell number changes order size.
  • fee, depth, and impact explain why the Buy amount changes.
Sell
ETH
balance 120.00 ETH
Buy
16.659722
UNI
balance 86.40 UNI
version
V1
fee
0.05%
depth
1x
impact
16.701%

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.

Running example

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.

ETH reserve
200
UNI reserve
200
scale
0.5
input
20 ETH
baseFee
0.05%

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.

VersionScenario assumptionFeeActive depthOutput UNIAvg (UNI/ETH)Impact
V1full-range CPMM0.05%1x16.65970.83316.701%
V2pair primitive (same path)0.05%1x16.65970.83316.701%
V3tick-segment concentrated liquidity0.05%1.53x18.34990.91758.25%
V4tick-segment + hook fee policy0.035%1.53x18.35240.91768.238%

From V1 to V4 at a glance

Read this table as the map before the details.

VersionPool modelLP styleExtensibilityOperational complexity
V1full-range x·y=kpassive full-rangelowlow
V2pair-based x·y=kpassive full-rangemediumlow
V3concentrated by ticksactive range mgmtmediumhigh
V4concentrated + hooksactive + programmablevery highvery high

Uniswap V1

V1 curve (full-range x·y=k)

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:

xy=kx\cdot y = k

In the running case, V1 is the baseline: sell ETH, receive UNI, and let the reserve curve determine the output.

Trade path

For ETH input:

x=x+Δxeffx' = x + \Delta x_{eff} y=kxy' = \frac{k}{x'} Δy=yy\Delta y = y - y'

Fees are applied on input first, so effective input is smaller than raw input.

Why it mattered

  • Continuous, permissionless on-chain pricing
  • Deterministic execution from reserve state
  • Clear impact intuition: larger trades move farther on the curve

What it could not yet do

The mechanism worked, but the architecture was still narrow. V2 keeps the same pricing idea and turns the pair into a reusable building block.

Contract core

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.

uniswap_exchange.vy
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 / denominator

The 997 / 1000 term is the historical V1 fee treatment. Bigger input increases the denominator, so each additional ETH gets worse execution.

Uniswap V2

V2 curve (pair-based x·y=k)

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:

xy=kx\cdot y = k

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.

What changed

  • Token-token pairs became first-class
  • Pair interface became easier to route and integrate
  • Ecosystem composability improved materially

What stayed constrained

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.

Contract core

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.

UniswapV2Pair.sol
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.

Uniswap V3

V3 curve (concentrated liquidity)

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.

Execution model

  • Inside active range: familiar local reserve-curve behavior
  • Outside active range: liquidity can become inactive

In the running case, concentrating liquidity around the current price increases active depth and reduces impact.

What improved

  • Better capital efficiency
  • Better local execution when ranges are well placed

New tradeoff

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.

Contract core

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.

UniswapV3Pool.sol
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.

Uniswap V4

V4 curve (concentrated + hook layer)

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:

  • Hooks: pool-specific logic around swap/liquidity lifecycle
  • Singleton architecture: shared pool manager for better composability/gas behavior

In the running case, realized output depends on both curve state and a hook-like fee policy.

Why this matters

Protocol teams can express differentiated behavior natively, instead of rebuilding custom layers outside the AMM.

New risk surface

Evaluation shifts from "is the curve sound?" to "is the curve plus policy code sound?"

Contract core

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.

PoolManager.sol
(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.

Execution quality and LP economics

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.

Price impact is path-dependent

Execution quality is governed by order size / active liquidity, not notional size alone.

Price impact vs pool depth
probe q/L: 0.35
shallower pool impact: 25.93%
deeper pool impact: 10.45%

Model shape determines where depth exists

CPMM spreads depth across the full range; concentrated models place depth around selected ticks.

ModelLiquidity shape
CPMM (x·y=k)full-range hyperbola, uniform across prices
StableSwapnear-peg flatter zone, tails approach CPMM
CLMM (ticks)liquidity allocated by tick intervals
Pool depth by tick interval
active depth share: 88.2%
CLMM bucket count: 17
est. crossing rate: 1 / unit move

LP returns must be decomposed, not guessed from fees

Use net LP return ≈ fee income − LVR − operating costs.

LP return waterfall
net LP return: 6%

V4 hooks: power and responsibility

Hooks make behavior programmable and shift risk from pure curve math to policy logic and implementation quality.

Hook patternPrimary objectiveMain risk
dynamic_feeadapt spread to volatility regimewrong regime detection harms flow
limit_order_likeconditional execution around trigger priceexecution starvation / stale trigger logic
oracle_awarerobust parameter updates with external signalsoracle lag or manipulation surface