Uniswap V3 Swap - Part 1

Jay NalamJay Nalam
6 min read

We all know that Uniswap has become the most used DEX on the market and it has wide variety of features. The Uniswap v3 has many interesting features from v1 and v2. And in this guide, we will understand the technicalities on how to interact with the protocol. Let us understand how to make a token swap in our contract using Uniswap v3.

In Uniswap v3, swapping of tokens is possible in 2 different ways. They are

  • Single path token swap

  • Multi-path token swap

Let us first write contracts for the single path swapping in this article. We can implement the single path swapping in 2 ways. They are:

  • Swapping by a fixed amount of input tokens

  • Swapping by a fixed amount of output tokens.

We are going to use Ethereum and hardhat framework for testing. Below we have the solidity code for both ways.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
pragma abicoder v2;

import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract SingleHopSwap {
    using SafeERC20 for IERC20;

    ISwapRouter public immutable router;
    IERC20 public DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
    IERC20 public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    uint24 public poolFee = 3000; // 3000 / 10000 = 0.3% fee

    constructor(ISwapRouter _router) {
        router = _router;
    }

    function swapExactInputSingle(
        uint256 amountIn
    ) external returns (uint256 amountOut) {
        DAI.safeTransferFrom(msg.sender, address(this), amountIn);
        DAI.approve(address(router), amountIn);

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: address(DAI),
                tokenOut: address(USDC),
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        amountOut = router.exactInputSingle(params);
    }

    function swapExactOutputSingle(
        uint256 amountOut,
        uint256 amountInMaximum
    ) external returns (uint256 amountIn) {
        DAI.safeTransferFrom(msg.sender, address(this), amountInMaximum);
        DAI.approve(address(router), amountInMaximum);

        ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter
            .ExactOutputSingleParams({
                tokenIn: address(DAI),
                tokenOut: address(USDC),
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });

        amountIn = router.exactOutputSingle(params);

        if (amountIn < amountInMaximum) {
            DAI.approve(address(router), 0);
            DAI.safeTransfer(msg.sender, amountInMaximum - amountIn);
        }
    }
}

Full code can be found here

https://github.com/jveer634/uniswap-v3-demo

We will be using DAI and USDC for our example and exchanging DAI tokens with USDC tokens. And for the demo, we are using a 0.3% fee pool.

In the above contract, we can have 2 functions called swapExactInputSingle and swapExactOutputSingle that implements the two ways of swapping the tokens.

1. Swapping with a fixed amount of input tokens

In this method, we use a fixed amount of input tokens and try to swap for the maximum number of output tokens possible.

 function swapExactInputSingle(
        uint256 amountIn
    ) external returns (uint256 amountOut) {
        DAI.safeTransferFrom(msg.sender, address(this), amountIn);
        DAI.approve(address(router), amountIn);

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: address(DAI),
                tokenOut: address(USDC),
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        amountOut = router.exactInputSingle(params);
    }
  1. The swapExactInputSingle will first transform the input tokens from the user to the contracts,

  2. Then it will approve the token amount to the uniswap v3 swap router.

  3. Once that is done, it will call the exactInputSingle on the router that takes the fixed token amount for input tokens and returns the maximum amount of output tokens possible and return the amount of tokens swapped.

  4. Since, we have mentioned the recipient as msg.sender, the tokens are directly deposited into the user’s account.

2. Swapping with a fixed amount of output tokens

In this method, we use a specify a fixed number of output tokens to be swapped for the minimum possible amount of input tokens.

function swapExactOutputSingle(
        uint256 amountOut,
        uint256 amountInMaximum
    ) external returns (uint256 amountIn) {
        DAI.safeTransferFrom(msg.sender, address(this), amountInMaximum);
        DAI.approve(address(router), amountInMaximum);

        ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter
            .ExactOutputSingleParams({
                tokenIn: address(DAI),
                tokenOut: address(USDC),
                fee: poolFee,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });

        amountIn = router.exactOutputSingle(params);

        if (amountIn < amountInMaximum) {
            DAI.approve(address(router), 0);
            DAI.safeTransfer(msg.sender, amountInMaximum - amountIn);
        }
    }
}
  1. The swapExactOutputSingle also follows the same procedure as swapExactInputSingle. But it takes 2 parameters.

    1. amountOut - the fixed amount of output tokens specified

    2. amountInMax - the maximum amount of input tokens that can be swapped.

  2. First we have to approve the amountInMax amount of input tokens to the contract and then we must call the function.

  3. The function will first transfer the input tokens to the contract and then approves the tokens to the swap router before the exactOutputSingle function on the swap router.

  4. The function will return amountIn, the amount of input tokens used, and then we check if the amountIn is less that amountInMax. If it is, then we return the remaining tokens to the user.

