Multihop Swaps
Last modified:
Introduction
A multihop swap routes a trade through two or more pools in sequence — for example, DAI → USDC → WCTN9. The router resolves each intermediate pool from a packed byte path and executes the hops atomically.
This guide covers both swap styles:
- Exact input — fix the input amount, maximise the output.
- Exact output — fix the output amount, minimise the input.
These examples are for learning only and are not production-ready. Always use an oracle or SDK quote to set meaningful slippage bounds before swapping on-chain.
Contract setup
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;import '@centurion-dex/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@centurion-dex/v3-periphery/contracts/libraries/TransferHelper.sol';contract SwapExamples {
ISwapRouter public immutable swapRouter;Hardcoded addresses and fee tier for the example:
address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant WCTN9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
// For this example, we will set the pool fee to 0.3%.
uint24 public constant poolFee = 3000;
constructor(ISwapRouter _swapRouter) {
swapRouter = _swapRouter;
}Exact input multihop
Swaps a fixed amount of the input token for the maximum possible output, passing through one or more intermediate pools.
Parameters
path— a tightly-packed sequence of(tokenAddress, fee, tokenAddress, fee, …, tokenAddress). The router derives each pool address from consecutive(token, fee, token)triples and executes the hops in order.recipient— destination of the final output token.deadline— unix timestamp after which the transaction reverts.amountIn— amount of the first token to spend.amountOutMinimum— minimum acceptable output. Set to0here; in production, derive it from a quote.
Function
/// @notice swapExactInputMultihop swaps a fixed amount of DAI for a maximum possible amount of WCTN9 through an intermediary pool.
/// For this example, we will swap DAI to USDC, then USDC to WCTN9 to achieve our desired output.
/// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
/// @param amountIn The amount of DAI to be swapped.
/// @return amountOut The amount of WCTN9 received after the swap.
function swapExactInputMultihop(uint256 amountIn) external returns (uint256 amountOut) {
// Transfer `amountIn` of DAI to this contract.
TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);
// Approve the router to spend DAI.
TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);
// Multiple pool swaps are encoded through bytes called a `path`. A path is a sequence of token addresses and poolFees that define the pools used in the swaps.
// The format for pool encoding is (tokenIn, fee, tokenOut/tokenIn, fee, tokenOut) where tokenOut/tokenIn parameter is the shared token across the pools.
// Since we are swapping DAI to USDC and then USDC to WCTN9 the path encoding is (DAI, 0.3%, USDC, 0.3%, WCTN9).
ISwapRouter.ExactInputParams memory params =
ISwapRouter.ExactInputParams({
path: abi.encodePacked(DAI, poolFee, USDC, poolFee, WCTN9),
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0
});
// Executes the swap.
amountOut = swapRouter.exactInput(params);
}Exact output multihop
Swaps the minimum necessary input for a fixed output amount. The key difference: the path is encoded in reverse order because the router executes exact-output swaps backwards — it starts from the desired output token and works back to the input.
Parameters
path— same format as exact input, but reversed:(tokenOut, fee, intermediary, fee, tokenIn).recipient— destination of the output token.deadline— revert timestamp.amountOut— the exact output desired.amountInMaximum— ceiling on how much input to spend. Any unspent input is refunded.
Function
/// @notice swapExactOutputMultihop swaps a minimum possible amount of DAI for a fixed amount of WCTN through an intermediary pool.
/// For this example, we want to swap DAI for WCTN9 through a USDC pool but we specify the desired amountOut of WCTN9. Notice how the path encoding is slightly different in for exact output swaps.
/// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
/// @param amountOut The desired amount of WCTN9.
/// @param amountInMaximum The maximum amount of DAI willing to be swapped for the specified amountOut of WCTN9.
/// @return amountIn The amountIn of DAI actually spent to receive the desired amountOut.
function swapExactOutputMultihop(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
// Transfer the specified `amountInMaximum` to this contract.
TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
// Approve the router to spend `amountInMaximum`.
TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);
// The parameter path is encoded as (tokenOut, fee, tokenIn/tokenOut, fee, tokenIn)
// The tokenIn/tokenOut field is the shared token between the two pools used in the multiple pool swap. In this case USDC is the "shared" token.
// For an exactOutput swap, the first swap that occurs is the swap which returns the eventual desired token.
// In this case, our desired output token is WCTN9 so that swap happens first, and is encoded in the path accordingly.
ISwapRouter.ExactOutputParams memory params =
ISwapRouter.ExactOutputParams({
path: abi.encodePacked(WCTN9, poolFee, USDC, poolFee, DAI),
recipient: msg.sender,
deadline: block.timestamp,
amountOut: amountOut,
amountInMaximum: amountInMaximum
});
// Executes the swap, returning the amountIn actually spent.
amountIn = swapRouter.exactOutput(params);
// If the swap did not require the full amountInMaximum to achieve the exact amountOut then we refund msg.sender and approve the router to spend 0.
if (amountIn < amountInMaximum) {
TransferHelper.safeApprove(DAI, address(swapRouter), 0);
TransferHelper.safeTransfer(DAI, msg.sender, amountInMaximum - amountIn);
}
}Full contract
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;
import '@centurion-dex/v3-periphery/contracts/libraries/TransferHelper.sol';
import '@centurion-dex/v3-periphery/contracts/interfaces/ISwapRouter.sol';
contract SwapExamples {
// For the scope of these swap examples,
// we will detail the design considerations when using
// `exactInput`, `exactInputSingle`, `exactOutput`, and `exactOutputSingle`.
// It should be noted that for the sake of these examples, we purposefully pass in the swap router instead of inherit the swap router for simplicity.
// More advanced example contracts will detail how to inherit the swap router safely.
ISwapRouter public immutable swapRouter;
// This example swaps DAI/WCTN9 for single path swaps and DAI/USDC/WCTN9 for multi path swaps.
address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address public constant WCTN9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
// For this example, we will set the pool fee to 0.3%.
uint24 public constant poolFee = 3000;
constructor(ISwapRouter _swapRouter) {
swapRouter = _swapRouter;
}
/// @notice swapInputMultiplePools swaps a fixed amount of DAI for a maximum possible amount of WCTN9 through an intermediary pool.
/// For this example, we will swap DAI to USDC, then USDC to WCTN9 to achieve our desired output.
/// @dev The calling address must approve this contract to spend at least `amountIn` worth of its DAI for this function to succeed.
/// @param amountIn The amount of DAI to be swapped.
/// @return amountOut The amount of WCTN9 received after the swap.
function swapExactInputMultihop(uint256 amountIn) external returns (uint256 amountOut) {
// Transfer `amountIn` of DAI to this contract.
TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountIn);
// Approve the router to spend DAI.
TransferHelper.safeApprove(DAI, address(swapRouter), amountIn);
// Multiple pool swaps are encoded through bytes called a `path`. A path is a sequence of token addresses and poolFees that define the pools used in the swaps.
// The format for pool encoding is (tokenIn, fee, tokenOut/tokenIn, fee, tokenOut) where tokenOut/tokenIn parameter is the shared token across the pools.
// Since we are swapping DAI to USDC and then USDC to WCTN9 the path encoding is (DAI, 0.3%, USDC, 0.3%, WCTN9).
ISwapRouter.ExactInputParams memory params =
ISwapRouter.ExactInputParams({
path: abi.encodePacked(DAI, poolFee, USDC, poolFee, WCTN9),
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0
});
// Executes the swap.
amountOut = swapRouter.exactInput(params);
}
/// @notice swapExactOutputMultihop swaps a minimum possible amount of DAI for a fixed amount of WCTN through an intermediary pool.
/// For this example, we want to swap DAI for WCTN9 through a USDC pool but we specify the desired amountOut of WCTN9. Notice how the path encoding is slightly different in for exact output swaps.
/// @dev The calling address must approve this contract to spend its DAI for this function to succeed. As the amount of input DAI is variable,
/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
/// @param amountOut The desired amount of WCTN9.
/// @param amountInMaximum The maximum amount of DAI willing to be swapped for the specified amountOut of WCTN9.
/// @return amountIn The amountIn of DAI actually spent to receive the desired amountOut.
function swapExactOutputMultihop(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
// Transfer the specified `amountInMaximum` to this contract.
TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
// Approve the router to spend `amountInMaximum`.
TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);
// The parameter path is encoded as (tokenOut, fee, tokenIn/tokenOut, fee, tokenIn)
// The tokenIn/tokenOut field is the shared token between the two pools used in the multiple pool swap. In this case USDC is the "shared" token.
// For an exactOutput swap, the first swap that occurs is the swap which returns the eventual desired token.
// In this case, our desired output token is WCTN9 so that swap happens first, and is encoded in the path accordingly.
ISwapRouter.ExactOutputParams memory params =
ISwapRouter.ExactOutputParams({
path: abi.encodePacked(WCTN9, poolFee, USDC, poolFee, DAI),
recipient: msg.sender,
deadline: block.timestamp,
amountOut: amountOut,
amountInMaximum: amountInMaximum
});
// Executes the swap, returning the amountIn actually spent.
amountIn = swapRouter.exactOutput(params);
// If the swap did not require the full amountInMaximum to achieve the exact amountOut then we refund msg.sender and approve the router to spend 0.
if (amountIn < amountInMaximum) {
TransferHelper.safeApprove(DAI, address(swapRouter), 0);
TransferHelper.safeTransfer(DAI, msg.sender, amountInMaximum - amountIn);
}
}
}