IAMUVIN

DeFi Deep Dives

Decentralized Exchange Architecture: How DEXs Actually Work

Uvin Vindula·March 10, 2025·13 min read
Share

TL;DR

Decentralized exchanges are the backbone of DeFi, and most developers use them without understanding how they actually work under the hood. In this article, I break down the full architecture — from the constant product formula that powers Uniswap V2, to the concentrated liquidity tick system in V3, the router contract that orchestrates multi-hop swaps, the execution flow of a single trade from user click to on-chain settlement, slippage mechanics and MEV protection strategies, fee tier economics for liquidity providers, and how aggregators like 1inch split orders across multiple DEXs for optimal pricing. I've built DEX components for clients across Ethereum and L2s. This is the technical foundation I wish someone had written when I was first diving into AMM internals — not the whitepaper abstractions, but the actual contract architecture, the math, and the Solidity patterns that make it all work.


AMM vs Order Book

Every exchange — centralized or decentralized — needs a mechanism to match buyers with sellers. There are two fundamental approaches, and understanding the tradeoffs between them is the starting point for understanding DEX architecture.

Order book exchanges work the way traditional finance has worked for centuries. Buyers post bids, sellers post asks, and a matching engine pairs them up. Binance, Coinbase, the NYSE — they all use order books. The price is determined by the intersection of supply and demand curves formed by individual orders.

On-chain order books are technically possible, but they're expensive. Every limit order placement, cancellation, and modification is a transaction that costs gas. On Ethereum L1, running a full order book DEX is economically impractical. That's why projects like dYdX moved to their own appchain, and why on-chain order books mostly live on high-throughput chains like Solana (Serum, Phoenix) or purpose-built rollups.

Automated Market Makers (AMMs) took a completely different approach. Instead of matching individual orders, AMMs use liquidity pools and mathematical formulas to determine price. Anyone can deposit tokens into a pool, and anyone can trade against that pool. The price adjusts automatically based on the ratio of tokens in the pool.

Here's the fundamental comparison:

┌─────────────────────┬──────────────────────┬──────────────────────┐
│                     │ ORDER BOOK           │ AMM                  │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Price Discovery     │ Bid/ask matching     │ Mathematical formula │
│ Liquidity Source    │ Market makers        │ Passive LPs          │
│ Capital Efficiency  │ High                 │ Low (V2) / High (V3) │
│ Gas Cost per Trade  │ High (many txns)     │ Low (single swap)    │
│ MEV Exposure        │ Front-running orders │ Sandwich attacks     │
│ Impermanent Loss    │ None                 │ Yes                  │
│ Best For            │ High-frequency       │ Long-tail assets     │
└─────────────────────┴──────────────────────┴──────────────────────┘

The AMM model won on Ethereum because it solved the cold start problem. You don't need professional market makers to bootstrap a new trading pair. Anyone can deposit equal value of two tokens, and trading begins immediately. That single innovation is why DeFi exploded in 2020.


Uniswap V2 Architecture

Uniswap V2 is the most forked protocol in DeFi history, and for good reason — its architecture is elegant in its simplicity. Understanding V2 is the prerequisite to understanding everything that came after.

The core of V2 is the constant product formula:

x * y = k

Where x is the reserve of Token A, y is the reserve of Token B, and k is a constant that can only increase (from fees). When a trader swaps Token A for Token B, they add Token A to the pool and remove Token B. The formula ensures that the product of the reserves remains constant, which automatically determines the exchange rate.

Let's trace through a concrete example. A pool has 100 ETH and 200,000 USDC:

Initial state:
  x = 100 ETH
  y = 200,000 USDC
  k = 100 * 200,000 = 20,000,000
  Price: 200,000 / 100 = 2,000 USDC per ETH

Trader swaps 10 ETH for USDC:
  New x = 100 + 10 = 110 ETH
  New y = k / new_x = 20,000,000 / 110 = 181,818.18 USDC
  USDC received = 200,000 - 181,818.18 = 18,181.82 USDC
  Effective price: 18,181.82 / 10 = 1,818.18 USDC per ETH

