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:

  1. First hop: TokenA → TokenB

  2. 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:

  1. Basic Principles: How multi-hop swaps work

  2. Implementation Methods: Exact input and output multi-hop swaps

  3. Path Optimization: Optimal path finding and price comparison

  4. Advanced Strategies: Split swaps and dynamic path adjustment

  5. Monitoring Analytics: Event monitoring and efficiency analysis

  6. 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.