'Deploying Uniswap v2 / Sushiswap or similar in Brownie, Hardhat or Truffle test suite

I am writing an automated test suite that needs to test functions against Uniswap v2 style automated market marker: do swaps and use different order routing. Thus, routers need to be deployed.

Are there any existing examples of how to deploy a testable Uniswap v2 style exchange in Brownie? Because Brownie is a minority of smart contract developers, are there any examples for Truffle or Hardhat?

I am also exploring the option of using a mainnet fork, but I am not sure if this operation is too expensive (slow) to be used in unit testing.



Solution 1:[1]

For a quick and easy set up I would use Hardhat's mainnet fork.

Like @Xavier said, using a mainnet fork means you don't have to deploy each contract you want to interact with. If you're wanting to test your contract against multiple exchanges, this would be the easiest way. It does however require a connection to a node and therefore your unit tests will run slower.

As an example, let's say I want to test the following contract, which swaps ETH for an ERC20 token on Uniswap using the swapExactETHForTokens method.

pragma solidity ^0.6.6;

interface IUniswap {
  function swapExactETHForTokens(
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline)
  external
  payable
  returns (uint[] memory amounts);
  function WETH() external pure returns (address);
}

contract UniswapTradeExample {
  IUniswap uniswap;
  
  // Pass in address of UniswapV2Router02
  constructor(address _uniswap) public {
    uniswap = IUniswap(_uniswap);
  }

  function swapExactETHForTokens(uint amountOutMin, address token) external payable {
    address[] memory path = new address[](2);
    path[0] = uniswap.WETH();
    path[1] = token;
    uniswap.swapExactETHForTokens{value: msg.value}(
      amountOutMin, 
      path,
      msg.sender,
      now
    );
  }
}

From the Hardhat docs, the first thing to do is set up a connection to an archive node with Alchemy, which is free to use.

Once you have a url for the node, add it as a network to your hardhat.config.js file:

networks: {
  hardhat: {
    forking: {
      url: "https://eth-mainnet.alchemyapi.io/v2/<key>",
      blockNumber: 14189520
    }
  }
}

If you set blockNumber, Hardhat will fork off this block each time. This is recommended if you're using a test suite and want your tests to be deterministic.

Finally, here's the test class, which tests the swapExactETHForTokens in the above contract. As an example I'm swapping 1 ETH for DAI.

const { assert } = require("chai");
const { ethers } = require("hardhat");
const ERC20ABI = require('./ERC20.json');

const UNISWAPV2ROUTER02_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f";

describe("UniswapTradeExample", function () {
    it("Swap ETH for DAI", async function () {
        const provider = ethers.provider;
        const [owner, addr1] = await ethers.getSigners();
        const DAI = new ethers.Contract(DAI_ADDRESS, ERC20ABI, provider);

        // Assert addr1 has 1000 ETH to start
        addr1Balance = await provider.getBalance(addr1.address);
        expectedBalance = ethers.BigNumber.from("10000000000000000000000");
        assert(addr1Balance.eq(expectedBalance));

        // Assert addr1 DAI balance is 0
        addr1Dai = await DAI.balanceOf(addr1.address);
        assert(addr1Dai.isZero());

        // Deploy UniswapTradeExample
        const uniswapTradeExample =
            await ethers.getContractFactory("UniswapTradeExample")
                .then(contract => contract.deploy(UNISWAPV2ROUTER02_ADDRESS));
        await uniswapTradeExample.deployed();

        // Swap 1 ETH for DAI
        await uniswapTradeExample.connect(addr1).swapExactETHForTokens(
            0,
            DAI_ADDRESS,
            { value: ethers.utils.parseEther("1") }
        );

        // Assert addr1Balance contains one less ETH
        expectedBalance = addr1Balance.sub(ethers.utils.parseEther("1"));
        addr1Balance = await provider.getBalance(addr1.address);
        assert(addr1Balance.lt(expectedBalance));

        // Assert DAI balance increased
        addr1Dai = await DAI.balanceOf(addr1.address);
        assert(addr1Dai.gt(ethers.BigNumber.from("0")));
    });
});

Note the const ERC20ABI = require('./ERC20.json'); at the top, which imports the ERC20ABI needed to fetch the DAI contract and use it's balanceOf() method.

That's all. Running npx hardhat test should show that this test passes.

Solution 2:[2]

The answer from Xavier Hamel set me in the right direction.

Unfortunately, Uniswap v2 and its clones cannot be recompiled and redeployed without editing the source code. This is because

This was a pesky problem. I ended up building the whole library and tooling to solve it: Please meet Smart contacts for testing. It is based on Sushiswap v2 repo, as their contracts were the best maintained.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2