- Published on
Uniswap V2 — A Deep Dive into the Math
Overview
This post walks through Uniswap V2’s math and maps each concept to the actual Pair contract implementation. Equations use the constant-product model; code snippets are short quotes from the public Uniswap v2-core repository with attribution.
Note on snippets: Small excerpts are quoted from Uniswap v2-core (GPL-3.0) for explanation. See the full source: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol
Constant product and price
The core invariant maintained by every pair is:
Where:
- = reserve of token0, = reserve of token1
- = constant product (ignoring fees and protocol-fee adjustments)
The instantaneous price is given by the reserve ratio:
Swaps move you along this curve: input pushes one reserve up and the other down such that after fees.
Fees and the 997/1000 factor
V2 charges a 0.30% fee on the input amount. The Pair contract enforces the invariant with fee-adjusted balances. In swap(), it computes adjusted balances and requires the product not to shrink:
// UniswapV2Pair.swap — fee-adjusted K check (excerpt)
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’s equivalent to applying an effective multiplier of 997/1000 to the input side. If a trader sends of token0, the effective amount is:
Deriving output amounts
Given reserves and input of token0, with fee applied, the post-trade reserves must satisfy:
Solving for the output :
Numeric example:
- Start: , , .
- Trader sends → .
- , → .
This matches the behavior enforced by the fee-adjusted K-check above.
LP tokens and liquidity math
LP tokens represent a pro-rata share of the pool. For an existing pool (total supply ), adding mints:
At initialization (when ), V2 mints roughly:
(minus a small MINIMUM_LIQUIDITY burned to address(0)):
// UniswapV2Pair — constants (excerpt)
uint public constant MINIMUM_LIQUIDITY = 10**3;
Protocol fees (if enabled via Factory) are minted using the kLast mechanism when liquidity changes. The relevant part computes square roots of the product of reserves:
// UniswapV2Pair._mintFee (excerpt)
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(kLast);
// if rootK > rootKLast, mint a portion of growth to feeTo
Price oracles (TWAP)
Pairs maintain cumulative prices to enable time-weighted average prices (TWAP) without external price feeds. The pair updates cumulative prices inside _update whenever time has elapsed:
// UniswapV2Pair._update — cumulative price (exact logic abridged)
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// UQ112x112 fixed point: encode(reserve1).uqdiv(reserve0)
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
Key details:
- Prices are stored as UQ112x112 fixed-point numbers. Let
Q112 = 2^112. - Cumulatives add “price × seconds.” They only change when
_updateruns (e.g., on swap/mint/burn/sync). - Consumers form a TWAP by taking two cumulative snapshots and dividing the delta by elapsed seconds, then decoding by
Q112.
ext{TWAP}_{0\to1} = \frac{\Delta\,\text{price0CumulativeLast}}{\Delta t \cdot Q_{112}}
Practical usage tips:
- Snapshots must bracket your window. If no on-chain interaction occurs within a window, perform a cheap
sync()(or any state-changing call) at one end to advance cumulatives. - To avoid a “same-block” manipulation, use windows spanning multiple blocks (commonly ≥ 10–15 minutes for volatile pairs).
- A common pattern is “counterfactual” reading: compute current cumulatives off-chain by adding
(currentPrice * (now - blockTimestampLast))to the stored cumulative before forming deltas.
Worked micro-example (UQ112x112):
- Suppose at t0 reserves are
(x=1000, y=2000)→ price0 = y/x = 2.0. - After 300s with price ~2.0,
price0CumulativeLastincreases by2.0 * 300 * Q112. - If at t1=600s price is ~1.5 for the next 300s, delta cumulative adds
1.5 * 300 * Q112. - TWAP over [0,600] is
(2.0*300 + 1.5*300)/600 = 1.75.
Flash swaps: zero-upfront-capital flows
V2 can optimistically send tokens and require repayment (or equivalent value) in the same transaction. The flow is inside swap():
// UniswapV2Pair.swap — transfer out, optional callback, then invariant check (abridged)
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
// fee-adjusted K check (997/1000)
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');
What this means in practice:
- You can receive
amount0Out/amount1Outbefore paying. - Inside your
uniswapV2Call, you must return enough value so that—after fees—the fee-adjusted K check passes. - Repayment can be either returning the same token(s) or performing a trade elsewhere and repaying with the opposite token.
ext{Repay condition:}\quad (x' \cdot y')_{\text{after fees}} \ge x \cdot y
Notes and safeguards:
- No “flash minting” of LP tokens occurs; only existing reserves are borrowed.
- Reentrancy is mitigated by the simple post-callback balance accounting and final K check, plus the minimal surface of Pair.
- Protocol fees (fee-on) are unrelated to swap fees; they accrue on liquidity events via
kLast, not per-swap. - Typical strategies: triangular arbitrage across pairs, refinancing collateral, deleveraging/leveraging within a single atomic tx.
Edge cases and gotchas
- Fee-on-transfer tokens can break assumptions; Router02 includes helpers, but pairs still enforce balance-based accounting.
- Slippage must be handled at the caller via
amountOutMin/amountInMax(in the Router), not inside the Pair. - Single-block manipulation is mitigated by using TWAPs rather than spot prices for sensitive logic.
References
- Pair contract: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol
- Factory contract: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Factory.sol
- Docs (archived): https://docs.uniswap.org/contracts/v2/overview
Attribution: Code excerpts are from Uniswap v2-core (GPL-3.0). Copyright © Uniswap contributors.
