← Protocols

Uniswap math, V1 to V4

This lesson shows what the contracts compute when you press Swap, one version at a time.

How to read the formulas
x
Always the amount of the first token in a pool. In the examples this is ETH, and it is always blue.
y
Always the amount of the second token. In the examples this is USDC, and it is always purple.
Δ
"Delta", the Greek letter D. It means "change in". Δx reads "the change in x", for example the 1 ETH you sell into the pool.
√
Square root. √9 = 3, because 3 × 3 = 9. It shows up a lot from V3 onward.

Two words you need before anything else. An AMM (automated market maker) is a contract that quotes prices from a formula instead of from an order book. A liquidity pool is the pile of two tokens the contract holds. The amounts in the pile are the pool's reserves. People who deposit into the pile are liquidity providers, or LPs. Traders swap against the pile and pay a fee, and the fee goes to the LPs.

V1
November 2018 · Vyper · ETH paired with one token

One curve does all the pricing

Uniswap V1 had one contract per token, and every pool paired that token with ETH. There was a DAI/ETH pool, an MKR/ETH pool, and so on. The whole pricing rule fits in one line.

The constant product rule

x · y = k The reserve of token X times the reserve of token Y must stay equal to k (or grow) after every trade.
Symbols in play
x
ETH in the pool.
y
Tokens (USDC here) in the pool.
k
The invariant: a number the trade is not allowed to shrink. It only changes when LPs add or remove liquidity, or when fees pile up.

Take a pool with 10 ETH and 20,000 USDC. Then k = 10 × 20,000 = 200,000. A trader who adds ETH must take out enough USDC that the product is still 200,000. Nobody sets the price. The price falls out of the ratio of the reserves.

spot price of ETH = yx = 20,00010 = 2,000 USDC The spot price is the price for a tiny trade right now: how much Y one unit of X is worth.

What a swap computes

You sell Δx ETH into the pool. The pool now holds x + Δx ETH. To keep the product at k, the pool's USDC must drop to k ÷ (x + Δx). Whatever it drops by is what you receive. Rearranged, that gives the output formula every AMM course shows:

Δy = y · Δxx + Δx USDC out = USDC in pool × ETH you add ÷ (ETH in pool + ETH you add)

Now the fee. V1 charged 0.3%. The contract does not take the fee out of your output. It shrinks your input: only 99.7% of your ETH counts in the formula, but 100% of it stays in the pool. The extra 0.3% sits in the reserves and makes k grow a little with every trade. That growth is how LPs earn.

Δy = y · 0.997Δxx + 0.997Δx
Worked exampleSell 1 ETH into the 10 ETH / 20,000 USDC pool
  1. Apply the fee to the input. 1 × 0.997 = 0.997 ETH counts
  2. Plug into the formula. Δy = 20,000 × 0.997 / (10 + 0.997) = 19,940 / 10.997 = 1,813.22 USDC
  3. Compare with the spot price. At 2,000 USDC per ETH you might expect 2,000 USDC. You get 1,813.22. The 0.3% fee costs about 5 USDC. The other 182 USDC is price impact: your own trade moved the price while you traded.
  4. New reserves: 11 ETH and 18,186.78 USDC. The new spot price is 18,186.78 / 11 = 1,653.34 USDC. Selling ETH made ETH cheaper in this pool.
  5. New k: 11 × 18,186.78 = 200,054.6. It grew by about 55 because the fee stayed inside.
The x · y = k curveDrag the slider to sell ETH into the pool
USDC you get
.
Your average price
.
Spot price after
.
Price impact
.

Play with the slider and watch two things. Small trades barely move the dot, so their average price stays near 2,000. Large trades slide far down the curve and get worse prices on every extra unit. The curve never touches the axes, so the pool can never be fully drained: buying the last USDC would need infinite ETH.

Two words that people mix up. Price impact is how much worse your average price is than the spot price, because your own trade moves the price, and you can compute it before you trade. Slippage is the difference between the quote you saw and what you got, which also includes other trades landing before yours. A swap's "max slippage" setting caps the second one.

Question 1
A pool holds 50 ETH and 100,000 USDC. Someone buys ETH from it (they add USDC, they take ETH out). What happens to the spot price of ETH in this pool?
Your turn: calculate it
Same pool: 50 ETH and 100,000 USDC. Ignore the fee. You sell 5 ETH. How many USDC do you get?

