The Mathematics of Uniswap: How Easy It Is to Build a DEX from Scratch

The Mathematics of Uniswap: How Easy It Is to Build a DEX from Scratch

In traditional finance, exchanges rely on Central Limit Order Books (CLOBs), where market makers continuously place bids and asks. On early Ethereum, order books were catastrophic: matching orders on-chain caused exorbitant gas fees and suffered front-running delays.

In 2018, Uniswap revolutionized decentralized finance by proving you don’t need order books at all. You just need a single elegant mathematical equation:

x⋅y=kx \cdot y = k

In this tutorial, we will break down the exact mathematics behind Automated Market Makers (AMMs), derive the exact swap and liquidity formulas, and build a minimal, gas-efficient Decentralized Exchange (DEX) pair contract in Solidity.


1. The Constant Product Invariant

An AMM pool holds reserves of two ERC-20 tokens: Token XX with reserve xx, and Token YY with reserve yy.

The core invariant dictates that before and after any trade, the product of these reserves must remain constant:

x⋅y=kx \cdot y = k

   Reserve Y (Token B)
        |
     y1 +------------------* (x1, y1)
        |                 / 
        |                /   Invariant Curve: x * y = k
        |               /
     y2 +--------------+------* (x2, y2)
        |              |      |
        +--------------+------+----------> Reserve X (Token A)
                      x1     x2

Deriving the Output Amount (Δy\Delta y)

Suppose a trader deposits Δx\Delta x amount of Token XX. What amount Δy\Delta y of Token YY should the pool return to maintain the invariant kk?

Let the initial state be (x,y)(x, y). After depositing Δx\Delta x, the new Token XX reserve becomes (x+Δx)(x + \Delta x), and the new Token YY reserve becomes (y−Δy)(y - \Delta y).

(x+Δx)(y−Δy)=k=x⋅y(x + \Delta x)(y - \Delta y) = k = x \cdot y

Expanding both sides:

x⋅y−x⋅Δy+Δx⋅y−Δx⋅Δy=x⋅yx \cdot y - x \cdot \Delta y + \Delta x \cdot y - \Delta x \cdot \Delta y = x \cdot y

Canceling x⋅yx \cdot y from both sides:

Δx⋅y=x⋅Δy+Δx⋅Δy\Delta x \cdot y = x \cdot \Delta y + \Delta x \cdot \Delta y

Factoring out Δy\Delta y:

Δx⋅y=Δy(x+Δx)\Delta x \cdot y = \Delta y (x + \Delta x)

Solving for Δy\Delta y:

Δy=y⋅Δxx+Δx\Delta y = \frac{y \cdot \Delta x}{x + \Delta x}


2. Incorporating the 0.3% Trading Fee

Uniswap charges a 0.30% fee on every swap to incentivize Liquidity Providers (LPs). Only 99.7%99.7\% of the deposited input Δx\Delta x contributes to the swap product.

Let the fee parameter be γ=0.997=9971000\gamma = 0.997 = \frac{997}{1000}.

Replacing Δx\Delta x with γ⋅Δx\gamma \cdot \Delta x:

Δy=y⋅γ⋅Δxx+γ⋅Δx=y⋅997⋅Δx1000⋅x+997⋅Δx\Delta y = \frac{y \cdot \gamma \cdot \Delta x}{x + \gamma \cdot \Delta x} = \frac{y \cdot 997 \cdot \Delta x}{1000 \cdot x + 997 \cdot \Delta x}

[!NOTE] Notice how the denominator increases by 997⋅Δx997 \cdot \Delta x, which prevents integer truncation issues in Solidity where all arithmetic is performed with unsigned integers (uint256).


3. Spot Price, Execution Price, and Price Slippage

Spot Price (Marginal Price)

The instantaneous spot price PP of Token XX in terms of Token YY is the derivative of the invariant curve:

y=kx  ⟹  P=−dydx=kx2=x⋅yx2=yxy = \frac{k}{x} \implies P = -\frac{dy}{dx} = \frac{k}{x^2} = \frac{x \cdot y}{x^2} = \frac{y}{x}

Execution Price

The actual average price PexecP_{exec} received by the trader for their trade of size Δx\Delta x is:

Pexec=ΔyΔx=yx+ΔxP_{exec} = \frac{\Delta y}{\Delta x} = \frac{y}{x + \Delta x}

Price Slippage

The relative price slippage SS caused by the trade is:

S=Pspot−PexecPspot=yx−yx+Δxyx=1−xx+Δx=Δxx+ΔxS = \frac{P_{spot} - P_{exec}}{P_{spot}} = \frac{\frac{y}{x} - \frac{y}{x + \Delta x}}{\frac{y}{x}} = 1 - \frac{x}{x + \Delta x} = \frac{\Delta x}{x + \Delta x}

When Δx≪x\Delta x \ll x (trade size is tiny relative to pool reserves), slippage approaches 0. As Δx\Delta x grows larger, slippage increases linearly.


4. Liquidity Provision & LP Tokens

When a user deposits reserves to become an LP, they must deposit both tokens in equal proportion to the current spot price:

Δxx=Δyy\frac{\Delta x}{x} = \frac{\Delta y}{y}

Initial Liquidity Minting

For the very first liquidity provider (Ltotal=0L_{total} = 0), the geometric mean of the deposited amounts defines the initial LP token supply:

L0=Δx⋅ΔyL_0 = \sqrt{\Delta x \cdot \Delta y}

Subsequent Liquidity Minting

