Skip to content

Uniswap and AMMs: Automated Market Makers Explained

DodaTech Updated 2026-06-22 9 min read

In this tutorial, you'll learn automated market makers including Uniswap v2 and v3, the constant product formula, concentrated liquidity, and how to build your own AMM for learning. Why it matters: Uniswap is the largest DeFi protocol with over $5 billion daily volume, pioneering the AMM model that revolutionized decentralized trading without order books. By the end, you'll understand and implement the core AMM mechanics.

An Automated Market Maker (AMM) is a decentralized exchange type that uses a mathematical formula to price assets, allowing users to trade directly against a liquidity pool instead of matching orders.

The Constant Product Formula

The core of Uniswap V2 is the constant product formula: x * y = k, where x and y are token reserves in the pool.

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

// Minimal Uniswap V2 style pair
contract MinimalPair {
    uint256 public reserve0;
    uint256 public reserve1;
    
    event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out);
    
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
        require(amount0Out > 0 || amount1Out > 0, "Invalid output");
        require(amount0Out < reserve0 && amount1Out < reserve1, "Insufficient liquidity");
        
        // Calculate invariant
        uint256 balance0 = reserve0 - amount0Out;
        uint256 balance1 = reserve1 - amount1Out;
        
        // Amount in must satisfy x * y >= k
        uint256 amount0In = balance0 > reserve0 ? balance0 - reserve0 : 0;
        uint256 amount1In = balance1 > reserve1 ? balance1 - reserve1 : 0;
        
        require(amount0In > 0 || amount1In > 0, "No input");
        
        uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3); // 0.3% fee
        uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);
        
        require(balance0Adjusted * balance1Adjusted >= uint256(reserve0) * uint256(reserve1) * 1000**2, "K invariant");
        
        reserve0 = balance0;
        reserve1 = balance1;
        
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out);
    }
}

Expected behavior: The swap ensures the constant product (k) increases slightly due to the 0.3% fee, which is distributed to liquidity providers.

flowchart LR
  A[User sends
Token A] --> B[AMM Pool] B --> C[Formula: x * y = k] C --> D[Calculate output
based on reserves] D --> E[Apply 0.3% fee] E --> F[User receives
Token B] B --> G[Reserves updated] G --> H[New price = y / x] H --> I[Price impact
depends on trade size]

Price Impact Calculation

Trades change the pool ratio, creating price impact. Large trades relative to pool size cause significant slippage.

function calculatePriceImpact(amountIn, reserveIn, reserveOut) {
  // Constant product: x * y = k
  // After swap: (reserveIn + amountIn) * (reserveOut - amountOut) = k
  const fee = 0.003; // 0.3%
  const amountInAfterFee = amountIn * (1 - fee);
  
  const amountOut = (reserveOut * amountInAfterFee) / (reserveIn + amountInAfterFee);
  
  // Price without impact (mid-market rate)
  const midPrice = reserveIn / reserveOut;
  const executionPrice = amountIn / amountOut;
  
  const priceImpact = ((executionPrice - midPrice) / midPrice) * 100;
  
  return {
    amountOut,
    executionPrice,
    midPrice: 1 / midPrice,
    priceImpact: Math.abs(priceImpact),
  };
}

// Example: Trading against a 100 ETH / 200,000 DAI pool
const result1 = calculatePriceImpact(1, 100, 200000);
console.log(`Swap 1 ETH: Output = ${result1.amountOut.toFixed(2)} DAI, Impact = ${result1.priceImpact.toFixed(2)}%`);
// Expected: Swap 1 ETH: Output = 1980.20 DAI, Impact = 0.99%

const result2 = calculatePriceImpact(10, 100, 200000);
console.log(`Swap 10 ETH: Output = ${result2.amountOut.toFixed(2)} DAI, Impact = ${result2.priceImpact.toFixed(2)}%`);
// Expected: Swap 10 ETH: Output = 18118.81 DAI, Impact = 10.0%

Uniswap V3 Concentrated Liquidity

Uniswap V3 allows LPs to concentrate liquidity within custom price ranges, dramatically improving capital efficiency.

