Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Part 3 β€” Module 4: DEX Aggregation & Intents

Difficulty: Intermediate

Estimated reading time: ~35 minutes | Exercises: ~2-3 hours

πŸ“š Table of Contents

  1. The Routing Problem
  2. Split Order Math
  3. Aggregator On-Chain Patterns
  4. Build Exercise: Split Router
  5. The Intent Paradigm
  6. EIP-712 Order Structures
  7. Dutch Auction Price Decay
  8. Build Exercise: Intent Settlement
  9. Settlement Contract Architecture
  10. Solvers & the Filler Ecosystem
  11. CoW Protocol: Batch Auctions
  12. Summary
  13. Resources

πŸ’‘ The Routing Problem

In practice, no single DEX has the best price for every trade. DEX aggregators solve the routing problem β€” finding optimal execution across fragmented liquidity. More recently, intent-based trading is replacing explicit transaction construction: users sign what they want, and solvers compete to figure out how to fill it.

This module covers both models β€” from traditional split-routing to the intent/solver paradigm that’s reshaping DeFi execution. The emphasis is on intents: that’s where the ecosystem is heading and where the job opportunities are.

πŸ’‘ Concept: Why Aggregation Exists

The problem: No single DEX has the best price for every trade.

Liquidity is fragmented across Uniswap V2/V3/V4, Curve, Balancer, SushiSwap, and hundreds of other pools. A 100 ETH trade on a single pool takes massive slippage. Split across multiple pools, total slippage drops dramatically.

This is the same insight that drives order routing in traditional finance β€” NBBO (National Best Bid and Offer) ensures trades execute at the best available price across exchanges. DEX aggregators are DeFi’s equivalent.

Why this matters for you:

  • Every DeFi protocol needs to think about where swaps happen
  • Liquidation bots, arbitrage bots, and MEV searchers all solve routing problems
  • If you build anything that swaps tokens, you’ll either use an aggregator or build routing logic

The Three Execution Models

Before diving into math, understand the evolution:

Traditional Swap     β†’    Aggregated Swap      β†’    Intent-Based Swap
──────────────────────────────────────────────────────────────────────
User picks one pool  β†’  Router finds best path  β†’  User signs what they want
User submits tx      β†’  Router submits tx        β†’  Solver fills the order
User takes slippage  β†’  Less slippage via splits β†’  Solver absorbs MEV risk
100% on-chain        β†’  Off-chain routing,       β†’  Off-chain solver,
                        on-chain execution          on-chain settlement

This module covers all three, with emphasis on the intent model β€” that’s where the ecosystem is heading.


πŸ’‘ Split Order Math

πŸ’‘ Concept: When Does Splitting Beat a Single Pool?

This connects directly to your AMM math from Part 2 Module 2. Recall the constant product formula:

amountOut = reserveOut Γ— amountIn / (reserveIn + amountIn)

The key insight: price impact is nonlinear. Doubling the trade size MORE than doubles the slippage. This means splitting a large trade across two pools produces less total slippage than routing through one.

πŸ” Deep Dive: Optimal Split Calculation

Setup: Two constant-product pools for the same pair (e.g., ETH/USDC):

  • Pool A: reserves (xA, yA), k_A = xA Γ— yA
  • Pool B: reserves (xB, yB), k_B = xB Γ— yB
  • Total trade: sell Ξ” of token X

Single pool output:

outSingle = yA Γ— Ξ” / (xA + Ξ”)

Split output (Ξ΄A to pool A, Ξ΄B = Ξ” - Ξ΄A to pool B):

outSplit = yA Γ— Ξ΄A / (xA + Ξ΄A)  +  yB Γ— Ξ΄B / (xB + Ξ΄B)

Optimal split β€” maximize total output. Taking the derivative and setting to zero:

The optimal split gives equal marginal price in both pools after the trade. For constant-product pools, the marginal price after trading Ξ΄ is dy/dx = y Γ— x / (x + Ξ΄)Β². Setting equal across pools:

After trading, both pools should have the same marginal price:

  yA Γ— xA / (xA + Ξ΄A)Β²  =  yB Γ— xB / (xB + Ξ΄B)Β²

For equal-price pools (yA/xA = yB/xB), this simplifies to:

  Ξ΄A / Ξ΄B β‰ˆ xA / xB

Split proportional to pool depth.

Intuition: Send more volume to the deeper pool. If pool A has 2x the reserves of pool B, send roughly 2x the amount through pool A.

Worked Example: 100 ETH β†’ USDC

Pool A: 1000 ETH / 2,000,000 USDC  (spot price: $2,000/ETH)
Pool B:  500 ETH / 1,000,000 USDC  (spot price: $2,000/ETH)
Total trade: 100 ETH

──── Single pool (all to A) ────
  out = 2,000,000 Γ— 100 / (1000 + 100) = 181,818 USDC
  Effective price: $1,818/ETH
  Slippage: 9.1%

──── Split (67 ETH to A, 33 ETH to B β€” proportional to reserves) ────
  outA = 2,000,000 Γ— 67 / (1000 + 67) = 125,585 USDC
  outB = 1,000,000 Γ— 33 / (500 + 33)  =  61,913 USDC
  Total: 187,498 USDC
  Effective price: $1,875/ETH
  Slippage: 6.25%

──── Savings ────
  187,498 - 181,818 = 5,680 USDC  (+3.1% better)

But there’s a cost: Each additional pool interaction costs gas. On L1, that’s ~100k gas β‰ˆ $5-50 depending on gas prices. On L2, it’s negligible.

Break-even formula:

Split is worth it when:  slippageSavings > gasCostOfExtraPoolCall

Our example:
  If gas cost = $10:    saves $5,670 net β†’ absolutely split
  If gas cost = $6,000: loses  $320 net  β†’ single pool wins

This is why L2s enable more aggressive routing β€” the gas overhead of extra hops is near-zero.

