A multi-hop swap atomically routes a single transaction through one or more intermediate pools to exchange tokens that lack a direct pair, or to get a better price than a direct pool offers. It works because the transaction bundles every hop into one all-or-nothing call: either every pool trade executes or the whole thing reverts with no partial execution risk.
Here's the quick math that decides whether you should bother:
- Use multi-hop when the expected price improvement exceeds the sum of extra pool fees plus the added gas cost.
- Skip it for small trades where one extra hop's fee eats the entire benefit.
- Skip it when the direct pool already has deep liquidity and tight spreads.
Most DEX aggregators structure this as a hub-and-spoke model, where a liquid token like WETH or USDC acts as the connector between two otherwise unpaired assets. Uniswap v3's router formalizes this with dedicated exactInput and exactOutput functions built for chaining pools in a single call, which is the pattern this guide walks through end to end.
TL;DR:
- Multi-hop swaps are most beneficial when the expected price improvement exceeds the combined cost of additional fees and gas for intermediate pools.
- Setting appropriate slippage parameters and comparing route costs before executing can prevent unnecessary reverts and optimize trade outcomes.
- Hardcoding fee tiers or token addresses reduces flexibility, so parameterize these values to enable reconfiguration for different pools and networks.
- Refund unspent amounts and revoke approvals after
exactOutputswaps to improve security and avoid stale allowances.- Using tools like Omnirout to compare multi-chain routes ensures selecting the most cost-effective path before signing the transaction.
Table of Contents
- Setting Up Your Contract for Multi-Hop Swaps
- Building an ExactInput Multi-Hop Swap
- Building an ExactOutput Multi-Hop Swap
- A Complete Two-Hop Swap Contract
- When Multi-Hop Actually Pays Off
- How Route Comparison Changes the Multi-Hop Calculus
- What I'd Actually Default To
- Compare Your Route Before You Send the Transaction
- Key Takeaways
- Sources
Setting Up Your Contract for Multi-Hop Swaps
Before writing any swap logic, your contract needs a few things wired correctly, or the transaction will fail in ways that are annoying to debug.
- Pin your Solidity version and enable ABI coder v2. Path encoding for multi-hop swaps relies on packed byte sequences, and older compiler defaults handle this poorly. Anything from Solidity 0.7.6 onward with
pragma abicoder v2;(or 0.8.x, where it's default) works fine. - Import the router interface. You'll want
ISwapRouterfrom the Uniswap v3 periphery contracts, since it exposes theexactInputandexactOutputfunctions your contract will call. - Use a transfer helper for approvals.
TransferHelper.safeApprovehandles the quirks of ERC20 tokens (like USDT) that don't return a boolean onapprove. Skipping this is a classic source of silent failures. - Store token addresses and fee tiers as constants or immutable variables. Hardcoding a fee tier like
3000(0.3%) inline makes your contract brittle. Pull it into a named constant so route changes don't require touching your core logic. - Set a sane deadline. Every swap call takes a
deadlineparameter.block.timestamp + 300(five minutes) is a common default, but you should let callers override it for slower chains.
The security detail most tutorials skip: don't grant unlimited approvals to the router as a matter of habit. Approve only the amount you're about to swap, and if your contract holds custody of funds between transactions, consider zeroing approvals after each swap completes.
Pro Tip: Parameterize your fee tiers and intermediate token addresses as constructor arguments instead of hardcoding them. It turns a single-route contract into one you can redeploy or reconfigure for a dozen different paths without touching the swap logic.
Building an ExactInput Multi-Hop Swap
exactInput is what you reach for when you know exactly how much you're spending and want to guarantee a minimum acceptable output. The path encoding follows a strict forward order: tokenIn, then the pool fee, then the intermediate token, then the next fee, then tokenOut, all packed together with abi.encodePacked.
bytes memory path = abi.encodePacked(
DAI,
poolFee1,
USDC,
poolFee2,
WETH9
);
ISwapRouter.ExactInputParams memory params =
ISwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
deadline: block.timestamp + 300,
amountIn: amountIn,
amountOutMinimum: amountOutMinimum
});
amountOut = swapRouter.exactInput(params);
Before that call, the contract needs to pull tokens from the caller and approve the router:
- Transfer
amountInoftokenInfrommsg.senderinto the contract withsafeTransferFrom. - Approve the router for exactly
amountInusingTransferHelper.safeApprove. - Call
exactInputand let Uniswap's pool contracts handle the intermediate legs internally.
Here's the part that trips up a lot of developers: the intermediate token (USDC in the DAI → USDC → WETH example) never touches the caller's wallet. It moves straight from the first pool into the second within the same call, which is exactly what makes the operation atomic and gas-efficient compared to two separate transactions.
| Parameter | ExactInput Role |
|---|---|
path | Encoded token/fee sequence, forward order |
amountIn | Fixed, known input amount |
amountOutMinimum | Your slippage floor, computed off-chain from a quote |
recipient | Address receiving the final output token |
Set amountOutMinimum too tight and legitimate trades revert during normal price movement. Set it too loose and you're exposed to worse execution than you'd accept.
Building an ExactOutput Multi-Hop Swap
exactOutput flips the problem: you want a fixed amount of the destination token and you're willing to spend up to a cap to get it. The path encoding reverses, too. You write tokenOut first and tokenIn last, since Uniswap's router reads the path back to front for this function.
- Encode the reverse path:
abi.encodePacked(tokenOut, fee2, tokenMid, fee1, tokenIn). - Build
ExactOutputParamswithpath,recipient,deadline,amountOut, andamountInMaximum. - Transfer
amountInMaximumfrom the caller, approve the router for that amount, then callexactOutput. - Refund whatever the router didn't spend, since
exactOutputalmost never consumes the full maximum.
The refund step is where sloppy implementations create real risk. After the swap call returns, check the actual amount spent, transfer the difference back to the caller, and then revoke or zero out the router's remaining approval rather than leaving a stale allowance sitting on your contract. Uniswap's own developer guidance flags this refund and cleanup pattern as a standard defensive practice, since a forgotten non-zero approval is exactly the kind of loose end an attacker looks for down the line.
The UX tradeoff between the two functions is straightforward:
exactInputgives you a predictable cost with uncertain output, which fits swap-then-spend flows.exactOutputgives you a predictable output with uncertain cost, which fits pay-a-fixed-bill flows like settling an exact invoice in a specific token.
Neither is strictly better. The right choice depends on which side of the trade your application actually needs to pin down.
A Complete Two-Hop Swap Contract
Here's how the pieces assemble into something deployable. This example handles a DAI → USDC → WETH swap using exactInput, with the constants and approval flow from earlier sections folded in.
contract MultiHopSwap {
ISwapRouter public immutable swapRouter;
address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address public constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
uint24 public constant feeTier1 = 3000;
uint24 public constant feeTier2 = 3000;
constructor(ISwapRouter _swapRouter) {
swapRouter = _swapRouter;
}
function swapExactInputMultihop(uint256 amountIn, uint256 amountOutMinimum)
external returns (uint256 amountOut)
{
TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);
TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);
bytes memory path = abi.encodePacked(DAI, feeTier1, USDC, feeTier2, WETH9);
ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({
path: path,
recipient: msg.sender,
deadline: block.timestamp + 300,
amountIn: amountIn,
amountOutMinimum: amountOutMinimum
});
amountOut = swapRouter.exactInput(params);
}
}
| Contract element | Production hardening notes |
|---|---|
| Hardcoded addresses | Move to constructor args or a registry contract for multi-network deployment |
| No reentrancy guard | Add nonReentrant from OpenZeppelin if the contract holds custody between calls |
| No event emission | Emit a SwapExecuted event with amounts and path hash for off-chain tracking |
| Fixed fee tiers | Accept fee tiers as parameters so the same contract serves multiple routes |
For anything beyond a demo, wrap the swap call in try/catch logic at the calling layer so a revert produces a readable error instead of a silent failed transaction hash.
When Multi-Hop Actually Pays Off
The rule of thumb holds up well in practice: multi-hop is worth it exactly when the price improvement clears the combined cost of extra pool fees and the additional gas those extra hops burn. Below that line, you're paying more to save less.
A few heuristics make that judgment call faster:
- Route through low-fee stable pools for stablecoin conversions. A DAI to USDC leg through a 0.05% pool, then USDC to your target, often beats a direct route through a higher-fee pool, especially on large trades where the fee percentage compounds against size.
- Treat atomicity as your safety net, not your slippage strategy. The whole path either completes or reverts, but you still need to set
amountOutMinimumoramountInMaximumdeliberately at the path level, not per hop. - Watch for MEV exposure on multi-hop paths. More hops mean more opportunities for sandwich attacks between pools. A tight deadline, a conservative slippage tolerance, and, where your aggregator supports it, splitting a large order across multiple routes all reduce that exposure.
- Don't force multi-hop on small trades. If the trade size is modest and a direct pool has solid depth, the extra hop's fee tier alone can wipe out any price benefit.
- Recompute your quote right before execution. Pool reserves shift block to block, and a quote that's even a few blocks stale can already be wrong by more than your slippage buffer allows.
Pro Tip: Cache candidate routes and pre-warm gas estimates for the two or three most likely paths before a user submits a trade. It shaves noticeable latency off the quote-to-execution window, which matters most exactly when prices are moving fast enough for that gap to cost you.
One distinction worth holding onto: same-chain multi-hop routing through pools is a different engineering problem from cross-chain swaps that rely on bridges or interchain messaging. Bridging introduces its own latency, gas costs on both chains, and trust assumptions about the bridge itself. Don't conflate the two when you're designing a router.

