Decentralized Exchange (DEX) and Automated Market Maker (AMM)

Why DEXs Matter

A Decentralized Exchange (DEX) is the core infrastructure of DeFi. Unlike Binance or Coinbase — where you deposit funds and trust the exchange to hold them — a DEX lets you trade directly from your wallet. No registration, no KYC, no withdrawal limits. Your assets never leave your custody.

Uniswap, the first major AMM-based DEX, launched in 2018 and revolutionized crypto trading. Today, DEXs process over $100 billion in monthly volume across Ethereum, BNB Chain, Arbitrum, and other networks.

Why this matters for your career:

  • DEXs are the backbone of DeFi — every token needs liquidity to be tradeable
  • Understanding AMM mechanics is essential for any Web3 developer
  • Building a DEX is a classic smart contract interview challenge
  • DEX concepts (liquidity pools, impermanent loss, slippage) appear in every DeFi protocol

What Is an Automated Market Maker (AMM)?

Before AMMs, crypto exchanges used order books — buyers and sellers post orders, and a matching engine finds counterparts. This works well for popular pairs (BTC/USDT) but fails for new or low-volume tokens.

Uniswap's breakthrough was the Constant Product Formula:

$$x \times y = k$$

  • $x$ = the amount of Token A in the pool
  • $y$ = the amount of Token B in the pool
  • $k$ = a constant (the product never changes)

This single formula guarantees that the pool always has liquidity, no matter how large the trade. When someone buys Token A, $x$ decreases and $y$ increases. The price adjusts automatically based on the ratio.

How the Formula Works in Practice

| Scenario | Token A in Pool | Token B in Pool | Price (A/B) | |:---------|:---------------:|:---------------:|:-----------:| | Initial | 100 ETH | 200,000 USDC | 1 ETH = 2,000 USDC | | Buy 10 ETH | 90 ETH | ~222,222 USDC | 1 ETH = ~2,469 USDC | | Buy another 10 ETH | 80 ETH | ~250,000 USDC | 1 ETH = ~3,125 USDC |

Notice that each purchase makes ETH more expensive — this is slippage, and it protects the pool from being drained. The larger the trade relative to the pool size, the more slippage occurs.

Implementing the AMM

// contracts/SimpleDEX.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract SimpleDEX is Ownable {
    IERC20 public tokenA;
    IERC20 public tokenB;
    
    uint256 public reserveA;
    uint256 public reserveB;
    
    uint256 public totalLiquidity;
    mapping(address => uint256) public liquidity;
    
    // Events
    event LiquidityAdded(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 shares
    );
    event LiquidityRemoved(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 shares
    );
    event Swapped(
        address indexed trader,
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );
    
    constructor(address _tokenA, address _tokenB) Ownable(msg.sender) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }
    
    // === Add Liquidity ===
    function addLiquidity(
        uint256 _amountA,
        uint256 _amountB
    ) external returns (uint256 shares) {
        require(_amountA > 0 && _amountB > 0, "Amounts must be greater than 0");
        
        // Transfer tokens to contract
        tokenA.transferFrom(msg.sender, address(this), _amountA);
        tokenB.transferFrom(msg.sender, address(this), _amountB);
        
        if (totalLiquidity == 0) {
            // First time adding liquidity
            shares = _amountA;  // Simplified: shares = amountA
        } else {
            // Calculate proportional shares
            shares = (_amountA * totalLiquidity) / reserveA;
        }
        
        reserveA += _amountA;
        reserveB += _amountB;
        
        liquidity[msg.sender] += shares;
        totalLiquidity += shares;
        
        emit LiquidityAdded(msg.sender, _amountA, _amountB, shares);
    }
    
    // === Remove Liquidity ===
    function removeLiquidity(
        uint256 _shares
    ) external returns (uint256 amountA, uint256 amountB) {
        require(_shares > 0, "Shares must be greater than 0");
        require(liquidity[msg.sender] >= _shares, "Insufficient shares");
        
        // Calculate withdrawable amounts
        amountA = (reserveA * _shares) / totalLiquidity;
        amountB = (reserveB * _shares) / totalLiquidity;
        
        liquidity[msg.sender] -= _shares;
        totalLiquidity -= _shares;
        
        reserveA -= amountA;
        reserveB -= amountB;
        
        // Transfer tokens back
        tokenA.transfer(msg.sender, amountA);
        tokenB.transfer(msg.sender, amountB);
        
        emit LiquidityRemoved(msg.sender, amountA, amountB, _shares);
    }
    
    // === Calculate Swap Output ===
    function getAmountOut(
        uint256 _amountIn,
        uint256 _reserveIn,
        uint256 _reserveOut
    ) public pure returns (uint256) {
        require(_amountIn > 0, "Input amount must be greater than 0");
        require(_reserveIn > 0 && _reserveOut > 0, "Insufficient liquidity");
        
        // 0.3% fee
        uint256 amountInWithFee = _amountIn * 997;
        uint256 numerator = amountInWithFee * _reserveOut;
        uint256 denominator = (_reserveIn * 1000) + amountInWithFee;
        
        return numerator / denominator;
    }
    
    // === Swap Tokens ===
    function swapAForB(uint256 _amountIn) external returns (uint256 amountOut) {
        require(_amountIn > 0, "Input amount must be greater than 0");
        
        amountOut = getAmountOut(_amountIn, reserveA, reserveB);
        require(amountOut > 0, "Output is zero");
        
        tokenA.transferFrom(msg.sender, address(this), _amountIn);
        tokenB.transfer(msg.sender, amountOut);
        
        reserveA += _amountIn;
        reserveB -= amountOut;
        
        emit Swapped(msg.sender, address(tokenA), address(tokenB), _amountIn, amountOut);
    }
    
    function swapBForA(uint256 _amountIn) external returns (uint256 amountOut) {
        require(_amountIn > 0, "Input amount must be greater than 0");
        
        amountOut = getAmountOut(_amountIn, reserveB, reserveA);
        require(amountOut > 0, "Output is zero");
        
        tokenB.transferFrom(msg.sender, address(this), _amountIn);
        tokenA.transfer(msg.sender, amountOut);
        
        reserveB += _amountIn;
        reserveA -= amountOut;
        
        emit Swapped(msg.sender, address(tokenB), address(tokenA), _amountIn, amountOut);
    }
    
    // === View Current Prices ===
    function getPriceA() external view returns (uint256) {
        require(reserveA > 0 && reserveB > 0, "Insufficient liquidity");
        return (reserveB * 1e18) / reserveA;
    }
    
    function getPriceB() external view returns (uint256) {
        require(reserveA > 0 && reserveB > 0, "Insufficient liquidity");
        return (reserveA * 1e18) / reserveB;
    }
}

Testing the AMM