πŸ’» Quick Try:

Deploy this in Remix to feel split routing:

contract SplitDemo {
    // Pool A: 1000 ETH / 2,000,000 USDC
    uint256 xA = 1000e18;  uint256 yA = 2_000_000e18;
    // Pool B:  500 ETH / 1,000,000 USDC
    uint256 xB =  500e18;  uint256 yB = 1_000_000e18;

    function singlePool(uint256 amtIn) external view returns (uint256) {
        return yA * amtIn / (xA + amtIn);
    }

    function splitPools(uint256 amtIn) external view returns (uint256) {
        // Split proportional to reserves: 2/3 to A, 1/3 to B
        uint256 toA = amtIn * 2 / 3;
        uint256 toB = amtIn - toA;
        return yA * toA / (xA + toA) + yB * toB / (xB + toB);
    }
}

Try singlePool(100e18) vs splitPools(100e18) β€” the split wins by ~5,680 USDC. Now try 1e18 (tiny trade) β€” almost no difference. Splitting only matters when trade size is large relative to pool depth.

πŸ”— DeFi Pattern Connection

Where split routing appears:

  • DEX aggregators (1inch, Paraswap, 0x) β€” their entire value proposition
  • Liquidation bots β€” finding the best path to sell seized collateral
  • Arbitrage bots β€” routing through multiple pools to capture price discrepancies
  • Protocol integrations β€” any protocol that swaps tokens internally (vaults, CDPs, etc.)

The math is the same as Part 2 Module 2’s AMM analysis, applied to optimization across pools instead of within one.


πŸ’‘ Aggregator On-Chain Patterns

πŸ’‘ Concept: The Multi-Call Executor Pattern

Every aggregator β€” 1inch, Paraswap, 0x, CowSwap β€” uses the same on-chain pattern. The off-chain router determines the optimal path; the on-chain executor just follows instructions:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              User                        β”‚
β”‚  1. approve(router, amount)             β”‚
β”‚  2. router.swap(encodedRoute)           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Router Contract                  β”‚
β”‚  1. transferFrom(user, self, amountIn)  β”‚
β”‚  2. For each hop in route:              β”‚
β”‚     a. approve(pool, hopAmount)         β”‚
β”‚     b. pool.swap(params)               β”‚
β”‚  3. transfer(user, finalOutput)         β”‚
β”‚  4. Verify: output >= minOutput         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Simplified router pattern:

contract SimpleRouter {
    struct SwapStep {
        address pool;
        address tokenOut;
    }

    function swap(
        IERC20 tokenIn,
        IERC20 tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        SwapStep[] calldata steps
    ) external returns (uint256 amountOut) {
        // Pull tokens from user
        tokenIn.transferFrom(msg.sender, address(this), amountIn);

        // Execute each step
        uint256 currentAmount = amountIn;
        IERC20 currentToken = tokenIn;

        for (uint256 i = 0; i < steps.length; i++) {
            currentToken.approve(steps[i].pool, currentAmount);
            currentAmount = IPool(steps[i].pool).swap(
                address(currentToken),
                steps[i].tokenOut,
                currentAmount,
                0  // router checks min at the end, not per-hop
            );
            currentToken = IERC20(steps[i].tokenOut);
        }

        // Final check + transfer
        require(currentAmount >= minAmountOut, "Insufficient output");
        currentToken.transfer(msg.sender, currentAmount);

        return currentAmount;
    }
}

Key design decisions in this pattern:

  • Min output check at the end, not per-hop. Intermediate steps might give β€œbad” prices that result in good final output via multi-hop routing
  • Pull pattern (transferFrom) β€” the user initiates by calling the router
  • Approval management β€” some routers use infinite approvals to trusted pools, others approve per-swap
  • Dust handling β€” rounding can leave tiny amounts in the router; production routers sweep these back

Gas Optimization: Packed Calldata

Production aggregators go far beyond the simple struct-based pattern:

// 1inch uses packed uint256 arrays instead of struct arrays:
function unoswap(
    IERC20 srcToken,
    uint256 amount,
    uint256 minReturn,
    uint256[] calldata pools  // each uint256 packs: address + direction + flags
) external returns (uint256);

Why? Calldata costs 16 gas per non-zero byte, 4 gas per zero byte. Packing a pool address (20 bytes) + direction flag (1 bit) + fee tier (2 bytes) into a single uint256 saves significant calldata gas. On L2s (where calldata is the dominant cost), this matters even more.

πŸ“– How to Study: 1inch AggregationRouterV6

  1. Start with unoswap() β€” single-pool swap, simplest path
  2. Read swap() β€” the general multi-hop/multi-split executor
  3. Study how GenericRouter uses delegatecall to protocol-specific handlers
  4. Look at calldata encoding β€” how pools, amounts, and flags are packed

Don’t try to understand the full router in one pass. The core pattern is the multi-call loop above; everything else is gas optimization and edge-case handling.

πŸ” Code: 1inch Limit Order Protocol β€” V6 aggregation router source is not publicly available; the limit-order-protocol repo is the best open-source reference for 1inch’s on-chain patterns


🎯 Build Exercise: Split Router

Exercise 1: SplitRouter

Build a simple DEX router that splits trades across two constant-product pools.

What you’ll implement:

  • getAmountOut() β€” constant-product AMM output calculation (refresher from Part 2)
  • getOptimalSplit() β€” find the best split ratio across two pools
  • splitSwap() β€” execute a split trade, pulling tokens and swapping through both pools
  • singleSwap() β€” execute a single-pool trade (for comparison)

Concepts exercised:

  • AMM output formula applied to routing
  • Split order optimization math
  • Multi-call execution pattern (the core of every aggregator)
  • Gas-aware decision making (when splitting beats single-pool)

🎯 Goal: Prove that splitting a large trade across two unequal pools gives more output than routing through either pool alone.

