Uniswap V3 Swap - Part 2

Uniswap V3 Swap - Part 2

·

4 min read

In our last article, we understood on how to make a token swap on Uniswap V3 in our smart contract using single hop method. In this article, let us look into the process of making token swaps using Multihop method.

Just like the previous one, here also we have 2 options for swapping. They are

  • With fixed amount of Input tokens

  • With fixed amount of Output tokens

You can find the full code here: https://github.com/jveer634/uniswap-v3-demo

I made some changes to the hardhat project (mainly Solidity version) from the previous article, since we are using the interfaces from the uniswap periphery library. If we want to use the interfaces with latest solidity version, copy the interface code into a new solidity file and change the compiler version.

Note that, for this demo we are using DAI as input token and WETH9 as output token. And also we are using the USDC token as the intermediate for the swapping.

This method is usually used when the multihop swap produce more output than the single hop swapping. For this demo, we are only using 1 interemediatary token, but Uniswap allows multiple interemediatary tokens to be used.

We are using 0.3% fee pool for the demo and during the multihop, we have to specify the fee for every swap that happens. And here we are using 0.3% fee pool for all the swaps.

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

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

contract MultiHopSwap {
    using SafeERC20 for IERC20;

    ISwapRouter public immutable router;

    IERC20 public DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
    IERC20 public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IERC20 constant WETH9 = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

    uint24 public constant poolFee = 3000; // 3000 / 1000 = 0.3%

    constructor(ISwapRouter _router) {
        router = _router;
    }

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

        ISwapRouter.ExactInputParams memory params = ISwapRouter
            .ExactInputParams({
                path: abi.encodePacked(DAI, poolFee, USDC, poolFee, WETH9),
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0
            });

        amountOut = router.exactInput(params);
    }

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

        ISwapRouter.ExactOutputParams memory params = ISwapRouter
            .ExactOutputParams({
                path: abi.encodePacked(WETH9, poolFee, USDC, poolFee, DAI),
                recipient: msg.sender,
                deadline: block.timestamp,
                amountOut: amountOut,
                amountInMaximum: amountInMaximum
            });

        amountIn = router.exactOutput(params);

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

The contract contains 2 functions to perform the swap on both the options.

  1. swapExactInputMultihop

    • Takes in the fixed DAI token amount and transfers the amount from the user to the contract and then approves it to the swap router.

    • After tokens approval, then it calls the exactInput method on the router with the input parameters in the form of ExactInputParams struct.

    • The input parameters require a path variable which is an abi encoded value of token addresses and pool fees in format tokenAddress , poolFee , tokenAddress .

  2. swapExactOutputMulithop

    • Takes two parameters - amountOut and amountInMax. Transfer the amounInMax of DAI tokens and the approves those tokens to the swap router.

    • Once the DAI tokens are approved, then it calls the exactOutput method on the router with the input parameters in the form of ExactOutputParams struct.

    • Note that the path should contain the pool path in REVERSE order for exact output.

Below, we have the test script for testing our contract.

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

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

    const DAI_WHALE = "0xe5F8086DAc91E039b1400febF0aB33ba3487F29A";
    const DAI_TOKEN = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
    const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const ROUTER = "0xe592427a0aece92de3edee1f18e0157c05861564";

    const WETH_AMOUNT = ethers.parseUnits("0.0025", 18);
    const DAI_AMOUNT = ethers.parseUnits("10", 18);

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

        // 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("MultiHopSwap", [ROUTER]);

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

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

        console.log(
            "WETH after after: ",
            ethers.formatUnits(await weth.balanceOf(user), 18)
        );
        console.log(
            "DAI after after: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
    });

    it(".. test swapExactOutputMultihop function", async () => {
        console.log(
            "WETH after before: ",
            ethers.formatUnits(await weth.balanceOf(user), 18)
        );
        console.log(
            "DAI after before: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
        await dai.approve(swap.target, DAI_AMOUNT);
        await expect(
            swap.swapExactOutputMultihop(WETH_AMOUNT, DAI_AMOUNT)
        ).to.changeTokenBalance(weth, user, WETH_AMOUNT);

        console.log(
            "WETH after after: ",
            ethers.formatUnits(await weth.balanceOf(user), 18)
        );
        console.log(
            "DAI after after: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
    });
});

With that we are done with the swapping on Uniswap v3. In our upcoming blogs, let us look into how to perform flash swaps, provide liquidity, create pools etc.