CenturionDEX
Launch App

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:

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

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

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);
        }
    }
}