Run: forge test --match-contract SplitRouterTest -vvv

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow does a DEX aggregator find the optimal route?”
    • Good answer: β€œAggregators query multiple pools off-chain, run an optimization algorithm to find the best split and routing path, then encode the solution as calldata for an on-chain executor contract.”
    • Great answer: β€œThe routing problem is a constrained optimization β€” maximize output given pools with different liquidity profiles. For constant-product pools, the optimal split is approximately proportional to pool depth, because price impact is nonlinear β€” doubling the trade size more than doubles slippage. In practice, aggregators use heuristics because the general multi-hop, multi-split problem is NP-hard. The on-chain part is just a multi-call executor with a min-output check β€” all the intelligence is off-chain. On L2s, routing gets more aggressive because the gas overhead of extra hops is near-zero, so more splits become profitable.”

Interview Red Flags:

  • 🚩 Thinking aggregators only do single-pool routing (the whole point is multi-pool, multi-hop optimization)
  • 🚩 Not distinguishing on-chain vs off-chain components (the intelligence is off-chain, execution is on-chain)
  • 🚩 Ignoring gas costs in split analysis (extra hops have different economics on L1 vs L2)

Pro tip: When discussing aggregator architecture, mention that the on-chain executor is deliberately simple (multi-call + min-output check) while the off-chain router is where all the complexity lives. This separation of concerns is a key design pattern across DeFi infrastructure.


πŸ“‹ Summary: Traditional Aggregation

Covered:

  • The routing problem: fragmented liquidity across multiple DEXs and pools
  • Split order math: why splitting large trades across pools reduces price impact
  • Optimal split calculation based on relative pool reserves
  • On-chain vs off-chain routing trade-offs (gas costs vs computation flexibility)
  • Executor patterns: how aggregators construct and execute multi-hop, multi-pool swaps
  • Gas-aware optimization: when the gas cost of splitting outweighs the benefit

Next: The intent paradigm β€” a fundamental shift from users constructing transactions to users signing what they want and solvers competing to fill it.


πŸ’‘ The Intent Paradigm

πŸ’‘ Concept: From Transactions to Intents

This is arguably the most important paradigm shift in DeFi since AMMs:

TRANSACTION MODEL (2020-2023):
──────────────────────────────
User: "Swap 1 ETH for USDC on Uniswap V3, 0.3% pool,
       min 1900 USDC, via the public mempool"
Problem: User specifies HOW β†’ gets sandwiched β†’ takes MEV loss

INTENT MODEL (2023+):
─────────────────────
User: "I want at least 1900 USDC for my 1 ETH.
       I don't care how you do it."
Solver: "I'll give you 1920 USDC β€” routing through V3 + Curve,
         or using my private inventory, or going through a CEX."

Why this matters:

  • User gets better prices (solvers compete on execution quality)
  • MEV goes to user (via solver competition) instead of to searchers
  • Cross-chain execution becomes possible (solver handles complexity)
  • User doesn’t need to know which pools exist or how to route

The key innovation: Separate WHAT (user’s intent) from HOW (execution strategy). The competitive market for solvers ensures good execution quality.

The Intent Lifecycle

1. USER SIGNS ORDER (off-chain, gasless)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  I want to sell: 1 ETH           β”‚
   β”‚  I want at least: 1900 USDC      β”‚
   β”‚  Deadline: block 19000000        β”‚
   β”‚  Signature: 0xabc...             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
2. SOLVERS COMPETE (off-chain)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Solver A   β”‚  β”‚ Solver B   β”‚  β”‚ Solver C   β”‚
   β”‚ Via V3:    β”‚  β”‚ Via CEX:   β”‚  β”‚ Inventory: β”‚
   β”‚ 1915 USDC  β”‚  β”‚ 1920 USDC  β”‚  β”‚ 1918 USDC  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚ ← Best offer wins
                        β–Ό
3. SETTLEMENT (on-chain, solver pays gas)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Settlement Contract              β”‚
   β”‚  1. Verify user's EIP-712 sig    β”‚
   β”‚  2. Check: 1920 >= 1900 βœ“        β”‚
   β”‚  3. Transfer 1 ETH from user     β”‚
   β”‚  4. Transfer 1920 USDC to user   β”‚
   β”‚  5. Emit OrderFilled event       β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œWhat’s the difference between intent-based and transaction-based execution?”
    • Good answer: β€œIn transaction-based, the user specifies exact routing. In intent-based, the user signs what they want and solvers compete to fill it.”
    • Great answer: β€œThe key insight is separation of concerns. Transaction-based systems couple the WHAT (swap ETH for USDC) with the HOW (via Uniswap V3, 0.3% pool). Intent-based systems decouple them β€” the user specifies only the WHAT, and a competitive market of solvers handles the HOW. This is strictly better because solvers have access to more liquidity sources than any individual user β€” CEX inventory, cross-chain bridges, private pools β€” and competition drives execution toward optimal. The tradeoff is trust assumptions: you need a settlement contract that cryptographically guarantees the user gets their minimum output, and you need a healthy solver ecosystem for competitive pricing.”

Interview Red Flags:

  • 🚩 Thinking intents are β€œgasless” (the solver pays gas, not the user, but gas still exists and affects solver economics)
  • 🚩 Not knowing about Permit2 and its role in the intent flow (how users approve tokens for intent-based protocols)
  • 🚩 Confusing intents with simple limit orders (intents are a broader paradigm, not just price limits)

Pro tip: The intent paradigm is the defining trend in DeFi execution for 2024-2026. Framing intents as β€œseparation of WHAT from HOW” with a competitive solver market shows you understand the architecture, not just the buzzword.


πŸ’‘ EIP-712 Order Structures

πŸ’‘ Concept: How Intent Orders Are Signed

