Multi-Hop Swaps Guide
Overview
Multi-hop swaps allow users to trade between tokens that don't have direct trading pairs by using intermediate tokens as bridges. JAMM DEX supports flexible multi-hop paths with different fee tiers for each hop, providing users with optimal swap routes.
Multi-Hop Swap Principles
Basic Concept
Multi-hop swaps break down a complex swap into multiple simple swaps:
TokenA → TokenB → TokenC
Each hop is an independent swap operation:
First hop: TokenA → TokenB
Second hop: TokenB → TokenC
Paths and Fees
In JAMM DEX, multi-hop swaps require specifying:
Path array:
[TokenA, TokenB, TokenC]
Fees array:
[fee1, fee2]
Note: The fees array length is always one less than the path array length.
Multi-Hop Swap Implementation
Exact Input Multi-Hop Swap
async function multiHopSwapExactInput(
amountIn,
amountOutMin,
path,
fees,
to,
referrer,
deadline,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
// Validate path and fees array lengths
if (path.length - fees.length !== 1) {
throw new Error("Path and fees array length mismatch");
}
// Execute multi-hop swap
const tx = await router.swapExactTokensForTokens(
amountIn,
amountOutMin,
path,
fees,
to,
referrer,
deadline
);
return tx;
}
// Usage example: USDC → WJU → TokenX
const path = [USDC_ADDRESS, WJU_ADDRESS, TOKENX_ADDRESS];
const fees = [50, 100]; // USDC/WJU uses 0.5% fee, WJU/TokenX uses 1% fee
const amountIn = ethers.utils.parseUnits("100", 6); // 100 USDC
const amountOutMin = ethers.utils.parseEther("0.95"); // Minimum 0.95 TokenX
await multiHopSwapExactInput(
amountIn,
amountOutMin,
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline,
signer
);
Exact Output Multi-Hop Swap
async function multiHopSwapExactOutput(
amountOut,
amountInMax,
path,
fees,
to,
referrer,
deadline,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const tx = await router.swapTokensForExactTokens(
amountOut,
amountInMax,
path,
fees,
to,
referrer,
deadline
);
return tx;
}
// Usage example: Get exactly 100 TokenX
const amountOut = ethers.utils.parseEther("100"); // Exact output 100 TokenX
const amountInMax = ethers.utils.parseUnits("110", 6); // Maximum pay 110 USDC
await multiHopSwapExactOutput(
amountOut,
amountInMax,
path,
fees,
userAddress,
ethers.constants.AddressZero,
deadline,
signer
);
Path Optimization
Optimal Path Finding
class PathFinder {
constructor(factoryAddress, provider) {
this.factory = new ethers.Contract(factoryAddress, factoryABI, provider);
this.provider = provider;
this.commonBases = [WJU_ADDRESS, USDC_ADDRESS, USDT_ADDRESS]; // Common intermediate tokens
}
async findBestPath(tokenIn, tokenOut, amountIn) {
const paths = [];
// 1. Direct path
const directPath = await this.checkDirectPath(tokenIn, tokenOut, amountIn);
if (directPath) {
paths.push(directPath);
}
// 2. Paths through common base tokens
for (const base of this.commonBases) {
if (base !== tokenIn && base !== tokenOut) {
const path = await this.checkTwoHopPath(tokenIn, base, tokenOut, amountIn);
if (path) {
paths.push(path);
}
}
}
// 3. Select optimal path (maximum output)
return paths.reduce((best, current) =>
current.outputAmount.gt(best.outputAmount) ? current : best
);
}
async checkDirectPath(tokenIn, tokenOut, amountIn) {
const fees = [50, 100, 200, 300]; // Check all fee tiers
let bestOutput = ethers.BigNumber.from(0);
let bestFee = 0;
for (const fee of fees) {
try {
const pairAddress = await this.factory.getPair(tokenIn, tokenOut, fee);
if (pairAddress === ethers.constants.AddressZero) continue;
const pair = new ethers.Contract(pairAddress, pairABI, this.provider);
const reserves = await pair.getReserves();
if (reserves.reserve0.gt(0) && reserves.reserve1.gt(0)) {
const output = this.calculateOutput(amountIn, reserves, fee, tokenIn, tokenOut);
if (output.gt(bestOutput)) {
bestOutput = output;
bestFee = fee;
}
}
} catch (error) {
continue;
}
}
if (bestOutput.gt(0)) {
return {
path: [tokenIn, tokenOut],
fees: [bestFee],
outputAmount: bestOutput,
hops: 1
};
}
return null;
}
async checkTwoHopPath(tokenIn, intermediate, tokenOut, amountIn) {
// Check first hop: tokenIn → intermediate
const firstHop = await this.checkDirectPath(tokenIn, intermediate, amountIn);
if (!firstHop) return null;
// Check second hop: intermediate → tokenOut
const secondHop = await this.checkDirectPath(intermediate, tokenOut, firstHop.outputAmount);
if (!secondHop) return null;
return {
path: [tokenIn, intermediate, tokenOut],
fees: [firstHop.fees[0], secondHop.fees[0]],
outputAmount: secondHop.outputAmount,
hops: 2
};
}
calculateOutput(amountIn, reserves, fee, tokenIn, tokenOut) {
// Simplified output calculation (should use JAMMLibrary logic in practice)
const [reserveIn, reserveOut] = tokenIn < tokenOut
? [reserves.reserve0, reserves.reserve1]
: [reserves.reserve1, reserves.reserve0];
const amountInWithFee = amountIn.mul(10000 - fee);
const numerator = amountInWithFee.mul(reserveOut);
const denominator = reserveIn.mul(10000).add(amountInWithFee);
return numerator.div(denominator);
}
}
// Usage example
const pathFinder = new PathFinder(FACTORY_ADDRESS, provider);
const bestPath = await pathFinder.findBestPath(
TOKEN_A_ADDRESS,
TOKEN_B_ADDRESS,
ethers.utils.parseEther("100")
);
console.log("Optimal path:", bestPath);
Price Comparison
async function compareMultiHopPrices(tokenIn, tokenOut, amountIn) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
const results = [];
// Path 1: Direct swap
try {
const directPath = [tokenIn, tokenOut];
const directFees = [100]; // 1% fee
const directAmounts = await router.getAmountsOut(amountIn, directPath, directFees);
results.push({
path: directPath,
fees: directFees,
output: directAmounts[directAmounts.length - 1],
description: "Direct swap"
});
} catch (error) {
console.log("Direct path not available");
}
// Path 2: Through WJU
try {
const wjuPath = [tokenIn, WJU_ADDRESS, tokenOut];
const wjuFees = [100, 100]; // Both use 1% fee
const wjuAmounts = await router.getAmountsOut(amountIn, wjuPath, wjuFees);
results.push({
path: wjuPath,
fees: wjuFees,
output: wjuAmounts[wjuAmounts.length - 1],
description: "Through WJU"
});
} catch (error) {
console.log("WJU path not available");
}
// Path 3: Through USDC
try {
const usdcPath = [tokenIn, USDC_ADDRESS, tokenOut];
const usdcFees = [50, 50]; // Both use 0.5% fee
const usdcAmounts = await router.getAmountsOut(amountIn, usdcPath, usdcFees);
results.push({
path: usdcPath,
fees: usdcFees,
output: usdcAmounts[usdcAmounts.length - 1],
description: "Through USDC"
});
} catch (error) {
console.log("USDC path not available");
}
// Sort by output amount
results.sort((a, b) => b.output.gt(a.output) ? 1 : -1);
return results;
}
JU Token Multi-Hop Swaps
JU → Token → Token
async function swapJUMultiHop(
tokenIntermediate,
tokenOut,
juAmount,
amountOutMin,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const path = [WJU_ADDRESS, tokenIntermediate, tokenOut];
const fees = [100, 100]; // Can be adjusted based on actual conditions
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.swapExactETHForTokens(
amountOutMin,
path,
fees,
await signer.getAddress(),
ethers.constants.AddressZero,
deadline,
{ value: juAmount }
);
return tx;
}
// Usage example: JU → USDC → TokenX
await swapJUMultiHop(
USDC_ADDRESS,
TOKENX_ADDRESS,
ethers.utils.parseEther("1"), // 1 JU
ethers.utils.parseEther("95"), // Minimum 95 TokenX
signer
);
Token → Token → JU
async function swapMultiHopToJU(
tokenIn,
tokenIntermediate,
amountIn,
amountOutMin,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const path = [tokenIn, tokenIntermediate, WJU_ADDRESS];
const fees = [100, 100];
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.swapExactTokensForETH(
amountIn,
amountOutMin,
path,
fees,
await signer.getAddress(),
ethers.constants.AddressZero,
deadline
);
return tx;
}
Advanced Multi-Hop Strategies
Split Swaps
Split large swaps into multiple smaller swaps to reduce price impact:
async function splitMultiHopSwap(
amountIn,
path,
fees,
splits,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const splitAmount = amountIn.div(splits);
const results = [];
for (let i = 0; i < splits; i++) {
try {
// Recalculate minimum output before each swap
const amounts = await router.getAmountsOut(splitAmount, path, fees);
const minOutput = amounts[amounts.length - 1].mul(95).div(100); // 5% slippage
const tx = await router.swapExactTokensForTokens(
splitAmount,
minOutput,
path,
fees,
await signer.getAddress(),
ethers.constants.AddressZero,
Math.floor(Date.now() / 1000) + 60 * 20
);
results.push(tx);
// Wait before next swap
if (i < splits - 1) {
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
console.error(`Split swap ${i + 1} failed:`, error.message);
}
}
return results;
}
Dynamic Path Adjustment
Adjust swap paths based on real-time liquidity:
class DynamicRouter {
constructor(routerAddress, factoryAddress, provider) {
this.router = new ethers.Contract(routerAddress, routerABI, provider);
this.factory = new ethers.Contract(factoryAddress, factoryABI, provider);
this.provider = provider;
}
async executeOptimalSwap(tokenIn, tokenOut, amountIn, maxSlippage = 300) {
// 1. Find all possible paths
const paths = await this.findAllPaths(tokenIn, tokenOut);
// 2. Calculate output for each path
const pathResults = await Promise.all(
paths.map(path => this.calculatePathOutput(path, amountIn))
);
// 3. Select optimal path
const bestPath = pathResults.reduce((best, current) =>
current.output.gt(best.output) ? current : best
);
// 4. Check slippage
const priceImpact = this.calculatePriceImpact(bestPath, amountIn);
if (priceImpact.gt(maxSlippage)) {
throw new Error(`Price impact too high: ${priceImpact.toString()} basis points`);
}
// 5. Execute swap
const minOutput = bestPath.output.mul(10000 - maxSlippage).div(10000);
return await this.router.swapExactTokensForTokens(
amountIn,
minOutput,
bestPath.path,
bestPath.fees,
await this.provider.getSigner().getAddress(),
ethers.constants.AddressZero,
Math.floor(Date.now() / 1000) + 60 * 20
);
}
async findAllPaths(tokenIn, tokenOut, maxHops = 3) {
// Implement path finding logic
// Return all possible path combinations
}
async calculatePathOutput(pathInfo, amountIn) {
try {
const amounts = await this.router.getAmountsOut(
amountIn,
pathInfo.path,
pathInfo.fees
);
return {
...pathInfo,
output: amounts[amounts.length - 1]
};
} catch (error) {
return {
...pathInfo,
output: ethers.BigNumber.from(0)
};
}
}
calculatePriceImpact(pathInfo, amountIn) {
// Calculate price impact
// Return basis points value
}
}
Monitoring and Analytics
Multi-Hop Swap Event Monitoring
async function monitorMultiHopSwaps(userAddress) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
// Monitor user's multi-hop swaps
const filter = router.filters.SwapExactTokensForTokens(null, null, null, null, userAddress);
router.on(filter, async (sender, amountIn, amountOutMin, path, fees, to, referrer, deadline, event) => {
if (path.length > 2) {
console.log("Multi-hop swap detected:", {
sender,
path: path.map(addr => addr.slice(0, 6) + '...'),
fees,
hops: path.length - 1,
transactionHash: event.transactionHash
});
// Get actual output amount
const receipt = await event.getTransactionReceipt();
// Parse logs to get actual swap data
}
});
}
Path Efficiency Analysis
async function analyzePathEfficiency(tokenIn, tokenOut, amountIn) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, provider);
const paths = [
{ path: [tokenIn, tokenOut], fees: [100], name: "Direct" },
{ path: [tokenIn, WJU_ADDRESS, tokenOut], fees: [100, 100], name: "Through WJU" },
{ path: [tokenIn, USDC_ADDRESS, tokenOut], fees: [50, 50], name: "Through USDC" }
];
const analysis = [];
for (const pathInfo of paths) {
try {
const amounts = await router.getAmountsOut(amountIn, pathInfo.path, pathInfo.fees);
const output = amounts[amounts.length - 1];
const totalFee = pathInfo.fees.reduce((sum, fee) => sum + fee, 0);
analysis.push({
name: pathInfo.name,
path: pathInfo.path,
output: output,
totalFee: totalFee,
efficiency: output.mul(10000).div(amountIn), // Output/input ratio
hops: pathInfo.path.length - 1
});
} catch (error) {
analysis.push({
name: pathInfo.name,
error: error.message
});
}
}
// Sort by efficiency
analysis.sort((a, b) => {
if (!a.efficiency || !b.efficiency) return 0;
return b.efficiency.gt(a.efficiency) ? 1 : -1;
});
return analysis;
}
Best Practices
1. Path Selection Strategy
const pathSelectionStrategy = {
// Small amounts: prioritize lowest fees
smallAmount: (paths) => paths.sort((a, b) => a.totalFee - b.totalFee)[0],
// Large amounts: prioritize deepest liquidity
largeAmount: (paths) => paths.sort((a, b) => b.liquidity.gt(a.liquidity) ? 1 : -1)[0],
// Balanced strategy: consider both output and fees
balanced: (paths) => paths.sort((a, b) => {
const scoreA = a.output.mul(10000).div(a.totalFee + 1);
const scoreB = b.output.mul(10000).div(b.totalFee + 1);
return scoreB.gt(scoreA) ? 1 : -1;
})[0]
};
2. Slippage Management
function calculateDynamicSlippage(pathLength, marketVolatility) {
let baseSlippage = 50; // 0.5% base slippage
// Increase slippage based on number of hops
const hopPenalty = (pathLength - 1) * 25; // 0.25% per hop
// Adjust based on market volatility
const volatilityAdjustment = marketVolatility * 100;
return Math.min(baseSlippage + hopPenalty + volatilityAdjustment, 500); // Maximum 5%
}
3. Gas Optimization
async function optimizeMultiHopGas(paths, gasPrice) {
const gasEstimates = await Promise.all(
paths.map(async (path) => {
try {
const gasEstimate = await router.estimateGas.swapExactTokensForTokens(
ethers.utils.parseEther("1"),
0,
path.path,
path.fees,
userAddress,
ethers.constants.AddressZero,
Math.floor(Date.now() / 1000) + 60 * 20
);
return {
...path,
gasEstimate,
gasCost: gasEstimate.mul(gasPrice)
};
} catch (error) {
return { ...path, gasEstimate: null };
}
})
);
// Find optimal path considering gas costs
return gasEstimates.filter(p => p.gasEstimate).sort((a, b) => {
const netA = a.output.sub(a.gasCost);
const netB = b.output.sub(b.gasCost);
return netB.gt(netA) ? 1 : -1;
})[0];
}
Summary
Multi-hop swaps are a powerful feature of JAMM DEX. This guide covers:
Basic Principles: How multi-hop swaps work
Implementation Methods: Exact input and output multi-hop swaps
Path Optimization: Optimal path finding and price comparison
Advanced Strategies: Split swaps and dynamic path adjustment
Monitoring Analytics: Event monitoring and efficiency analysis
Best Practices: Path selection, slippage management, gas optimization
By properly using multi-hop swaps, users can efficiently trade between a wider range of token pairs.