Deploying Uniswap v2 / Sushiswap or similar in Brownie, Hardhat or Truffle test suite
Asked Answered
A

3

5

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.

Alys answered 25/1, 2022 at 9:48 Comment(1)
Uniswap uses hardhat, so I'm sure their repo has it. I've done some real real real small stuff... but like 1% of all uniswap contracts. You might be in unchartered waters here.Shanta
T
6

Using a local testnet allows you to control very precisely the state of the blockchain during your test. However, it will require you to deploy every contract you need manually.

A fork of the mainnet will save you from having to deploy every contract already deployed on the mainnet. However you will sacrifice control over the environment and will require a connection to a node.

I've deployed Uniswap 2V on a testnet a few times. To do it you will need the bytecode and ABI for the following contracts: UniswapV2Factory, UniswapV2Pair, UniswapV2Router02 (I suppose you want the second version of the router). The Uniswap docs explains very well how to download them from NPM. For the router to work properly you will also need to deploy a WETH contract. I suggest deploying the one from this github page.

Before running this code, just make sure that your chain is running. For hardhat run the following command:

npx hardhat node

Start by connecting your signer to your dev chain:

var provider = new ethers.providers.WebSocketProvider("ws://localhost:8545");
var signer = provider.getSigner();

Using the ethers.js library, you first deploy the factory:

const compiledUniswapFactory = require("@uniswap/v2-core/build/UniswapV2Factory.json");
var uniswapFactory = await new ethers.ContractFactory(compiledUniswapFactory.interface,compiledUniswapFactory.bytecode,signer).deploy(await signer.getAddress());

Then the WETH contract:

const compiledWETH = require("canonical-weth/build/conrtacts/WETH.json";
var WETH = await new ethers.ContractFactory(WETH.interface,WETH.bytecode,signer).deploy();

You can now deploy the router.

const compiledUniswapRouter = require("@uniswap/v2-periphery/build/UniswapV2Router02");
var router = await new ethers.ContractFactory(compiledUniswapRouter.abi,compiledUniswapRouter.bytecode,signer).deploy(uniswapFactory.address,WETH.address);

You will also need to deploy the ERC20 tokens you need (Here is an example with tokens I've written):

const compiledERC20 = require("../../../Ethereum/Ethereum/sources/ERC20.sol/Token.json");
var erc20Factory = new ethers.ContractFactory(compiledERC20.abi,compiledERC20.bytecode,signer);

var erc20_0 = await erc20Factory.deploy("1000000", "Token 0", "5", "T0");
var erc20_1 = await erc20Factory.deploy("1000000", "Token 1", "5", "T1");

The parameters of the deploy function will depend on the constructor of the token you wish to deploy.

You will also want to create pairs using the createPair method of the Uniswap factory.

uniswapFactory.createPair(erc20_0.address,erc20_1.address);

Keep in mind that in the pair the tokens will be ordered arbitrarly by the contract. ERC20_0 might not be the first of the two.

After that just wait for all the transactions to go through and you should be good to start your test.

Theoretician answered 28/1, 2022 at 5:19 Comment(2)
I made the supposition that you were using javascript and the ethers.js library. But the process should be very similar using other tools.Theoretician
Thanks Xavier. Please see my comments on the other answer.Alys
S
5

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.

Separative answered 20/2, 2022 at 1:59 Comment(1)
Thanks dude you saved a lot of time. Before, I was deploying all the uniswap contracts before to test.Phosphorous
A
3

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.

Alys answered 31/1, 2022 at 13:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.