Being an LP in V1

When you deposit, you must add both tokens at the current ratio. In the 10 ETH / 20,000 USDC pool, adding 1 ETH means adding 2,000 USDC too. You receive LP tokens, which are receipts for your share of the pool. If you own 10% of all LP tokens, you can withdraw 10% of both reserves at any time, including 10% of all fees collected since.

V1 had one weak spot for traders. Since every pool was token/ETH, a DAI to MKR swap went DAI → ETH → MKR. That is two swaps, two fees (about 0.6% in total) and price impact in two pools.

Impermanent loss, the cost of being an LP

Here is the part LPs often learn the hard way. Say ETH goes from 2,000 to 4,000 on other exchanges. Arbitrageurs (traders who buy where it is cheap and sell where it is expensive) buy ETH from your pool until the pool price is 4,000 too. To do that, they take ETH out and put USDC in. So your pool now holds less of the thing that went up.

The result: whenever the price has moved since the deposit, an LP holds less value than someone who held the same tokens in a wallet, before counting fees. That gap is called impermanent loss (also called divergence loss). It depends only on how far the price moved from where you deposited:

IL = 2√r1 + r − 1 r is the price ratio: new price ÷ price when you deposited. ETH from 2,000 to 4,000 gives r = 2.

"Impermanent" because if the price returns to where you deposited, the loss goes back to zero. If you withdraw while the price is elsewhere, it is permanent. Fees are what is supposed to pay for it.

Worked exampleWhere the formula comes from, with numbers
  1. You deposit 1 ETH + 2,000 USDC at a price of 2,000. Your deposit is worth 4,000 USDC.
  2. ETH doubles to 4,000. Arbitrage pushes the pool until y/x = 4,000 while x·y stays 2,000 (your share of k). Solve: x = √(2,000 / 4,000) = 0.7071 ETH and y = 0.7071 × 4,000 = 2,828.43 USDC.LP value = 0.7071 × 4,000 + 2,828.43 = 5,656.85 USDC
  3. If you had held instead: hold value = 1 × 4,000 + 2,000 = 6,000 USDC
  4. Loss vs holding: 5,656.85 / 6,000 − 1 = −5.72% The formula agrees: 2√2 / 3 − 1 = 2.8284 / 3 − 1 = −5.72%.
Impermanent loss vs price changeDrag the slider or move across the chart
Impermanent loss
.
$10,000 held in a wallet
.
$10,000 as V1/V2 LP
.
Question 2
You LP in ETH/USDC. Scenario A: ETH doubles. Scenario B: ETH halves. In which one is the impermanent loss bigger?
V2
May 2020 · Solidity · any ERC-20 pair

Same curve, better plumbing

V2 keeps x·y = k and the 0.3% fee exactly. The swap math from the V1 chapter still applies. What changed is around the curve: which tokens can pair, how other protocols can read the price, and how tokens move during a swap.

1. Any token with any token

A pool can now hold DAI/MKR directly, so the DAI to MKR route no longer has to pay two fees. ETH itself is not an ERC-20, so V2 pools use WETH (wrapped ETH, an ERC-20 that is 1:1 backed by ETH). The router contract wraps and unwraps for you and chains pools together for multi-hop routes.

2. A price oracle that is hard to fake

An oracle is a source of prices for other contracts, such as a lending protocol that needs to know what your collateral is worth. The spot price from the previous chapter is a bad oracle. Anyone with a flash loan can push it far for one transaction, let a lending protocol read the fake price, and then push it back. Many 2020 hacks did exactly this.

V2's answer is a TWAP, a time-weighted average price. The pool keeps a running total, the price accumulator. At the first interaction in each block, before any trade, it adds "price × seconds since the last update":

a ← a + P · Δt The arrow means "becomes". The accumulator a grows by the current price P times the seconds Δt that price was in force.

A contract that wants the average price over the last hour reads the accumulator now and one hour ago, then divides:

TWAP = anow − athentnow − tthen
Worked exampleReading a TWAP
  1. At t = 0 the accumulator reads 0. The price is 2,000 for 10 seconds. a = 0 + 2,000 × 10 = 20,000
  2. The price is then 2,100 for 20 seconds. a = 20,000 + 2,100 × 20 = 62,000
  3. TWAP over those 30 seconds: (62,000 − 0) / (30 − 0) = 2,066.67 The price that lasted twice as long counts twice as much.
