This project is designed to capitalize on MEV attacks within the BSC network to generate profits.
MEV Attack on the BSC
English | 한국어
Overview
This repository implements how to profit from a Maximal Extractable Value (MEV) attack against a Decentralized Exchange (DEX) on the Binance Smart Chain (BSC) network.- Since MEV development is directly tied to monetary gain, it's not easy to find resources that are actually helpful. I hope this resource will inspire other developers and add real value to their projects.
- The project was developed and operational from January 2024 to May 2024.
- For technical questions and feedback, please use the Issues page in the repository, and for all other inquiries, please contact donggeunyu@icloud.com.
Content
* Overview * Content * Transaction Sample for Sandwich Attack + Sandwich Attack used bloXroute + Sandwich Attack used 48 Club + Sandwich Attack used General * Theoretical Explanation of MEV + Acronym + The Ideal Flow of the Expected Blockchain + Vulnerabilities of DEX Swap + How to Protect from MEV Attacker - Are the Block Proposer and Builder Separated? * Detailed Explanation of Source Code and Formulas + Project Structure - Directory Structure + IAC Tips + About MEV-Relay + Explain the Solidity Code - What is the difference between sandwichFrontRun and sandwichFrontRunDifficult? - About the BackRun Function - Swap Router - Supported DEX List - Solidity Test Code + Explain the Python Code - Important Note - Explain the Process in Writing - Why is Arbitrage Only Used for General Path? - Trace Transaction + Formula Parts - Reasons Why Understanding DEX AMM is Important - Structure of Uniswap V2, V3 - Optimize the Uniswap V2 Swap to the Extreme * 1. Uniswap V2 Amount Out Formula * 2. Analyze Uniswap V2 Swap * 3. Optimization of Swap Formula * 4. Verification of the Formula Optimized to the Extreme - Multi-hop Arbitrage Formula * 1. 2-hop Arbitrage Formula * 2. Multi-hop Arbitrage Formula * EigenPhi is Overhyped + When Sandwich is Recognized as Arbitrage + When Doing Sandwich Using 48 Club * Frequently Asked Questions - Why you select the BSC network? - Do you plan to continue developing arbitrage or MEV in the future? * More InformationTable of contents generated with markdown-toc
Transaction Sample for Sandwich Attack
Sandwich Attack used bloXroute
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:---------:|:--------:|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|------------------| | Front Run | 0 | 0.21913 WBNB
-> 0xE3...3E90C -> 35,489.53984 IRT
0x906bf0f8bd71319bfb2938d6a07f29c823011d8d30a3afa306afb52773f38203 | -0.21913 | | -0.00013 | | | Victim | 1 | 0.362844 WBNB
-> 0xE3...3E90C -> 58,272.54022 IRT
0x3f63df509a8708f75f7280a38ee7b70ac18817eb0c8c9d18c100d2dc73e48380 | | | | | | Back Run | 2 | 35,489.53984 IRT
-> 0xE3...3E90C -> 0.22019 WBNB
0x1737935a6ade5e3c0cbfa1fa2bb75dce27d3d1a7312e5b2c093bdab20cd369e0 | +0.22019 | -0.00005 | -0.00077 | | | | | | +0.00106 | -0.00005 | -0.0009 | +0.00012 (+$0.7) |
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:---------:|:--------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|-------------------| | Front Run | 0 | 0.48336 WBNB
-> 0x37...8a8dc -> 37,926,166,753,754.67 PROMISE
0x3b8f2e39dddbb6decf2c1e8b15549b7312835503f3eeddcb19bb242c77c9366c | -0.48336 | | -0.00013 | | | Victim | 1 | 488 USDT
-> 0x16...b0daE -> 0.81841 WBNB
-> 0x37...8a8dc -> 63,607,618,504,143.055 PROMISE
0xab8f068806754ec0c0ac33570aed59901d3a71873c9d2b84bf440e0da016866b | | | | | | Back Run | 2 | 37,926,166,753,754.67 PROMISE
-> 0x37...8a8dc -> 0.486725 WBNB
0x01ae865a8e199a41acb5a57071610ee5b60fc241f181d7e08f70c8fa520afed2 | +0.48672 | -0.00273 | -0.00039 | | | | | | +0.00336 | -0.00273 | -0.00052 | +0.00011 (+$0.066) |
Sandwich Attack used 48 Club
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:------------:|:--------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|-------------------| | Transfer Fee | 0 | 0x5eee8dce6d7314ef53261f81c3afb6d3e0c2f26955e67197b22a431a7cf989f2 | | -0.00248 | | | | Front Run | 1 | 0.03326 WBNB
-> 0x9D...103A5 -> 45.30036 LFT
0xfd0528069b42a762c77a1dd311fa073149f6068e63325011cff77cf3c13f2969 | -0.03326 | | -0.00066 | | | Victim | 2 | 0.31134 WBNB
-> 0x9D...103A5 -> 397.67103 LFT
0xc76f1a83452ce383fa6cc49f0183172f79eb345538c760448f4afd8e51489f60 | | | | | | Back Run | 3 | 45.30036 LFT
-> 0x9D...103A5 -> 0.03658 WBNB
0xc4ac3154678e594615cc97bcb3a924bf65ef58a5c033394cf158c0b21b4bc619 | +0.03658 | | -0.00014 | | | | | | +0.00332 | -0.00248 | -0.00080 | +0.00004 (+$0.02) |
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:------------:|:--------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|-------------------| | Transfer Fee | 0 | 0xb8adc24e0ff82126b6f6dbecf38098ea69fec8704454d5b2d6c43d5c79f52c62 | | -0.00306 | | | | Front Run | 26 | 0.93624 WBNB
-> 0x84...E4A73 -> 7,851,389,136,191.288 DoveCoin
0x77df425af519f8f7a0f276fe18506f5994ce3a40a345b33c123cb43637391ef3 | -0.93624 | | -0.00060 | | | Victim | 27 | 0.022 WBNB
-> 0x84...E4A73 -> 164,582,801,699.83146 DoveCoin
0x16f893d195bfdc461bb0d98e4c997d55c04ff21fb52bf3249bae8ea1383e2866 | | | | | | Back Run | 90 | 7,851,389,136,191.288 DoveCoin
-> 0x84...E4A73 -> 0.93976 WBNB
0x3d2ebf6d5bf6574fc59383dfc11013794c7c14f560c2083bcaa80258910b84a8 | +0.93976 | | -0.00017 | | | | | | +0.00352 | -0.00306 | -0.00077 | -0.00031 (-$0.18) |
Sandwich Attack used General
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:----------------------------:|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|--------------------| | Front Run | 0 | 0.69236 WBNB
-> 0x36...52050 -> 419.05221 USDT
-> 0xCB...50043 -> 829.78960 ARTY
0x19228eff3516d7b868994a64d7800c01f2ab0a8f8dfa3d2548c74b050c62978d | -0.69236 | | -0.00386 | | | Victim (Maybe Try Front Run) | 1 | 914.41511 USDT
-> 0xCB...50043 -> 1,768.01385 ARTY
0x5a54f193680c5cfe34bc1729f9047975d059a0463156191c3ee3e8c34bc5c959 | | | | | | Victim | 2 | 244.84935 USDT
-> 0xCB...50043 -> 470.47796 ARTY
0xc88560c1b4dae7289f4d788a591a3385020d41814b6b9e8f41b5776b9d202513 | | | | | | Victim (Maybe Try Front Run) | 3 | 489.27772 USDT
-> 0xCB...50043 -> 931.07042 ARTY
0x36c74124c73fe589e62b1a8f6ccd08f9bca6b702c2a932db13b30b2ef6dc9397 | | | | | | Back Run | 4 | 829.78960 ARTY
-> 0xCB...50043 -> 434.07649 USDT
-> 0x36...52050 -> 0.71646 WBNB
0x01cf62f0434cb1afebec9b2cfae79cac199c2901f2891e1f1a7dc6bb2f38a5f1 | +0.71646 | | -0.00335 | | | | | | +0.02410 | | -0.00721 | +0.01689 (+$10.13) |
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:---------:|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|--------------------| | Front Run | 0 | 0.42776 WBNB
-> 0x25...E3fE6 -> 741.06721 QORPO
0xd327fbc24c6bbf39e93692daa06e149e00441160cae16e5936d11a9c2275f92b | -0.42776 | | -0.00582 | | | Victim | 72 | 3,780 USDT
-> 0x17...6f849 -> 6.27670 WBNB
-> 0x25...E3fE6 -> 10,543.53051 QORPO
0x556e50fe2625706c949d0b071f80bb7f9a9b730561d065f4e5efeb2fa27a8e65 | | | | | | Back Run | 86 | 741.06721 QORPO
-> 0x25...E3fE6 -> 0.45089 WBNB
0xbc5a1c5107f4f40b4e3c091d94339dbd863883018eaa9bd2aa2021154baf2f74 | +0.45089 | | -0.00046 | | | | | | +0.02313 | | -0.00628 | +0.01685 (+$10.11) |
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:-----------:|:----------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|-----------------|-------------------| | Front Run | 17 | 0.42469 WBNB
-> 0x6e6...90A278 -> 175.20479 METFI
-> 0x562...08F3d -> 0.46990 MOGUL
0xc422d6687869388727d3a41c0697039fa9ddf4f6e48be3feecd4e5a92b5a0248 | -0.42469 | | -0.00074 | | | Victim | 23 | 1592.55603 METFI
-> 0x562...08F3d -> 4.12 MOGUL
0x7f03c55bbf741e9781cffd80040bbfec3b21b1d8eb173f47afb7d38890b6d537 | | | | | | Back Run | 38 | 0.46990 MOGUL
-> 0x562...08F3d -> 180.349207 MOGUL
-> 0x6e6...90A278 -> 0.434972 WBNB
0x4dea3aa40d1c651fc6cb7f8c8a93f8fe116fe67ac3647cf163621969a6b280ce | +0.43497 | | -0.00070 | | | | | | +0.01028 | | -0.00144 | +0.00884 ($+5.30) |
| Type | Position | Transaction Hash | Transfer (WBNB) | MEV-Relay (BNB) | Transaction Fee (BNB) | Profit (BNB) | |:---------:|:--------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|-----------------------|-------------------| | Front Run | 0 | 0.42622 WBNB
-> 0x36...52050 -> 255.56599 USDT
-> 0xdb...252D4 -> 12,605.92082 ZEBEC
0x80153922bc5451d890cfe882f37c77ff4465dc23a76df592c450f444f4951166 | -0.42622 | | -0.00557 | | | Victim | 42 | 293.28351 USDT
-> 0xdb...252D4 -> 14,371.14467 ZEBEC
0x74cedb99bedfa47b2c281707ebef3918f318a05ad802f567e9d2d657dc44f720 | | | | | | Back Run | 54 | 12,605.92082 ZEBEC
-> 0xdb...252D4 -> 256.36776 USDT
-> 0x36...52050 -> 0.42713 WBNB
0x6068134cea60f8f0d81422b54d35a12c90ec814f9342a1175046f990963f0644 | +0.42713 | | -0.00074 | | | | | | +0.00091 | | -0.00631 | -0.00540 (-$3.24) |
Theoretical Explanation of MEV
Acronym
- EL: Execution Layer
- CL: Consensus Layer
- DEX: Decentralized Exchange
- MEV: Maximal Extractable Value
The Ideal Flow of the Expected Blockchain
This section is theoretical in nature and contains only the core information needed to develop an MEV attack. For specific implementation details, please refer to the other sections.The theoretically expected block creation process on a blockchain is as follows:
- User Transaction Creation:
- Transaction Submission:
- Transaction Validation and Pooling:
- Peer-to-Peer Synchronization:
- Validator Selection:
- Block Construction:
- Attestation Collection:
- Block Validation and Attestation Submission:
- Block Finalization:
Vulnerabilities of DEX Swap
On the blockchain, DEXs make it easier to exchange various tokens. However, users who use DEXs to exchange tokens risk losing their assets. The key to this vulnerability lies in the order of transactions within a block, which can be manipulated to create a security hole.Arbigrage Attack
- Definition: Arbitrage is a technique that exploits the price difference between two exchanges to make a profit.
- The process
- However, to make a profit, you must always be ahead of others.
- The optimization process
Sandwich Attack

- Definition: A technique that manipulates the liquidity of a DEX pool by placing attack transactions in front of or behind the victim's transactions in order to profit from the victim's transactions.
- Note: This technique is a more complex attack than arbitrage, and unlike arbitrage, it results in a monetary loss to the victim. Victims of a sandwich attack actually suffer a loss of assets.
- The process
How to Protect from MEV Attacker
MEV attacks have a serious adverse impact on the blockchain ecosystem, but blockchain's structural limitations make it difficult to solve this problem fundamentally. Currently, there are only technologies that partially mitigate these issues.MEV attackers need to know the victim's transaction information to launch an attack, and this information is exposed to the attacker at the Mempool stage. Therefore, the following strategies can be used to protect the victim's transactions.
Steps
- select a Block Proposer and a Block Builder to build the proposed block.
- The victim submits transactions to the private mempool instead of the public mempool.
- the attacker analyzes the victim's transactions in the public or private mempool to identify profit opportunities.
- The attacker bundles their front-running and back-running transactions with the victim's transactions and submits them to the private Orderflow.
- The process involves the attacker returning a portion of the profit gained from the victim (e.g., the attacker may submit MEV fees as gas fees or pay fees to a specific contract address).
- Validate the bundle's execution and the appropriateness of the fees to be paid. If multiple bundles are submitted for the same victim's transaction, the bundle with the highest fee is selected.
- The Block Proposer looks up transactions in the public mempool and private orderflow. It then organizes and proposes a block that will bring the highest benefit to the Validator.
- The selected Validator builds the block proposed by the Block Proposer.
- The victim receives a portion of the profit returned by the attacker in step 5.
- Victim: Losses can be reduced.
- Attacker: By fixing the order of transactions into a bundle, they can eliminate the risk of failures and errors due to the order of transactions. The risk of errors is eliminated because when you submit a bundle, you test it to make sure it runs without errors. (However, you may incur some losses because you must return some profit to the victim).
- Validator: You can maximize your profits. (The Flashbots team claims that validators using MEV-Boost can increase their profits by over 60%).
Services such as Flashbots on ETH, bloXroute on BSC, and 48 Club on BSC provide this functionality. However, in the case of bloXroute, some have claimed that it destroys the BSC ecosystem (see the More Information page below for more details).
Are the Block Proposer and Builder Separated?
Yes. The concept of separating the Block Proposer and Builder is called Proposer/Builder Separation (PBS).A good place to start is with Ethereum's MEV-Boost.
PBS promotes decentralization of the Ethereum network, increasing the overall benefit to network participants through competition among block builders.
Detailed Explanation of Source Code and Formulas
The cost of using the node server and bloXroute was significant, and the need to see results quickly prevented us from giving enough thought or refactoring to the structure of the project. I apologize if the code is hard to read. 🙇Project Structure
Directory Structure
- contract: This is a Solidity project based on Hardhat.
- docker
- iac: Infrastructure as Code (IAC) written in Pulumi for running nodes.
- pyrevm: This is a Python wrapper of REVM, a Rust implementation of the Ethereum Virtual Machine (EVM). The original can be found at paradigmxyz/pyrevm.
- src: Python code to capture MEV opportunities.
tests/testdex/testcurve_.py for details on supported Curve pools.
* multicall: Provides MultiCall functionality. The original can be found at banteg/multicall.py.
- tests: This is code to test the
srccode.
IAC Tips
- For the region, I recommend New York, the USA, or Germany.
- Do not separate clients and nodes performing MEV attacks from load balancers, etc.
- It's important to utilize snapshots.
geth can be somewhat less reliable.
* I have had several experiences with chain data corruption when geth crashes, so I recommend actively using the snapshot feature.
About MEV-Relay
The project supports three paths
- General: Go through Public Mempool without using MEV-Relay.
- 48 Club: Use the Puissant API to perform MEV operations.
- bloXroute: There is a base cost, and you must pay a separate fee for each bundle.
In [Transaction Sample for Sandwich Attack] (#transaction-sample-for-sandwich-attack), you can see that bloXroute and 48 Club have lower profits compared to General. Since MEV attacks are not carried out in isolation, there is inevitably competition from other competitors, which comes at a cost.
First, let's analyze General: General does not use MEV-Relay, so transaction execution and order are not guaranteed during an attack. If you fail to perform DEX transactions in the proper transaction order, you may be victimized by other attackers. If you try to attack after the MEV attack is complete, you may lose the transaction fee. Also, if the amount of tokens in the pool you tried to attack changes, the transaction may fail (assuming another attacker has already tried). Even if it fails, you will still incur gas costs.
Next, let's look at bloXroute and 48 Club. There are two main advantages to using MEV-Relay. First, you are guaranteed the order of your transactions, and second, you are not charged if a transaction fails. To eliminate this risk, you need to pay a de-risking fee. The cost of de-risking basically includes the fee you pay to MEV-Relay and the cost of bidding to win the competition.
For this reason, the cost of General may not be apparent when analyzing a single transaction.
More information
- BSC MEV Overview on the bnbchain docs
- bloxroute exploiting users for MEV on the bnb-chain/bsc GitHub issue
Explain the Solidity Code
What is the difference between sandwichFrontRun and sandwichFrontRunDifficult?
Let's assume that Pool A and Pool B exist.- sandwichFrontRun: The transactions are performed in the following order
My Contract -> A Pool -> My Contract -> B Pool -> My Contract.
- sandwichFrontRunDifficult: The transaction is performed in the following order:
My Contract -> A Pool -> B Pool -> My Contract. In this case, each pool needs to verify that it can send tokens to the next pool and that the next pool can receive tokens, which can reduce the number of token transfers.
About the BackRun Function
There are three functions- sandwichBackRunWithBloxroute: This function performs a BackRun, and then the fee is paid via bloXroute.
- sandwichBackRun: This function performs a BackRun. It is functionally equivalent to
sandwichBackRunWithBloxroute, except that it does not involve paying a fee.
- sandwichBackRunDifficult: This function performs a BackRun similar to
sandwichBackRun, but requires each pool to validate that it can send and receive tokens before the transaction can proceed.
Swap Router
The contractcontract/contracts/dexes/SwapRouter.sol facilitates swaps at a high level. This contract can execute swaps with just the DEX ID, pool address, token address, and amount information.
Supported DEX List
Swap supports the following DEXs- Uniswap V2 Family: UniswapV2, SushiswapV2, PancakeSwapV2, THENA, BiswapV2, ApeSwap, MDEX, BabySwap, Nomiswap, WaultSwap, GibXSwap
- Uniswap V3 Family: UniswapV3, SushiswapV3, PancakeSwapV3, THENA FUSION
- Curve (ETH): Supports most major pools.
Solidity Test Code
When writing Solidity test code, i considered the following:
- Make sure DEX swaps are working properly
- Connection state when swapping across multiple pools in succession (arbitrage)
- Works with different chains
- Utilize hardhat-exposed to test private functions as well as public functions
contract/test/dexes.tscontract/test/ArbitrageSwap.ts
Explain the Python Code
Important Note
- Follow along using the
main_sandwich.pyfile as a reference. This project contains several unused files or functions. - This code was working as of May 2024, and due to the nature of the blockchain space, things can change quickly, most notably the way 48 Club uses Puissant.
Explain the Process in Writing
- on startup, the following features are enabled
main function with the MEV function as a multiprocess.
- Filter transactions received from the Mempool stream are based on the presence of gas information and data. (If data is empty, the contract function is not called.)
- Trace the transaction using
debug_traceCall:
src/apis/tracetx.py::searchdex_transaction, verify if the transaction involves a DEX swap. (You can get the exchange type, pool address, and amount information.) If there are multiple calls by the same swap, merge them into a single SwapEvent.
3. The Transaction class contains not only the SwapEvent but also several pieces of information about the transaction.
4. export the Transaction through the queue pipe.
- pull a
Transactionout of the queue from eachmainthat ran as a multiprocess.
- Analyze if the transaction can be attacked for profit:
calculateuniswapv2_sandwich function.
2. swaps in the Uniswap V3 series are validated using EVM (pyREVM).
- If the transaction is determined to have a profit potential, get the validator information to build the next block. Based on the validator address, determine whether to route the attack transaction as General, 48 Club, or bloXroute:
- Create a sandwich transaction and send the bundle via the selected path.
Why is Arbitrage Only Used for General Path?
The MEV analysis revealed a few important trends:- Arbitrage vs. Sandwich Attacks: When given the opportunity to attack, sandwich attacks tended to yield higher profits than arbitrage. Therefore, when submitting a bundle to MEV-Relay with arbitrage, it was likely to lose in competition with other sandwich attack bundles.
- Cautions for General Paths: If you submit Arbitrage with the General path, the transaction will always be executed unless canceled. This will always result in a GAS cost. If your transaction is placed right after the victim's transaction, you may benefit, but if you lose the race to other arbitrage attackers, you may lose money due to the GAS cost.
- Speed up Mempool registration: Due to the lack of node optimization, we did not have enough opportunity to compete with other attackers via the generic path. Researching ways to make it faster for attackers to register their arbitrage transactions to the mempool could increase profits (however, the large cost outlay prevented continued experimentation).
Trace Transaction
When you trace a transaction, the results are provided on a per-function call basis. This allows you to see if the swap function is called and analyze the values passed as arguments to the function.
~~~ json { "jsonrpc": "2.0", "result": [ { "action": { "callType": "call", "from": "0x83806d539d4ea1c140489a06660319c9a303f874", "gas": "0x1a1f8", "input": "0x", "to": "0x1c39ba39e4735cb65978d4db400ddd70a72dc750", "value": "0x7a16c911b4d00000" }, "blockHash": "0x7eb25504e4c202cf3d62fd585d3e238f592c780cca82dacb2ed3cb5b38883add", "blockNumber": 3068185, "result": { "gasUsed": "0x2982", "output": "0x" }, "subtraces": 2, "traceAddress": [], "transactionHash": "0x17104ac9d3312d8c136b7f44d4b8b47852618065ebfa534bd2d3b5ef218ca1f3", "transactionPosition": 2, "type": "call" }, { "action": { "callType": "call", "from": "0x1c39ba39e4735cb65978d4db400ddd70a72dc750", "gas": "0x13e99", "input": "0x16c72721", "to": "0x2bd2326c993dfaef84f696526064ff22eba5b362", "value": "0x0" }, "blockHash": "0x7eb25504e4c202cf3d62fd585d3e238f592c780cca82dacb2ed3cb5b38883add", "blockNumber": 3068185, "result": { "gasUsed": "0x183", "output": "0x0000000000000000000000000000000000000000000000000000000000000001" }, "subtraces": 0, "traceAddress": [ 0 ], "transactionHash": "0x17104ac9d3312d8c136b7f44d4b8b47852618065ebfa534bd2d3b5ef218ca1f3", "transactionPosition": 2, "type": "call" }, { "action": { "callType": "call", "from": "0x1c39ba39e4735cb65978d4db400ddd70a72dc750", "gas": "0x8fc", "input": "0x", "to": "0x70faa28a6b8d6829a4b1e649d26ec9a2a39ba413", "value": "0x7a16c911b4d00000" }, "blockHash": "0x7eb25504e4c202cf3d62fd585d3e238f592c780cca82dacb2ed3cb5b38883add", "blockNumber": 3068185, "result": { "gasUsed": "0x0", "output": "0x" }, "subtraces": 0, "traceAddress": [ 1 ], "transactionHash": "0x17104ac9d3312d8c136b7f44d4b8b47852618065ebfa534bd2d3b5ef218ca1f3", "transactionPosition": 2, "type": "call" } ], "id": 0 } ~~~ source alchemy example
This repo detects three kinds of swaps. A value expressed as a hexadecimal number, such as 0x022c0d9f, is called a Function Selector. Each function has a unique Function Selector value, which can be found in the Ethereum Signature Database. The Function Selector is located at the very beginning of call[“input”]. If detected, it is assumed that a swap event will occur in that transaction.
The call[“to”] represents the contract address where the function is executed, and since swaps take place in a pool, it means the pool address. The extracted DEX type and pool address are combined and recorded in swap_events.
~~~ python from_ = call["from"] to = call["to"] input = call["input"]
if "input" not in call: return []
if input.startswith("0x022c0d9f"): swapevents = setswapevent("UNISWAPV2", swap_events, to) if input.startswith("0x6d9a640a"): swapevents = setswapevent("BAKERYSWAP", swapevents, to) if input.startswith("0x128acb08"): swapevents = setswapevent("UNISWAPV3", swap_events, to) ~~~
The only remaining necessary information is the token address and token amount. The complicated part is that the token transfer function is not called inside the swap function.
Uniswap V2 Swap cases
- In case 1
- *In case 2
Uniswap V3 Swap cases
- In case 1
Depending on the order of the swap, extracting the required values can be difficult. Therefore, we detect the transfer and transferFrom function calls and extract the sender, recipient, and value from call[“input”]. Simply extracting the value is not enough; we need to validate that the token transfer is associated with a pool swap. To do this, we utilize a union with the swap_events variable.
~~~ python # transfer isintransfer = False if input.startswith("0xa9059cbb"): recipient = "0x" + input[2 + 32: 2 + 32 + 40] value = hextouint256(input[2 + 32 + 40: 2 + 32 + 40 + 64]) if value > 0: swapevents = settransferevent(swapevents, from_, to, recipient, value) isintransfer = True # transferFrom if input.startswith("0x23b872dd"): sender = "0x" + input[2 + 32: 2 + 32 + 40] recipient = "0x" + input[2 + 32 + 40 + 64 - 40: 2 + 32 + 40 + 64] value = hextouint256(input[2 + 32 + 40 + 64: 2 + 32 + 40 + 64 + 64]) if value > 0: swapevents = settransferevent(swapevents, sender, to, recipient, value) isintransfer = True ~~~
The result of extracting the required data from the swap function and token transfer function and combining them is accumulated in the form swap_events: List[SwapEvent].
~~~ python class SwapEvent: def init(self, dex=None, address=None, tokenin=None, tokenout=None, amountin=0, amountout=0): self.dex = dex self.address = address self.tokenin = tokenin self.tokenout = tokenout self.amountin = amountin self.amountout = amountout ~~~
The following cases exist:
- Normal Swap: If
dex,address,tokenin,tokenout,amountin, andamountoutare all present in theSwapEvent, this indicates a normal swap. - If Not a Token Transfer: If only
tokenout,amountin, andamount_outexist in aSwapEvent, it is removed because it is not a token transfer for a swap. - Problem**: If the
SwapEventonly hasdex,address, there may be a problem with the data or code.
Formula Parts
Reasons Why Understanding DEX AMM is Important
The fundamental reason attackers can seize profit opportunities lies within DEX AMMs. This is because the same trade can yield different profits depending on the AMM's calculation.In this part, we will analyze the AMMs on the DEX and implement an efficient formula to maximize profit.
Structure of Uniswap V2, V3
While Uniswap V2 and V3 have similar overall structures, their main difference is their Automated Market Makers (AMMs).Uniswap has three main contract addresses:
- Router Address
- Factory Address
- Pair (Pool) Address
Optimize the Uniswap V2 Swap to the Extreme
The official formula for calculating the number of tokens obtained during a swap on Uniswap V2 is as follows.1. Uniswap V2 Amount Out Formula
Variable definitions- $x$ = the amount of token that you want to trade
- $y$ = the amount of token that you want to receive
- $R_{in}$ = the reserve of the input token
- $R_{out}$ = the reserve of the output token
- $y = \frac{997R{out} x}{1000 R{in} + 997 x}$
~~~ Solidity function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { require(amountIn > 0, 'UniswapV2Library: INSUFFICIENTINPUTAMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); uint amountInWithFee = amountIn.mul(997); uint numerator = amountInWithFee.mul(reserveOut); uint denominator = reserveIn.mul(1000).add(amountInWithFee); amountOut = numerator / denominator; } ~~~ function getAmountOut
Many MEV open-source projects use the formula above, but the swap formula can be optimized to even greater extremes.
The main goal is to optimize the formula so that there are no errors during the swap transaction and the maximum number of tokens are received. To do this, we need to analyze how we can validate that the proper tokens are coming in and going out when performing a swap transaction on a DEX contract.
2. Analyze Uniswap V2 Swap
Source code (Solidity)When calling the function, specify the quantity of tokens to receive as argument.
~~~ Solidity function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { ~~~
Sends the requested amount of tokens (amount0Out or amount1Out) to the user.
~~~ Solidity uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != token0 && to != token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) safeTransfer(token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) safeTransfer(token1, to, amount1Out); // optimistically transfer tokens ~~~
You can send the token to pay directly to the pair address before requesting the swap function, or you can call the uniswapV2Call callback function to send it to the pair address as shown below.
~~~ Solidity if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); ~~~
The following code validates that the appropriate amount of tokens were sent to the user for the tokens received from the user in pair contract. If the amount of tokens returned is not appropriate, the transaction will fail.
~~~ Solidity balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > reserve0 - amount0Out ? balance0 - (reserve0 - amount0Out) : 0; uint amount1In = balance1 > reserve1 - amount1Out ? balance1 - (reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENTINPUTAMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(reserve0).mul(reserve1).mul(1000**2), 'UniswapV2: K'); }
update(balance0, balance1, reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); } ~~~ swap function
3. Optimization of Swap Formula
Variable definitions
The values of $n$ and $s$ are different for different exchanges (e.g. Uniswap, SushiSwap, etc.). More information can be found in the src/apis/contract.py:L431.
- $x$ = amount in
- $y$ = optimized amount out
- $n$ = 1000 where Uniswap V2
- $s$ = 3 where Uniswap V2
- $R_{in}$ = reserve in of pool
- $R_{out}$ = reserve out of pool
- $B_{in}$ = balance in after swap
- $B_{out}$ = balance out after swap
- $B_{in, adj}$ = balance in adjusted
- $B_{out, adj}$ = balance out adjusted
- $B{in} = R{in} + x$
- $B{out} = R{out} - y$
- $B{in, adj} = 1000B{in} - 3x$
- $B{out, adj} = 1000B{out} - 0$
- $B{in, adj}B{out, adj} >= 1000^2 R{in} R{out}$
- $B{out, adj} = 1000(R{out} - y)$
- $1000b{in, adj}(R{out} - y) >= 1000^2 R{in} R{out}$
- $B{in, adj}(R{out} - y) >= R{in} R{out} * 1000$
- $-y >= \lfloor \frac{R{in} R{out} * 1000} {B{in, adj}} \rfloor - Ro$
- $y <= R{out} - \lfloor \frac{1000 R{in} R{out}} {B{in, adj}} \rfloor$
- $y <= R{out} - \lfloor \frac{1000 R{in} R{out}} {1000(Ri + x) - 3x} \rfloor$
- $y <= R{out} - \lfloor \frac{nR{in} R{out}} {n(R{in} + x) - sx} \rfloor$
- $y <= R{out} - \lfloor \frac{nR{in} R{out}} {n(R{in} + x) - sx} \rfloor$
~~~ python def getoptimalamountout(amountin, n, s, reservein, reserve_out): try: return reserveout - ((reservein reserveout n) // (n (reservein + amountin) - s amountin)) except ZeroDivisionError: return 0 ~~~
4. Verification of the Formula Optimized to the Extreme
Validate optimized formulas.- block number = 39744974
- Uniswap V2 USDT/WBNB address = 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE
- input token = WBNB
- output token = DAI
- $n$ = 10000
- $s$ = 25
- $R_{in}$ = 12124290984572619906122
- $R_{out}$ = 7262381719977943429386502
- $x$ = 10000000000000000000 (10 WBNB)
- $y$ = 5967066790489861652480 (5967.06 DAI)
- $x$ = 10000000000000000000 (10 WBNB)
- $y$ = 5970056841417710950357 (5970.05 DAI)
Multi-hop Arbitrage Formula
1. 2-hop Arbitrage Formula
Suppose you have two pools with the same pair of tokens. The process of putting $x$ into the first pool, and then putting the tokens obtained into the second pool to get $y$, which is the same token as $x$, is as follows:$x \rightarrow R{1, in} \rightarrow R{1, out} \rightarrow R{2, in} \rightarrow R{2, out} \rightarrow y$
In this case, find a formula to calculate the value of $x$ for maximum profit.
Variable definitions
- $x$ = amount in
- $y_1$ = amount out of 1st pool
- $y_2$ = amount out of 2nd pool
- $n$ = 1000 where Uniswap V2
- $s$ = 3 where Uniswap V2
- $R_{1, in}$ = reserve in of 1st pool
- $R_{1, out}$ = reserve out of 1st pool
- $R_{2, in}$ = reserve in of 2nd pool
- $R_{2, out}$ = reserve out of 2nd pool
- $y1 = \frac{(n - s)R{1, out}x}{nR_{1, in} + (n - s)x}$
- $y2 = \frac{(n - s)R{2, out}y1}{nR{2, in} + (n - s)y_1}$
- $y2 = \{ (n - s)R{2, out} \times \frac{(n - s)R{1, out}x}{nR{1, in} + (n - s)x} \} \div \{ nR{2, in} + (n - s) \frac{(n - s)R{1, out}x}{nR_{1, in} + (n - s)x} \}$
- $y2 = \{ (n - s)R{2, out} \times (n - s)R{1, out}x \} \div [ nR{2, in} \{ nR{1, in} + (n - s)x \} + (n - s) (n - s)R{1, out}x ]$
- $y2 = \{ (n - s)^2R{1, out}R{2, out}x \} \div \{ n^2R{1, in}R{2, in} + (n - s)nR{2, in}x + (n - s)^2R_{1, out}x \}$
- $y2 = \frac {(n - s)^2R{1, out}R{2, out}x} {n^2R{1, in}R{2, in} + x \{ (n - s)nR{2, in} + (n - s)^2R_{1, out} \}}$
- $F(x) = y_2 - x$
- $F^{\prime}(x) = y^{\prime}_1 - 1$
- $f = (n - s)^2R{1, out}R{2, out}x$
- $g = n^2R{1, in}R{2, in} + x \{ (n - s)nR{2, in} + (n - s)^2R{1, out} \}$
- $y^{\prime}_1 = \frac {f^{\prime}g - fg^{\prime}} {g^2}$
- $g^2 = f^{\prime}g - fg^{\prime}$
- $f^{\prime}g - fg^{\prime} = (n - s)^2R{1, out}R{2, out} [ n^2R{1, in}R{2, in} + x \{ (n - s)nR{2, in} + (n - s)^2R{1, out} \} ] - (n - s)^2R{1, out}R{2, out}x \{ (n - s)nR{2, in} + (n - s)^2R{1, out} \}$
- $f^{\prime}g - fg^{\prime} = (n - s)^2R{1, out}R{2, out} \{ n^2R{1, in}R{2, in} + (n - s)nR{2, in} x \} + (n - s)^4R^2{1, out}R{2, out} x - (n - s)^3nR{2, in}R{1, out}R{2, out}x - (n - s)^4R^2{1, out}R{2, out}x$
- $f^{\prime}g - fg^{\prime} = (n - s)^2R{1, out}R{2, out} \{ n^2R{1, in}R{2, in} + (n - s)nR{2, in} x \} - (n - s)^3nR{2, in}R{1, out}R{2, out}x$
- $f^{\prime}g - fg^{\prime} = (n - s)^2n^2R{1, in}R{2, in}R{1, out}R{2, out}$
- $g^2 = [ n^2R{1, in}R{2, in} + x \{ (n - s)nR{2, in} + (n - s)^2R{1, out} \}]^2$
- $k = (n - s)nR{2, in} + (n - s)^2R{1, out}$
- $g^2 = (n^2R{1, in}R{2, in} + kx)^2$
- $g^2 = (n^2R{1, in}R{2, in})^2 + 2n^2R{1, in}R{2, in} kx + (kx)^2$
- $(n^2R{1, in}R{2, in})^2 + 2n^2R{1, in}R{2, in} kx + (kx)^2 = (n - s)^2n^2R{1, in}R{2, in}R{1, out}R{2, out}$
- $k^2x^2 + 2n^2R{1, in}R{2, in} kx + (n^2R{1, in}R{2, in})^2 - (n - s)^2n^2R{1, in}R{2, in}R{1, out}R{2, out} = 0$
- $a = k^2$
- $b = 2n^2R{1, in}R{2, in} k$
- $c = (n^2R{1, in}R{2, in})^2 - (n - s)^2n^2R{1, in}R{2, in}R{1, out}R{2, out}$
- $x^* = \frac {-b + \sqrt {b^2 - 4ac}} {2a}$
- The formula expanded above is validated.

2. Multi-hop Arbitrage Formula
What if there are $n$ pools instead of two pools
README truncated. View on GitHub