Uniswap V3 - Flash Swap

Uniswap V3 - Flash Swap

·

4 min read

Hello everyone.

Welcome to another day of understanding the Web3 World.

So far, in this series, we are trying to understand the Uniswap V3 and how to integrate it into our smart contracts. In today's blog, let us see how to perform flash swaps in our contracts with V3.

What is Flash Swap ?

Uniswap Flash swap allows you to borrow tokens from any pool with no upfront cost for a block time and then able to perform your logic such as arbitrage etc., with a small amount of fee for the loan amount. In other words, you can borrow tokens with small fee, do your thing and return the tokens including the fee in the same transaction.

How can we do that ?

Typically the flash swaps are used to perform arbitrage and earn profits. The process involves 2 transactions from the user. The approval of the fee tokens and the invoke flash call. Check the below code to understand it better.

contract FlashSwap is IUniswapV3FlashCallback {
    using LowGasSafeMath for uint256;
    using LowGasSafeMath for int256;
    using SafeERC20 for IERC20;

    IUniswapV3Pool public pool;
    IERC20 public token0;
    IERC20 public token1;

    constructor(IUniswapV3Pool _pool) {
        pool = _pool;
        token0 = IERC20(pool.token0());
        token1 = IERC20(pool.token1());
    }

    function uniswapV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external override {
        require(msg.sender == address(pool), "not authorized");

        FlashCallbackData memory decoded = abi.decode(
            data,
            (FlashCallbackData)
        );

        // write custom code here in this block
        uint token0Bal = token0.balanceOf(address(this));
        console.log("Contract - Token 0 Balance: ", token0Bal / 1e18);
        uint token1Bal = token1.balanceOf(address(this));
        console.log("Contract - Token 1 Balance: ", token1Bal / 1e6);

        // payback to the pool
        uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
        uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);

        // collect fee from the user
        if (fee0 > 0)
            token0.safeTransferFrom(decoded.payer, address(this), fee0);

        if (fee1 > 0)
            token1.safeTransferFrom(decoded.payer, address(this), fee1);

        if (amount0Owed > 0) token0.safeTransfer(address(pool), amount0Owed);
        if (amount1Owed > 0) token1.safeTransfer(address(pool), amount1Owed);
    }

    struct FlashCallbackData {
        uint256 amount0;
        uint256 amount1;
        address payer;
    }

    function initFlash(uint amount0, uint amount1) external {
        bytes memory data = abi.encode(
            FlashCallbackData({
                amount0: amount0,
                amount1: amount1,
                payer: msg.sender
            })
        );

        pool.flash(address(this), amount0, amount1, data);
    }
}

Full contract is available here: https://github.com/jveer634/uniswap-v3-demo/blob/master/contracts/FlashSwap.sol

In the above contract, we have 2 functions.

  • initFlash - Called by the user to start the process. The function will call the flash method on the pool, with 4 parameters.

    • recipient - address of the recipient of the borrowed tokens

    • amount0 - amount of token0 to borrow

    • amount1 - amount of token1 to borrow

    • data - optional data to be passed to the flash callback function.

uniswapV3FlashCallback - The callback function invoked by the pool, once the tokens are transferred to the caller. The name of the shouldn't be changed, and it is provided by the interface IUniswapV3FlashCallback. This function should have 3 arguments

  • fee0 - amount of fee for borrowed token0

  • fee1 - amount of fee for borrowed token1

  • data - the optional data passed in the initFlash function

In the callback function, we are first restricting the access to only pool and then we are decoding the data using our FlashCallbackData struct. Once that is done, we can perform our logic there.

For this demo, I am logging the balances of our contract using the console.sol contract provided the hardhat framework.

After out task, then we are returning the tokens received, along with the fees for the loan. Below we have the script for testing our contract.

import { ethers } from "hardhat";
import { FlashSwap, IERC20 } from "../typechain-types";

describe("FlashSwap", () => {
    let flash: FlashSwap, dai: IERC20, user: string;

    const DAI_WHALE = "0xe5F8086DAc91E039b1400febF0aB33ba3487F29A";
    const DAI_TOKEN = "0x6B175474E89094C44Da98b954EedeAC495271d0F";

    const POOL = "0xa63b490aa077f541c9d64bfc1cc0db2a752157b5";

    const DAI_AMOUNT = ethers.parseUnits("0.03", 18);

    beforeEach(async () => {
        const daiSigner = await ethers.getImpersonatedSigner(DAI_WHALE);
        const signers = await ethers.getSigners();

        await signers[0].sendTransaction({
            value: ethers.parseEther("5"),
            to: DAI_WHALE,
        });

        dai = await ethers.getContractAt("IERC20", DAI_TOKEN);

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

        const s = await ethers.deployContract("FlashSwap", [POOL]);
        flash = await s.waitForDeployment();
    });

    it(".. test flashswap", async () => {
        await dai.approve(flash.target, DAI_AMOUNT);
        console.log(
            "User - Before DAI balance: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );

        await flash.initFlash(ethers.parseEther("10"), 0);
        console.log(
            "User - After DAI balance: ",
            ethers.formatUnits(await dai.balanceOf(user), 18)
        );
    });
});

With that, we are done with integrating the Uniswap V3 features into our contracts. Stay tuned for more interesting Web3 topics.