EIP-712 enables typed, structured data signing β€” the user sees exactly what they’re signing in their wallet, not just a hex blob. This is the foundation of every intent protocol.

Recall from Part 1 Module 3: EIP-712 defines domain separators and type hashes for structured signing.

UniswapX order structure (simplified):

struct Order {
    address offerer;          // who is selling
    IERC20 inputToken;        // token being sold
    uint256 inputAmount;      // amount being sold
    IERC20 outputToken;       // token being bought
    uint256 outputAmount;     // minimum amount to receive
    uint256 deadline;         // order expiration
    address recipient;        // who receives the output (usually = offerer)
    uint256 nonce;            // replay protection
}

EIP-712 signing flow β€” the four steps:

// 1. Define the type hash (compile-time constant)
bytes32 constant ORDER_TYPEHASH = keccak256(
    "Order(address offerer,address inputToken,uint256 inputAmount,"
    "address outputToken,uint256 outputAmount,uint256 deadline,"
    "address recipient,uint256 nonce)"
);

// 2. Hash the struct fields
function hashOrder(Order memory order) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        ORDER_TYPEHASH,
        order.offerer,
        order.inputToken,
        order.inputAmount,
        order.outputToken,
        order.outputAmount,
        order.deadline,
        order.recipient,
        order.nonce
    ));
}

// 3. Create the EIP-712 digest (domain separator + struct hash)
function getDigest(Order memory order) public view returns (bytes32) {
    return keccak256(abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        hashOrder(order)
    ));
}

// 4. Recover signer and verify
function verifyOrder(Order memory order, bytes memory signature)
    public view returns (address)
{
    bytes32 digest = getDigest(order);
    return ECDSA.recover(digest, signature);
}

Why EIP-712 and not just keccak256(abi.encode(...))?

  • User sees β€œSell 1 ETH for at least 1900 USDC” in MetaMask β€” not 0x5a3b7c...
  • Domain separator prevents cross-protocol replay (can’t reuse a UniswapX signature on CoW Protocol)
  • Nonce prevents same-order replay (fill it twice)
  • Type hash ensures the struct layout is part of the hash (prevents field reordering attacks)

πŸ’» Quick Try:

In Foundry, you can sign EIP-712 messages in tests with vm.sign:

// Setup: create a user with a known private key
uint256 userPK = 0xA11CE;
address user = vm.addr(userPK);

// Build the order
Order memory order = Order({
    offerer: user,
    inputToken: IERC20(weth),
    inputAmount: 1e18,
    outputToken: IERC20(usdc),
    outputAmount: 1900e6,
    deadline: block.timestamp + 1 hours,
    recipient: user,
    nonce: 0
});

// Sign it
bytes32 digest = settlement.getDigest(order);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);

// Verify
address recovered = settlement.verifyOrder(order, signature);
assertEq(recovered, user);

This is exactly how your Exercise 2 tests will work.


πŸ’‘ Dutch Auction Price Decay

πŸ’‘ Concept: How Price Discovery Works in Intents

In traditional limit orders, the user sets a fixed price. In intent-based trading, a Dutch auction finds the market price through time decay. The output the solver must provide starts high and decreases over time:

Output solver must provide
        β”‚
  1950  β”‚ ●                          ← Start: bad for solver (almost no profit)
        β”‚    ●
  1930  β”‚       ●
        β”‚          ●
  1910  β”‚             ●              ← Someone fills here (profitable enough)
        β”‚                ●
  1900  │───────────────────●────    ← End: user's minimum (max solver profit)
        β”‚
        └─────────────────────── Time
        t=0    30s    60s    90s

The Formula

decayedOutput = startOutput - (startOutput - endOutput) Γ— elapsed / decayPeriod

Where:

  • startOutput = initial (high) output β€” almost no profit for solver
  • endOutput = final (low) output β€” user’s limit price
  • decayPeriod = total auction duration
  • elapsed = time since auction started (clamped to decayPeriod)

πŸ” Deep Dive: Step-by-Step

Parameters:  startOutput = 1950, endOutput = 1900, decayPeriod = 90s

At t =  0s:  1950 - (1950 - 1900) Γ— 0/90   = 1950 - 0.0    = 1950 USDC
At t = 30s:  1950 - (1950 - 1900) Γ— 30/90  = 1950 - 16.67  = 1933 USDC
At t = 45s:  1950 - (1950 - 1900) Γ— 45/90  = 1950 - 25.0   = 1925 USDC
At t = 60s:  1950 - (1950 - 1900) Γ— 60/90  = 1950 - 33.33  = 1917 USDC
At t = 90s:  1950 - (1950 - 1900) Γ— 90/90  = 1950 - 50.0   = 1900 USDC
After 90s:   Clamped to endOutput = 1900 USDC

In Solidity (from UniswapX’s DutchDecayLib):

function resolve(
    uint256 startAmount,
    uint256 endAmount,
    uint256 decayStartTime,
    uint256 decayEndTime
) internal view returns (uint256) {
    if (block.timestamp <= decayStartTime) {
        return startAmount;
    }
    if (block.timestamp >= decayEndTime) {
        return endAmount;
    }

    uint256 elapsed = block.timestamp - decayStartTime;
    uint256 duration = decayEndTime - decayStartTime;
    uint256 decay = (startAmount - endAmount) * elapsed / duration;

    return startAmount - decay;
}

πŸ’» Quick Try:

Deploy this in Remix to watch Dutch auction decay in action:

contract DutchDemo {
    uint256 public startTime;
    uint256 public startOutput = 1950;  // best for user
    uint256 public endOutput   = 1900;  // user's limit
    uint256 public duration    = 90;    // seconds

    constructor() { startTime = block.timestamp; }

    function currentOutput() external view returns (uint256) {
        uint256 elapsed = block.timestamp - startTime;
        if (elapsed >= duration) return endOutput;
        return startOutput - (startOutput - endOutput) * elapsed / duration;
    }

    function reset() external { startTime = block.timestamp; }
}

