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:
Analyzes available paths - Finds all possible routes between your input and output tokens
Calculates optimal pricing - Determines which path offers the best exchange rate
Executes the trade - Performs the swap through the optimal path in a single transaction
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
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:
Problem : No direct path exists between tokens or insufficient liquidity in intermediate pools.Solution :
Try different intermediate tokens (WETH, USDC, USDT)
Reduce trade size
Check pool liquidity before attempting swap
Problem : Price moved significantly during transaction execution.Solution :
Increase slippage tolerance (but be careful with MEV attacks)
Use shorter paths when possible
Execute during lower volatility periods
Problem : Transaction fails due to various reasons.Solution :
Check token approvals
Verify sufficient token balance
Ensure deadline hasn’t passed
Check for blacklisted tokens
Best Practices
Always check liquidity
Use getAmountsOut
to verify sufficient liquidity exists before executing swaps. This prevents failed transactions and wasted gas.
Set appropriate slippage
Balance between protection and execution success. Too low slippage may cause failed transactions, too high may result in poor pricing.
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.
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" );
});
});