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:
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 with reserve , and Token with reserve .
The core invariant dictates that before and after any trade, the product of these reserves must remain constant:
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 ()
Suppose a trader deposits amount of Token . What amount of Token should the pool return to maintain the invariant ?
Let the initial state be . After depositing , the new Token reserve becomes , and the new Token reserve becomes .
Expanding both sides:
Canceling from both sides:
Factoring out :
Solving for :
2. Incorporating the 0.3% Trading Fee
Uniswap charges a 0.30% fee on every swap to incentivize Liquidity Providers (LPs). Only of the deposited input contributes to the swap product.
Let the fee parameter be .
Replacing with :
[!NOTE] Notice how the denominator increases by , 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 of Token in terms of Token is the derivative of the invariant curve:
Execution Price
The actual average price received by the trader for their trade of size is:
Price Slippage
The relative price slippage caused by the trade is:
When (trade size is tiny relative to pool reserves), slippage approaches 0. As 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:
Initial Liquidity Minting
For the very first liquidity provider (), the geometric mean of the deposited amounts defines the initial LP token supply:
Subsequent Liquidity Minting
For all subsequent deposits, the LP tokens minted must be proportional to their share of the existing pool:
5. Mathematical Derivation of Impermanent Loss
What happens to an LP when token prices change externally?
Let initial price , so . Since , the reserve quantities are:
The total portfolio value held in the pool at price is:
If the price changes by a factor :
If the LP had simply held (HODL) their initial tokens outside the pool:
Dividing the two yields the famous Impermanent Loss Ratio:
| Price Ratio () | Price Change | Impermanent Loss () |
|---|---|---|
| 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.