Price impact: (2,000 - 1,818.18) / 2,000 = 9.09%

Notice the trader didn't get 20,000 USDC for their 10 ETH — they got 18,181.82. That difference is price impact, and it's a fundamental property of the constant product curve. Larger trades relative to pool size create more price impact.

The V2 contract architecture has three main components:

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Factory    │────>│    Pair      │<────│    Router    │
│              │     │  (Pool)      │     │              │
│ createPair() │     │ swap()       │     │ swapExact*() │
│ getPair()    │     │ mint()       │     │ addLiquidity()│
│ allPairs()   │     │ burn()       │     │ removeLiq()  │
└──────────────┘     └──────────────┘     └──────────────┘
       │                    │                     │
       │   Deploys pairs    │   Users interact    │
       │   via CREATE2      │   through Router    │
       └────────────────────┘─────────────────────┘

Factory: Deploys new pair contracts using CREATE2 for deterministic addresses. One pair per unique token combination. The factory stores the mapping of token pairs to pool addresses.

Pair (Pool): Holds the reserves of two tokens. Implements swap(), mint() (add liquidity), and burn() (remove liquidity). The pair contract is also an ERC-20 token — LP tokens represent your proportional share of the pool.

Router: The user-facing contract. Handles the complexity of multi-hop swaps, deadline enforcement, and minimum output amounts. Users never interact with pair contracts directly.

One detail that trips up most developers: Uniswap V2 uses a 0.3% fee on every swap. This fee stays in the pool, increasing k over time. LPs earn this fee proportionally to their share of the pool. There's also an optional protocol fee (1/6 of the LP fee, or 0.05%) that can be turned on by governance.


Uniswap V3 — Concentrated Liquidity

V2's biggest weakness is capital inefficiency. In a V2 pool, liquidity is spread uniformly across the entire price range from 0 to infinity. But most trading happens within a narrow band. If ETH is trading at $2,000, the liquidity sitting at prices of $50 or $50,000 is effectively doing nothing.

Uniswap V3 solved this with concentrated liquidity. LPs can choose a specific price range to provide liquidity in. Instead of spreading capital from 0 to infinity, you can concentrate it between, say, $1,800 and $2,200. The result is dramatically higher capital efficiency — up to 4,000x more capital-efficient than V2 for stablecoin pairs.

The math behind concentrated liquidity uses a tick system. The full price range is divided into discrete ticks, each representing a 0.01% price change:

tick = log(price) / log(1.0001)

At price $2,000:
  tick = log(2000) / log(1.0001) = 76,012

Tick spacing depends on fee tier:
  0.01% fee → tick spacing 1    (stablecoins)
  0.05% fee → tick spacing 10   (correlated pairs)
  0.30% fee → tick spacing 60   (most pairs)
  1.00% fee → tick spacing 200  (exotic pairs)

When an LP provides liquidity between price A and price B, they're providing liquidity between tick A and tick B. The contract tracks liquidity at each tick boundary. As the price moves through ticks during a swap, liquidity is added or removed from the active range.

Here's the key insight: within each tick, the math is the same as V2's constant product formula, but applied to a virtual reserve that's much larger than the actual deposited capital. That's where the capital efficiency comes from.

V2 LP: $100K spread across all prices
  → Effective liquidity at current price: ~$500

V3 LP: $100K concentrated in ±5% range
  → Effective liquidity at current price: ~$50,000
  → 100x more capital efficient

The tradeoff is that concentrated liquidity positions are not fungible. In V2, all LPs in a pool hold the same position — they receive standard ERC-20 LP tokens. In V3, each position is unique (different tick ranges), so positions are represented as NFTs (ERC-721 tokens). This makes V3 LP positions harder to compose with other DeFi protocols.

The other tradeoff is increased impermanent loss risk. Concentrated liquidity amplifies returns when price stays in range, but if price moves outside your range, you end up holding 100% of the less valuable token. Active position management becomes essential — which is why protocols like Arrakis Finance and Gamma Strategies exist to automate V3 LP management.