How Route Comparison Changes the Multi-Hop Calculus
Manually comparing every viable path for a trade across fee tiers, pool depth, and current gas prices is tedious work, and it's exactly the kind of comparison an aggregator is built to automate. Omnirout scans multi-hop options across more than 30 blockchains and surfaces the route with the best net outcome after fees, gas, and slippage are all accounted for, not just the one with the best headline price.
- Route comparison happens before you sign anything, non-custodial, with your keys staying in your own wallet the entire time.
- Developers integrating swap logic can query Omnirout's route comparison ahead of constructing the final
exactInputorexactOutputcall, catching a cheaper path before gas gets spent on a suboptimal one.
What I'd Actually Default To
Most tutorials give you the happy-path code and skip the defaults that keep it from breaking in production.
Test on a fork before touching a testnet, since forked mainnet state gives you real liquidity conditions. Then run unit tests against mocked router calls, integration tests on a testnet with real pool contracts, and a gas regression check comparing your multi-hop path against a direct swap baseline. If the multi-hop version doesn't beat the direct route in your test harness, it won't beat it in production either.
— Emanuele
Compare Your Route Before You Send the Transaction
Every code pattern above still leaves one open question: is this actually the best path available right now, across every chain and pool that could serve this trade? That's the gap Omnirout closes. Instead of hand-checking fee tiers and gas estimates across chains yourself, Omnirout pulls the comparison into one screen, showing fees, gas costs, and slippage side by side across more than 30 blockchains before you commit to a route.