Deploy, call currentOutput() immediately (1950). Wait 30+ seconds, call again β€” watch it drop. Call reset() to restart. The solver’s decision: fill now (less profit) or wait (risk someone else fills first).

Why Dutch auctions are brilliant for intents:

  1. Price discovery without an order book. The auction finds the market clearing price automatically through time.
  2. Solver competition compressed into time. The first solver to fill profitably wins. Earlier fill = less profit for solver = better for user.
  3. No wasted gas. Unlike English auctions where everyone bids on-chain, Dutch auctions have a single on-chain transaction (the fill).
  4. MEV-resistant. The auction is the price discovery mechanism β€” there’s nothing to sandwich.

The tradeoff: Decay parameters matter. Too fast a decay β†’ solver gets a cheap fill. Too slow β†’ user waits too long. Production protocols tune these per-pair and per-market-condition.

πŸ”— DeFi Pattern Connection

Dutch auctions appear everywhere in DeFi:

  • UniswapX β€” solver competition for order fills (this module)
  • MakerDAO β€” collateral auctions in liquidation (Part 2 Module 6)
  • Part 2 Module 9 capstone β€” your stablecoin’s Dutch auction liquidator uses the same formula!
  • Gradual Dutch Auctions (GDAs) β€” Paradigm’s design for NFTs and token sales

The formula is identical across all of these. What changes is: who’s buying, what’s being sold, and how the decay parameters are tuned.

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œExplain how UniswapX’s Dutch auction works and why it’s MEV-resistant.”
    • Good answer: β€œUsers sign an order with a start and end output amount. The required output decays from start to end over time. Solvers fill when it becomes profitable β€” earlier fills give users better prices.”
    • Great answer: β€œThe Dutch auction creates continuous solver competition compressed into time. The output starts above market price β€” unprofitable for solvers β€” and decays toward the user’s limit price. A solver fills when the auction price crosses below marketPrice - gasCost. This is MEV-resistant because the price discovery IS the auction β€” there’s no pending swap transaction to sandwich. The exclusive filler window adds another layer: a designated solver gets priority in exchange for committing to better starting prices. And the callback pattern lets solvers source liquidity just-in-time during the fill β€” they can flash-swap from AMMs, meaning they don’t need pre-funded inventory.”

Interview Red Flags:

  • 🚩 Not knowing what a Dutch auction is or confusing it with an English auction
  • 🚩 Conflating MEV protection with privacy (related but distinct β€” intents avoid the public mempool, but the core protection is the auction mechanism itself)
  • 🚩 Missing the callback pattern (IReactorCallback) that enables just-in-time liquidity sourcing

Pro tip: The Dutch auction decay formula is the same pattern you saw in P2M6’s liquidation auctions and Paradigm’s GDAs. Connecting this cross-module pattern shows you see the underlying math, not just protocol-specific details.


🎯 Build Exercise: Intent Settlement

Exercise 2: IntentSettlement

Build a simplified intent settlement system with EIP-712 orders and Dutch auction price decay.

What you’ll implement:

  • hashOrder() β€” EIP-712 struct hashing for the order type
  • getDigest() β€” full EIP-712 digest with domain separator
  • resolveDecay() β€” Dutch auction price calculation at current timestamp
  • fill() β€” complete settlement: verify signature, check deadline, resolve decay, execute atomic swap

Concepts exercised:

  • EIP-712 typed data hashing and domain separators
  • Signature verification with ECDSA recovery
  • Dutch auction formula (linear decay)
  • Settlement contract security: replay protection, deadline enforcement, minimum output

🎯 Goal: Build the core of a UniswapX-style settlement contract. Sign orders off-chain in tests using vm.sign, fill them on-chain with Dutch auction price decay.

Run: forge test --match-contract IntentSettlementTest -vvv


πŸ“‹ Summary: Intent-Based Trading

Covered:

  • The intent paradigm shift: users sign desired outcomes, solvers handle execution
  • EIP-712 typed data structures for off-chain order signing
  • Dutch auction price decay: starting generous and decaying to attract solvers at the right moment
  • Solver competition: how fillers race to provide the best execution
  • Replay protection, deadline enforcement, and nonce management
  • User experience improvements: no gas needed, MEV protection, cross-chain potential

Next: Settlement contract architecture β€” how UniswapX’s Reactor pattern enforces trust guarantees on-chain regardless of solver behavior.


πŸ’‘ Settlement Contract Architecture

πŸ’‘ Concept: The UniswapX Reactor Pattern

The Reactor is UniswapX’s on-chain settlement engine. It’s where the trust guarantee lives β€” no matter what the solver does off-chain, the on-chain contract enforces that the user gets what they signed for.

Simplified settlement flow:

contract IntentSettlement {
    bytes32 public immutable DOMAIN_SEPARATOR;
    mapping(address => mapping(uint256 => bool)) public nonces;

    /// @notice Fill a signed order. Called by the solver.
    function fill(
        Order calldata order,
        bytes calldata signature,
        uint256 fillerOutputAmount
    ) external {
        // 1. Verify the order signature
        address signer = verifyOrder(order, signature);
        require(signer == order.offerer, "Invalid signature");

        // 2. Check order hasn't expired
        require(block.timestamp <= order.deadline, "Order expired");

        // 3. Check nonce not used (replay protection)
        require(!nonces[order.offerer][order.nonce], "Already filled");
        nonces[order.offerer][order.nonce] = true;

        // 4. Resolve Dutch auction decay
        uint256 minOutput = resolveDecay(order);

        // 5. Verify solver provides enough
        require(fillerOutputAmount >= minOutput, "Insufficient output");

        // 6. Execute the swap atomically
        order.inputToken.transferFrom(order.offerer, msg.sender, order.inputAmount);
        order.outputToken.transferFrom(msg.sender, order.recipient, fillerOutputAmount);
    }
}