The Router Contract

The Router is where user intent meets on-chain execution. It's the contract that most users and frontends actually interact with, and understanding its role is critical for building anything that touches a DEX.

The Router serves several functions:

Multi-hop routing: If there's no direct pool for the pair you want to swap, the Router chains swaps through intermediate tokens. Want to swap LINK for AAVE? The Router might route through LINK → ETH → AAVE, executing two swaps in a single transaction.

Direct swap:        TokenA → TokenB (single pool)
2-hop swap:         TokenA → WETH → TokenB (two pools)
3-hop swap:         TokenA → USDC → WETH → TokenB (three pools)

The path is encoded as: [tokenA, fee, tokenB, fee, tokenC]

Deadline enforcement: Every Router function accepts a deadline parameter — a Unix timestamp after which the transaction reverts. This prevents stale transactions sitting in the mempool from executing at outdated prices.

Minimum output protection: The amountOutMin parameter is the user's slippage tolerance encoded as a minimum acceptable output. If the swap would produce fewer tokens than this threshold, the entire transaction reverts.

WETH wrapping: Ethereum's native ETH doesn't conform to the ERC-20 standard, so the Router transparently wraps and unwraps ETH to WETH for pool interactions.

Here's the flow of a swapExactTokensForTokens call:

1. User approves Router to spend TokenA
2. User calls router.swapExactTokensForTokens(
     amountIn,          // exact amount of TokenA to swap
     amountOutMin,      // minimum TokenB to receive
     path,              // [TokenA, TokenB] or multi-hop
     to,                // recipient address
     deadline           // revert if past this timestamp
   )
3. Router transfers TokenA from user to first pool
4. Pool executes swap, sends TokenB to next pool (or to user)
5. Router checks: amountOut >= amountOutMin
6. If check fails → entire transaction reverts

Swap Execution Flow

Let me trace through exactly what happens on-chain when you click "Swap" in the Uniswap interface. This is the flow I walk through with every client who's building a DEX integration.

┌─────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  User    │────>│ Frontend │────>│  Router  │────>│   Pool   │
│ (Wallet) │     │  (dApp)  │     │ Contract │     │ Contract │
└─────────┘     └──────────┘     └──────────┘     └──────────┘
     │                │                │                │
     │  1. Connect    │                │                │
     │  wallet        │                │                │
     │                │  2. Fetch      │                │
     │                │  quotes via    │                │
     │                │  Quoter        │                │
     │                │  contract      │                │
     │  3. Approve    │                │                │
     │  token spend   │                │                │
     │  (ERC-20)      │                │                │
     │                │  4. Build tx   │                │
     │                │  with slippage │                │
     │                │  + deadline    │                │
     │  5. Sign &     │                │                │
     │  submit tx     │                │                │
     │                │                │  6. Transfer   │
     │                │                │  tokenA in     │
     │                │                │  7. Calculate  │
     │                │                │  output via    │
     │                │                │  x*y=k         │
     │                │                │  8. Transfer   │
     │                │                │  tokenB out    │
     │                │                │  9. Update     │
     │                │                │  reserves      │
     │                │                │  10. Emit      │
     │                │                │  Swap event    │
     │                │  11. Verify    │                │
     │                │  amountOut >=  │                │
     │                │  amountOutMin  │                │

Step 7 is where the actual pricing math happens. The pool contract reads its current reserves, applies the fee, calculates the output using the invariant formula, and transfers the output tokens. The reserves are then updated to reflect the new balances.

One critical implementation detail: Uniswap V2 uses a flash swap pattern where the output tokens are sent first, and the contract verifies the input was received afterward. This is the same pattern that enables flash loans. V3 uses a callback pattern — the pool calls back into the Router, which then transfers the input tokens.


Slippage and MEV Protection

Slippage and MEV are two sides of the same coin. Slippage is the difference between the expected price and the execution price. MEV (Maximal Extractable Value) is profit extracted by validators or searchers by reordering, inserting, or censoring transactions.