Why one bad block barely moves a TWAP30 blocks of 12 seconds each
Spot price each block30-block TWAP
Spot price in block 22
.
30-block TWAP
.
TWAP moved by
.

The spike is 1,000 USDC (50%), yet the 30-block TWAP moves by only about 33 (1,000 ÷ 30). To move the TWAP by 50% the attacker must hold the fake price for the whole window. Every block they hold it, arbitrageurs trade against them and take their money. That makes the attack expensive instead of free.

The accumulator is updated before the first trade of a block, using the price at the end of the previous block. So a manipulation inside one transaction never enters the accumulator. The attacker must hold the price across at least one block boundary. Short TWAP windows are still cheap to move, especially on low-liquidity pools.

3. Flash swaps

In V2 the pair sends you the output tokens first, optionally calls your contract, and only then checks that it got paid. The check is the curve itself, run on the pool's real token balances after your callback:

(x′ − 0.003 · in) · y′ ≥ x · y x′ and y′ (read "x prime", "y prime") are the balances after the trade. The left side removes the fee from what came in. If the new product is not at least the old k, the whole transaction reverts. (The contract does this for both tokens, scaled by 1000 to avoid decimals.)

Because only the end state is checked, you can borrow tokens from the pool with nothing up front, use them for an arbitrage, and repay in the same transaction. That is a flash swap. If you cannot repay, everything reverts as if it never happened.

4. Small details that matter

The first LP in a pool receives √(x·y) LP tokens, minus 1,000 units that are locked forever (MINIMUM_LIQUIDITY). The square root makes the value of one LP share independent of the price at which the first LP deposited. The locked 1,000 units make it about 1,000 times more expensive for an attacker to inflate the value of one LP share until later depositors lose their deposit to rounding.

There is also a protocol fee switch. Governance turned it on for Ethereum mainnet in December 2025: 1/6 of the 0.3% (so 0.05% of volume) goes to the protocol and 0.25% to LPs.

Question 3
An attacker uses a flash loan to push a V2 pool's price from 2,000 to 6,000 and back inside one transaction. What does the pool's 1-hour TWAP show?
Question 4
Why can a V2 pair safely send you tokens before you pay?
V3
May 2021 · concentrated liquidity · ticks

Put your money where the price is

Look again at the V1 curve. It runs from a price near zero to a price near infinity, and a V2 LP's money is spread across all of it. If ETH trades between 1,500 and 3,000 for a year, the part of your deposit reserved for ETH at 50 USDC or 500,000 USDC does nothing. It earns no fees.

V3 lets each LP pick a price range, written [pa, pb] (lower price, upper price). Your money only works while the price is inside your range, and in exchange it works much harder. This is concentrated liquidity.

A new main character: liquidity L

In V2 the natural measure of a pool was k. V3 uses its square root instead:

L = √(x · y)   and   √P = √(y / x)
Symbols in play
L
Liquidity. How deep the pool is around the current price. Bigger L means a trade of the same size moves the price less.
P
Price of X in units of Y, the same y/x as before.
√P
The square root of the price. V3 stores this instead of P. At P = 2,000, √P = 44.72.
pa, pb
Lower and upper price of an LP's range. The position only works between these two prices.

Why the square roots? Because with them, swap math turns into multiplication. When the price moves within a range where L is constant:

Δy = L · Δ√P      Δx = L · Δ1√P USDC that moves = liquidity × how far √price moved. ETH that moves = liquidity × how far 1/√price moved.
Think of L as the slope of a straight line. If L = 400, every step of 1 in √P costs exactly 400 USDC. No curve to solve, no division by a sum. That is why the swap math works on √P and L instead of on reserves.

Real reserves for a range

An LP in range [pa, pb] only needs enough tokens to cover price moves inside that range. Starting from the current price P, the price can fall to pa (the pool would hand out USDC and collect ETH) or rise to pb (it would hand out ETH). So the position holds exactly that much of each:

x = L · ( 1√P − 1√pb )     y = L · ( √P − √pa )

These are the real reserves: the tokens you deposit. The pool behaves as if it held much more, the virtual reserves L/√P and L·√P that a V2 pool with the same L would need. The gap between the two is the efficiency gain.

