MEV: Flashbot’s Simple Arbitrage Code Breakdown

David Tilšer
on Jan 13, 2023

Introduction and a little MEV history

You've probably heard about what MEV is and started to wonder how it all works. Until recently, the MeV was only a unit of the electron volt. But it changed on August 13, 2017. Iddo Bentov, Lorenz Breidenbach, Phil Daian, Ari Juels, Yunqi Li, and Xueyuan Zhao have written this article about the issue, that web3 users are exposed to arbitrages and miner frontrunning. They also developed the first MEV bot to prove it is possible and they make $1M/y profit with it. And then in the year 2019, the same group of guys have written a document where “MEV” and “PGA” terms have been defined. The document is named Flash Boys 2.0 and you can read it here.
If you like math and a more formal explanation, I got you covered. Later on, Kushal Babel, Philip Daian, Mahimna Kelkar, and Ari Juels wrote a paper giving a more mathematical and formal definition of MEV. The link to the document is here. And here are the formulas for defining MEV.

In simple terms - we basically look over all blocks a miner can produce and we’re looking at the difference between balance at the end of the block and the beginning of the block. If you want a more detailed explanation, I recommend reading the paper. I'm going to quote one of the authors of the document and founders of Flashbots here, where he described the high level idea in this math. Phil Daian said on Mev.day conference - “The high level idea that’s captured in this math is that we want to look at the maximum amount a miner can increase their balance across a block or series of blocks given a state of the world that they can’t change and all the actions they have available to them in this game.”

In this article, I assume you know what MEV is and have some basic programming knowledge. There are MEV opportunities such as sandwiching, arbitrage, JIT and liquidations. If you have no idea what I’m talking about, I recommend you to google an article about it or write to me that I should write the article about it :) But to explain what MEV bots do or are in one sentence, I will put it as this: “MEV bots seek opportunities, where they capitalize on the fact that the order of transactions in the block matters.” This article is about to explain the code in this repository - https://github.com/flashbots/simple-arbitrage. Scott Bigelow has written this code to show what a simple arbitrage bot can look like. Do you ask what arbitrage is? Simple example - a bot can buy an asset on one DEX and immediately sell the same asset on another DEX and earn a profit. Of course, this is only possible if the prices on the DEXes are different. And exactly this kind of opportunities arbitrage bots are seeking.
 

Let’s get our hands dirty

Let’s dive into the code and explain it. First and foremost, clone the code from the repository and open it. Open the index.ts and let’s check it out. The first lines of the code are imports. I guess the only one which is worth mentioning is FlashbotsBundleProvider.

If you interact with the Flashbots, this repo is handy. It helps you create and submit bundles to Flashbots relay. Maybe you ask why you need to send anything to Flashbots. If you are a searcher (this is the marking of the MEV bot in this game :) ), you create bundles that contain your transactions in order you need to make a profit. Then you have to submit them to the relay or builder. You do not have to submit it to Flashbots. Either way, Flashbots is one of the most used relays with 70% dominance and is used in this example. If you do not want to use their FlashbotsBundleProvider or you use a programming language in which you cannot import the provider, you can always call them directly via RPC endpoints. More info about it is in their documentation here. There are definitely more relays or builders you can send your bundles to - builder0x69, beaverbuild, bloxRoute and so on.

Under the imports, we have environment variables. They are explained in the README.md file. If some of them are unclear, just check them there.

Let’s move into the main function. It will get more interesting. As the first thing we create a FlashbotsProvider on line 49. This object is used to create bundles and send them to the Flashbot’s relay. The first parameter is our static RPC provider and our waller for signing bundles.
On line 50 we are creating an arbitrage object with parameters which will never change in future. An object has more functionalities and we will explain them once we get to the part where we call them in code.

On line 55 we are getting markets. Let’s go into the implementation of getUniswapMarketsByToken.

Lines 80-82: It takes all factory addresses from the file “addresses.ts”. If you open the file, you can see we have addresses to all markets we are interested in. And for each factory address, we call “getUniswappyMarkets”. Open the function and you can see we call the function getPairsByIndexRange on the uniswapQuery contract. Before we will dive deeper into it, we need to first understand how Uniswap and its forks work. Those markets always have a Factory contract which deploys a bunch of other contracts. These other contracts are pairs, and on these contracts, you are trading and we also have information about the reserves. If you want to do arbitrage, you need to know the prices of all pairs on all markets and compare them to find the opportunity. And “getUniswappyMarkets” is looking into the factories and asking where all the pairs are. To better understand, open the Etherscan and let’s try it yourself. Find the Uniswap V2 factory, open the read contract and open the function allPairs. If you are too lazy to do that, just click here. As a parameter use a random number and click submit. You will get an address to the smart contract of the random pair. Open it. If you know the addresses of all pairs, you can get reserves and you can trade on pair addresses directly to make arbitrage. If you opened a random pair, go to read the contract and call getReserves and query it.

So do we really just call “allPairs” in a cycle and every call is one eth_call request to the node? We can, but no. We do not do it. If you call “allPairsLength” on the Uniswap factory, at the time of writing of this article there are 137 993 pairs. That's a really huge number. The trick in our repo is that we do not do eth_call for every pair, but we do it in batches. Check the UniswapFlashQuery.sol in our repo. You will find it in the “contracts” file. Or you can find it on Etherscan as well here.
We are in the function “UniswappyV2EthPair.getUniswappyMarkets” and on line 53 we call the function “getPairsByIndexRange” on the smart contract I mentioned above called UniswapFlashQuery. Let’s open it.

I guess the parameters are clear. First is the UniswapV2Factory address and start and stop are indexes where we should start and stop. The first line gets how many pairs there are. Then we go through the pairs in the cycle and get the info that we need - token0, token1 and the address. Token0 and token1 are just addresses to tokens which are in the pair. For example, if we have a pair USDP/USDC, it would return this:

Token0 - 0x8E870D67F660D95d5be530380D0eC0bd388289E1
Token1 - 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
uniswapPair - 0x3139Ffc91B99aa94DA8A2dc13f1fC36F9BDc98eE

So instead of sending a query for each pair, we do it in a batch. Why is it better actually? When you interact with your node, it takes some time to get your data to your node, process it in the node and get data from your node back and simply if you can do it in one query in a batch, it is always much quicker. Someone can say that he read that cycles are hell in Solidity, because it takes so much gas. Yeah, but this is a view function and if you just query data from Ethereum it will cost you nothing. It would cost something only if you would call this function from another contract. This is a nice trick that you should remember. If you need a lot of data from a node, it is always a good idea to try to get them in batches.

Let’s go back to the “UniswappyV2EthPair.getUniswappyMarkets”. Once we have all the pairs, we are doing another cycle on line 54. We can see “if else” on line 59, where we filter what we found. We care only about pairs which have WETH on one of the sides. Why? This is just an implementation decision to keep it simple, otherwise, you can of course do arbitrages on all pairs. Why is it simpler to use only pairs with WETH? Because we pay our costs in WETH, it is convenient to have the same token for paying costs and having a profit in. Basically, if I start with 1WETH and I end up with 1.5WETH, I know I made a profit. That means I do not have to handle pricing risk all the time. So for example, if instead of WETH I would have DAI there, I would have known the relation between DAI and WETH to identify if some opportunity is profitable for me. Of course, it is possible to do and advanced searchers do that, but this is an example of a simple searcher bot, so we work here only with WETH pairs. 

To summarize the “getUniswappyMarkets” function: it will get all pairs from all markets we are interested in and return them.

Let’s go back to the “UniswappyV2EthPair.getUniswapMarketsByToken” function. We got all pairs on line 80 and we are moving to line 96 where we are calling the “updateReserves” function.

On line 110 we create an object for interacting with the UniswapFlashQuery.sol contract. On the other line we get all pair addresses and on line 113 we call the function “getReservesByPairs” on the UniswapFlashQuery.sol contract.

Very similar to the Solidity function before. We again use batch. That means we do not call eht_call for every pair, but we process everything within a single eht_call.
Every pair is one smart contract. In the cycle, we call on every pair smart contract function “getReserves”. We already mentioned this function. Let’s remind ourselves what it’s doing. It returns three objects, a reserve of token0, a reserve of token1 and a timestamp when the last time was interaction with the pair. You can try it yourself here. We care only about reserves, we do not care about timestamps, so this function could be simplified into this:


Let’s go back to the “updateReserves” function. Line 114: Subsequently we loop all markets and set reserves information to each pair. We will need this information later on in the code to know if something is profitable or not.
In UniswappyV2EthPair.getUniswapMarketsByToken we have another filter on line 98.

This is just a filter which filters pairs which have a balance of at least 1WETH. If it has less, it is probably not profitable and we don’t want to do any work on this pair. I think that’s it for “getUniswapMarketsByToken” and let’s go back to the main function in “index.ts”.

We need to update reserves for each block. On line 56 we have an event listener and for every block, we process code inside the brackets. We see there, familiar to us, the updateReserves function. As the function name suggests it will update reserves for each pair. This should not be anything new for us. Another line is interesting. Let’s go into the “evaluateMarkets” function.

This function gets to buy and sell prices for each token across the market on lines 98-103 and then on line 110 is a simple condition - can you sell a token for a bigger sum than buy it on a different market? For example, can I buy DAI on Uniswap for less than I can sell on SushiSwap? If so, let’s add it to the crossedMarkets array object. This is an arbitrage opportunity for us. On line 116 we call “getBestCrossedMarket”. Inside the function, there is an array TEST_VOLUMES. These are volumes that we use in our test of profitability.

Basically for each volume test how much profit I would make if I would buy this amount (in the code it is “size”) of tokens and sell it? Line 40 is clear, if we already found bestCrossedMarket and the profit is less than what we already have found, we try to optimize it. Then on line 46 if the optimized profit is bigger, we update bestCrossedMarket, if not, we do not update anything and we are going to the next object in the loop.
We’re coming back to “index.ts”. “printCrossedMarket” function just prints interesting arbitrage into the console for us. Then we pass in our bestCrossedMarkets into the “takeCrossedMarkets” in “Arbitrage.ts”.

The first few lines create targets and payloads. Targets are addresses of the markets and payloads are bytecode of calling of function on target smart contract with correct parameters. They are paired as the first target is for the first payload, the second target is for the second payload and so on. On line 137 we count the reward to the miner, because we have to pay the miner to include our bundle. The miner will always pick up the most profitable bundles to include in the block. Line 138 is starting to be interesting. Let’s jump into the “uniswapWeth” function. Implementation is in “BundleExecutor.sol”. You will find it in the “contracts” folder.

The first line is just to check that we have the same count of targets and payloads. Line 57 is important, we note what is the current balance. Balance before we do any operation. We will need it, later on, to find out if we make a profit or not. Line 58 transfers the amount of WETH to the first pair. Then we loop targets and on every target, we execute our payload, which is bytecode for calling the swap function on a pair Uniswap smart contract. If you check the swap function on Uniswap V2, you can see it does not take any tokens from us. It just assumes we already have sent tokens into the contract. That’s why we first send WETH to the pair contract and afterwards, we execute the call on the swap function.

After we make all the trades on line 60 and everything is successful, which we check on line 61, we move to line 65 where we check whether we make a profit. If the balance after trades is bigger than the original balance, we made a profit. Nothing surprising. If you do now want to pay anything to the miner, you have “_ethAmountToCoinbase” equal to zero and on the line 66 function ends. In this case, you would not get into the block, you have to always pay to the miner. Let’s assume that we send some amount as a parameter. Lines 68-71 are just to make sure we have enough ETH to pay the amount to the miner.
Line 72 is very important. Let’s understand what transaction the miner wants to include in the block. When you send ordinary transactions, you are always paying the gas fee. A bigger gas fee means a bigger profit for miners, so miners are picking up the transactions with the biggest gas fee. And the function block.coinbase.transfer(); is for paying extra tips to miners for a smart contract call. If you check the function again, it is worth pointing out that on line 65 we make sure we make some profit and only if we make some profit do we pay the miner fee on line 72. That means in case you would not make any profit, you will not pay anything. You can play with it, you can, for example, implement the calculation of the amount to coinbase from the profit instead of just sending the amount in the function parameter and so on.

Let's go back to the typescript code - Arbitrage.takeCrossedMarkets. On line 138 we have our transaction and we are going on to gas estimation on line 144. If gas estimation fails we just throw the transaction away and continue to the other transaction. This usually makes sense, because you do not know why it failed and usually if gas estimation fails it means something is wrong with the trades. It can be some scam token for instance.
After gas estimation, we create flashbot’s bundle. It is an array of transactions. In our example, there is one transaction, but you can put more there as in the screenshot below.

The rule of the bundle is that all transactions are executed in the order you set. From top to down. It will always execute all of them or none. You probably noticed that I have some “signedTransaction” in the bundle. You can put in the bundle the already signed transaction and this transaction does not have to be signed by you. So if you search in a public mempool and you find some interesting transaction, you can just take it and put it in your bundle. That is a big thing. Let’s imagine you find a transaction in which someone swaps a large number of tokens. If you are quick enough, you can put this transaction as first in your bundle and immediately after the transaction does arbitrage. You are able to count how the price is going to change after the swap. This is called backrunning arbitrage and it does not harm the user. Of course, you can do frontrunning, which is called sandwiching. That means that you put your transaction before the transaction which swaps a big amount. You buy it cheaper and you raise the price to the user. The user swaps the tokens and you sell immediately after him. Anyways this is only for understanding that this exists, please do not do any frontrunning MEVs and always do only backrunning.

On line 167 we simulate the bundle execution and on line 174 we are sending bundles to the Flashbot relay. On line 173 you can see [blockNumber + 1, blockNumber + 2] and that means that bundles can be included in the current block or two futures blocks. It is important, because you can send your bundle and the current block can be finished in a few seconds and your bundle will not be included at all. And that’s it 🙂

What is important to know is that this simple example is not ready for production. It does not filter scams. Some of the coins can have big reserves so they look very profitable, but they do not have to be transferable. This is a good start to understanding of simple arbitrage bots and now you can continue with your research. I recommend reading, for example, An analysis of Uniswap markets. In section 2.1, there is an interesting reading on how to count profit in arbitrage. 

I hope you learn something new today and wish you luck and have fun during the development of your searcher. Happy coding.

 

Development

Let's change the Web3 experience together.

We build stellar products with elite blockchain engineers for global Web3 startups.

Get in touch

Cleevio ventures

Are you looking for a business partner?

We collaborate with founding teams at the earliest stages, and invest broadly across all categories within the blockchain economy.

Get investment