The most common MEV attack on DEX users is the sandwich attack:

Mempool contains: User wants to buy 10 ETH with USDC

Attacker sees this and:
  1. FRONT-RUN: Buy ETH before user (pushes price up)
  2. USER TX: User buys ETH at inflated price
  3. BACK-RUN: Attacker sells ETH at higher price (profit)

The user gets fewer tokens. The attacker pockets the difference.

Protection strategies exist at multiple layers:

Smart contract level: The amountOutMin parameter is the first line of defense. If the sandwich attack pushes the price too far, the user's transaction reverts. Setting slippage tolerance to 0.5% means an attacker can extract at most 0.5% of your trade value before the transaction fails.

Transaction level: Private mempools (Flashbots Protect, MEV Blocker) route transactions directly to block builders, bypassing the public mempool entirely. The attacker never sees your transaction. I recommend every frontend integration use private RPCs by default.

Protocol level: Some DEXs implement time-weighted average prices (TWAP) for oracle-resistant execution, or batch auctions (CoW Protocol) that match trades at a single clearing price, eliminating the possibility of sandwich attacks entirely.

Permit2 and signature-based approvals: Uniswap's Permit2 contract reduces the attack surface by allowing gasless, time-limited token approvals. Instead of an open-ended ERC-20 approval, users sign a permit that's valid for a specific amount and duration.

solidity
// Bad: unlimited approval, permanent attack surface
token.approve(router, type(uint256).max);

// Better: exact amount approval
token.approve(router, amountIn);

// Best: Permit2 with deadline
permit2.permit(token, spender, amount, deadline, signature);

Fee Tiers and LP Economics

Uniswap V3 introduced multiple fee tiers per token pair, allowing the market to find the optimal fee for each pair's volatility profile.

┌──────────┬────────────────────────────────────────┐
│ Fee Tier │ Best For                               │
├──────────┼────────────────────────────────────────┤
│ 0.01%    │ Stablecoin pairs (USDC/USDT)           │
│ 0.05%    │ Correlated pairs (ETH/stETH)           │
│ 0.30%    │ Standard pairs (ETH/USDC)              │
│ 1.00%    │ Exotic/volatile pairs (PEPE/ETH)       │
└──────────┴────────────────────────────────────────┘

LP economics in V3 are fundamentally different from V2. In V2, you deposit and forget — your position earns fees proportional to your share of the pool. In V3, your position only earns fees when the current price is within your chosen range. An out-of-range position earns zero fees and holds 100% of the less valuable token.

The key metrics for evaluating LP profitability:

Fee APR: Total fees earned by the pool divided by total liquidity, annualized. For active V3 positions, this needs to be calculated per tick range.

Impermanent loss: The difference between holding tokens in a pool vs holding them in your wallet. For V3 concentrated positions, IL is amplified by the concentration factor.

Net APR = Fee APR - Impermanent Loss - Gas Costs

In my experience building LP dashboards, the LPs who consistently profit in V3 are either running automated rebalancing strategies or providing liquidity in stablecoin pairs where directional risk is minimal. Passive V3 LPing in volatile pairs is negative EV for most participants.


Building a Simple DEX — Solidity

Here's a minimal AMM implementation that captures the core mechanics. This is stripped down for clarity — production DEXs need reentrancy protection, oracle integration, and proper access control.

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

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