Worked exampleDeposit 1 ETH into an ETH/USDC range of 1,600 to 2,500 while ETH is 2,000
  1. Square roots first. √2,000 = 44.7214 √1,600 = 40 √2,500 = 50
  2. Find L from the ETH side. 1 = L × (1/44.7214 − 1/50) = L × (0.022361 − 0.020000) = L × 0.0023607 → L = 423.61
  3. The USDC you must add with it. y = 423.61 × (44.7214 − 40) = 423.61 × 4.7214 = 2,000 USDC Total deposit: 1 ETH + 2,000 USDC, worth 4,000 USDC.
  4. What a V2 LP needs for the same L = 423.61: x = L/√P = 9.472 ETH, y = L·√P = 18,944 USDC, worth 37,889 USDC
  5. Same depth around 2,000 for 4,000 instead of 37,889 USDC. That is capital efficiency of 37,889 ÷ 4,000 ≈ 9.5×. Put the other way: with equal money you earn about 9.5× the fees of a V2 LP while the price stays in range.

The 50/50 split above is not a coincidence. 2,000 is the geometric middle of 1,600 and 2,500 (1,600 × 2,500 = 2,000²). A range centered that way always starts half in each token. Move the current price toward the top of the range and the position holds more USDC; toward the bottom and it holds more ETH.

Range explorerSet your range and the market price
V3 liquidity for your moneyV2 liquidity for the same money
Status
.
Capital efficiency vs V2
.
What your position holds right now, by value

Drag the market price above the upper price of your range. The position is 100% USDC: as ETH rose through your range, buyers took all your ETH. Drag it below the lower price and you hold 100% ETH. Out of range you earn nothing, and the position is the "losing" side of the move. That is impermanent loss with the multiplier turned up. A tight range earns more fees and loses more to price moves.

The same mechanism gives you range orders. A thin range just above the current price, funded only with ETH, sells your ETH as the price crosses it, like a limit order that also earns the fee.

Security checkV3 and V4 integrations
  • If one-sided or uneven deposits are possible, can the pool be used for a free swap?

    A vault that sits on top of a range might accept only ETH and pay out shares of a position that holds ETH and USDC. If a user can deposit one token and withdraw both at the position's current mix, they have swapped without paying the fee or the price impact. The vault's other depositors pay for it. Check how deposits are converted into the position's ratio and who pays for that conversion.

Question 5
Your ETH/USDC range is 1,800 to 2,200. ETH rises to 2,600 and stays there. What do you hold, and what do you earn?

Ticks: how ranges are stored

An LP cannot pick just any price as the lower or upper price of a range. The contract only allows prices on a fixed grid of steps called ticks. Tick number i stands for the price:

P(i) = 1.0001i Each tick is 0.01% (one basis point) above the one before it. Tick 0 is price 1. Negative ticks are prices below 1.

The power ("to the i") means "multiply 1.0001 by itself i times". To go from a price back to a tick you use a logarithm, which is the undo button for powers:

i = floor( log(P)log(1.0001) ) floor means "round down to a whole number".
Worked exampleWhich tick is a price of 2,000?
  1. Take the log of both numbers. These are natural logs (the ln key on a calculator), but any log works as long as top and bottom use the same one. log(1.0001) is tiny because 1.0001 is barely above 1, and log(1) = 0. log(2,000) = 7.6009 log(1.0001) = 0.000099995
  2. Divide. 7.6009 / 0.000099995 = 76,012.8
  3. Round down: tick 76,012. Check: 1.000176,012 ≈ 1,999.84, and the next tick up is ≈ 2,000.04. The real price sits between them.
  4. Not every tick can be used. Each pool has a tick spacing, and a range must start and end on a tick that is a multiple of it. A 0.3% pool uses spacing 60, so the nearest usable tick below 76,012 is 75,960.

Why do higher fees come with wider tick spacing? Every initialized tick a swap crosses costs extra gas, because the pool has to update its active liquidity there. Low-fee pools (0.01%, 0.05%) are meant for pairs whose price barely moves, such as two stablecoins. LPs there need fine steps to place tight ranges, and swaps rarely cross many ticks. High-fee pools (0.3%, 1%) are for volatile pairs. Their price moves through many ticks, so wider spacing keeps swaps cheap, and LPs there use wide ranges anyway. On Ethereum, V3 offers these four pairs of fee and spacing, and only governance can add a new one. It launched with three; governance added 0.01% in November 2021. V4 lets each pool choose any fee and spacing.

