How to Swap Tokens with Uniswap V2

·

In a previous article, we explored how Uniswap works with roughly 100 lines of code. This guide dives deeper, showing you how to interact with the official Uniswap V2 to perform token swaps. We’ll also write tests to verify the swap functionality—a crucial practice to ensure your code behaves as expected.

Additionally, you’ll learn how to fork the Ethereum mainnet and impersonate an on-chain account to simulate real transactions in a test environment.

Understanding Uniswap V2

Uniswap is a decentralized exchange (DEX) operating on the Ethereum blockchain (mainnet and other networks). As the name implies, it facilitates the trading of ERC20 tokens.

Uniswap offers three core functions:

  1. Swapping between different tokens
  2. Adding liquidity to token pairs to receive LP ERC-20 tokens
  3. Burning LP ERC-20 tokens to withdraw the underlying ERC-20 tokens

This article focuses on the first function: swapping tokens using a mainnet fork.

Let’s get started! 🥳

1. Create and Initialize a Project

Start by creating a new project directory and initializing it with npm:

mkdir uni_swap && cd uni_swap
npm init -y

Install the necessary dependencies:

npm install --save hardhat @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle ethers @uniswap/v2-core dotenv

2. Set Up a Hardhat Project

Initialize a Hardhat project by running:

npx hardhat

Create an empty hardhat.config.js file and customize it to fork the mainnet. Your configuration should resemble the following:

require("@nomiclabs/hardhat-waffle");
require("dotenv").config();

const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;

module.exports = {
  solidity: "0.8.0",
  networks: {
    hardhat: {
      forking: {
        url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
      },
    },
  },
};

Replace ALCHEMY_API_KEY with your own Alchemy API key.

3. Write the Swap Contract

Create directories to organize your contracts, scripts, and tests:

mkdir contracts scripts tests

Inside the contracts directory, create a file named TestSwap.sol. Start by importing the necessary interfaces and defining the contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Router02.sol";

contract TestSwap {
    // Address of the Uniswap V2 Router
    address private constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;

    // Swap function
    function swap(
        address _tokenIn,
        address _tokenOut,
        uint256 _amountIn,
        address _to,
        uint256 _deadline
    ) external {
        // Transfer tokens from the user to this contract
        IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn);

        // Approve the Uniswap router to spend the tokens
        IERC20(_tokenIn).approve(UNISWAP_V2_ROUTER, _amountIn);

        // Define the path for the swap
        address[] memory path = new address[](2);
        path[0] = _tokenIn;
        path[1] = _tokenOut;

        // Get the expected output amount
        uint256[] memory amountsExpected = IUniswapV2Router(UNISWAP_V2_ROUTER).getAmountsOut(
            _amountIn,
            path
        );

        // Perform the swap with a 1% slippage tolerance
        IUniswapV2Router(UNISWAP_V2_ROUTER).swapExactTokensForTokens(
            amountsExpected[0],
            (amountsExpected[1] * 990) / 1000,
            path,
            _to,
            _deadline
        );
    }
}

Compile the contract to check for errors:

npx hardhat compile

4. Write the Test Script

In the tests directory, create a file named sample-test.js. Start by importing the necessary ABIs and defining the addresses:

const { expect } = require("chai");
const { ethers } = require("hardhat");
const ERC20ABI = require("@uniswap/v2-core/build/ERC20.json").abi;

describe("Test Swap", function () {
    const DAIAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
    const WETHAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const MyAddress = "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B";
    const DAIHolder = "0x5d38b4e4783e34e2301a2a36c39a03c45798c4dd";

    let TestSwapContract;

    beforeEach(async () => {
        const TestSwapFactory = await ethers.getContractFactory("TestSwap");
        TestSwapContract = await TestSwapFactory.deploy();
        await TestSwapContract.deployed();
    });

    it("should swap", async () => {
        // Impersonate the DAI holder account
        await hre.network.provider.request({
            method: "hardhat_impersonateAccount",
            params: [DAIHolder],
        });
        const impersonateSigner = await ethers.getSigner(DAIHolder);

        // Get initial balances
        const DAIContract = new ethers.Contract(DAIAddress, ERC20ABI, impersonateSigner);
        const DAIHolderBalance = await DAIContract.balanceOf(impersonateSigner.address);
        const WETHContract = new ethers.Contract(WETHAddress, ERC20ABI, impersonateSigner);
        const myBalance = await WETHContract.balanceOf(MyAddress);
        console.log("Initial WETH Balance:", ethers.utils.formatUnits(myBalance.toString()));

        // Approve the TestSwap contract to spend DAI
        await DAIContract.approve(TestSwapContract.address, DAIHolderBalance);

        // Get the current block timestamp for the deadline
        const latestBlock = await ethers.provider.getBlockNumber();
        const timestamp = (await ethers.provider.getBlock(latestBlock)).timestamp;

        // Execute the swap
        await TestSwapContract.connect(impersonateSigner).swap(
            DAIAddress,
            WETHAddress,
            DAIHolderBalance,
            MyAddress,
            timestamp + 1000
        );

        // Check updated balances
        const myBalance_updated = await WETHContract.balanceOf(MyAddress);
        console.log("Balance after Swap:", ethers.utils.formatUnits(myBalance_updated.toString()));
        const DAIHolderBalance_updated = await DAIContract.balanceOf(impersonateSigner.address);

        // Verify the swap was successful
        expect(DAIHolderBalance_updated.eq(ethers.constants.Zero)).to.be.true;
        expect(myBalance_updated.gt(myBalance)).to.be.true;
    });
});

Run the test with:

npx hardhat test

If everything is set up correctly, you should see the initial and updated balances, along with passing tests.

Frequently Asked Questions

What is Uniswap V2?

Uniswap V2 is a decentralized exchange protocol on Ethereum that enables users to swap ERC20 tokens without intermediaries. It uses an automated market maker (AMM) system, relying on liquidity pools instead of order books.

Why fork the mainnet for testing?

Forking the mainnet allows you to simulate real-world conditions without spending real ETH or tokens. You can interact with live contracts and impersonate accounts to test your code in a realistic environment.

How do I handle slippage in Uniswap swaps?

Slippage is the difference between the expected and actual output amount. In the code above, we set a 1% slippage tolerance by calculating (amountsExpected[1] * 990) / 1000. This ensures the transaction only executes if the output is within 1% of the expected value.

What is the deadline parameter in the swap function?

The deadline is a timestamp after which the transaction will fail. It prevents pending transactions from being executed when market conditions have changed significantly.

Can I use this code on other networks?

Yes, but you’ll need to update the Uniswap router address and token addresses to match the network you’re using. Uniswap V2 is deployed on multiple networks, including Arbitrum, Polygon, and Optimism.

How can I optimize gas costs for swaps?

Gas costs depend on network congestion and contract complexity. To reduce costs, consider batching transactions or using gas-efficient coding patterns. 👉 Explore more strategies for optimizing blockchain transactions.

What are the risks of using Uniswap?

Risks include impermanent loss for liquidity providers, smart contract vulnerabilities, and market volatility. Always audit code and understand the mechanics before investing significant funds.