contract SimpleAMM is ERC20, ReentrancyGuard {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;

    uint256 public reserveA;
    uint256 public reserveB;

    uint256 public constant FEE_NUMERATOR = 997;
    uint256 public constant FEE_DENOMINATOR = 1000; // 0.3% fee

    event Swap(
        address indexed sender,
        uint256 amountIn,
        uint256 amountOut,
        bool aToB
    );

    event LiquidityAdded(
        address indexed provider,
        uint256 amountA,
        uint256 amountB,
        uint256 lpTokens
    );

    constructor(
        address _tokenA,
        address _tokenB
    ) ERC20("LP Token", "LP") {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }

    function addLiquidity(
        uint256 amountA,
        uint256 amountB
    ) external nonReentrant returns (uint256 lpTokens) {
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);

        if (totalSupply() == 0) {
            lpTokens = sqrt(amountA * amountB);
        } else {
            lpTokens = min(
                (amountA * totalSupply()) / reserveA,
                (amountB * totalSupply()) / reserveB
            );
        }

        require(lpTokens > 0, "Insufficient liquidity minted");
        _mint(msg.sender, lpTokens);

        reserveA += amountA;
        reserveB += amountB;

        emit LiquidityAdded(msg.sender, amountA, amountB, lpTokens);
    }

    function swap(
        address tokenIn,
        uint256 amountIn,
        uint256 amountOutMin
    ) external nonReentrant returns (uint256 amountOut) {
        require(
            tokenIn == address(tokenA) || tokenIn == address(tokenB),
            "Invalid token"
        );

        bool aToB = tokenIn == address(tokenA);
        (uint256 resIn, uint256 resOut) = aToB
            ? (reserveA, reserveB)
            : (reserveB, reserveA);

        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);

        uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
        amountOut = (amountInWithFee * resOut) /
            (resIn * FEE_DENOMINATOR + amountInWithFee);

        require(amountOut >= amountOutMin, "Slippage exceeded");

        if (aToB) {
            reserveA += amountIn;
            reserveB -= amountOut;
            tokenB.transfer(msg.sender, amountOut);
        } else {
            reserveB += amountIn;
            reserveA -= amountOut;
            tokenA.transfer(msg.sender, amountOut);
        }

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

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

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

This contract demonstrates the core pattern: reserves track token balances, the constant product formula determines output amounts, fees are applied before the calculation, and LP tokens represent proportional ownership. In a production contract, you'd add oracle price feeds for manipulation resistance, use the Checks-Effects-Interactions pattern properly (here I've used ReentrancyGuard as a guard rail), implement flash swap callbacks, and add emergency pause functionality.


DEX Aggregators — How 1inch Works

Single-DEX swaps are rarely optimal. Liquidity for any given pair is fragmented across dozens of DEXs — Uniswap, SushiSwap, Curve, Balancer, and many more. DEX aggregators solve this by finding the best execution across all available liquidity sources.

1inch, the largest aggregator, uses the Pathfinder algorithm to optimize swap routes:

User wants to swap 100 ETH → USDC

Aggregator checks all sources:
  Uniswap V3 (0.3% pool):  1 ETH = 1,998 USDC
  Uniswap V3 (0.05% pool): 1 ETH = 2,001 USDC
  SushiSwap:                1 ETH = 1,995 USDC
  Curve:                    1 ETH = 2,000 USDC
  Balancer:                 1 ETH = 1,997 USDC

Optimal route (split order):
  ├── 40 ETH → Uniswap V3 0.05% pool  (best rate)
  ├── 35 ETH → Curve                   (deep stable liquidity)
  ├── 15 ETH → Uniswap V3 0.30% pool  (absorbs remaining)
  └── 10 ETH → Balancer               (fills the rest)

Result: Better average price than any single source

The aggregator architecture has three layers:

Off-chain routing engine: The Pathfinder runs off-chain, simulating swaps across all DEXs to find the optimal split. This includes direct swaps, multi-hop routes, and split routes. The computation is too expensive to run on-chain.

On-chain execution contract: The AggregationRouterV6 contract receives the optimized route from the off-chain engine and executes all swaps in a single transaction. It uses delegate calls to protocol-specific adapters for each DEX.

Protocol adapters: Each DEX has a unique interface. The aggregator maintains adapters that translate the unified swap interface into protocol-specific calls — swap() for Uniswap, exchange() for Curve, swap() with different parameters for Balancer.

┌──────────────────────────────────────────────┐
│              Aggregator Frontend              │
├──────────────────────────────────────────────┤
│          Off-Chain Routing Engine             │
│  (Pathfinder / Simulations / Gas Estimation)  │
├──────────────────────────────────────────────┤
│         On-Chain Router Contract              │
├──────┬──────┬──────┬──────┬─────────────────┤
│ Uni  │ Sushi│ Curve│ Bal  │  ... adapters   │
│ V3   │ Swap │      │ancer │                 │
└──────┴──────┴──────┴──────┴─────────────────┘

One thing I always tell clients building DEX integrations: use an aggregator API rather than routing to a single DEX directly. The gas overhead of the aggregator contract is small compared to the price improvement from optimal routing. For trades above a few hundred dollars, aggregators almost always deliver better execution than a single source.


The Future of DEX Design

DEX architecture is evolving rapidly. Here are the patterns I'm watching and building toward:

Intent-based trading: Instead of submitting a transaction with exact parameters, users sign an intent ("I want to swap 10 ETH for at least 20,000 USDC before 5pm"). Solvers compete to fill the intent at the best price. UniswapX and CoW Protocol are leading this shift. It fundamentally changes the MEV dynamic — solvers absorb MEV risk instead of users.

Hook-based customization: Uniswap V4 introduces hooks — custom contracts that execute at specific points in the swap lifecycle (before/after swap, before/after LP operations). This turns Uniswap from a single AMM into a platform for building custom AMMs. Dynamic fees, on-chain limit orders, TWAP execution — all become possible as hooks without forking the core protocol.

Appchain DEXs: High-frequency trading requires throughput that shared L1s and L2s struggle to provide. dYdX built its own Cosmos appchain for order book trading. Sei, Injective, and Hyperliquid are purpose-built chains optimized for exchange operations with sub-second finality and built-in order matching.

Hybrid models: The line between AMMs and order books is blurring. Maverick Protocol uses directional liquidity that follows price. Ambient (formerly CrocSwap) combines concentrated liquidity with limit orders in a single pool. Trader Joe's Liquidity Book uses discrete bins instead of continuous curves. These hybrid approaches try to capture the capital efficiency of order books with the permissionless composability of AMMs.

Cross-chain DEXs: As liquidity fragments across L2s, cross-chain swaps become essential. Across Protocol, Stargate, and Squid Router enable atomic swaps across chains without requiring users to bridge tokens manually. The next generation of aggregators will route across chains as naturally as they route across DEXs on a single chain today.

The DEX landscape in 2025 looks nothing like 2020. We went from a single AMM formula to an ecosystem of specialized exchange mechanisms, each optimized for different trading patterns. Understanding the architecture of each model is essential for any developer building in DeFi.


Key Takeaways

  • AMMs replaced order books on Ethereum because they solved the cold start problem — no market makers needed, just passive liquidity.
  • The constant product formula (x * y = k) is the mathematical foundation of Uniswap V2 and most AMM forks. Price impact is a natural consequence of the curve shape.
  • Concentrated liquidity (V3) improved capital efficiency up to 4,000x but introduced complexity — positions are NFTs, IL is amplified, and active management becomes necessary.
  • The Router contract is the user-facing interface that handles multi-hop swaps, deadline enforcement, and slippage protection. Never let users interact with pool contracts directly.
  • MEV protection requires defense at multiple layers: amountOutMin in contracts, private mempools for transactions, and intent-based systems at the protocol level.
  • Aggregators almost always deliver better execution than single-DEX swaps for trades of meaningful size. Use the API, not a direct DEX integration.
  • The future is intent-based: users express what they want, solvers compete to deliver it. Hooks and appchains will specialize DEXs for different trading profiles.

*I'm Uvin Vindula — a Web3 and AI engineer building production DeFi systems across Ethereum and L2s from Sri Lanka and the UK. I've built DEX components, LP management tools, and aggregator integrations for clients shipping real protocols. If you're building something in the DEX space and need architecture guidance or smart contract development, let's talk.*

Working on a Web3 or AI project?

Share
Uvin Vindula

Uvin Vindula

Web3 and AI engineer based in Sri Lanka and the UK. Author of The Rise of Bitcoin. Director of Blockchain and Software Solutions at Terra Labz. Founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.