CenturionDEX
Launch App

Single Swaps

Last modified:

Swaps are the most common interaction with the CenturionDEX protocol. This guide builds a contract with two functions:

Both route through a single pool via the ISwapRouter interface.

For simplicity the example hardcodes token addresses and the fee tier. A production contract would accept these as parameters so the caller can target any pool per transaction.

These examples are for learning only and are not production-ready. Any contract that executes swaps on-chain must reference an external price source (SDK quote, on-chain oracle, etc.) to set a meaningful amountOutMinimum / amountInMaximum. Without one, the trade is vulnerable to front-running sandwich attacks.

Contract setup

Declare the compiler version and enable abicoder v2 (required for the nested structs used by the router):

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;

Import the router interface and the transfer helper:

import '@centurion-dex/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@centurion-dex/v3-periphery/contracts/libraries/TransferHelper.sol';

Create the contract and store an immutable reference to the deployed SwapRouter:

contract SwapExamples {
    ISwapRouter public immutable swapRouter;

Hardcode token addresses and the pool fee for the example. In production, pass these as function arguments.

    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 swap

The caller must first approve this contract to spend its DAI. Because the contract itself is not the caller, it must also approve the router to spend tokens it holds.

Transfer DAI from the caller into the contract, then approve the router for the same amount:

    /// @notice swapExactInputSingle swaps a fixed amount of DAI for a maximum possible amount of WCTN9
    /// using the DAI/WCTN9 0.3% pool by calling `exactInputSingle` in the swap router.
    /// @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 exact amount of DAI that will be swapped for WCTN9.
    /// @return amountOut The amount of WCTN9 received.
    function swapExactInputSingle(uint256 amountIn) external returns (uint256 amountOut) {
        // msg.sender must approve this contract
 
        // Transfer the specified amount 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);
 

Swap parameters

Populate ExactInputSingleParams. Key fields:

ParameterPurpose
tokenIn / tokenOutThe token pair.
feeIdentifies which pool to use.
recipientWhere the output tokens go.
deadlineUnix timestamp after which the transaction reverts — protects against stale execution.
amountOutMinimumSet to 0 here for simplicity. In production, use an oracle or SDK quote.
sqrtPriceLimitX96Caps how far the swap can move the pool price. 0 deactivates the limit. A non-zero value can cause the swap to consume less than amountIn — any contract using this must refund unswapped tokens.

Execute the swap

        // Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
        // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: DAI,
                tokenOut: WCTN9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });
 
        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    }

Exact output swap

An exact output swap fixes the output amount and lets the input float. This is less common but useful when the caller needs a precise quantity of the output token.

Because the input amount is unknown at call time, the caller transfers a maximum they are willing to spend (amountInMaximum). After the swap, any unspent input is refunded.

Execute the swap

/// @notice swapExactOutputSingle swaps a minimum possible amount of DAI for a fixed amount of WCTN.
/// @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 exact amount of WCTN9 to receive from the swap.
/// @param amountInMaximum The amount of DAI we are willing to spend to receive the specified amount of WCTN9.
/// @return amountIn The amount of DAI actually spent in the swap.
function swapExactOutputSingle(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified amount of DAI to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
 
        // Approve the router to spend the specified `amountInMaximum` of DAI.
        // In production, you should choose the maximum amount to spend based on oracles or other data sources to achieve a better swap.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);
 
        ISwapRouter.ExactOutputSingleParams memory params =
            ISwapRouter.ExactOutputSingleParams({
                tokenIn: DAI,
                tokenOut: WCTN9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });
 
        // Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
        amountIn = swapRouter.exactOutputSingle(params);
 
        // For exact output swaps, the amountInMaximum may not have all been spent.
        // If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter 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 swapExactInputSingle swaps a fixed amount of DAI for a maximum possible amount of WCTN9
    /// using the DAI/WCTN9 0.3% pool by calling `exactInputSingle` in the swap router.
    /// @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 exact amount of DAI that will be swapped for WCTN9.
    /// @return amountOut The amount of WCTN9 received.
    function swapExactInputSingle(uint256 amountIn) external returns (uint256 amountOut) {
        // msg.sender must approve this contract
 
        // Transfer the specified amount 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);
 
        // Naively set amountOutMinimum to 0. In production, use an oracle or other data source to choose a safer value for amountOutMinimum.
        // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: DAI,
                tokenOut: WCTN9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });
 
        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    }
 
    /// @notice swapExactOutputSingle swaps a minimum possible amount of DAI for a fixed amount of WCTN.
    /// @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 exact amount of WCTN9 to receive from the swap.
    /// @param amountInMaximum The amount of DAI we are willing to spend to receive the specified amount of WCTN9.
    /// @return amountIn The amount of DAI actually spent in the swap.
    function swapExactOutputSingle(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified amount of DAI to this contract.
        TransferHelper.safeTransferFrom(DAI, msg.sender, address(this), amountInMaximum);
 
        // Approve the router to spend the specified `amountInMaximum` of DAI.
        // In production, you should choose the maximum amount to spend based on oracles or other data sources to achieve a better swap.
        TransferHelper.safeApprove(DAI, address(swapRouter), amountInMaximum);
 
        ISwapRouter.ExactOutputSingleParams memory params =
            ISwapRouter.ExactOutputSingleParams({
                tokenIn: DAI,
                tokenOut: WCTN9,
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });
 
        // Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
        amountIn = swapRouter.exactOutputSingle(params);
 
        // For exact output swaps, the amountInMaximum may not have all been spent.
        // If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
        if (amountIn < amountInMaximum) {
            TransferHelper.safeApprove(DAI, address(swapRouter), 0);
            TransferHelper.safeTransfer(DAI, msg.sender, amountInMaximum - amountIn);
        }
    }
}