Feature Uniswap V2 Uniswap V3
Liquidity Full range (0 to infinity) Custom price range
Capital efficiency Low Up to 4000x higher
Fee tiers 0.3% 0.05%, 0.30%, 1.00%
LP risk Impermanent loss IL + concentrated range loss
Position NFTs No Yes (ERC-721)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Simplified V3 concentrated liquidity concept
contract ConcentratedLiquidity {
    struct Position {
        address owner;
        int24 tickLower;
        int24 tickUpper;
        uint128 liquidity;
    }
    
    mapping(uint256 => Position) public positions;
    uint256 public nextPositionId;
    
    // Tick math: price = 1.0001^tick
    function tickToPrice(int24 tick) public pure returns (uint256) {
        return uint256(1.0001 ** int256(tick));
    }
    
    function calculateLiquidityForRange(
        uint256 amount0,
        uint256 amount1,
        int24 tickLower,
        int24 tickUpper,
        int24 currentTick
    ) public pure returns (uint128) {
        // Simplified: returns liquidity amount based on position range
        if (currentTick < tickLower) {
            // Only token0 is used (price below range)
            return uint128(amount0);
        } else if (currentTick >= tickUpper) {
            // Only token1 is used (price above range)
            return uint128(amount1);
        } else {
            // Both tokens used (price in range)
            return uint128((amount0 + amount1) / 2);
        }
    }
}

Expected behavior: LPs can provide liquidity in specific price ranges. If the price exits their range, their liquidity becomes inactive (only one token remains) and they stop earning fees until the price re-enters their range.

Adding Liquidity to a Pool

Calculate how much of each token to deposit for a given pool share.

async function calculateLiquidityAddition(reserve0, reserve1, totalSupply, desiredSharePercent) {
  const share = desiredSharePercent / 100;
  const amount0 = (reserve0 * share) / (1 - share);
  const amount1 = (reserve1 * share) / (1 - share);
  
  return {
    amount0,
    amount1,
    share,
    liquidityTokens: totalSupply * share,
  };
}

// Example: You want 1% of a pool with 100 ETH and 200,000 DAI
const result = calculateLiquidityAddition(100, 200000, 1000000, 1);
console.log(`Deposit: ${result.amount0.toFixed(4)} ETH and ${result.amount1.toFixed(2)} DAI`);
console.log(`Receive: ${result.liquidityTokens.toFixed(0)} LP tokens`);
// Expected:
// Deposit: 1.0101 ETH and 2020.20 DAI
// Receive: 10000 LP tokens

Building a Simple AMM

Create a complete minimal AMM contract and test it.

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

contract SimpleAMM {
    IERC20 public token0;
    IERC20 public token1;
    uint256 public reserve0;
    uint256 public reserve1;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    
    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1);
    event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out);
    
    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }
    
    function mint(uint256 amount0, uint256 amount1) external returns (uint256 liquidity) {
        token0.transferFrom(msg.sender, address(this), amount0);
        token1.transferFrom(msg.sender, address(this), amount1);
        
        if (totalSupply == 0) {
            liquidity = Math.sqrt(amount0 * amount1);
        } else {
            liquidity = Math.min(
                (amount0 * totalSupply) / reserve0,
                (amount1 * totalSupply) / reserve1
            );
        }
        
        require(liquidity > 0, "Insufficient liquidity minted");
        balanceOf[msg.sender] += liquidity;
        totalSupply += liquidity;
        reserve0 += amount0;
        reserve1 += amount1;
        
        emit Mint(msg.sender, amount0, amount1);
    }
    
    function burn(uint256 liquidity) external returns (uint256 amount0, uint256 amount1) {
        amount0 = (reserve0 * liquidity) / totalSupply;
        amount1 = (reserve1 * liquidity) / totalSupply;
        
        balanceOf[msg.sender] -= liquidity;
        totalSupply -= liquidity;
        reserve0 -= amount0;
        reserve1 -= amount1;
        
        token0.transfer(msg.sender, amount0);
        token1.transfer(msg.sender, amount1);
        
        emit Burn(msg.sender, amount0, amount1);
    }
    
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
        require(amount0Out > 0 || amount1Out > 0, "Invalid output");
        require(amount0Out < reserve0 && amount1Out < reserve1, "Insufficient liquidity");
        
        uint256 balance0 = reserve0 - amount0Out;
        uint256 balance1 = reserve1 - amount1Out;
        
        uint256 amount0In = balance0 > reserve0 ? balance0 - reserve0 : 0;
        uint256 amount1In = balance1 > reserve1 ? balance1 - reserve1 : 0;
        
        require(amount0In > 0 || amount1In > 0, "No input");
        
        uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3);
        uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);
        
        require(balance0Adjusted * balance1Adjusted >= uint256(reserve0) * uint256(reserve1) * 1e6, "K");
        
        reserve0 = balance0;
        reserve1 = balance1;
        
        if (amount0Out > 0) token0.transfer(to, amount0Out);
        if (amount1Out > 0) token1.transfer(to, amount1Out);
        
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out);
    }
}