Critical security properties:

  • Signature verification β€” only the offerer can authorize selling their tokens
  • Nonce β€” prevents the same order from being filled twice
  • Min output check β€” user ALWAYS gets at least the decayed auction amount
  • Atomic execution β€” both transfers succeed or both revert
  • No solver trust β€” the contract enforces rules; it doesn’t trust the solver

UniswapX’s Full Architecture

UniswapX adds several production features on top of the basic pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            ExclusiveDutchOrderReactor               β”‚
β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Permit2       β”‚   β”‚ ResolvedOrder             β”‚   β”‚
β”‚  β”‚ (approvals)   β”‚   β”‚  - decay applied          β”‚   β”‚
β”‚  β”‚               β”‚   β”‚  - outputs resolved       β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                     β”‚
β”‚  fill()  ──→  validate   ──→  resolve  ──→  settle  β”‚
β”‚               signature       decay        execute  β”‚
β”‚                                                     β”‚
β”‚  Exclusive filler window (optional):                β”‚
β”‚  First N seconds: only designated filler can fill   β”‚
β”‚  After N seconds: open to all fillers               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Three key patterns from UniswapX:

1. Permit2 integration β€” Users approve the Permit2 contract once, then sign per-order permits. No separate approve() transaction per order β€” huge UX improvement.

2. Exclusive filler window β€” For the first N seconds, only one designated solver can fill. The solver gets guaranteed exclusivity in exchange for committing to better starting prices. After the window, any solver can fill.

3. Callback pattern β€” Solvers can receive a callback before providing output tokens, letting them source liquidity just-in-time:

// The Reactor calls the filler's callback before checking output
IReactorCallback(msg.sender).reactorCallback(resolvedOrders, callbackData);
// THEN verifies that output tokens arrived at the recipient

This is powerful β€” the solver can flash-swap from Uniswap, arbitrage across pools, or bridge from another chain inside the callback. They don’t need to pre-fund the fill.

πŸ“– How to Study: UniswapX

  1. Start with ExclusiveDutchOrderReactor.sol β€” the main entry point
  2. Read DutchDecayLib.sol β€” the decay math (short, pure functions)
  3. Study ResolvedOrder β€” how raw orders become executable orders
  4. Look at IReactorCallback β€” the solver callback interface
  5. Skip Permit2 internals initially β€” just know it handles gasless approvals

πŸ” Code: UniswapX β€” start with src/reactors/


πŸ’‘ Solvers & the Filler Ecosystem

πŸ’‘ Concept: What Solvers Actually Do

A solver (or β€œfiller”) is a service that fills intent orders profitably. This is one of the hottest areas in DeFi right now β€” teams are actively hiring solver engineers.

A solver’s job, step by step:

1. MONITOR β€” Watch for new signed orders (from UniswapX API, CoW API, etc.)

2. EVALUATE β€” Can I fill this profitably?
   - What's the current DEX price for this pair?
   - What's the Dutch auction output RIGHT NOW?
   - Is the gap (market price - required output) > my costs?

3. ROUTE β€” Find the cheapest way to source the output tokens
   - AMM swap (Uniswap, Curve, Balancer)
   - CEX hedge (Binance, Coinbase)
   - Private inventory (already holding the tokens)
   - Flash loan + arbitrage combo

4. FILL β€” Submit the fill transaction to the settlement contract
   - Provide enough output to satisfy the decayed auction price
   - Beat other solvers to the fill (speed matters)

Solver economics (worked example):

User's order: selling 1 ETH, wants at least 1920 USDC (current auction output)
Market price: 1 ETH = 1935 USDC on Uniswap V3

Solver fills:
  Receives: 1 ETH from user (via settlement contract)
  Provides: 1920 USDC to user (minimum required)
  Then sells 1 ETH on Uniswap: gets 1935 USDC

  Revenue: 1935 USDC
  Cost:    1920 USDC (paid to user) + ~$3 gas
  Profit:  ~$12

The competitive dynamic: If solver A fills at the minimum (1920), solver B might fill earlier (at 1928) when the Dutch auction output is higher β€” less profit per fill but winning more fills. Competition pushes fill prices toward market price, benefiting users.

The Solver Callback Pattern

From a Solidity perspective, the most important pattern is the callback:

// Simplified ResolvedOrder β€” the Reactor resolves raw orders into this struct
// before passing them to the solver callback. The decay math has already been
// applied, so `input.amount` and `outputs[i].amount` reflect current prices.
//
// struct ResolvedOrder {
//     OrderInfo info;           // deadline, reactor address, swapper
//     InputToken input;         // { token, amount } β€” what the user is selling
//     OutputToken[] outputs;    // [{ token, amount, recipient }] β€” what user wants
//     bytes sig;                // EIP-712 signature
//     bytes32 hash;             // Order hash
// }

contract MySolver is IReactorCallback {
    ISwapRouter public immutable uniswapRouter;

    function reactorCallback(
        ResolvedOrder[] memory orders,
        bytes memory callbackData
    ) external override {
        // Called by the Reactor BEFORE output is checked.
        // We just received the user's input tokens.
        // Source liquidity and send output tokens to the recipient.

        for (uint i = 0; i < orders.length; i++) {
            // Option A: Swap on Uniswap using the input tokens we received
            uniswapRouter.exactInputSingle(ISwapRouter.ExactInputSingleParams({
                tokenIn: address(orders[i].input.token),
                tokenOut: address(orders[i].outputs[0].token),
                fee: 3000,
                recipient: orders[i].outputs[0].recipient,
                amountIn: orders[i].input.amount,
                amountOutMinimum: orders[i].outputs[0].amount,
                sqrtPriceLimitX96: 0
            }));

            // Option B: Transfer from inventory
            // outputToken.transfer(recipient, amount);

            // Option C: More complex routing, flash loans, etc.
        }
        // The Reactor checks output arrived after this returns
    }
}

What makes a competitive solver:

  1. Low-latency market data β€” Know DEX prices across all pools in real-time
  2. Gas optimization β€” Cheaper fill transactions = more competitive
  3. Multiple liquidity sources β€” CEX + DEX + private inventory
  4. Cross-chain capability β€” For cross-chain intents (UniswapX v2)
  5. Risk management β€” Handle inventory risk, failed fills, gas spikes

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œIf you were building a solver, what would your architecture look like?”
    • Good answer: β€œMonitor order APIs for new orders, evaluate profitability, route through DEXes, submit fill transactions.”
    • Great answer: β€œThree components: (1) An off-chain monitoring service that streams new orders from UniswapX/CoW APIs alongside real-time DEX prices. (2) A pricing engine that evaluates profitability at the current Dutch auction price β€” factoring in DEX quotes, gas costs, and expected competition. (3) An on-chain fill contract implementing IReactorCallback that sources liquidity just-in-time. Start with single-DEX routing using the callback pattern β€” you receive the user’s input tokens, swap them on Uniswap, and the output goes directly to the user. Then add multi-DEX splits, then CEX hedging for large orders. The callback is key: you don’t need inventory, you just need to source the output tokens between when you receive the input and when the Reactor checks the output.”

Interview Red Flags:

  • 🚩 Describing solver architecture without mentioning the callback pattern (IReactorCallback is the key to capital-efficient filling)
  • 🚩 Assuming solvers need pre-funded inventory (just-in-time sourcing via callbacks is the standard approach)
  • 🚩 Ignoring competition dynamics and gas cost estimation in profitability analysis

Pro tip: Solver architecture is a hot interview topic at intent-focused protocols. Showing you can reason about the full stack β€” off-chain monitoring, pricing engine, on-chain callback contract β€” demonstrates systems-level thinking that goes beyond smart contract development.


πŸ’‘ CoW Protocol: Batch Auctions

πŸ’‘ Concept: A Different Approach to Intents

While UniswapX uses Dutch auctions for individual orders, CoW Protocol collects orders into batches and finds optimal execution for the entire batch at once.

The batch auction flow:

                        Total batch window: ~60 seconds

Phase 1: ORDER COLLECTION (~30 seconds)
β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”
β”‚ Buy  β”‚  β”‚ Sell β”‚  β”‚ Sell β”‚  β”‚ Buy  β”‚
β”‚ ETH  β”‚  β”‚ ETH  β”‚  β”‚ DAI  β”‚  β”‚ USDC β”‚
β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜

Phase 2: SOLVER COMPETITION (~30 seconds)
  Solver A: routes all through Uniswap
  Solver B: uses CoW matching + Curve for remainder
  Solver C: direct P2P for two orders + Balancer for rest

  β†’ Winner: whichever solution gives users the most total surplus

Phase 3: ON-CHAIN SETTLEMENT
  GPv2Settlement.settle(trades, interactions)  // single transaction

Coincidence of Wants (CoW) β€” the killer feature:

When User A sells ETH for USDC and User B sells USDC for ETH, they can trade directly:

Without CoW (two separate AMM trades):
  A sells 1 ETH on Uniswap β†’ gets 1935 USDC (pays LP fee + slippage)
  B sells 2000 USDC on Uniswap β†’ gets 1.03 ETH (pays LP fee + slippage)
  Total cost: ~$10 in fees and slippage