Tick calculatorType any price
Tick
.
Price at that tick
.
Nearest usable tick below
.
sqrtPriceX96
.

The last box is how the contract stores √P: as sqrtPriceX96, which is √P × 296 as an integer. Solidity has no decimals, so the contract multiplies by a huge fixed number to keep precision. This format is called Q64.96: 64 bits for the whole part, 96 bits for the fraction.

On-chain, P is the raw ratio of token1 units to token0 units, decimals included. In the real USDC/WETH pool, token0 is USDC (6 decimals) and token1 is WETH (18 decimals), so the stored price is WETH-wei per USDC-unit. At an ETH price of 2,000 that is 5 × 10⁸, which is tick 200,311, not 76,012. Many integration bugs come from getting the token order or the decimals wrong.
Security checkV3 and V4
  • Are the min and max ticks always multiples of the tick spacing?

    The tick range itself runs from −887,272 to 887,272, and those numbers are not multiples of 60 or 200. Code that uses them as a "full range" position reverts. Round them toward zero to the spacing first, for example (MAX_TICK / spacing) * spacing. The same applies to any lower or upper tick computed from a price.

Question 6
About what price does tick 6,932 stand for? (Hint: 1.00016,932.)

A swap that crosses ticks

The pool keeps one number for the active liquidity L right now. Each tick where some LP's range starts or ends stores liquidityNet: how much L to add or remove when the price crosses it. A swap runs in a loop:

  1. Find the next initialized tick in the direction of the swap.
  2. With the current L, compute how much input moves √P to that tick (Δy = L · Δ√P).
  3. If the trade has that much input left, move there, add or subtract that tick's liquidityNet from L, and repeat.
  4. If not, move √P only part of the way and stop.

Each tick can be referenced by only so much liquidity, maxLiquidityPerTick. It is set from the tick spacing so that L summed over every usable tick still fits in a uint128. A mint that would push a tick past it reverts.

Worked exampleBuy ETH with 1,000 USDC, inside the range from the earlier example (L = 423.61, no fee)
  1. How far does √P move? Δ√P = Δy / L = 1,000 / 423.61 = 2.3607
  2. New √P and price. √P = 44.7214 + 2.3607 = 47.0820 → P = 47.0820² = 2,216.72 2,216.72 is below 2,500, the top of the range, so no tick is crossed.
  3. ETH out. Δx = L × (1/44.7214 − 1/47.0820) = 423.61 × 0.0011212 = 0.4749 ETH
  4. Average price paid: 1,000 / 0.47493 = 2,105.57 USDC. In a V2 pool with only 4,000 USDC of capital this trade would cost far more. Concentrated liquidity makes small capital feel deep.

How does the pool get paid? Like V2's flash swaps, a V3 pool sends a swap's output first and then calls a callback on the contract that started it (uniswapV3SwapCallback). For a mint there is no output; the pool calls uniswapV3MintCallback to collect the tokens. Inside the callback, your contract transfers what it owes. Afterwards the pool checks its balances and reverts if it was not paid.

Security checkV3 and V4
  • Is the callback secure, callable only by the allowed pool?

    The callback is a public function that sends tokens. If it does not check msg.sender, anyone can call it directly and ask your contract to pay them. In V3, verify the caller is the pool computed from the factory, token pair and fee, not just "any address that claims to be a pool". In V4 the callback is unlockCallback, and only the PoolManager may call it.

Find the bug 1
This router pays V3 pools during swaps. Users approve the router to spend their tokens. What is wrong?
function uniswapV3SwapCallback(
    int256 amount0Delta, int256 amount1Delta, bytes calldata data
) external {
    (address tokenIn, address payer) = abi.decode(data, (address, address));
    uint256 owed = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta);
    IERC20(tokenIn).transferFrom(payer, msg.sender, owed);
}

Fees in V3

Pools come in fee tiers: 0.01%, 0.05%, 0.3% and 1%. Stable pairs pick low fees, volatile pairs high. The contract writes fees in hundredths of a basis point, called pips: fee = 3000 means 0.3%. Fees are no longer added to the reserves. Each pool tracks "fees earned per unit of L" globally and per tick, and a position's share is computed from the difference when you collect. That is why the V3 position manager issues each position as an NFT: every position has its own range, so the shares are not interchangeable like V2 LP tokens.

