Dev

Anatomy of Uniswap Front-running Bot

Jun 24, 2022

Author: Jasper | Security Researcher

This is Jasper, a Security Researcher at Sooho. For a certain reason in the past, I conducted an analysis of a bot that executes FRONT-RUNNING attacks on UNISWAP TRANSACTIONS. There isn't anything particularly groundbreaking, but I have organized it into a written form.



What is front-running?


Front-running refers to the act of trading stocks or other financial assets through a broker, where a broker or an insider at the exchange who knows the pending trades first executes trades in a favorable direction to gain profit.



Don't decentralized exchanges (DEX) have intermediaries?


However, decentralized exchanges like Uniswap do not have intermediaries, and smart contracts take on that role, making it impossible for an intermediary to execute a trade first. Here, we must focus on the fact that Ethereum transactions are not processed immediately. When we create a trading transaction for Uniswap, that transaction enters a waiting pool and remains there until it is included in a block.


The Uniswap Front-running Bot targets that timing. It monitors pending transactions, and when it sees a trading transaction via Uniswap, it ensures its own attacking transaction is processed before that transaction, allowing it to make a profitable trade, and then liquidates after the targeted transaction has been executed to take the profit.


This explanation may be hard to understand, so let me give a practical example to show how the attack is conducted, followed by a detailed code analysis.


Our victim, 0xd7680084e582ac04fb0243a489d2f709dbaeef06 (shortened to 0xd768), wants to purchase FalconSwap Token (shortened to FSW) worth 10 WETH through Uniswap. Therefore, they place a purchase transaction into the waiting pool.


The Front-running bot, which has been constantly watching the waiting pool, submits an attack transaction purchasing FSW worth 30 WETH, paying higher gas fees to ensure that it gets included in the block faster than the above purchase transaction. In doing so, a smart contract is used to create the attack transaction to eliminate the risk of the targeted transaction being processed first.


Simultaneously, the Front-running bot creates a liquidation transaction that pays the same amount of gas fees as the targeted transaction, because it needs to ensure that the liquidation transaction is executed after the targeted transaction has been processed to gain a profit. If the liquidation transaction executes before the attack transaction, it will end up costing just the transaction fee for buying and then immediately selling back. The liquidation transaction realizes profit by converting all the purchased FSW back into WETH.


Ah, there are various strategies for creating a Front-running bot. The Front-running bot I analyzed this time is one of the bots that utilizes the Mempool Hunter strategy, which has effectively carried out attacks for quite some time and is still active.



Let's take a closer look at the code!


Now that we have a rough understanding of how the Uniswap Front-running Bot operates, let's dive into the code and see how it's implemented. The Front-running bot consists of a server-side program that monitors the waiting pool and issues attack commands to the smart contract, and the smart contract that performs the attack. Since we can't know how the server-side program is written, let's examine the code of the smart contract that is publicly available on the Ethereum network.


It would have been nice if the contract code had been uploaded to Etherscan, but that isn't usually the case. Therefore, we have to read the bytecode, which can be quite harsh for humans. Fortunately, there is an Online Solidity Decompiler that converts bytecode into a more human-readable form. From here, we will explore how the smart contract code is written based on the decompiled code and the example transaction.


For convenience, let's refer to the transaction that is executed before the targeted transaction as the Opening transaction, and the liquidation transaction that is performed after the attack as the Closing transaction.



Opening transaction


First, here is the data contained in the Opening transaction.


0x0000000d000001b57100010000b973bf19253770ae139f7eda5f22c3538066eeac5db2100000000000000035291039930971a3b80000000000000001a055690d9db8000000000000000000310c19b9fe491977c000000000000000008ac7230489e80000


The first 8 characters of the data in the contract call transaction is the Method Signature. The Method with the Method Signature of 0x0000000d is func_01EC. By examining what func_01EC does, we can understand how the remaining values are used.


When we go to func_01EC, there is a check to see if msg.sender is 0xdd07249e403979bd79848c27aa5454c7e66bdee7 or 0xe73c1e4d7992a4a4f19f31531ae7b5dc352b74b0. This shows that the two above addresses are the owners of the contract we are analyzing.


if ((arg0 >> 0xdc) & 0xffff >= block.number % 0x2710) { goto label_0C8C; }


From the above code, we can see that the value 0x00001b57 after the method signature indicates the last four digits of the block number where the targeted transaction will enter. If the attack transaction enters a different block than the target transaction, everything will be in vain, hence the inclusion of this code.




The next section performs an XOR operation on part of the data, which is used to calculate the pair address of Uniswap. Interestingly, it could have just provided the Pair address, but I don't understand why such calculations are performed.


Anyway, the obtained Pair address is used for a staticcall to a Method Signature 0x0902f1ac. The 0x0902f1ac is the method signature for getReserves().


The WETH reserve obtained in this way is compared with the parameter value provided by the server 00000000000000310c19b9fe491977c0. When expressed in decimal, this is 904.7623921164402, which is exactly the value of 10 WETH plus the amount the targeted transaction intends to purchase. The code sets it so that the WETH reserve must be less than this value to proceed. This is to prevent losses due to interference from other transactions.


After confirming that buying is indeed possible, it calls func_26B5, passing in the amount of WETH to be used for the purchase, the current WETH reserve, and the current FSW reserve to compute the price.


Looking at the above function, var2 calculates a constant K by multiplying the amount of WETH to purchase minus the 0.3% fee with the FSW reserve, and var3 computes the total WETH reserve after the transaction to divide and figure out how much FSW can be purchased with that WETH amount.



Using the same function, it also calculates the amount of FSW that will be purchased from the targeted transaction after the attack.


Now that we know the amount of WETH that the target will use for purchasing, the amount of WETH that will be used for the attack, and the corresponding amount of FSW to be purchased from each, it performs another call to func_26B5 to calculate the profit that will be gained in the Closing process after the targeted transaction is executed.


Here, the attack transaction purchases 111879.58729439399 FSW with 30 WETH, and the targeted transaction purchases 35698.50508403116765319 FSW with 10 WETH. Therefore, if sold after, it would be:


FSW Reserve = 3458775.179288110579731962 (Original Reserve) - 111879.58729439399 (Bought by Bot) - 35698.50508403116765319 (Bought by Victim) = 3311197.0869096853
WETH Reserve = 894.762392116440168384 (Original Reserve) + 30 (Sold by Bot) + 10 (Sold by Victim) = 934.7623921164


In func_26B5,
var2 = 111879.58729439399 * 934.7623921164 * 997
var3 = 111879.58729439399 * 997 + 3311197.0869096853 * 1000
var2/var3 = 30.46303739508232


This means the bot will be able to profit by approximately 0.46 WETH with 30.46 WETH; thus, since this exceeds the combined value of the original investment of 30 WETH and the attack cost of 0x0000b973 Wei, the attack proceeds. If the gas fee cannot be covered, then the attack won't occur.


Once the calculations regarding the profit from the attack are complete, it executes the attack in func_26DE. This function performs a straightforward job: sending WETH to Uniswap and proceeding with the Swap. However, it allows for options to direct the outcome of the Swap to another address, though it is unclear what this is used for.


One interesting aspect to observe in the Opening transaction is that while executing the attack, if the potential profit is low or if it fails due to rising gas fees, it calls func_0CB2 to minimize losses. In func_0CB2, the contract created with CREATE2 is called, and examining the called contract reveals that it simply calls SELFDESTRUCT. This means that through repeatedly invoking the SELFDESTRUCT contract, it aims to recover as much gas cost lost from a failed attack as possible. For why SELFDESTRUCT returns gas fees, please refer to this link.



Closing transaction


The Closing transaction, on the other hand, is processed in func_0796 with the method signature 0xbea175cb. Instead of cautiously calculating something like in the Opening, it simply checks if the targeted transaction has been processed and if WETH has been paid, and then proceeds directly to func_29EE for the Swap. During this process, if it fails or exceeds the gas fee, it employs func_0CB2 to call SELFDESTRUCT just like in the Opening transaction to minimize gas fees.



Conclusion


So far, we have partially analyzed the smart contract of a bot using the Mempool Hunter strategy among Uniswap Front-running Bots. There are many other methods for managing, and while there are multiple options designed for the Closing process, the reason for starting the analysis was not to fully understand the operation of the bot, but rather to grasp the larger logic of how it processes the information received from the server to decide to attack. As I followed my analysis, I realize that it may come off as somewhat rambling, and that the code references are quite insufficient.


While conducting this analysis, one question arose: typically when conducting an attack, one would assume that the same amount of currency as in the targeted transaction would be invested to make a profit. However, observing how it operates shows that a random budget is set by the server for the attack. In the case above, 30 WETH was used to attack a transaction to purchase an amount worth 10 WETH. How the attack budget is calculated remains a mystery.


Given that it has been attacking for quite a long time, I expected the bot to be complicated, but I was surprised to find its structure to be much simpler than anticipated. If I have time in the future, I might try to create a simple imitation contract and server program to execute a Proof of Concept (PoC) with a small amount.



Need more detailed information or code consultancy? Contact Sooho.io!

👉 Contact Us



SOOHO.IO Official Channels