Part 3 β Module 4: DEX Aggregation & Intents
Difficulty: Intermediate
Estimated reading time: ~35 minutes | Exercises: ~2-3 hours
π Table of Contents
- The Routing Problem
- Split Order Math
- Aggregator On-Chain Patterns
- Build Exercise: Split Router
- The Intent Paradigm
- EIP-712 Order Structures
- Dutch Auction Price Decay
- Build Exercise: Intent Settlement
- Settlement Contract Architecture
- Solvers & the Filler Ecosystem
- CoW Protocol: Batch Auctions
- Summary
- 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
- Start with
unoswap()β single-pool swap, simplest path - Read
swap()β the general multi-hop/multi-split executor - Study how
GenericRouterusesdelegatecallto protocol-specific handlers - 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 poolssplitSwap()β execute a split trade, pulling tokens and swapping through both poolssingleSwap()β 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:
- β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:
- β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 solverendOutput= final (low) output β userβs limit pricedecayPeriod= total auction durationelapsed= 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:
- Price discovery without an order book. The auction finds the market clearing price automatically through time.
- Solver competition compressed into time. The first solver to fill profitably wins. Earlier fill = less profit for solver = better for user.
- No wasted gas. Unlike English auctions where everyone bids on-chain, Dutch auctions have a single on-chain transaction (the fill).
- 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:
- β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 typegetDigest()β full EIP-712 digest with domain separatorresolveDecay()β Dutch auction price calculation at current timestampfill()β 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
- Start with
ExclusiveDutchOrderReactor.solβ the main entry point - Read
DutchDecayLib.solβ the decay math (short, pure functions) - Study
ResolvedOrderβ how raw orders become executable orders - Look at
IReactorCallbackβ the solver callback interface - 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:
- Low-latency market data β Know DEX prices across all pools in real-time
- Gas optimization β Cheaper fill transactions = more competitive
- Multiple liquidity sources β CEX + DEX + private inventory
- Cross-chain capability β For cross-chain intents (UniswapX v2)
- Risk management β Handle inventory risk, failed fills, gas spikes
πΌ Job Market Context
What DeFi teams expect you to know:
- β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
IReactorCallbackthat 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
| Aspect | UniswapX | CoW Protocol |
|---|---|---|
| Model | Individual Dutch auctions | Batch auctions |
| Price discovery | Time decay per order | Solver competition on full batch |
| CoW matching | No (one order at a time) | Yes (batch-level P2P matching) |
| Fill speed | Seconds (continuous) | ~60s (batch window) |
| MEV protection | Dutch auction + exclusive filler | Batch settlement + uniform pricing |
| Cross-chain | Yes (v2) | Limited |
| Best for | Speed-sensitive, large individual orders | MEV-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
- Start with
GPv2Settlement.solβ thesettle()function is the entry point - Read
GPv2Trade.solβ how individual trades are encoded and decoded - Study the three interaction phases (pre, intra, post) β understand the solverβs flexibility
- Look at
GPv2Signing.solβ how order signatures are verified (supports multiple schemes) - Skip the off-chain solver infrastructure initially β focus on the on-chain settlement guarantees
πΌ Job Market Context
What DeFi teams expect you to know:
- β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.
π Cross-Module Concept Links
β 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:
| # | Repository | Why Study This | Key Files |
|---|---|---|---|
| 1 | 1inch AggregationRouterV6 | Executor pattern, multi-source routing β the classic aggregator architecture (V6 router source not public; limit order protocol is the best open reference) | contracts/LimitOrderProtocol.sol |
| 2 | UniswapX DutchOrderReactor | Intent settlement, Dutch auction price decay, callback pattern for just-in-time liquidity | src/reactors/DutchOrderReactor.sol, src/lib/DutchDecayLib.sol |
| 3 | UniswapX ExclusiveDutchOrderReactor | Exclusive filler period, priority ordering, enhanced MEV protection | src/reactors/ExclusiveDutchOrderReactor.sol |
| 4 | CoW Protocol GPv2Settlement | Batch settlement, uniform clearing price, coincidence of wants matching | src/contracts/GPv2Settlement.sol, src/contracts/GPv2AllowListAuthentication.sol |
| 5 | 0x Exchange Proxy | Multi-source routing, transform ERC20 pattern, feature-based architecture | contracts/zero-ex/contracts/src/ZeroEx.sol |
| 6 | Paraswap Augustus | Multi-DEX aggregation, adapter pattern for different AMM interfaces | contracts/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
- UniswapX β ExclusiveDutchOrderReactor, DutchDecayLib, IReactorCallback
- CoW Protocol (GPv2) β GPv2Settlement
- 1inch Limit Order Protocol β V6 aggregation router source is not public; this is the best open-source reference
Documentation
Key Reading
- Paradigm: An Analysis of Intent-Based Markets
- Frontier Research: Order Flow Auctions and Centralisation
- Flashbots: MEV, Intents, and the Suave Future
Navigation: β Module 3: Yield Tokenization | Part 3 Overview | Next: Module 5 β MEV Deep Dive β