The V3 oracle also changed. It accumulates the tick instead of the price. Since a tick is a log of the price, averaging ticks gives a geometric mean price, so the average price of ETH in USDC is exactly the inverse of the average price of USDC in ETH. V2 needed two accumulators for that; V3 needs one. A geometric mean also damps a spike up, but a crash toward zero pulls it down hard.

V4
January 2025 · singleton · hooks · flash accounting

Same math, new machine around it

V4 pools use the V3 curve unchanged. L, √P, ticks, ranges and the swap loop all carry over. What V4 changes is where pools live, how tokens move, and who can add logic.

1. The singleton

In V2 and V3 every pool is its own contract, deployed by a factory. In V4 all pools live inside one contract, the PoolManager. This design is called a singleton. Creating a pool is a storage write instead of a contract deployment, so it is much cheaper. A pool is identified by its PoolKey:

PoolKey { currency0, currency1, fee, tickSpacing, hooks } The PoolId is the hash of these five fields. Change any one of them, for example the hook address, and you have a different pool.

Fees and tick spacing are no longer limited to a fixed list, and native ETH works again (no WETH needed), as in V1.

2. Flash accounting

Since all pools share one contract, the PoolManager can wait until the end to move tokens. You call unlock(), the PoolManager calls back into your contract, and inside that callback you can swap, add liquidity and remove liquidity as many times as you like. Each action only updates a running tally per token, called a delta. When your callback returns, every delta must be zero, or the whole transaction reverts.

Sign convention for deltas (from your side)
−
You owe the PoolManager this much. Pay it with settle().
+
The PoolManager owes you this much. Withdraw it with take(), or keep it inside as an ERC-6909 claim token.
A two-hop swap, ETH → USDC → DAIStep through it
StepETHUSDCDAIToken transfers

The USDC never moves. In V3 the same route sends USDC out of pool 1 and into pool 2, because they are separate contracts with separate balances. Fewer transfers means less gas, and it makes long routes nearly as cheap as single swaps.

Flash accounting needs a cheap scratchpad that is wiped after each transaction. V4 uses transient storage (EIP-1153, the TSTORE/TLOAD opcodes), which costs far less than normal storage and resets to zero when the transaction ends.

Security checkV4 hooks
  • Does the hook make decisions from pool state that can be faked inside an unlock?

    Inside an unlock, the PoolManager only demands that every debt is repaid at the end. Until then, prices and balances can be whatever the caller wants.

    Details
    What
    A caller can borrow tokens from the PoolManager with take() without putting up capital, push the pool price around, add or remove liquidity, trigger the hook, and repay everything before unlock returns. A hook that reads the current price, liquidity or a balance at that moment reads numbers the caller chose.
    Check
    Find every place the hook reads slot0, the pool's liquidity, a PoolManager balance or a token balance, and uses it to allow an action or to price something.
    Fix
    Price from a TWAP or from a value stored before the unlock started. In the hook's own external functions, refuse to read pool state while an unlock is in progress.
Question 7
Inside an unlock callback you swap 1 ETH for 2,000 USDC, then return without calling settle or take. What happens?

3. Hooks

A hook is an external contract that the PoolManager calls at fixed points in a pool's life. Each pool picks one hook address when it is created, and it cannot change later. The hook can run before or after initialize, add liquidity, remove liquidity, swap and donate. With that you can build dynamic fees, on-chain limit orders, TWAMMs (orders split over time), oracles, KYC gates, or LP rewards, without forking Uniswap.

The clever part is how the PoolManager knows which functions to call. It reads them from the hook's address. The lowest 14 bits of the address are on/off switches. If bit 7 is 1, call beforeSwap. If it is 0, skip it. Hook developers mine a CREATE2 salt until the deployed address has the right bits.

Hook permissions live in the addressTick the hooks you want
Security checkV4 hooks
  • Is validateHookPermissions called with the right permissions?

    The address bits decide which functions the PoolManager calls. If the code expects beforeSwap to run but the deployed address does not have bit 7 set, the hook silently never runs. Calling Hooks.validateHookPermissions in the constructor (BaseHook does this) makes deployment fail when the bits and the intended permissions differ. Check that the permissions passed in match the functions the hook actually implements.

  • Do the hook functions accept calls only from the PoolManager, and check the sender parameter where it matters?

    Hook functions are public. Without an onlyPoolManager check, anyone can call afterSwap directly and trigger rewards, accounting updates or payouts. Separately, the sender argument is the contract that called the PoolManager, usually a router, not the user. If the hook gates or credits by sender, check that it is a trusted router and that the real user is identified correctly.

