Multihop swaps allow you to trade tokens that don’t have a direct liquidity pool by routing your trade through multiple pools. This is essential for accessing a wider range of tokens and often results in better pricing than single-hop swaps.

Overview

In ShibaSwap v2, multihop swaps are powered by the Router contract, which automatically finds the optimal path for your trade. The router can route trades through multiple pools to find the best execution price and ensure your trade goes through even when direct liquidity is limited.
Multihop swaps are particularly useful when trading between tokens that don’t have a direct pair, such as trading SHIB for a newly listed token through an intermediate token like WETH.

How Multihop Swaps Work

When you execute a multihop swap, the Router contract:
  1. Analyzes available paths - Finds all possible routes between your input and output tokens
  2. Calculates optimal pricing - Determines which path offers the best exchange rate
  3. Executes the trade - Performs the swap through the optimal path in a single transaction
  4. Handles slippage - Ensures your trade executes within your specified slippage tolerance

Router Contract Integration

The ShibaSwap v2 Router contract handles all multihop swap logic. Here’s how to integrate it into your application:

Basic Multihop Swap

contracts/MultihopSwap.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IShibaSwapRouter {
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
    
    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
}

contract MultihopSwap {
    using SafeERC20 for IERC20;
    
    IShibaSwapRouter public immutable router;
    
    constructor(address _router) {
        router = IShibaSwapRouter(_router);
    }
    
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts) {
        // Transfer tokens from user to this contract
        IERC20(path[0]).safeTransferFrom(msg.sender, address(this), amountIn);
        
        // Approve router to spend tokens
        IERC20(path[0]).approve(address(router), amountIn);
        
        // Execute the swap
        amounts = router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            to,
            deadline
        );
    }
}

JavaScript Integration

scripts/multihop-swap.js
const { ethers } = require("hardhat");

async function executeMultihopSwap() {
    // Router contract address for ShibaSwap v2
    const ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
    
    // Token addresses (example addresses - replace with actual addresses)
    const SHIB_ADDRESS = "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE";
    const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const USDC_ADDRESS = "0xA0b86a33E6441b8C4C8C8C8C8C8C8C8C8C8C8C8C";
    
    // Get signer
    const [signer] = await ethers.getSigners();
    
    // Router contract
    const router = new ethers.Contract(ROUTER_ADDRESS, [
        "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)"
    ], signer);
    
    // Swap parameters
    const amountIn = ethers.utils.parseEther("1000000"); // 1M SHIB
    const amountOutMin = ethers.utils.parseUnits("100", 6); // 100 USDC minimum
    const path = [SHIB_ADDRESS, WETH_ADDRESS, USDC_ADDRESS];
    const to = signer.address;
    const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes
    
    try {
        // Approve router to spend SHIB
        const shibToken = new ethers.Contract(SHIB_ADDRESS, [
            "function approve(address spender, uint256 amount) external returns (bool)"
        ], signer);
        
        const approveTx = await shibToken.approve(ROUTER_ADDRESS, amountIn);
        await approveTx.wait();
        console.log("Approved SHIB spending");
        
        // Execute multihop swap
        const swapTx = await router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            to,
            deadline
        );
        
        const receipt = await swapTx.wait();
        console.log("Multihop swap executed successfully!");
        console.log("Transaction hash:", receipt.transactionHash);
        
    } catch (error) {
        console.error("Swap failed:", error.message);
    }
}

executeMultihopSwap();

Path Optimization

The Router automatically finds the optimal path for your trade, but you can also specify custom paths for more control:

Finding Optimal Paths

scripts/path-optimization.js
const { ethers } = require("hardhat");

async function findOptimalPath() {
    const ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
    
    // Get amounts out for different paths
    const router = new ethers.Contract(ROUTER_ADDRESS, [
        "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)"
    ]);
    
    const amountIn = ethers.utils.parseEther("1000000"); // 1M SHIB
    
    // Path 1: SHIB -> WETH -> USDC
    const path1 = [
        "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", // SHIB
        "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
        "0xA0b86a33E6441b8C4C8C8C8C8C8C8C8C8C8C8C8C"  // USDC
    ];
    
    // Path 2: SHIB -> USDT -> USDC
    const path2 = [
        "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", // SHIB
        "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
        "0xA0b86a33E6441b8C4C8C8C8C8C8C8C8C8C8C8C8C"  // USDC
    ];
    
    try {
        const amounts1 = await router.getAmountsOut(amountIn, path1);
        const amounts2 = await router.getAmountsOut(amountIn, path2);
        
        console.log("Path 1 (SHIB -> WETH -> USDC):", 
            ethers.utils.formatUnits(amounts1[2], 6), "USDC");
        console.log("Path 2 (SHIB -> USDT -> USDC):", 
            ethers.utils.formatUnits(amounts2[2], 6), "USDC");
        
        // Choose the better path
        if (amounts1[2].gt(amounts2[2])) {
            console.log("Path 1 is better");
            return path1;
        } else {
            console.log("Path 2 is better");
            return path2;
        }
        
    } catch (error) {
        console.error("Error finding optimal path:", error.message);
    }
}