For all subsequent deposits, the LP tokens minted ΔL\Delta L must be proportional to their share of the existing pool:

ΔL=min⁡(Δxx,Δyy)⋅Ltotal\Delta L = \min\left(\frac{\Delta x}{x}, \frac{\Delta y}{y}\right) \cdot L_{total}


5. Mathematical Derivation of Impermanent Loss

What happens to an LP when token prices change externally?

Let initial price P=yxP = \frac{y}{x}, so y=x⋅Py = x \cdot P. Since k=x⋅y=x2⋅Pk = x \cdot y = x^2 \cdot P, the reserve quantities are:

x=kP,y=k⋅Px = \sqrt{\frac{k}{P}}, \quad y = \sqrt{k \cdot P}

The total portfolio value held in the pool VAMMV_{AMM} at price PP is:

VAMM=x⋅P+y=kP⋅P+k⋅P=2k⋅PV_{AMM} = x \cdot P + y = \sqrt{\frac{k}{P}} \cdot P + \sqrt{k \cdot P} = 2\sqrt{k \cdot P}

If the price changes by a factor r=PnewPoldr = \frac{P_{new}}{P_{old}}: VAMM(r)=2k⋅(r⋅P)=2rk⋅PV_{AMM}(r) = 2\sqrt{k \cdot (r \cdot P)} = 2\sqrt{r} \sqrt{k \cdot P}

If the LP had simply held (HODL) their initial tokens (x0,y0)(x_0, y_0) outside the pool: VHODL(r)=x0⋅(r⋅P)+y0=kP⋅r⋅P+k⋅P=(1+r)k⋅PV_{HODL}(r) = x_0 \cdot (r \cdot P) + y_0 = \sqrt{\frac{k}{P}} \cdot r \cdot P + \sqrt{k \cdot P} = (1 + r)\sqrt{k \cdot P}

Dividing the two yields the famous Impermanent Loss Ratio:

VAMMVHODL=2r1+r\frac{V_{AMM}}{V_{HODL}} = \frac{2\sqrt{r}}{1 + r}

Impermanent Loss IL(r)=2r1+r−1\text{Impermanent Loss } IL(r) = \frac{2\sqrt{r}}{1 + r} - 1

Price Ratio (rr)Price ChangeImpermanent Loss (ILIL)
1.25x+25%-0.6%
1.50x+50%-2.0%
2.00x+100%-5.7%
5.00x+400%-25.5%

6. Building the DEX in Solidity

Now let’s translate this mathematical foundation into a clean, working Solidity pair contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MiniDexPair is ERC20 {
    IERC20 public immutable token0;
    IERC20 public immutable token1;

    uint256 public reserve0;
    uint256 public reserve1;

    event Mint(address indexed sender, uint256 amount0, uint256 amount1, uint256 liquidity);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1, uint256 liquidity);
    event Swap(address indexed sender, uint256 amountIn, uint256 amountOut, bool isZeroForOne);

    constructor(address _token0, address _token1) ERC20("MiniDex LP Token", "MDX-LP") {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    /// @notice Add liquidity to the pool
    function addLiquidity(uint256 amount0Desired, uint256 amount1Desired) 
        external 
        returns (uint256 liquidity) 
    {
        token0.transferFrom(msg.sender, address(this), amount0Desired);
        token1.transferFrom(msg.sender, address(this), amount1Desired);

        uint256 _totalSupply = totalSupply();
        if (_totalSupply == 0) {
            liquidity = sqrt(amount0Desired * amount1Desired);
        } else {
            liquidity = min(
                (amount0Desired * _totalSupply) / reserve0,
                (amount1Desired * _totalSupply) / reserve1
            );
        }

        require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");
        _mint(msg.sender, liquidity);

        _updateReserves();
        emit Mint(msg.sender, amount0Desired, amount1Desired, liquidity);
    }

    /// @notice Swap token0 for token1 or vice versa with a 0.3% fee
    function swap(uint256 amountIn, bool isZeroForOne) external returns (uint256 amountOut) {
        require(amountIn > 0, "INSUFFICIENT_INPUT_AMOUNT");

        IERC20 inputToken = isZeroForOne ? token0 : token1;
        IERC20 outputToken = isZeroForOne ? token1 : token0;
        uint256 reserveIn = isZeroForOne ? reserve0 : reserve1;
        uint256 reserveOut = isZeroForOne ? reserve1 : reserve0;

        inputToken.transferFrom(msg.sender, address(this), amountIn);

        // Constant Product Swap Formula with 0.3% fee
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = (reserveIn * 1000) + amountInWithFee;
        amountOut = numerator / denominator;

        require(amountOut > 0 && amountOut < reserveOut, "INSUFFICIENT_OUTPUT_AMOUNT");

        outputToken.transfer(msg.sender, amountOut);
        _updateReserves();

        emit Swap(msg.sender, amountIn, amountOut, isZeroForOne);
    }

    function _updateReserves() internal {
        reserve0 = token0.balanceOf(address(this));
        reserve1 = token1.balanceOf(address(this));
    }

    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }

    function min(uint256 x, uint256 y) internal pure returns (uint256) {
        return x < y ? x : y;
    }
}

Conclusion

The power of Uniswap lies in the fact that complex market equilibria can be reduced to deterministic algebraic invariants. In fewer than 100 lines of Solidity, you have an unstoppable, automated market maker capable of processing billions in trade volume without a central operator.

DX

Written by DX

Systems Engineer • Focused on high-performance distributed systems, low-level OS internals, and financial engineering.

← Back to all archives