Skip to content

Commit

Permalink
[feat] sandwich maker
Browse files Browse the repository at this point in the history
  • Loading branch information
phureewat29 committed Sep 25, 2023
0 parents commit d77f515
Show file tree
Hide file tree
Showing 643 changed files with 131,357 additions and 0 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: ci

on: [push]

jobs:
tests:
name: Tests with Foundry
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install Huff
uses: huff-language/huff-toolchain@v2
with:
version: nightly

- name: Run Tests
run: forge test -vvv

scripts:
strategy:
matrix: { script: [build] }
fail-fast: true
name: Run Scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install Huff
uses: huff-language/huff-toolchain@v2
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
continue-on-error: true
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bot/target
bot/Cargo.lock
contract/out
contract/cache
.env
output.log
__TEMP__*
run-*.json
*.cfmms-checkpoint.json
21 changes: 21 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[submodule "contract/lib/forge-std"]
path = contract/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contract/lib/foundry-huff"]
path = contract/lib/foundry-huff
url = https://github.com/huff-language/foundry-huff
[submodule "contract/lib/v3-core"]
path = contract/lib/v3-core
url = https://github.com/Uniswap/v3-core
[submodule "contract/lib/v2-core"]
path = contract/lib/v2-core
url = https://github.com/Uniswap/v2-core
[submodule "contract/lib/v2-periphery"]
path = contract/lib/v2-periphery
url = https://github.com/Uniswap/v2-periphery
[submodule "contract/lib/solmate"]
path = contract/lib/solmate
url = https://github.com/transmissions11/solmate
[submodule "contract/lib/v3-periphery"]
path = contract/lib/v3-periphery
url = https://github.com/Uniswap/v3-periphery
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 phureewat29

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Sandwich Maker ![license](https://img.shields.io/badge/License-MIT-green.svg?label=license)

A sandwich making machine to perform V2/V3 and multi-meat sandwich attacks written using Rust and Huff.

## Brief Explanation
Anytime that a transaction interacts with a Uniswap V2/V3 pool and its forks, there is some slippage introduced (routers, aggregators, other MEV bots). Sandwich bots, like this one, are a toxic form of MEV as they profit off this slippage by frontrunning the transaction pushing the price of an asset up to the slippage limit, and then immediately selling the asset through a backrun transaction.

**Bot Logic Breakdown** can be found under [bot/README.md](https://github.com/phureewat29/sandwich-maker/tree/master/bot)

**Contract Logic Breakdown** can be found under [contract/README.md](https://github.com/phureewat29/sandwich-maker/tree/master/contract)

## Features
- **Fully Generalized**: Sandwich any tx that introduces slippage.
- **V2 and V3 Logic**: Logic to handle Uniswap V2/V3 pools.
- **Multi-Meat**: Build and send multi-meat sandwiches.
- **Gas Optimized**: Contract written in Huff using unconventional gas optimizations.
- **Local Simulations**: Fast concurrent EVM simulations to find sandwich opportunities.
- **Token Dust**: Stores dust at the end of every bundle for lower gas usage the next time the token is traded.
- **Salmonella Checks**: Detect if ERC20's transfer function uses any unusual opcodes that may produce different mainnet results.

## Notice
If any bugs or optimizations are found, feel free to create a pull request. **All pull requests are welcome!**

> **Warning**
>
> **This software is highly experimental and should be used at your own risk.** Although tested, this bot is experimental software and is provided on an "as is" and "as available" basis under the MIT license. We cannot guarantee the stability or reliability of this codebase and are not responsible for any damage or loss caused by its use. We do not give out warranties.
## Acknowledgments
- [subway](https://github.com/libevm/subway)
- [subway-rs](https://github.com/refcell/subway-rs)
- [cfmms-rs](https://github.com/0xKitsune/cfmms-rs)
- [revm](https://github.com/bluealloy/revm)
- [artemis](https://github.com/paradigmxyz/artemis)
- [huff-language](https://github.com/huff-language/huff-rs)
- [foundry](https://github.com/foundry-rs/foundry)
- [reth](https://github.com/paradigmxyz/reth)
- [ethers-rs](https://github.com/gakonst/ethers-rs)
- [ethers-flashbots](https://github.com/onbjerg/ethers-flashbots)
- [mev-template-rs](https://github.com/degatchi/mev-template-rs)
5 changes: 5 additions & 0 deletions bot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
WSS_RPC=ws://localhost:8545
SEARCHER_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001
FLASHBOTS_AUTH_KEY=0000000000000000000000000000000000000000000000000000000000000002
SANDWICH_CONTRACT=0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa
SANDWICH_INCEPTION_BLOCK=...
6 changes: 6 additions & 0 deletions bot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[workspace]
members = [
"crates/artemis-core",
"crates/strategy",
"sandwich-bin",
]
72 changes: 72 additions & 0 deletions bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Sandwich Maker/Bot ![license](https://img.shields.io/badge/License-MIT-green.svg?label=license)

Bot logic relies heavily on REVM simulations to detect sandwichable transactions. The simulations are done by injecting a modified router contract called [`LilRouter.sol`](https://github.com/phureewat29/sandwich-maker/blob/master/contract/src/LilRouter.sol) into a new EVM instance. Once injected, a concurrent binary search is performed to find an optimal input amount that results in the highest revenue. After sandwich calculations, the bot performs a [salmonella](https://github.com/Defi-Cartel/salmonella) check. If the sandwich is salmonella free, the bot then calculates gas bribes and sends the bundle to the fb relay.

Performing EVM simulations in this way allows the bot to detect sandwichable opportunities against any tx that introduces slippage.

## Logic Breakdown
- At startup, index all pools from a specific factory by parsing the `PairCreated` event. And fetch all token dust stored on addy.
- Read and decode tx from mempool.
- Send tx to [`trace_call`](https://openethereum.github.io/JSONRPC-trace-module#trace_call) to obtain `stateDiff`.
- Check if `statediff` contains keys that equal to indexed pool addresses.
- For each pool that tx touches:
- Find the optimal amount in for a sandwich attack by performing a concurrent binary search.
- Check for salmonella by checking if tx uses unconventional opcodes.
- If profitable after gas calculations, send the bundle to relays.

## Usage

1. This repo requires you to run an [Erigon](https://github.com/ledgerwatch/erigon) archive node. The bot relies on the `newPendingTransactionsWithBody` subscription rpc endpoint which is a Erigon specific method. The node needs to be synced in archive mode to index all pools.

2. [Install Rust](https://www.rust-lang.org/tools/install) if you haven't already.

3. Fill in the searcher address in Huff contract and deploy either straight onchain or via create2 using a [metamorphic](https://github.com/0age/metamorphic) like factory.
> If you are using create2, you can easily mine for an address containing 7 zero bytes, saving 84 gas of calldata every time the contract address is used as an argument. [read more](https://medium.com/coinmonks/deploy-an-efficient-address-contract-a-walkthrough-cb4be4ffbc70).
4. Copy `.env.example` into `.env` and fill out values.

```console
cp .env.example .env
```

```
WSS_RPC=ws://localhost:8545
SEARCHER_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001
FLASHBOTS_AUTH_KEY=0000000000000000000000000000000000000000000000000000000000000002
SANDWICH_CONTRACT=0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa
SANDWICH_INCEPTION_BLOCK=...
```

5. Run the integration tests

```console
cargo test -p strategy --release --features debug
```

6. Run the bot in `debug mode`
Test bot's sandwich finding functionality without a deployed or funded contract (no bundles will be sent)

```
cargo run --release --features debug
```

1. Running the bot

```console
cargo run --release
```
> **Warning**
>
> **By taking this codebase into production, you are doing so at your own risk under the MIT license.** I prefer this codebase to be used as a case study of what MEV could look like using Rust and Huff.
## Improvements

This repo explores only basic and simple multi V2 and V3 sandwiches, however, sandwiches come in many flavors and require some modifications to the codebase to capture them:

- Stable coin pair sandwiches.
- Sandwiches involving pairs that have a transfer limit, an [example](https://eigenphi.io/mev/ethereum/tx/0xe7c1e7d96e63d31f937af48b61d534e32ed9cfdbef066f45d49b967caeea8eed). Transfer limit can be found using a method similar to [Fej:Leuros's implementation](https://twitter.com/FejLeuros/status/1633379306750767106).
- Multi-meat sandwiches that target more than one pool. example: [frontrun](https://etherscan.io/tx/0xa39d28624f6d18a3bd5f5289a70fdc2779782f9a2e2c36dddd95cf882a15da45), [meat1](https://etherscan.io/tx/0xd027b771da68544279262439fd3f1cdef6a438ab6219b510c73c033b4e377296), [meat2](https://etherscan.io/tx/0x288da393cb7c937b8fe29ce0013992063d252372da869e31c6aad689f8b1aaf3), [backrun](https://etherscan.io/tx/0xcf22f2a3c9c67d56282e77e60c09929e0451336a9ed38f037fd484ea29e3cd41).
- Token -> Weth sandwiches by using a 'flashswap' between two pools. Normally we can only sandwich Weth -> Token swaps as the bot has Weth inventory, however you can use another pool's reserves as inventory to sandwich swaps in the other direction. [example](https://eigenphi.io/mev/ethereum/tx/0x502b66ce1a8b71098decc3585c651745c1af55de19e8f29ec6fff4ed2fcd1589).
- Longtail sandwiches to target TOKEN->(WETH or STABLE) swaps by pre buying and holding token.
- Sandwiches that include a user's token approval tx + swap tx in one bundle.
- Sandwiches that include a user's pending tx/s + swap tx in one bundle if swap tx nonce is higher than pending tx.
23 changes: 23 additions & 0 deletions bot/crates/artemis-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "artemis-core"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

## eth
ethers = { version = "2", features = ["ws", "rustls"]}
ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots", features = ["rustls"] }

## async
async-trait = "0.1.64"
reqwest = { version = "0.11.14", default-features = false, features = ["rustls-tls"] }
tokio = { version = "1.18", features = ["full"] }
tokio-stream = { version = "0.1", features = ['sync'] }

## misc
anyhow = "1.0.70"
thiserror = "1.0.40"
tracing = "0.1.37"
57 changes: 57 additions & 0 deletions bot/crates/artemis-core/src/collectors/block_collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::types::{Collector, CollectorStream};
use anyhow::Result;
use async_trait::async_trait;
use ethers::{
prelude::Middleware,
providers::PubsubClient,
types::{U256, U64},
};
use std::sync::Arc;
use tokio_stream::StreamExt;

/// A collector that listens for new blocks, and generates a stream of
/// [events](NewBlock) which contain the block number and hash.
pub struct BlockCollector<M> {
provider: Arc<M>,
}

/// A new block event, containing the block number and hash.
#[derive(Debug, Clone)]
pub struct NewBlock {
pub number: U64,
pub gas_used: U256,
pub gas_limit: U256,
pub base_fee_per_gas: U256,
pub timestamp: U256,
}

impl<M> BlockCollector<M> {
pub fn new(provider: Arc<M>) -> Self {
Self { provider }
}
}

/// Implementation of the [Collector](Collector) trait for the [BlockCollector](BlockCollector).
/// This implementation uses the [PubsubClient](PubsubClient) to subscribe to new blocks.
#[async_trait]
impl<M> Collector<NewBlock> for BlockCollector<M>
where
M: Middleware,
M::Provider: PubsubClient,
M::Error: 'static,
{
async fn get_event_stream(&self) -> Result<CollectorStream<'_, NewBlock>> {
let stream = self.provider.subscribe_blocks().await?;
let stream = stream.filter_map(|block| match block.number {
Some(number) => Some(NewBlock {
number,
gas_limit: block.gas_limit,
gas_used: block.gas_used,
base_fee_per_gas: block.base_fee_per_gas.unwrap_or_default(),
timestamp: block.timestamp,
}),
None => None,
});
Ok(Box::pin(stream))
}
}
38 changes: 38 additions & 0 deletions bot/crates/artemis-core/src/collectors/mempool_collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use async_trait::async_trait;

use ethers::{prelude::Middleware, providers::PubsubClient, types::Transaction};
use std::sync::Arc;

use crate::types::{Collector, CollectorStream};
use anyhow::Result;

/// A collector that listens for new transactions in the mempool, and generates a stream of
/// [events](Transaction) which contain the transaction.
pub struct MempoolCollector<M> {
provider: Arc<M>,
}

impl<M> MempoolCollector<M> {
pub fn new(provider: Arc<M>) -> Self {
Self { provider }
}
}

/// Implementation of the [Collector](Collector) trait for the [MempoolCollector](MempoolCollector).
/// This implementation uses the [PubsubClient](PubsubClient) to subscribe to new transactions.
#[async_trait]
impl<M> Collector<Transaction> for MempoolCollector<M>
where
M: Middleware,
M::Provider: PubsubClient,
M::Error: 'static,
{
async fn get_event_stream(&self) -> Result<CollectorStream<'_, Transaction>> {
let stream = self
.provider
.subscribe(["newPendingTransactionsWithBody"])
.await
.map_err(|_| anyhow::anyhow!("Failed to create mempool stream"))?;
Ok(Box::pin(stream))
}
}
9 changes: 9 additions & 0 deletions bot/crates/artemis-core/src/collectors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Collectors are responsible for collecting data from external sources and
//! turning them into internal events. For example, a collector might listen to
//! a stream of new blocks, and turn them into a stream of `NewBlock` events.

/// This collector listens to a stream of new blocks.
pub mod block_collector;

/// This collector listens to a stream of new pending transactions.
pub mod mempool_collector;
Loading

0 comments on commit d77f515

Please sign in to comment.