findOptimalPath();

Advanced Multihop Features

Slippage Protection

Always set appropriate slippage tolerance to protect against price movements during transaction execution.
contracts/SlippageProtection.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SlippageProtection {
    function calculateMinimumOutput(
        uint amountIn,
        uint expectedOutput,
        uint slippageTolerance // in basis points (e.g., 50 = 0.5%)
    ) public pure returns (uint) {
        uint slippageAmount = (expectedOutput * slippageTolerance) / 10000;
        return expectedOutput - slippageAmount;
    }
    
    function executeSwapWithSlippageProtection(
        uint amountIn,
        uint expectedOutput,
        uint slippageTolerance,
        address[] calldata path
    ) external {
        uint amountOutMin = calculateMinimumOutput(
            amountIn, 
            expectedOutput, 
            slippageTolerance
        );
        
        // Execute swap with calculated minimum output
        // ... swap logic here
    }
}

Gas Optimization

Use swapExactTokensForTokens when you know the exact input amount, and swapTokensForExactTokens when you need a specific output amount.
contracts/GasOptimizedSwap.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GasOptimizedSwap {
    // Batch multiple swaps in a single transaction
    function batchMultihopSwaps(
        uint[] calldata amountsIn,
        uint[] calldata amountsOutMin,
        address[][] calldata paths,
        address to,
        uint deadline
    ) external {
        require(
            amountsIn.length == amountsOutMin.length && 
            amountsIn.length == paths.length,
            "Array lengths must match"
        );
        
        for (uint i = 0; i < amountsIn.length; i++) {
            // Execute each swap
            // ... swap logic here
        }
    }
}

Error Handling

Common issues and solutions for multihop swaps:

Best Practices

1

Always check liquidity

Use getAmountsOut to verify sufficient liquidity exists before executing swaps.
This prevents failed transactions and wasted gas.
2

Set appropriate slippage

Balance between protection and execution success.
Too low slippage may cause failed transactions, too high may result in poor pricing.
3

Use optimal paths

Let the router find the best path, but verify with multiple options for large trades.
For trades over $10,000, manually check 2-3 different paths to ensure optimal execution.
4

Handle errors gracefully

Implement proper error handling and user feedback in your application.
Provide clear error messages to help users understand and resolve issues.

Testing Multihop Swaps

Hardhat Test Example

test/multihop-swap.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Multihop Swap", function () {
    let router, tokenA, tokenB, tokenC, user;
    
    beforeEach(async function () {
        [user] = await ethers.getSigners();
        
        // Deploy mock tokens
        const MockToken = await ethers.getContractFactory("MockERC20");
        tokenA = await MockToken.deploy("Token A", "TKA");
        tokenB = await MockToken.deploy("Token B", "TKB");
        tokenC = await MockToken.deploy("Token C", "TKC");
        
        // Deploy router (mock for testing)
        const MockRouter = await ethers.getContractFactory("MockShibaSwapRouter");
        router = await MockRouter.deploy();
        
        // Setup liquidity pools (mock)
        await router.addLiquidity(tokenA.address, tokenB.address, 1000, 1000);
        await router.addLiquidity(tokenB.address, tokenC.address, 1000, 1000);
    });
    
    it("Should execute multihop swap successfully", async function () {
        const amountIn = ethers.utils.parseEther("100");
        const amountOutMin = ethers.utils.parseEther("90");
        const path = [tokenA.address, tokenB.address, tokenC.address];
        
        // Approve tokens
        await tokenA.approve(router.address, amountIn);
        
        // Execute swap
        const tx = await router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            user.address,
            Math.floor(Date.now() / 1000) + 1200
        );
        
        await expect(tx).to.emit(router, "Swap");
    });
    
    it("Should revert with insufficient liquidity", async function () {
        const amountIn = ethers.utils.parseEther("10000"); // Too large
        const amountOutMin = ethers.utils.parseEther("9000");
        const path = [tokenA.address, tokenB.address, tokenC.address];
        
        await tokenA.approve(router.address, amountIn);
        
        await expect(
            router.swapExactTokensForTokens(
                amountIn,
                amountOutMin,
                path,
                user.address,
                Math.floor(Date.now() / 1000) + 1200
            )
        ).to.be.revertedWith("INSUFFICIENT_OUTPUT_AMOUNT");
    });
});