Test Script

Below, we can find a hardhat test script to call the above functions.

We are forking the mainnet and picked a random DAI whale address to impersonate as we need the DAI tokens to test.

First, we have impersonated the account and transferred some DAI tokens to our signer.

Here while dealing with different tokens like USDC and DAI, we have to carefully pass the amounts to the functions since USDC has 6 decimal places but DAI have 18 decimal places. Thus, we have declared 2 variables to specify amounts respectively.

import { ethers } from "hardhat";
import { SingleHopSwap, IERC20 } from "../typechain-types";
import { expect } from "chai";

describe("SingleHopSwap", () => {
    let swap: SingleHopSwap, dai: IERC20, usdc: IERC20, user: string;

    const DAI_WHALE = "0xe5F8086DAc91E039b1400febF0aB33ba3487F29A";
    const DAI_TOKEN = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
    const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
    const ROUTER = "0xe592427a0aece92de3edee1f18e0157c05861564";

    const USDC_AMOUNT = ethers.parseUnits("5", 6);
    const DAI_AMOUNT = ethers.parseUnits("10", 18);

    beforeEach(async () => {
        const daiSigner = await ethers.getImpersonatedSigner(DAI_WHALE);
        dai = await ethers.getContractAt("IERC20", DAI_TOKEN);
        usdc = await ethers.getContractAt("IERC20", USDC);

        // transfer tokens to our signer to make transactions
        const signers = await ethers.getSigners();
        user = signers[0].address;
        await dai.connect(daiSigner).transfer(user, DAI_AMOUNT);

        const s = await ethers.deployContract("SingleHopSwap", [ROUTER]);

        swap = await s.waitForDeployment();
    });

    it(".. test swapExactInputSingle function", async () => {
        console.log(
            "USDC after before: ",
            ethers.formatUnits(await usdc.balanceOf(user), 6)
        );
        console.log(
            "DAI after before: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
        await dai.approve(swap.target, DAI_AMOUNT);
        await expect(
            swap.swapExactInputSingle(DAI_AMOUNT)
        ).to.changeTokenBalance(dai, user, -DAI_AMOUNT);

        console.log(
            "USDC after after: ",
            ethers.formatUnits(await usdc.balanceOf(user), 6)
        );
        console.log(
            "DAI after after: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
    });

    it(".. test swapExactOutputSingle function", async () => {
        console.log(
            "USDC after before: ",
            ethers.formatUnits(await usdc.balanceOf(user), 6)
        );
        console.log(
            "DAI after before: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
        await dai.approve(swap.target, DAI_AMOUNT);
        await expect(
            swap.swapExactOutputSingle(USDC_AMOUNT, DAI_AMOUNT)
        ).to.changeTokenBalance(usdc, user, USDC_AMOUNT);

        console.log(
            "USDC after after: ",
            ethers.formatUnits(await usdc.balanceOf(user), 6)
        );
        console.log(
            "DAI after after: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
    });
});

First test case will approve the DAI tokens to the contract and then call the swapExactInputSingle function. The function also prints the DAI and USDC balances of the user before and after the swapping.

And the second test case will call the swapExactOutputSingle function after approving the tokens to the contract. We can see the differences in the balances with the console log statements.

In the next article, we will see how to perform a Multihop token swap on Uniswap version 3.

0
Subscribe to my newsletter

Read articles from Jay Nalam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jay Nalam
Jay Nalam

Hi, I'm Jay Nalam, a seasoned Web3 Engineer committed to advancing decentralized technologies. Specializing in EVM-based blockchains, smart contracts, and web3 protocols, I've developed NFTs, DeFi protocols, and more, pushing boundaries in the crypto realm.