Find the bug 2
This hook gives traders reward points for every swap. Its address has the afterSwap bit set, and the permissions were validated. What is wrong?
function afterSwap(
    address, PoolKey calldata, SwapParams calldata,
    BalanceDelta, bytes calldata hookData
) external returns (bytes4, int128) {
    address user = abi.decode(hookData, (address));
    rewards[user] += REWARD_PER_SWAP;
    return (this.afterSwap.selector, 0);
}

Four of the flags are "returns delta" flags. With them a hook can change the amounts in a swap or a liquidity action, by taking a cut or by doing the whole swap itself. A hook with beforeSwapReturnsDelta can skip the built-in V3 curve entirely and price the trade with its own formula. That is how custom curves (for example stable-swap math) can run inside V4.

Fees can be dynamic too. If the PoolKey's fee is the flag value 0x800000, the hook can set or override the fee per swap, for example raising it when the market is volatile so LPs lose less to arbitrage.

Worked exampleA dynamic-fee hook in numbers
  1. Pool is ETH/USDC with the dynamic fee flag. The hook sets 0.05% in calm markets and 0.5% when the last hour's price moved more than 3%.
  2. Calm hour, a trader sells 1 ETH at a spot price of 2,000 in a deep pool. Fee: 1 ETH × 0.0005 = 0.0005 ETH ≈ 1 USDC to LPs
  3. Volatile hour, an arbitrageur sells the same 1 ETH. Fee: 1 ETH × 0.005 = 0.005 ETH ≈ 10 USDC to LPs
  4. The curve math is the same in both cases. Only the 0.997 from the V1 chapter becomes 0.9995 or 0.995. The hook chooses which.
Security checkV4 hook fees
  • Does the hook charge the same fee whether a swap is exact-input or exact-output?

    amountSpecified is negative for exact-input swaps and positive for exact-output swaps. A fee calculated from it is taken from the input in one case and from the output in the other.

    Details
    What
    Taking a percentage of amountSpecified means an exact-input swap pays it on what goes in, while an exact-output swap pays it on what comes out, which is already reduced by the LP fee and price impact. The same trade costs a different amount depending on how the trader specifies it, so traders pick the cheaper form. The higher the fee, the bigger the difference.
    Check
    How the fee is computed in beforeSwap and afterSwap, once for a negative and once for a positive amountSpecified.
    Fix
    Always base the fee on the same side of the trade, for example the input amount, whichever way the swap is specified.
Hooks are where most V4 risk sits. A hook can hold user funds, can be upgradeable, can revert to block withdrawals, and with return-delta flags can take value from swappers or LPs. A pool is only as safe as its hook, and the PoolManager does not check what the hook does beyond keeping deltas balanced.

Launchpads that graduate a token from a bonding curve into a V4 pool bring their own risks. The Bonding Curves lesson covers them in its graduation chapter.

Question 8
A hook is deployed at an address ending in …00C0. Which hook functions will the PoolManager call?
Question 9
Which part of the pricing math changed from V3 to V4?
∑
Recap

Four versions side by side

V1 (2018)V2 (2020)V3 (2021)V4 (2025)
PairsToken / ETH onlyAny ERC-20 pair (WETH for ETH)Any ERC-20 pairAny pair, native ETH
Curvex·y = kx·y = kx·y = k per range, tracked as L and √PSame as V3, or a custom curve in a hook
Fee0.3%0.3% (0.05% to the protocol since Dec 2025)0.01 / 0.05 / 0.3 / 1%Any fee, or dynamic via hook
LP receiptERC-20 LP tokenERC-20 LP tokenNFT per position (via periphery)Position inside PoolManager (NFT via periphery)
OracleNoneArithmetic TWAP from price accumulatorGeometric TWAP from tick accumulatorNone built in; possible as a hook
Pool contractsOne per tokenOne per pairOne per pair and fee tierOne PoolManager for all
New ideaThe constant product AMMFlash swaps, TWAPConcentrated liquidity, ticksHooks, flash accounting
Question 10
A friend LPs $10,000 in a very tight V3 range and is excited about 10× the fees of V2. What should you warn them about?

A–Z
Every term and symbol from the lesson

Dictionary