The flow stays non-custodial the entire time. Your keys never leave your wallet, whether you're routing a simple swap or a chained multi-hop trade through three pools. For developers, that same comparison logic is available to check ahead of constructing your own exactInput or exactOutput calls, so you're not shipping a route that looked fine in testing but loses to a cheaper path in production. Head to Omnirout and run a route comparison on your next multi-hop trade before you sign anything.
Key Takeaways
Multi-hop swaps work because they bundle routing through intermediate pools into one atomic transaction, and they only make sense financially when price improvement outpaces the added fees and gas.
| Point | Details |
|---|---|
| Atomicity is your safety net | The full path reverts if any hop fails, so no partial execution ever leaves you holding an intermediate token. |
| Path order determines the function | ExactInput encodes forward, ExactOutput encodes in reverse, and mixing them up silently breaks the swap. |
| Refund and revoke after exactOutput | Return unspent input and zero out router approvals to avoid leaving stale allowances exposed. |
| Route selection beats blind execution | Compare fee tiers, pool depth, and gas across candidate paths before committing to one, ideally with a tool like Omnirout that runs that comparison across 30+ chains non-custodially. |
Sources
- Uniswap developers — Multi-hop swapping
- Crescent docs — multi-hop swap (slipless swap)
- Cedra Docs — multi-hop routing
- Chainlink education — What are cross-chain swaps?