// scripts/test_dex.js
async function main() {
  const [deployer, user1, user2] = await hre.ethers.getSigners();
  
  // Deploy tokens
  const VibeToken = await hre.ethers.getContractFactory("VibeToken");
  const tokenA = await VibeToken.deploy(deployer.address);
  await tokenA.waitForDeployment();
  
  const tokenB = await VibeToken.deploy(deployer.address);
  await tokenB.waitForDeployment();
  
  // Deploy DEX
  const SimpleDEX = await hre.ethers.getContractFactory("SimpleDEX");
  const dex = await SimpleDEX.deploy(
    await tokenA.getAddress(),
    await tokenB.getAddress()
  );
  await dex.waitForDeployment();
  
  console.log("DEX deployed at:", await dex.getAddress());
  
  // Prepare liquidity
  const amountA = hre.ethers.parseEther("1000");
  const amountB = hre.ethers.parseEther("1000");
  
  await tokenA.approve(await dex.getAddress(), amountA);
  await tokenB.approve(await dex.getAddress(), amountB);
  
  // Add liquidity
  await dex.addLiquidity(amountA, amountB);
  console.log("Liquidity added: 1000 A + 1000 B");
  
  // Check price
  const priceA = await dex.getPriceAInB();
  console.log("1 A = ", hre.ethers.formatEther(priceA), "B");
  
  // Swap A for B
  const swapAmount = hre.ethers.parseEther("100");
  await tokenA.connect(user1).approve(await dex.getAddress(), swapAmount);
  
  // Give user1 some A tokens
  await tokenA.transfer(user1.address, swapAmount);
  
  const result = await dex.connect(user1).swapAForB(swapAmount);
  console.log(`Swapped 100 A for B tokens`);
  
  // Check new price (slippage effect)
  const newPriceA = await dex.getPriceAInB();
  console.log("After swap, 1 A = ", hre.ethers.formatEther(newPriceA), "B");
  console.log("Slippage occurred! Deeper pools reduce slippage.");
}

main().catch(console.error);

How to Use Vibe Coding to Build a DEX

Instead of writing the AMM math yourself, describe your DEX requirements to AI. Here is a battle-tested prompt:

🔥 Vibe Coding Prompt for DEX Creation "Build a decentralized exchange with the following features:

  1. Support ETH/Token trading pairs (not just Token/Token)
  2. Use the x * y = k AMM formula from Uniswap V2
  3. Charge 0.3% fee on every swap, automatically added to the pool
  4. Liquidity providers can add or remove liquidity at any time
  5. Write a test script: deploy → add liquidity → swap → remove liquidity
  6. Calculate and display slippage for each trade"

AMM Security Considerations

| Issue | Why It Matters | Mitigation | |:------|:---------------|:-----------| | Front-running | Attackers see pending swaps and trade before them | Use a commit-reveal scheme or a private mempool | | Oracle manipulation | Attackers can drain pools by manipulating the spot price | Use TWAP (Time-Weighted Average Price) oracles | | Flash loan attacks | Attackers borrow millions, manipulate price, and repay in one transaction | Integrate a TWAP oracle for critical operations | | Impermanent loss | LPs lose value when prices diverge from the initial ratio | Educate LPs about IL; consider concentrated liquidity | | Slippage sandwich attacks | Bots sandwich user trades to extract profit | Let users set a minimum output amount (slippage tolerance) | | Infinite approval | DEX can drain all tokens from users who approved it | Encourage users to approve only the amount they are swapping |

Summary

You have now built a fully functional Automated Market Maker — the same technology that powers Uniswap, PancakeSwap, and SushiSwap:

  • What you built: A constant-product AMM with liquidity pools, swap functions, price queries, and LP token tracking
  • Why it matters: DEXs are the backbone of DeFi — they enable permissionless trading without intermediaries
  • How it works: The $x \times y = k$ formula guarantees liquidity at any price; the 0.3% fee compensates liquidity providers; slippage protects the pool from being drained

Key takeaways:

  • The constant product formula $x \times y = k$ is the foundation of all AMM-based DEXs
  • Slippage is not a bug — it is a feature that protects pools from large, price-moving trades
  • The 0.3% fee (997/1000) is the Uniswap standard; it gets distributed to liquidity providers
  • Liquidity providers earn fees proportional to their share of the pool
  • Impermanent loss is the main risk for LPs — it occurs when the external price diverges from the pool price
  • Front-running and sandwich attacks are the main security concerns — use slippage tolerance to protect users

What Is Next: Lending Protocol

Now that you understand how decentralized trading works, the next chapter teaches you how to build a lending protocol — the other pillar of DeFi alongside DEXs. You will learn how users can deposit assets to earn interest, borrow assets against collateral, and how liquidation works when positions become undercollateralized. This is the technology behind Aave, Compound, and Morpho.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!