logo
Published on

Uniswap V2 — A Short Explaination

Introducing

Uniswap V2 launched in May 2020 as a major upgrade to the original Uniswap protocol. While Uniswap V1 (2018) introduced the groundbreaking automated market maker (AMM) model and the concept of permissionless liquidity pools, V2 solved its early limitations and expanded what DeFi could do.

Did you know?

V2 was originally developed during COVID lockdowns

  • Uniswap V2’s development happened almost entirely during early 2020 lockdowns. Many of its design changes (like flash swaps) were shaped by the sudden explosion of interest in DeFi during that period.

Uniswap V2 contracts are only ~1,000 lines of Solidity

  • Despite dominating billions in liquidity, the V2 core contracts are extremely small and simple. This simplicity is a major reason for its security track record.

Uniswap V2 Architecture

1. Factory:

Very solid implementation of the factory contract that creates and tracks all pairs. In fact, the factory contract from Uniswap V2 is reused in Uniswap V3 with minimal changes. It's so well designed that it has stood the test of time.

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';

contract UniswapV2Factory is IUniswapV2Factory {
    address public feeTo;
    address public feeToSetter;

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    constructor(address _feeToSetter) public {
        feeToSetter = _feeToSetter;
    }

    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
}

Let's break down some key features of Uniswap V2's architecture:

1. One pool per token pair

Guarantees exactly one canonical pool for every token combination. Prevents duplicates and fragmentation.

2. Deterministic pool addresses (via CREATE2)

Anyone can compute a pair’s address off-chain before it exists. Enables seamless integration across the entire DeFi ecosystem.

3. Deploys new Pair contracts

The factory is the only contract allowed to create new liquidity pools. Ensures uniform Pair bytecode across all pools.

4. Tracks all created pairs

getPair[tokenA][tokenB] → returns the pool address instantly.

allPairs → full on-chain list of every pool.

5. Emits PairCreated events

Allows frontends, DEXs, indexers, scanners, and analytics tools to pick up new pools in real time.

6. Admin-configurable fee recipient

The feeTo address collects protocol fees only if fee-on is enabled. Controlled by feeToSetter, with no other admin privileges.

7. Immutable core logic

Factory cannot upgrade, replace, or modify Pair contracts after creation.

Ensures consistent, predictable AMM behavior.

2. Pair Contracts:

Each liquidity pool is represented by a Pair contract that manages the reserves of two tokens and facilitates swaps, liquidity provision, and fee collection.

Core responsibilities

  • Maintain token reserves (reserve0, reserve1) and enforce the constant product invariant xy=kx \cdot y = k.
  • Mint and burn LP tokens proportional to liquidity added/removed.
  • Execute swaps with a 0.30% fee on input amount.
  • Track price accumulators for on-chain/oracle-friendly time-weighted average price (TWAP).

How a swap works (in one minute)

  • Let the pool hold x = reserve0 of token0 and y = reserve1 of token1.
  • A trader sends dx token0 to the pair. A fee of 0.30% is applied: dxAfterFee = dx * 997 / 1000.
  • The pair pays out dy token1 such that (x+dxAfterFee)(ydy)k(x + dxAfterFee) \cdot (y - dy) \ge k.
  • Solving gives: dy = floor( y - k / (x + dxAfterFee) ).

Quick numeric example:

  • Start: x=1000, y=1000 so k=1,000,000.
  • Trader sends dx=100. After fee: 97.7.
  • New x is 1097.7, so dy ≈ 1000 - 1,000,000 / 1097.7 ≈ 90.9 paid out.

Fees and protocol fee

  • Every swap charges 0.30% to liquidity providers by default.
  • If the factory’s feeTo is set, protocol fees accrue by skimming a portion of LP growth via kLast logic on mint/burn.

Mint/Burn overview

  • add liquidity → mint LP tokens proportional to contribution at current price.
  • remove liquidity → burn LP tokens to withdraw underlying tokens.

Built-in price oracles (TWAP)

V2 pairs keep cumulative prices price0CumulativeLast and price1CumulativeLast. External systems compute TWAP by sampling these accumulators across time windows, avoiding manipulation within a single block.


The Router (Periphery)

While the Factory/Pair are the minimal core, most users interact through the Router contracts (UniswapV2Router01/02). The router handles:

  • Safe transfers and approvals
  • Optimal liquidity provisioning and removal (zapping in/out)
  • Multi-hop routing across many pairs (path arrays)

Common calls you’ll see:

  • swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline)
  • addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin, to, deadline)

Notes for integrators:

  • Path order matters: [tokenIn, ..., tokenOut].
  • For ETH, V2 wraps via WETH helper routes.
  • Slippage is enforced client-side with amountOutMin.

Flash Swaps (zero-upfront-capital trades)

Uniswap V2 introduced flash swaps: the Pair can send you tokens before you pay for them, provided that by the end of your callback you either:

  • Return the tokens, or
  • Pay for them with the other asset via a swap

Uses include on-chain arbitrage, refinancing, collateral swaps, and atomic liquidation strategies. If repayment fails, the whole tx reverts.


V1 → V2: What improved?

  • Any ERC-20 to any ERC-20 pools (V1 forced all pairs through ETH).
  • Price oracles via cumulative timestamps (TWAP-ready).
  • Flash swaps for capital efficiency.
  • Deterministic pair addresses via CREATE2.

Gotchas and best practices

  • Always account for the 0.30% fee and slippage; set sane amountOutMin/amountInMax.
  • Don’t rely on spot price from a single block; use TWAPs for sensitive logic.
  • Beware of fee-on-transfer tokens; Router02 includes helpers but certain tokens still break assumptions.
  • Approvals: use minimal allowances and consider permit where supported.

Want the math behind it?

If you’d like the derivations for swap outputs, LP minting, TWAP oracles, and how flash swaps satisfy the invariant, see the companion deep dive: Uniswap V2 — A Deep Dive into the Math