By Pedro Bergamini and Coogan Brennan

In our last post, we went over the three main concepts behind our bot: arbitrage, contract-based transactions and optimistic transfer.

In this post, we’ll walk through how to structure a program that can watch for and execute on profitable arbitrage trades.

Here is the overall flow of what we’re doing:

1) Program (“Bot”) is watching the price pairing of ETH-Dai on both Uniswap and Sushiswap

2) When it spots a profitable arbitrage opportunity, it sends a transaction to a contract we’ve deployed

3) Within one transaction, the contract:

a) Uses flash swaps to optimistically borrow an asset from the lower priced pool
b) Immediately sells the asset in the higher-priced pool
c) Repays the flash swap loan and pockets the difference.

Let’s walk through the stack!

Our program is written in Node.js using the Ethers.js library and incorporating an Infura endpoint. Our arbitrage smart contract is written in Solidity.

Infura

Our backend structure is written in Node and we will use our Infura node to watch the price of ETH and Dai on the Uniswap and Sushiswap contracts. We’ll be relying on our Infura endpoint to get those prices for each new block produced on mainnet. (If you don’t have a free Infura account, you can sign up here.)  

.env

This is super important! We’ll also need to store our private keys to sign mainnet transactions. We put all our sensitive information in the .env file (an example of which is in the repo). We also put in the address of our arbitrage contract and the key of our Infura mainnet endpoint:

PRIVATE_KEY=
FLASH_LOANER=
INFURA_KEY=

Make sure PRIVATE_KEY is the same that deployed the FLASH_LOANER contract. Also, the Ethereum account associated with PRIVATE_KEY needs to have enough funds to cover gas costs, which can be high.

If you’re unsure about why we’re doing this, please read this excellent piece explaining how to avoid uploading your private keys to Github. As the article explains, we need to put sensitive information in this .env file, then we add it to our .gitignore file, like so:

.env
yarn.lock
package-lock.json
node_modules

This way, when we push our information to Github, this file will not be included. This is super, duper important!

Ethers.js

We’re using Ethers.js because of its compatibility with Typescript, the original language for project. This is an age-old questions for Ethereum developers but for more on the differences between ethers.js and web3,js, please see this article.

Contract Instantiation

Next, we instantiate the Uniswap and Sushiswap contracts on lines 11 and 12.

// uni/sushiswap ABIs
const UniswapV2Pair = require('./abis/IUniswapV2Pair.json');
const UniswapV2Factory = require('./abis/IUniswapV2Factory.json');

Sushiswap is essentially a fork of Uniswap, so they have the exact same contract ABI and the exact same functions available to us….Another reason why they’re good for arbitrage! (To learn why they have the exact same ABI, read about the origins of SushiSwap here.)

provider.on('block')

Line 50 is the crux of the arbitrage bot. Every block time, we will ask Infura to check the price of ETH and Dai in Uniswap and Sushiswap. We’ll then compare those numbers to get the “spread,” or possible profit margin.

provider.on('block', async (blockNumber) => {
    try {
      console.log(blockNumber);
      const sushiReserves = await sushiEthDai.getReserves();
      const uniswapReserves = await uniswapEthDai.getReserves();
[...]
  }
}

A quick aside about frontrunning: Frontrunning, which is pushing small advantages in data speed, is common in centralized finance trading. (You may have read about it in Michael Lewis’ Flash Boys book.) We don’t have to worry about this as much here because Uniswap and Sushiswap are decentralized exchanges. Their price is kept on-chain and changes from block-to-block.

Even if we were to somehow get information before the majority of the network, the only way we can act on it is by including our trade in the next block, which is visible to all. We’ll do some frontrunning defense by paying a high gas fee, but that’s about the biggest gain available.

estimateGas

DeFi transactions like this can be very expensive. There may appear to be a profitable arbitrage, but any profit margin may be eaten up by the cost of gas. An important check of our program is to make sure our gas costs don’t eat into our spread. We do this here and include it in our final check for shouldSendTx.

      const shouldSendTx = shouldStartEth
        ? (gasCost / ETH_TRADE) < spread
        : (gasCost / (DAI_TRADE / priceUniswap)) < spread;

For a pairing as common as ETH and Dai, there are not many profitable opportunities. These are high volume pairs (many people using them) and Uniswap and Sushiswap are relatively popular exchanges. In economic terms, arbitrage opportunities result from market inefficiencies. If many people are using these pairs, we are unlikely to find many opportunities. We must find newer tokens or exchanges!

Contract

Follow along with the contract here.

Basically, the contract acts as an intermediary for us. When our program detects a profitable opportunity, it sends funds and instructions to this contract.

Our arbitrage contract is relatively simple. Most of the code comes from Uniswap’s own example in their repository.

We have a constructor, which we feed some hard coded data like Uniswap and Sushiswap’s contract addresses. We also have a single function, uniswapV2Call, where we optimistically borrow tokens on one exchange, execute a swap on the other, and repay the first swap back immediately.

pragma solidity =0.6.6;

import './UniswapV2Library.sol';
import './interfaces/IUniswapV2Router02.sol';
import './interfaces/IUniswapV2Pair.sol';
import './interfaces/IERC20.sol';

contract FlashLoaner {
  address immutable factory;
  uint constant deadline = 10 days;
  IUniswapV2Router02 immutable sushiRouter;

  constructor(address _factory, address _uniRouter, address _sushiRouter) public {
    factory = _factory;  
    sushiRouter = IUniswapV2Router02(_sushiRouter);
  }

  function uniswapV2Call(address _sender, uint _amount0, uint _amount1, bytes calldata _data) external {
      address[] memory path = new address[](2);
      uint amountToken = _amount0 == 0 ? _amount1 : _amount0;
      
      address token0 = IUniswapV2Pair(msg.sender).token0();
      address token1 = IUniswapV2Pair(msg.sender).token1();

      require(msg.sender == UniswapV2Library.pairFor(factory, token0, token1), "Unauthorized"); 
      require(_amount0 == 0 || _amount1 == 0);

      path[0] = _amount0 == 0 ? token1 : token0;
      path[1] = _amount0 == 0 ? token0 : token1;

      IERC20 token = IERC20(_amount0 == 0 ? token1 : token0);
      
      token.approve(address(sushiRouter), amountToken);

      // no need for require() check, if amount required is not sent sushiRouter will revert
      uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountToken, path)[0];
      uint amountReceived = sushiRouter.swapExactTokensForTokens(amountToken, amountRequired, path, msg.sender, deadline)[1];

      // YEAHH PROFIT
      token.transfer(_sender, amountReceived - amountRequired);
    
  }
}

If there is any profit, we send it to the address which initiated the transaction (_sender), who is us!

While this code is not production-ready, we hope it illustrates the basic concept of flash swaps. We also hope it shows you how powerful this simple tool, possible only on blockchain, can be.

Fork this repo, play with it, make it better, and let us know how you’re using it!

Want instant, scalable, and reliable API access to the Ethereum, Eth2, IPFS, and Filecoin networks? Sign up for our Core service to start building for free today. Need more request volume and direct support? Check out Infura+ to pick the right plan for your project.


Twitter | Newsletter | Community | Docs | Contact | Open Roles