Expected behavior: Users provide equal value of both tokens to mint LP tokens. Burning LP tokens redeems a proportional share of reserves. Swaps maintain the constant product with a 0.3% fee.

Common Errors and Misunderstandings

1. Assuming k Never Changes

k increases with fees (LP profit) and decreases slightly due to rounding. It is not perfectly constant in practice.

2. Ignoring Slippage Protection

Without slippage limits, MEV bots can sandwich your trade. Always set amountOutMin to at least 0.5-1% below the expected output.

3. Adding Imbalanced Liquidity

Adding single-sided liquidity in V2 creates arbitrage opportunities against you. Always add both tokens at the current pool ratio.

4. V3 Range Too Narrow

Setting too narrow a price range in V3 causes the position to exit range quickly, earning no fees until the price returns.

5. Forgetting Fee Tiers

Choosing the wrong fee tier in V3 affects returns. Stablecoin pairs should use 0.05%, volatile pairs 0.30%, exotic pairs 1.00%.

Practice Questions

  1. What is the constant product formula in Uniswap V2? x * y = k, where x and y are the reserves of two tokens. The product remains constant after trades, ensuring the pool never runs out of liquidity.

  2. How does concentrated liquidity differ from uniform liquidity? V3 allows LPs to provide liquidity within custom price ranges instead of the full range (0 to infinity). This concentrates capital where trades happen, improving efficiency up to 4000x.

  3. What causes price impact in AMMs? Large trades relative to pool size shift the reserve ratio, changing the price. The constant product formula ensures that larger trades receive progressively worse rates.

  4. How do LPs earn fees? Every trade pays a fee (0.3% V2, variable V3). Fees are added to the pool reserves, increasing k. LP tokens represent a share of the pool and can be burned to withdraw the accumulated fees.

  5. What happens to V3 liquidity when price exits the range? The position becomes inactive. Only one token remains (the one that appreciated). The LP stops earning fees until the price re-enters their specified range.

Challenge

Build a complete AMM with concentrated liquidity (like Uniswap V3) in Solidity with three fee tiers, tick-based pricing, position NFTs, and fee tracking per position. Write tests proving capital efficiency is higher than V2 for the same liquidity amount.

Real-World Task

Create a simulated trading bot using Node.js that monitors a Uniswap V3 pool, calculates optimal swap routes across multiple pools (multi-hop), executes arbitrage between Uniswap and other DEXes, and tracks profit/loss with gas cost accounting using TypeScript.

Frequently Asked Questions

What is the difference between Uniswap V2 and V3?

V2 provides uniform liquidity across all price ranges. V3 enables concentrated liquidity within custom ranges, dramatically improving capital efficiency but requiring active management. V3 also introduces multiple fee tiers and NFT-based position representation.

What is impermanent loss in AMMs?

Impermanent loss occurs when the price ratio of pooled tokens changes. LPs would have been better off holding the tokens. The loss is "impermanent" only if the ratio returns to the deposit ratio. In volatile markets, IL can exceed fee earnings.

How does an AMM determine prices?

The price is determined by the reserve ratio: price = reserve1 / reserve0. When a trade changes reserves, the ratio shifts, creating a new price. Large trades push the price further, creating slippage that protects the pool from draining.

Next Steps

After mastering AMMs, explore Layer 2 scaling on rollups, then dive into DeFi protocol composition for building complex financial applications.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro