Skip to content

Repository files navigation

Route Processor

Stateless, modular execution engine for composable DeFi routing

A highly gas-optimized, stateless route execution engine for executing complex token swaps across multiple DeFi protocols in a single atomic transaction.

Table of Contents


Overview

RouteProcessor is a modular swap aggregator built with Foundry that enables seamless token exchanges across:

  • Uniswap V2
  • Uniswap V3
  • Curve Finance
  • Liquid Staking Tokens (LST) - Lido, Rocket Pool, Frax, Swell, Binance, Stader, StakeWise, Ether.fi, Origin, Ankr
  • Liquid Restaking Tokens (LRT) - Renzo, Puffer, Mantle

The protocol interprets encoded command streams to execute multi-hop routes with maximum efficiency, utilizing inline assembly optimizations and transient storage for callback validation.

Key Features

  • Multi-Protocol Support - Single interface for Curve, Uniswap V2/V3, and 10+ LST/LRT protocols
  • Gas Optimized - Assembly-level optimizations with efficient calldata encoding
  • Security - Reentrancy protection, deadline validation, and callback authentication
  • Multi-Hop Routes - Execute complex paths across different protocols in one transaction
  • Permit2 Integration - Gasless approvals via Uniswap's Permit2 contract
  • Native ETH Support - Seamless handling of both ERC20 tokens and native ETH
  • Stateless Design - No storage dependencies, all execution state lives in calldata

Supported Protocols

DEX Protocols

  • Uniswap V2 (SushiSwap V2, PancakeSwap V2, etc.)
  • Uniswap V3 (SushiSwap V3, PancakeSwap V3, etc.)
  • Curve Finance

Liquid Staking Tokens (LST)

  • Lido - stETH, wstETH
  • Rocket Pool - rETH
  • Frax Ether - frxETH, sfrxETH
  • Swell - swETH
  • Binance - wBETH
  • Stader - ETHx
  • StakeWise - osETH
  • Ether.fi - eETH, weETH
  • Origin - OETH, WOETH
  • Ankr - ankrETH, aETHb

Liquid Restaking Tokens (LRT)

  • Mantle - mETH, cmETH
  • Renzo - ezETH, pzETH
  • Puffer - pufETH

Architecture

Core Components

RouteProcessor (Main Entry Point)
├── NativeWrapper → WETH/LST/LRT operations
├── Permit2Forwarder → Permit2 integration
├── CurveRoute → Curve integration
├── V2Route → Uniswap V2 integration
├── V3Route → Uniswap V3 integration
└── ReentrancyGuard → Reentrancy protection

Command System

Routes are encoded as byte streams containing commands:

CommandOpcodeDescription
SWAP0x00Execute swap route (V2/V3/Curve/WETH/LST/LRT)
PERMIT2_PERMIT0x01Single-token permit approval via Permit2
PERMIT2_PERMIT_BATCH0x02Batch-token permit approval via Permit2
PERMIT2_TRANSFER_FROM0x03Single-token transfer via Permit2
PERMIT2_TRANSFER_FROM_BATCH0x04Batch-token transfer via Permit2
SWEEP0x05Transfer remaining contract balance

Commands.sol

Protocol Identifiers

Each protocol is identified by a single byte in the route encoding:

ProtocolIDTypeDescription
WETH0x00WETHWrapped Ether (ETH ↔ WETH)
Curve0x01DEXCurve Finance AMMs
UniswapV20x02DEXUniswap V2 AMMs (constant product)
UniswapV30x03DEXUniswap V3 AMMs (concentrated liquidity)
Rocket0x04LSTRocket Pool (rETH)
Swell0x05LSTSwell Network (swETH)
Binance0x06LSTBinance Staking (wBETH)
Stader0x07LSTStader Labs (ETHx)
StakeWise0x08LSTStakeWise (osETH)
Lido0x09LSTLido Finance (stETH, wstETH)
EtherFi0x0aLSTEther.fi (eETH, weETH)
Frax0x0bLSTFrax Ether (frxETH, sfrxETH)
Origin0x0cLSTOrigin Protocol (OETH, WOETH)
Ankr0x0dLSTAnkr Liquid Staking (ankrETH, aETHb)
Mantle0x0eLST/LRTMantle Liquid Staking Platform (mETH, cmETH)
Renzo0x0fLRTRenzo Protocol (ezETH, pzETH)
Puffer0x10LRTPuffer Finance (pufETH)

Asset Types

TypeIDDescription
ETH0x00Native Ether
WETH0x01Wrapped Ether
LST0x02Liquid Staking Token
WLST0x03Wrapped Liquid Staking Token
LRT0x04Liquid Restaking Token

Enums.sol

Usage

Installation

# Clone the repository
git clone https://github.com/fomoweth/route-processor.git
cd route-processor
# Install dependencies
forge install
# Build the project
forge build

Project Structure

src/
├── interfaces/
│ └── IRouteProcessor.sol → Main interface
├── libraries/
│ ├── Commands.sol → Command opcodes
│ ├── Errors.sol → Custom errors
│ └── SafeTransferLib.sol → Safe token transfers
├── modules/
│ ├── CurveRoute.sol → Curve integration
│ ├── NativeWrapper.sol → WETH/LST/LRT operations
│ ├── Permit2Forwarder.sol → Permit2 integration
│ ├── V2Route.sol → Uniswap V2 integration
│ └── V3Route.sol → Uniswap V3 integration
├── types/
│ ├── Enums.sol → Protocol & asset types
│ └── Stream.sol → Calldata stream parser
├── utils/
│ ├── CallbackValidation.sol → V3 callback auth
│ └── ReentrancyGuard.sol → Reentrancy protection
└── RouteProcessor.sol → Main contract

Permit2 Commands

Permit Single
PermitDetails memory details =PermitDetails({
token: ...,
amount: ...,
expiration: ...,
nonce: ...
});
uint256 word = (uint256(details.nonce) <<208) | (uint256(details.expiration) <<160) |uint256(details.amount);
uint256 sigDeadline = ...
bytesmemory signature = ...
bytesmemory cmd =abi.encodePacked(
Commands.PERMIT2_PERMIT,
token,
word,
sigDeadline,
signature.length,
signature
);
Permit Batch
PermitDetails[] memory details =newPermitDetails[](n);
for (uint256 i =0; i < details.length; ++i) {
details[i] =PermitDetails({
token: ...,
amount: ...,
expiration: ...,
nonce: ...
});
}
bytesmemory encoded =abi.encode(details);
uint256 sigDeadline = ...
bytesmemory signature = ...
bytesmemory cmd =abi.encodePacked(
Commands.PERMIT2_PERMIT_BATCH,
encoded.length,
encoded,
sigDeadline,
signature.length,
signature
);
TransferFrom
bytesmemory cmd =abi.encodePacked(Commands.PERMIT2_TRANSFER_FROM, token, uint160(amount));
Batch TransferFrom
AllowanceTransferDetails[] memory transferDetails =newAllowanceTransferDetails[](n);
for (uint256 i =0; i < transferDetails.length; ++i) {
transferDetails[i] =AllowanceTransferDetails({
from: msg.sender, // must be the sender
to: address(routeProcessor), // must be the RouteProcessor
amount: ...,
token: ...
});
}
bytesmemory encoded =abi.encode(transferDetails);
bytesmemory cmd =abi.encodePacked(
Commands.PERMIT2_TRANSFER_FROM_BATCH,
encoded.length,
encoded
);

Sweep Command

Sweep
bytesmemory cmd =abi.encodePacked(Commands.SWEEP, token, recipient, amount);

WETH/LST/LRT Operations

WETH

ETH → WETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.WETH,
address(0), // zero address for pool
WETH,
AssetType.ETH,
AssetType.WETH
);
routeProcessor.processRoute{value: msg.value}(route);

WETH → ETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
WETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.WETH,
address(0), // zero address for pool
ETH,
AssetType.WETH,
AssetType.ETH
);
routeProcessor.processRoute(route);
Rocket Pool (rETH)

ETH → rETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Rocket,
RocketDepositPool,
rETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);
Swell (swETH)

ETH → swETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Swell,
address(0), // zero address for pool
swETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);
Binance (wBETH)

ETH → wBETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Binance,
address(0), // zero address for pool
wBETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);
Stader (ETHx)

ETH → ETHx

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Stader,
StaderStakePoolsManager,
ETHx,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);
StakeWise (osETH)

ETH → osETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.StakeWise,
EthGenesisVault,
osETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);
Lido (stETH, wstETH)

ETH → stETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Lido,
address(0), // zero address for pool
stETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

ETH → wstETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Lido,
address(0), // zero address for pool
wstETH,
AssetType.ETH,
AssetType.WLST
);
routeProcessor.processRoute{value: msg.value}(route);

stETH → wstETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
stETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Lido,
address(0), // zero address for pool
wstETH,
AssetType.LST,
AssetType.WLST
);
routeProcessor.processRoute(route);

wstETH → stETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
wstETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Lido,
address(0), // zero address for pool
stETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route);
Ether.fi (eETH, weETH)

ETH → eETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.EtherFi,
LiquidityPool,
eETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

ETH → weETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.EtherFi,
LiquidityPool,
weETH,
AssetType.ETH,
AssetType.WLST
);
routeProcessor.processRoute{value: msg.value}(route);

eETH → weETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
eETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.EtherFi,
address(0), // zero address for pool
weETH,
AssetType.LST,
AssetType.WLST
);
routeProcessor.processRoute(route);

weETH → eETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
weETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.EtherFi,
address(0), // zero address for pool
eETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route);
Frax (frxETH, sfrxETH)

ETH → frxETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Frax,
frxETHMinter,
frxETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

ETH → sfrxETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Frax,
frxETHMinter,
sfrxETH,
AssetType.ETH,
AssetType.WLST
);
routeProcessor.processRoute{value: msg.value}(route);

frxETH → sfrxETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
frxETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Frax,
address(0), // zero address for pool
sfrxETH,
AssetType.LST,
AssetType.WLST
);
routeProcessor.processRoute(route);

sfrxETH → frxETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
sfrxETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Frax,
address(0), // zero address for pool
frxETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route);
Origin (OETH, WOETH)

WETH → OETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
WETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Origin,
OETHVault,
OETH,
AssetType.WETH,
AssetType.LST
);
routeProcessor.processRoute(route);

WETH → WOETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
WETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Origin,
OETHVault,
WOETH,
AssetType.WETH,
AssetType.WLST
);
routeProcessor.processRoute(route);

OETH → WOETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
OETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Origin,
address(0), // zero address for pool
WOETH,
AssetType.LST,
AssetType.WLST
);
routeProcessor.processRoute(route);

WOETH → OETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
WOETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Origin,
address(0), // zero address for pool
OETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route);
Ankr (ankrETH, aETHb)

ETH → ankrETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Ankr,
GlobalPool,
ankrETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

ETH → aETHb

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Ankr,
GlobalPool,
aETHb,
AssetType.ETH,
AssetType.WLST
);
routeProcessor.processRoute{value: msg.value}(route);

ankrETH → aETHb

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
ankrETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Ankr,
address(0), // zero address for pool
aETHb,
AssetType.LST,
AssetType.WLST
);
routeProcessor.processRoute(route);

aETHb → ankrETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
aETHb,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Ankr,
address(0), // zero address for pool
ankrETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route);
Mantle (mETH, cmETH)

ETH → mETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Mantle,
Staking,
mETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

ETH → cmETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Mantle,
Teller,
cmETH,
AssetType.ETH,
AssetType.LRT
);
routeProcessor.processRoute{value: msg.value}(route);

mETH → cmETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
mETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Mantle,
Teller,
cmETH,
AssetType.LST,
AssetType.LRT
);
routeProcessor.processRoute(route);
Renzo (ezETH, pzETH)

ETH → ezETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Renzo,
RestakeManager,
ezETH,
AssetType.ETH,
AssetType.LST
);
routeProcessor.processRoute{value: msg.value}(route);

stETH → ezETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
stETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Renzo,
RestakeManager,
ezETH,
AssetType.LST,
AssetType.LRT
);
routeProcessor.processRoute(route);

wstETH → pzETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
wstETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Renzo,
address(0), // zero address for pool
pzETH,
AssetType.WLST,
AssetType.LRT
);
routeProcessor.processRoute(route);
Puffer (pufETH)

ETH → pufETH

bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
ETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Puffer,
address(0), // zero address for pool
pufETH,
AssetType.ETH,
AssetType.LRT
);
routeProcessor.processRoute{value: msg.value}(route);

WETH → pufETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
WETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Puffer,
address(0), // zero address for pool
pufETH,
AssetType.WETH,
AssetType.LRT
);
routeProcessor.processRoute(route);

stETH → pufETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
stETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Puffer,
address(0), // zero address for pool
pufETH,
AssetType.LST,
AssetType.LRT
);
routeProcessor.processRoute(route);

pufETH → WETH

bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
pufETH,
amountIn,
amountOutMin,
uint8(1), // number of hops
Protocol.Puffer,
address(0), // zero address for pool
WETH,
AssetType.LRT,
AssetType.WETH
);
routeProcessor.processRoute(route);

Basic Swap Executions

ETH → USDC via Uniswap V2
bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(2), // number of hops (2 = ETH → WETH → USDC)// ETH → WETH
Protocol.WETH,
address(0), // zero address for pool
WETH,
AssetType.ETH,
AssetType.WETH,
// WETH → USDC
Protocol.UniswapV2,
pool, // USDC/WETH: 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc
tokenOut, // USDCuint24(0) // pool fee default to: 3000
);
routeProcessor.processRoute{value: msg.value}(route);
ETH → USDC via Uniswap V3
bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(2), // number of hops (2 = ETH → WETH → USDC)// ETH → WETH
Protocol.WETH,
address(0), // zero address for pool
WETH,
AssetType.ETH,
AssetType.WETH,
// WETH → USDC
Protocol.UniswapV3,
pool, // USDC/WETH: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
tokenOut, // USDCbytes4(0) // callback selector default to: 0xfa461e33
);
routeProcessor.processRoute{value: msg.value}(route, deadline);
ETH → USDC via Curve
bytesmemory route =abi.encodePacked(
Commands.SWAP,
recipient,
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(1), // number of hops// ETH → USDC
Protocol.Curve,
pool, // TricryptoUSDC: 0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B
tokenOut, // USDCuint8(2), // iuint8(0), // jtrue, // isCryptoPoolfalse, // useUnderlyingtrue// useEth
);
routeProcessor.processRoute{value: msg.value}(route, deadline);

Multi-Hop Routes

Mixed Route (WETH → wstETH → sfrxETH → frxETH)
bytesmemory route =abi.encodePacked(
permitCmd,
transferCmd,
Commands.SWAP,
recipient,
tokenIn, // WETH
amountIn,
amountOutMin,
uint8(3), // number of hops (3 = WETH → wstETH → sfrxETH → frxETH)// WETH → wstETH
Protocol.UniswapV3,
UNI_V3_POOL, // wstETH/WETH: 0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa
wstETH,
bytes4(0), // callback selector default to: 0xfa461e33// wstETH → sfrxETH
Protocol.Curve,
CURVE_POOL, // TryLSD: 0x2570f1bD5D2735314FC102eb12Fc1aFe9e6E7193
sfrxETH,
uint8(0), // iuint8(2), // jtrue, // isCryptoPoolfalse, // useUnderlyingfalse, // useEth// sfrxETH → frxETH
Protocol.Frax,
address(0), // zero address for pool
frxETH,
AssetType.WLST,
AssetType.LST
);
routeProcessor.processRoute(route, deadline);
Split Routes (ETH → wstETH)
bytesmemory v2Route =abi.encodePacked(
Commands.SWAP,
address(2), // equivalent to `address(this)`
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(2), // number of hops (2 = ETH → WETH → stETH)// ETH → WETH
Protocol.WETH,
address(0), // zero address for pool
WETH,
AssetType.ETH,
AssetType.WETH,
// WETH → stETH
Protocol.UniswapV2,
UNI_V2_POOL, // stETH/WETH: 0x4028DAAC072e492d34a3Afdbef0ba7e35D8b55C4
stETH,
uint24(3000) // pool fee for Uniswap V2 pairs
);
bytesmemory v3Route =abi.encodePacked(
Commands.SWAP,
address(1), // equivalent to `msg.sender`
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(2), // number of hops (2 = ETH → WETH → wstETH)// ETH → WETH
Protocol.WETH,
address(0), // zero address for pool
WETH,
AssetType.ETH,
AssetType.WETH,
// WETH → wstETH
Protocol.UniswapV3,
UNI_V3_POOL, // wstETH/WETH: 0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa
wstETH,
bytes4(0xfa461e33) // callback selector for Uniswap V3 pools
);
bytesmemory crvRoute =abi.encodePacked(
Commands.SWAP,
address(2), // equivalent to `address(this)`
tokenIn, // ETH
amountIn,
amountOutMin,
uint8(1), // number of hops// ETH → stETH
Protocol.Curve,
CURVE_POOL, // ETH/stETH: 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022
stETH,
uint8(0), // iuint8(1), // jfalse, // isCryptoPoolfalse, // useUnderlyingfalse// useEth
);
bytesmemory wrapCmd =abi.encodePacked(
Commands.SWAP,
address(1), // equivalent to `msg.sender`
stETH,
CONTRACT_BALANCE, // 0x8000000000000000000000000000000000000000000000000000000000000000
amountOutMin,
uint8(1), // number of hops// stETH → wstETH
Protocol.Lido,
address(0), // zero address for pool
wstETH,
AssetType.LST,
AssetType.WLST
);
bytesmemory route =bytes.concat(v2Route, v3Route, crvRoute, wrapCmd);
routeProcessor.processRoute{value: msg.value}(route, deadline);
Split Routes (wstETH, weETH, sfrxETH → ETH)
address[] memory tokens =newaddress[](3);
tokens[0] = wstETH;
tokens[1] = weETH;
tokens[2] = frxETH;
uint160[] memory amounts = ...
PermitDetails[] memory _details =newPermitDetails[](tokens.length);
AllowanceTransferDetails[] memory _transferDetails =newAllowanceTransferDetails[](tokens.length);
for (uint256 i =0; i < tokens.length; ++i) {
_details[i] =PermitDetails({
token: tokens[i],
amount: amounts[i],
expiration: ...,
nonce: ...
});
_transferDetails[i] =AllowanceTransferDetails({
from: msg.sender, // must be the sender
to: address(routeProcessor), // must be the RouteProcessor
amount: amounts[i],
token: tokens[i]
});
}
bytesmemory details =abi.encode(_details);
bytesmemory transferDetails =abi.encode(_transferDetails);
uint256 sigDeadline = ...
bytesmemory signature = ...
bytesmemory permitCmd =abi.encodePacked(
Commands.PERMIT2_PERMIT_BATCH,
details.length,
details,
sigDeadline,
signature.length,
signature
);
bytesmemory transferCmd =abi.encodePacked(
Commands.PERMIT2_TRANSFER_FROM_BATCH,
transferDetails.length,
transferDetails
);
bytesmemory v2Route =abi.encodePacked(
Commands.SWAP,
address(2), // equivalent to `address(this)`
tokens[0], // wstETH
amounts[0],
amountOutMin,
uint8(2), // number of hops (2 = wstETH → stETH → WETH)// wstETH → stETH
Protocol.Lido,
address(0), // zero address for pool
stETH,
AssetType.WLST,
AssetType.LST,
// stETH → WETH
Protocol.UniswapV2,
UNI_V2_POOL, // stETH/WETH: 0x4028DAAC072e492d34a3Afdbef0ba7e35D8b55C4
WETH,
uint24(3000) // pool fee
);
bytesmemory v3Route =abi.encodePacked(
Commands.SWAP,
address(2), // equivalent to `address(this)`
tokens[1], // weETH
amounts[1],
amountOutMin,
uint8(1), // number of hops// weETH → WETH
Protocol.UniswapV3,
UNI_V3_POOL, // WETH/weETH: 0x202A6012894Ae5c288eA824cbc8A9bfb26A49b93
WETH,
bytes4(0xfa461e33), // callback selector
);
bytesmemory crvRoute =abi.encodePacked(
Commands.SWAP,
address(2), // equivalent to `address(this)`
tokens[2], // sfrxETH
amounts[2],
amountIn,
amountOutMin,
uint8(2), // number of hops (2 = sfrxETH → frxETH → WETH)// sfrxETH → frxETH
Protocol.Frax,
address(0), // zero address for pool
frxETH,
AssetType.WLST,
AssetType.LST,
// frxETH → WETH
Protocol.Curve,
CURVE_POOL, // WETH/frxETH: 0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc
WETH,
uint8(1), // iuint8(0), // jfalse, // isCryptoPoolfalse, // useUnderlyingfalse// useEth
);
bytesmemory unwrapCmd =abi.encodePacked(
Commands.SWAP,
address(1), // equivalent to `msg.sender`
WETH,
CONTRACT_BALANCE, // 0x8000000000000000000000000000000000000000000000000000000000000000
amountOutMin,
uint8(1), // number of hops
Protocol.WETH,
address(0), // zero address for pool
ETH,
AssetType.WETH,
AssetType.ETH
);
bytesmemory route =bytes.concat(permitCmd, transferCmd, v2Route, v3Route, crvRoute, unwrapCmd);
routeProcessor.processRoute(route, deadline);

Testing

# Run all tests (tests run on mainnet fork at block 23265742)
forge test# Run tests with verbosity (-vvv for detailed traces)
forge test -vvv
# Run specific test file
forge test --match-path test/modules/V3Route.t.sol

Test Structure

The project includes comprehensive test coverage across all modules with 100+ test cases covering edge cases, multi-hop routes, and complex scenarios.

test/
├── modules/
│ ├── curve/ → (Curve AMMs)
│ │ ├── CurveRouteCryptoPool.t.sol
│ │ ├── CurveRouteLendingPool.t.sol
│ │ ├── CurveRouteMetaPool.t.sol
│ │ └── CurveRouteStablePool.t.sol
│ ├── native/
│ │ ├── NativeWrapper.t.sol → (WETH, rETH, swETH, wBETH, ETHx, osETH)
│ │ ├── NativeWrapperLido.t.sol → (stETH, wstETH)
│ │ ├── NativeWrapperAnkr.t.sol → (ankrETH, aETHb)
│ │ ├── NativeWrapperEtherFi.t.sol → (eETH, weETH)
│ │ ├── NativeWrapperFrax.t.sol → (frxETH, sfrxETH)
│ │ ├── NativeWrapperMantle.t.sol → (mETH, cmETH)
│ │ ├── NativeWrapperOrigin.t.sol → (OETH, WOETH)
│ │ ├── NativeWrapperPuffer.t.sol → (pufETH)
│ │ └── NativeWrapperRenzo.t.sol → (ezETH, pzETH)
│ ├── Permit2Forwarder.t.sol → (Permit2 permits/transfers)
│ ├── V2Route.t.sol → (Uniswap V2 AMMs)
│ └── V3Route.t.sol → (Uniswap V3 AMMs)
├── shared/
│ ├── BaseTest.sol
│ ├── Constants.sol
│ ├── Permit2Utils.sol → Permit2 helpers
│ └── Planner.sol → Route builder DSL
├── types/
│ └── Stream.t.sol
└── RouteProcessor.t.sol

Security Considerations

Built-in Protections

  • Reentrancy Guard - All external entry points are protected
  • Deadline Validation - Transactions expire after specified timestamp
  • Callback Authentication - V3 callbacks use transient storage validation
  • Slippage Protection - Minimum output amounts enforced

Known Considerations

  • The contract is stateless and does not hold user funds
  • All operations are atomic - entire transaction reverts on any failure
  • Native ETH handling requires careful recipient address validation
  • Permit2 integration requires users to approve Permit2 contract first

Gas Optimization Techniques

  • Inline Assembly - Critical paths use hand-optimized assembly
  • Transient Storage - Callback validation uses TSTORE/TLOAD (EIP-1153)
  • Packed Encoding - Minimal calldata overhead with custom stream parser
  • No Storage - Stateless design eliminates SSTORE gas costs

Resources

Acknowledgments

Built with Foundry - A blazing fast, portable and modular toolkit for Ethereum application development.

Inspired by production routers from SushiSwap, Uniswap, and 1inch.

About

Modular, gas-optimized DeFi route processor for multi-protocol swaps, Permit2 operations, and LST/LRT conversions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages