import pandas as pd
import plotly.io as pio
from sqlalchemy import create_engine
from mev_attacks import utils
import requests
pd.options.plotting.backend = "plotly"
pio.renderers.default="notebook"
pio.templates.default = "seaborn"
DB_AVAIL = False
if DB_AVAIL:
user = "postgres"
password="password"
host = "localhost"
engine = create_engine(f'postgresql+psycopg2://{user}:{password}@{host}/mev_inspect', pool_recycle=3600)
conn = engine.connect();
A sandwich attack involves executing transactions immediately before and immediately after a swap transaction on a decentralized exchange that sets prices according to a known formula. Many of the largest decentralized exchanges utilize a constant product formula such that the product of the supply of each token in a pool must remain constant.
For example, if there were a pool of a pair of tokens A and B each with an initial supply of 10, the product would need to remain constant at 100. To determine the number of B tokens that would be received for 1 A token, you would take 100 divided by 11, the new supply of A, to determine that after the transaction there needs to be 9.090909 B tokens in order to keep the product of the supplies constant at 100. Therefore, you could swap 1 A token for 0.90909 B tokens.
A | B | k / price | |
---|---|---|---|
$t_0$ | 10.0000 | 10.0000 | 100.0000 |
$user$ | 1.0000 | -0.9091 | 0.9091 |
$t_3$ | 11.0000 | 9.0909 | 100.0000 |
Since transactions are written together in blocks, the final amount received for an individual transaction depends on the other transactions in the block that get executed before it. In order to protect users from unlimited exposure to changes in their received price, one of the parameters they specify is the amount of slippage they will suffer before their transaction will simply not be executed.
The sandwich attack will execute transactions on either side of a swap transaction to capture the maximum allowable slippage. The default slippage for Uniswap is 0.5%, but it can be set to anything. To illustrate the nature of the attack, assume that the allowable slippage in the above transaction was specified to be 10%, meaning that as few as 0.8181 B tokens could be received for 1 A token and the transaction would still be executed.
To execute an attack to capture the maximum possible slippage of 0.0909 a searcher would first determine what transaction they need to execute such that the supply of A and B tokens in the pool when the user’s transaction is executed results in the user receiving 0.8181 B tokens. Then after the user’s transaction, the searcher executes another transaction in the opposite direction that leaves the supply in the pools where it would have been without the attack, netting them the slippage at the user’s expense.
$$ \begin{align*} Let A_1 &= \text{the supply of token A after the attacker's initial transaction} \\ Let B_1 &= \text{the supply of token B after the attacker's initial transaction} \\ \end{align*} $$and in order for $A * B = 100$ to be true,
$$ \begin{align*} (A_1 + 1.0000) * (B_1 - 0.8181) &= 100 \\ (A_1 + 1.0000) * (\frac{100}{A_1} - 0.8181) &= 100 \\ A_1 = 10.5667 \end{align*} $$which means the swap the searcher executes initially is for 0.5363 B tokens in exchange for 0.5667 A tokens. After the user's transaction the searcher swaps their 0.5667 A tokens for 0.4454 B tokens to net 0.0909 B tokens from the sandwich attack.
A | B | k / price | |
---|---|---|---|
$t_0$ | 10.0000 | 10.0000 | 100.0000 |
$pre$ | 0.5667 | -0.5363 | 0.9463 |
$t_1$ | 10.5667 | 9.4637 | 100.0000 |
$user$ | 1.0000 | -0.8182 | -0.8182 |
$t_2$ | 11.5667 | 8.6455 | 100.0000 |
$post$ | -0.5667 | 0.4454 | 0.7860 |
$t_3$ | 11.0000 | 9.0909 | 100.0000 |
$profit$ | 0.0909 |
if False:
df_sandwich = pd.read_sql("SELECT * from sandwiches", conn)
df_sandwich.to_csv("data/sandwiches.csv", index=False)
df_sandwich = pd.read_csv("data/sandwiches.csv")
df_sandwich.head()
id | created_at | block_number | sandwicher_address | frontrun_swap_transaction_hash | frontrun_swap_trace_address | backrun_swap_transaction_hash | backrun_swap_trace_address | profit_token_address | profit_amount | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 783a005e-b3fa-401c-9a38-bfe13e701346 | 2022-01-13 21:34:44.042851 | 12634570.0 | 0x00000000003b3cc22af3ae1eac0440bcee416b40 | 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... | [1] | 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... | [1] | NaN | NaN |
1 | 4b83d760-b597-457a-84a6-ca017a8ea444 | 2022-01-13 21:34:44.042851 | 12634575.0 | 0x000000000035b5e5ad9019092c665357240f594e | 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... | [1] | 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... | [1] | NaN | NaN |
2 | 9929faff-c29a-41ff-a08d-3e8868f1bcb3 | 2022-01-13 21:34:44.042851 | 12634579.0 | 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e | 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... | [1] | 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... | [1] | NaN | NaN |
3 | 28ade08a-f5f3-4554-91ad-d4a550bc14c7 | 2022-01-13 21:34:44.417610 | 12634558.0 | 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 | 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... | [1] | 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... | [1] | NaN | NaN |
4 | fccf0e9a-8745-43fa-801f-e3551e824ff0 | 2022-01-13 21:34:44.417610 | 12634558.0 | 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d | 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... | [2] | 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... | [4] | NaN | NaN |
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].frontrun_swap_transaction_hash}")
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].backrun_swap_transaction_hash}")
https://etherscan.io/tx/0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec42ddf7fc4f880bc3def16 https://etherscan.io/tx/0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6b2d34e39e0d1bf7c64b3
The front run transaction was to swap 87.0484 WETH for 416,650.9839 KEEP and the back run was to swap the 416,650.9839 KEEP back for 88.3490 WETH, for a gross profit of 1.2265 WETH.
Here we check the simple profit sandwich profit calculation for a sequence of 65,000 blocks from 2021-06-14 to 2021-06-24.
df_sandwich
id | created_at | block_number | sandwicher_address | frontrun_swap_transaction_hash | frontrun_swap_trace_address | backrun_swap_transaction_hash | backrun_swap_trace_address | profit_token_address | profit_amount | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 783a005e-b3fa-401c-9a38-bfe13e701346 | 2022-01-13 21:34:44.042851 | 12634570.0 | 0x00000000003b3cc22af3ae1eac0440bcee416b40 | 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... | [1] | 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... | [1] | NaN | NaN |
1 | 4b83d760-b597-457a-84a6-ca017a8ea444 | 2022-01-13 21:34:44.042851 | 12634575.0 | 0x000000000035b5e5ad9019092c665357240f594e | 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... | [1] | 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... | [1] | NaN | NaN |
2 | 9929faff-c29a-41ff-a08d-3e8868f1bcb3 | 2022-01-13 21:34:44.042851 | 12634579.0 | 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e | 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... | [1] | 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... | [1] | NaN | NaN |
3 | 28ade08a-f5f3-4554-91ad-d4a550bc14c7 | 2022-01-13 21:34:44.417610 | 12634558.0 | 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 | 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... | [1] | 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... | [1] | NaN | NaN |
4 | fccf0e9a-8745-43fa-801f-e3551e824ff0 | 2022-01-13 21:34:44.417610 | 12634558.0 | 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d | 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... | [2] | 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... | [4] | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
41834 | ef17d513-7eb3-4340-848f-23cce359a979 | 2022-01-18 00:21:31.684638 | 14025471.0 | 0x7cf09d7a9a74f746edcb06949b9d64bcd9d1604f | 0x65e94424bb54a540de66feb6aad26924b29c779ec603... | [1] | 0x6f2bf1772fe1dc9260a13ca30e09d353e5193df209f3... | [1] | NaN | 1.082515e+17 |
41835 | 3e90b067-a89b-4dfd-9d04-772fa3b33b0f | 2022-01-18 00:21:31.684638 | 14025475.0 | 0x499dd900f800fd0a2ed300006000a57f00fa009b | 0x7d4fda37099ac9a7692db3641a0dd117f3d58ae2b67e... | [0] | 0xb4f24da6198fe3bbd1ad468e7daa85e33bafa6de8e40... | [3] | NaN | 7.293653e+16 |
41836 | 8ef44968-5e19-46a9-abaf-559c241e2d5c | 2022-01-18 00:21:52.525554 | 14025481.0 | 0x000000005736775feb0c8568e7dee77222a26880 | 0x8e7055b6297d480c2541ad57d2e5d5d4080c2ecf8887... | [1] | 0x2c2a514a387a72f92a76f59755402f7e667c296205f8... | [1] | NaN | 1.436136e+17 |
41837 | 335fb14e-cabf-48b6-a4e9-87d69210d3ce | 2022-01-18 00:21:52.525554 | 14025482.0 | 0x00000000500e2fece27a7600435d0c48d64e0c00 | 0xbb63d3f7b69c1c231c6d2028309365f0aff4dcd4a18a... | [1] | 0xe817af00f3fd460ce2ac3ebb3f9307ec3a781fa89578... | [1] | NaN | 7.034793e+17 |
41838 | e02ffbd9-fcea-454f-8901-0d58ef05ddfc | 2022-01-18 00:24:20.741957 | 14025662.0 | 0x00000000500e2fece27a7600435d0c48d64e0c00 | 0x5fbf4018974e41c0a2ea0e7c2e842bb07b80b6724f7b... | [1] | 0xece083062760678b37910671f5cd65208d52f60d9758... | [1] | NaN | 5.807259e+16 |
41839 rows × 10 columns
There are duplicates in the swaps table. This is a pull of 10,000 and ~4,500 have at least one duplicate.
if False:
df_swap = pd.read_sql("SELECT * FROM swaps LIMIT 10000", conn)
df_swap.to_csv("data/swaps.csv", index=False)
df_swap = pd.read_csv("data/swaps.csv")
df_swap.columns
Index(['created_at', 'abi_name', 'transaction_hash', 'block_number', 'protocol', 'contract_address', 'from_address', 'to_address', 'token_in_address', 'token_in_amount', 'token_out_address', 'token_out_amount', 'trace_address', 'error', 'transaction_position'], dtype='object')
df_swap.transaction_hash.value_counts().hist()
q = """
SELECT
id,
frontrun_swap_transaction_hash,
backrun_swap_transaction_hash,
swap_front.transaction_hash front_transaction_hash,
swap_front.token_in_amount front_token_in_amount,
swap_front.token_out_amount front_token_out_amount,
swap_back.token_in_amount back_token_in_amount,
swap_back.token_out_amount back_token_out_amount
FROM sandwiches
LEFT JOIN swaps swap_front ON sandwiches.frontrun_swap_transaction_hash = swap_front.transaction_hash
LEFT JOIN swaps swap_back ON sandwiches.backrun_swap_transaction_hash = swap_back.transaction_hash
"""
if False:
df_profit = pd.read_sql(q, conn)
df_profit.to_csv("data/profit.csv", index=False)
else:
df_profit = pd.read_csv("data/profit.csv")
df_profit.id.value_counts().hist()
df_profit["profit_front_token_in"] = df_profit.back_token_out_amount - df_profit.front_token_in_amount
df_profit["profit_front_token_out"] = df_profit.back_token_in_amount - df_profit.front_token_out_amount
Filtering out duplicates as first pass to check profit calcs - need to figure out why dupes exist and how to properly filter out.
df_profit = df_profit.drop_duplicates("id")
df_profit["profit_bin_front_out"] = pd.qcut(df_profit.profit_front_token_out / 1e18, 50, duplicates="drop")
df_profit["profit_bin_front_in"] = pd.qcut(df_profit.profit_front_token_in / 1e18, 50, duplicates="drop")
In theory, the front token out should exactly equal the back token in, with the actual profit being the delta between the front token in and the back token out. Here we check to see how often the front token out profit deviates from zero.
df_profit_freq = df_profit.groupby("profit_bin_front_out").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token Out Profit Freq")
Just to check to see whether profit might be being taken on the other side, we check the profit on the front token in.
df_profit_freq = df_profit.groupby("profit_bin_front_in").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token In Profit Freq")
if DB_AVAIL:
df_sand_new = pd.read_sql("SELECT * FROM sandwiches WHERE block_number > 14026126", conn)
df_sand_new.to_csv("data/sandwiches_new.csv", index=False)
else:
df_sand_new = pd.read_csv("data/sandwiches_new.csv")
df_sand_new.columns
Index(['id', 'created_at', 'block_number', 'sandwicher_address', 'frontrun_swap_transaction_hash', 'frontrun_swap_trace_address', 'backrun_swap_transaction_hash', 'backrun_swap_trace_address', 'profit_token_address', 'profit_amount'], dtype='object')
df_sand_new
id | created_at | block_number | sandwicher_address | frontrun_swap_transaction_hash | frontrun_swap_trace_address | backrun_swap_transaction_hash | backrun_swap_trace_address | profit_token_address | profit_amount | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 81c6efeb-c064-4b1f-ae93-f4eaa42b9cfb | 2022-01-18 01:06:43.957480 | 14026161.0 | 0x7cf09d7a9a74f746edcb06949b9d64bcd9d1604f | 0x2ce8417bc91a24482f2bea344e7436aa62ed7391ddf5... | [0] | 0xb27b5715fbde07b034a9042a530a31e9ac013a185c2a... | [0] | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 | 3.488567e+17 |
1 | 9927470f-b185-4809-af1d-f1bf5dbd49dd | 2022-01-18 01:06:43.957480 | 14026163.0 | 0x000000000035b5e5ad9019092c665357240f594e | 0xe794c35ec49bbb823f891b6410b13c67c71b8a067f15... | [1] | 0x0a8595b5ba0998101dd0ce63bde19898ce31bc454b79... | [1] | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 | 2.607730e+16 |
2 | 5c6e0b74-6fcc-4dbd-a3cd-97925b72855b | 2022-01-18 01:06:43.957480 | 14026165.0 | 0x000000000035b5e5ad9019092c665357240f594e | 0x12a5aecd9f6f770d0c46ee87e2e14757c9c43c598ce2... | [1] | 0xaefd22c5b1384704b8c5cc90ea3f0a68952d984c05f3... | [1] | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 | 3.568630e+16 |
3 | e8724f12-1e78-4f63-9450-e1e936ab1cd6 | 2022-01-18 01:12:02.777742 | 14026225.0 | 0x01ff6318440f7d5553a82294d78262d5f5084eff | 0xf331484d2de0a2439db8f7f7e5b8b61a6d65b49180ce... | [0] | 0x1f3e95df8f7db63887a0371974b932eeeebb2b5dcbc1... | [0] | 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 | 1.635118e+17 |
df_sand_new.sort_values("profit_amount", ascending=False).plot(kind="bar", x="id", y="profit_amount", title="Profit Amount")
df_sand_new[df_sand_new.profit_amount < 10 * 1e18].groupby("profit_token_address").sum()["profit_amount"].plot(kind="bar", title="Profit Amount")