With CoW (direct P2P matching):
  A gives 1 ETH to B
  B gives 1940 USDC to A
  Clearing price: 1940 USDC/ETH (between both users' limit prices)
  No LP fees, no slippage, no MEV
  Both get better prices than AMM
  Only the remainder (B needs more USDC) routes through an AMM

MEV protection: All orders in a batch execute at uniform clearing prices in a single transaction. There’s nothing to sandwich β€” the batch is the atomic unit.

GPv2Settlement Contract

// Simplified from CoW Protocol
contract GPv2Settlement {
    /// @notice Execute a batch of trades
    /// @param trades Array of user trades with signed orders
    /// @param interactions External calls (DEX swaps, approvals, etc.)
    function settle(
        Trade[] calldata trades,
        Interaction[][3] calldata interactions  // [pre, intra, post]
    ) external onlySolver {
        // Phase 1: Pre-interactions (setup: approvals, flash loans, etc.)
        executeInteractions(interactions[0]);

        // Phase 2: Execute user trades
        for (uint i = 0; i < trades.length; i++) {
            // Verify order signature
            // Transfer sellToken from user to settlement
            // Record buyToken owed to user
        }

        // Phase 3: Intra-interactions (source liquidity: DEX swaps)
        executeInteractions(interactions[1]);

        // Phase 4: Post-interactions (cleanup: return flash loans, sweep dust)
        executeInteractions(interactions[2]);

        // Final check: every user received their minimum buyAmount
    }
}

The three interaction phases allow solvers maximum flexibility:

  • Pre: Set up token approvals, initiate flash loans
  • Intra: Execute DEX swaps for liquidity the batch needs beyond CoW matches
  • Post: Clean up, return flash loans, sweep dust

UniswapX vs CoW Protocol

AspectUniswapXCoW Protocol
ModelIndividual Dutch auctionsBatch auctions
Price discoveryTime decay per orderSolver competition on full batch
CoW matchingNo (one order at a time)Yes (batch-level P2P matching)
Fill speedSeconds (continuous)~60s (batch window)
MEV protectionDutch auction + exclusive fillerBatch settlement + uniform pricing
Cross-chainYes (v2)Limited
Best forSpeed-sensitive, large individual ordersMEV-sensitive, many concurrent orders

Both are valid approaches with different tradeoffs. Understanding both gives you the complete picture.

πŸ” Code: CoW Protocol GPv2Settlement β€” start with GPv2Settlement.sol

πŸ“– How to Study: CoW Protocol

  1. Start with GPv2Settlement.sol β€” the settle() function is the entry point
  2. Read GPv2Trade.sol β€” how individual trades are encoded and decoded
  3. Study the three interaction phases (pre, intra, post) β€” understand the solver’s flexibility
  4. Look at GPv2Signing.sol β€” how order signatures are verified (supports multiple schemes)
  5. Skip the off-chain solver infrastructure initially β€” focus on the on-chain settlement guarantees

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow does CoW Protocol’s batch auction prevent MEV?”
    • Good answer: β€œAll orders in a batch execute at the same clearing price in a single transaction, so there’s nothing to sandwich.”
    • Great answer: β€œThree layers of MEV protection: (1) Orders are signed off-chain and submitted to a private API, never the public mempool β€” invisible to searchers. (2) Batch execution means all trades happen at uniform clearing prices in one transaction β€” you can’t insert a sandwich between individual trades. (3) Coincidence of Wants matching means some trades never touch AMMs at all β€” no pool interaction means zero MEV surface. The residual MEV from AMM interactions needed for unmatched volume is captured by solver competition β€” solvers internalize the MEV and return surplus to users in order to win the batch.”

Interview Red Flags:

  • 🚩 Conflating CoW’s batch auction model with UniswapX’s Dutch auction model (fundamentally different settlement approaches)
  • 🚩 Not understanding Coincidence of Wants as a distinct MEV protection layer (peer-to-peer matching that bypasses AMMs entirely)
  • 🚩 Thinking batch auctions eliminate MEV completely (residual MEV from unmatched AMM interactions still exists, but is redistributed via solver competition)

Pro tip: Knowing both UniswapX (individual Dutch auctions) and CoW Protocol (batch auctions) and being able to compare their tradeoffs β€” latency vs batch efficiency, exclusive fillers vs open solver competition β€” shows you understand the design space, not just one protocol.


πŸ“‹ Summary: DEX Aggregation & Intents

βœ“ Covered:

  • The routing problem and split order optimization math
  • The multi-call executor pattern shared by all aggregators
  • The intent paradigm shift: from transactions to signed intents
  • EIP-712 order structures and signature verification
  • Dutch auction price decay: formula, mechanics, and why it works
  • Settlement contract architecture (UniswapX Reactor pattern)
  • What solvers do and how to think about building one
  • CoW Protocol’s batch auction model and Coincidence of Wants
  • UniswapX vs CoW Protocol tradeoffs

Next: Cross-module concept links and resources.


← Backward References (where these patterns were introduced):

  • AMM integration β†’ P2M2 Uniswap V2/V3 swap interfaces β€” aggregators route through these pools, understanding their price impact curves is essential
  • Oracle prices for routing β†’ P2M3 Chainlink price feeds β€” off-chain routers use oracle prices as reference for optimal splitting
  • Flash loans in arbitrage β†’ P2M5 flash loan patterns β€” solvers use flash swaps to fill orders without pre-funded inventory
  • EIP-712 signatures β†’ P1M3 Permit/Permit2 signing β€” intent-based systems (UniswapX, CoW) rely on typed structured data signatures
  • Dutch auctions β†’ P2M6 liquidation auctions β€” similar time-decay math for price discovery (auction output decays over time)

β†’ Forward References (where aggregation concepts appear next in Part 3):

  • MEV protection β†’ P3M5 (MEV & Frontrunning) β€” sandwich attacks, private mempools, proposer-builder separation
  • Solver economics β†’ P3M5 (MEV & Frontrunning) β€” solver competition as MEV redistribution mechanism
  • Cross-chain routing β†’ P3M7 (Cross-Chain) β€” bridge-aware aggregation, cross-chain intents
  • Governance of solver sets β†’ P3M6 (Governance & Risk) β€” who can be a solver, slashing conditions, reputation systems

πŸ“– Production Study Order

Study these codebases in order β€” each builds on the previous one’s patterns:

#RepositoryWhy Study ThisKey Files
11inch AggregationRouterV6Executor pattern, multi-source routing β€” the classic aggregator architecture (V6 router source not public; limit order protocol is the best open reference)contracts/LimitOrderProtocol.sol
2UniswapX DutchOrderReactorIntent settlement, Dutch auction price decay, callback pattern for just-in-time liquiditysrc/reactors/DutchOrderReactor.sol, src/lib/DutchDecayLib.sol
3UniswapX ExclusiveDutchOrderReactorExclusive filler period, priority ordering, enhanced MEV protectionsrc/reactors/ExclusiveDutchOrderReactor.sol
4CoW Protocol GPv2SettlementBatch settlement, uniform clearing price, coincidence of wants matchingsrc/contracts/GPv2Settlement.sol, src/contracts/GPv2AllowListAuthentication.sol
50x Exchange ProxyMulti-source routing, transform ERC20 pattern, feature-based architecturecontracts/zero-ex/contracts/src/ZeroEx.sol
6Paraswap AugustusMulti-DEX aggregation, adapter pattern for different AMM interfacescontracts/AugustusSwapper.sol

Reading strategy: Start with UniswapX β€” it’s the cleanest intent-based codebase. Trace the full flow: user signs EIP-712 order β†’ solver calls execute β†’ Reactor validates β†’ callback to solver β†’ solver sources liquidity β†’ Reactor checks output. Then read CoW Protocol’s batch settlement as a contrasting model. The 1inch limit order protocol shows the hybrid approach. 0x and Paraswap show the traditional multi-call executor pattern β€” useful for understanding what intents are replacing.


πŸ“š Resources

Production Code

Documentation

Key Reading


Navigation: ← Module 3: Yield Tokenization | Part 3 Overview | Next: Module 5 β€” MEV Deep Dive β†’