├── bot ├── sando-bin │ ├── src │ │ ├── lib.rs │ │ ├── config.rs │ │ ├── initialization.rs │ │ └── main.rs │ └── Cargo.toml ├── crates │ ├── strategy │ │ ├── src │ │ │ ├── tx_utils │ │ │ │ ├── huff_sando_interface │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── common │ │ │ │ │ │ ├── weth_encoder.rs │ │ │ │ │ │ ├── mod.rs │ │ │ │ │ │ └── five_byte_encoder.rs │ │ │ │ │ ├── v2.rs │ │ │ │ │ └── v3.rs │ │ │ │ ├── mod.rs │ │ │ │ └── lil_router_interface.rs │ │ │ ├── managers │ │ │ │ ├── mod.rs │ │ │ │ ├── block_manager.rs │ │ │ │ ├── sando_state_manager.rs │ │ │ │ └── pool_manager.rs │ │ │ ├── lib.rs │ │ │ ├── simulator │ │ │ │ ├── mod.rs │ │ │ │ ├── salmonella_inspector.rs │ │ │ │ └── lil_router.rs │ │ │ ├── abi │ │ │ │ ├── mod.rs │ │ │ │ ├── IUniswapV2Factory.abi │ │ │ │ ├── IUniswapV3Factory.abi │ │ │ │ ├── IERC20.abi │ │ │ │ ├── IUniswapV2Pair.abi │ │ │ │ └── IUniswapV2Router.abi │ │ │ ├── helpers.rs │ │ │ ├── constants.rs │ │ │ ├── bot │ │ │ │ └── mod.rs │ │ │ └── types.rs │ │ ├── Cargo.toml │ │ └── tests │ │ │ └── main.rs │ └── artemis-core │ │ ├── src │ │ ├── utilities │ │ │ ├── mod.rs │ │ │ └── state_override_middleware.rs │ │ ├── executors │ │ │ ├── mod.rs │ │ │ └── flashbots_executor.rs │ │ ├── collectors │ │ │ ├── mod.rs │ │ │ ├── mempool_collector.rs │ │ │ └── block_collector.rs │ │ ├── lib.rs │ │ ├── types.rs │ │ └── engine.rs │ │ ├── Cargo.toml │ │ └── tests │ │ └── main.rs ├── Cargo.toml ├── .env.example └── README.md ├── contract ├── .solhint.json ├── foundry.toml ├── LICENSE ├── test │ ├── misc │ │ ├── GeneralHelper.sol │ │ ├── V3SandoUtility.sol │ │ ├── V2SandoUtility.sol │ │ └── SandoCommon.sol │ ├── LilRouter.t.sol │ └── Sando.t.sol ├── README.md └── src │ └── LilRouter.sol ├── .gitignore ├── .gitmodules ├── LICENSE ├── .github └── workflows │ └── ci.yaml └── README.md /bot/sando-bin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod initialization; 3 | -------------------------------------------------------------------------------- /contract/.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } 4 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod v2; 3 | pub mod v3; 4 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod huff_sando_interface; 2 | pub(crate) mod lil_router_interface; 3 | -------------------------------------------------------------------------------- /bot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/artemis-core", 4 | "crates/strategy", 5 | "sando-bin" 6 | ] 7 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/managers/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod block_manager; 2 | pub(crate) mod pool_manager; 3 | pub(crate) mod sando_state_manager; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bot/target 2 | bot/Cargo.lock 3 | contract/out 4 | contract/cache 5 | .env 6 | output.log 7 | __TEMP__* 8 | run-*.json 9 | *.cfmms-checkpoint.json 10 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/utilities/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for working with Artemis. 2 | 3 | /// This module implements state overriding middleware. 4 | pub mod state_override_middleware; 5 | -------------------------------------------------------------------------------- /bot/.env.example: -------------------------------------------------------------------------------- 1 | WSS_RPC=ws://localhost:8545 2 | SEARCHER_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001 3 | FLASHBOTS_AUTH_KEY=0000000000000000000000000000000000000000000000000000000000000002 4 | SANDWICH_CONTRACT=0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa 5 | SANDWICH_INCEPTION_BLOCK=... 6 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/executors/mod.rs: -------------------------------------------------------------------------------- 1 | //! Executors are responsible for taking actions produced by strategies and 2 | //! executing them in different domains. For example, an executor might take a 3 | //! `SubmitTx` action and submit it to the mempool. 4 | 5 | /// This executor submits transactions to the flashbots relay. 6 | pub mod flashbots_executor; 7 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod abi; 2 | mod constants; 3 | mod helpers; 4 | mod simulator; 5 | 6 | /// Module contains logic to manage info on onchain pools 7 | mod managers; 8 | 9 | /// Module contains logic related to transaction building 10 | mod tx_utils; 11 | 12 | /// Module contains core strategy implementation 13 | pub mod bot; 14 | 15 | /// Module contains the core type defenitions for sandwiching 16 | pub mod types; 17 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/collectors/mod.rs: -------------------------------------------------------------------------------- 1 | //! Collectors are responsible for collecting data from external sources and 2 | //! turning them into internal events. For example, a collector might listen to 3 | //! a stream of new blocks, and turn them into a stream of `NewBlock` events. 4 | 5 | /// This collector listens to a stream of new blocks. 6 | pub mod block_collector; 7 | 8 | /// This collector listens to a stream of new pending transactions. 9 | pub mod mempool_collector; 10 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/common/weth_encoder.rs: -------------------------------------------------------------------------------- 1 | use ethers::{prelude::Lazy, types::U256}; 2 | 3 | /// Constant used for encoding WETH amount 4 | pub static WETH_ENCODING_MULTIPLE: Lazy = Lazy::new(|| U256::from(100000)); 5 | 6 | pub struct WethEncoder {} 7 | 8 | impl WethEncoder { 9 | /// Encodes a weth value to be passed to the contract through `tx.value` 10 | pub fn encode(value: U256) -> U256 { 11 | value / *WETH_ENCODING_MULTIPLE 12 | } 13 | 14 | /// Decodes by multiplying amount by weth constant 15 | pub fn decode(value: U256) -> U256 { 16 | value * *WETH_ENCODING_MULTIPLE 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "artemis-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | ## eth 11 | ethers = { version = "2", features = ["ws", "rustls"]} 12 | ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots", features = ["rustls"] } 13 | 14 | ## async 15 | async-trait = "0.1.64" 16 | reqwest = { version = "0.11.14", default-features = false, features = ["rustls-tls"] } 17 | tokio = { version = "1.18", features = ["full"] } 18 | tokio-stream = { version = "0.1", features = ['sync'] } 19 | 20 | ## misc 21 | anyhow = "1.0.70" 22 | thiserror = "1.0.40" 23 | tracing = "0.1.37" 24 | -------------------------------------------------------------------------------- /contract/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc_version = '0.8.15' 3 | auto_detect_solc = false 4 | optimizer = true 5 | optimizer_runs = 200 # Default amount 6 | ffi = true 7 | fuzz_runs = 1_000 8 | remappings = [ 9 | "forge-std=lib/forge-std/src/", 10 | "foundry-huff=lib/foundry-huff/src/", 11 | "ds-test/=lib/forge-std/lib/ds-test/src/", 12 | "forge-std/=lib/forge-std/src/", 13 | "foundry-huff/=lib/foundry-huff/src/", 14 | "stringutils/=lib/foundry-huff/lib/solidity-stringutils/", 15 | "v2-core/=lib/v2-core/contracts/", 16 | "v2-periphery/=lib/v2-periphery/contracts/", 17 | "v3-core/=lib/v3-core/contracts/", 18 | "v3-periphery/=lib/v3-periphery/contracts/", 19 | "solmate=lib/solmate/src/", 20 | ] 21 | eth-rpc-url = "http://localhost:8545" 22 | evm_version = "shanghai" 23 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/simulator/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod huff_sando; 2 | pub(crate) mod lil_router; 3 | pub(crate) mod salmonella_inspector; 4 | 5 | use foundry_evm::{ 6 | executor::fork::SharedBackend, 7 | revm::{db::CacheDB, primitives::U256 as rU256, EVM}, 8 | }; 9 | 10 | use crate::{ 11 | constants::{COINBASE, ONE_ETHER_IN_WEI}, 12 | types::BlockInfo, 13 | }; 14 | 15 | fn setup_block_state(evm: &mut EVM>, next_block: &BlockInfo) { 16 | evm.env.block.number = rU256::from(next_block.number.as_u64()); 17 | evm.env.block.timestamp = next_block.timestamp.into(); 18 | evm.env.block.basefee = next_block.base_fee_per_gas.into(); 19 | // use something other than default 20 | evm.env.block.coinbase = *COINBASE; 21 | } 22 | 23 | pub fn eth_to_wei(amt: u128) -> rU256 { 24 | rU256::from(amt).checked_mul(*ONE_ETHER_IN_WEI).unwrap() 25 | } 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contract/lib/forge-std"] 2 | path = contract/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "contract/lib/foundry-huff"] 5 | path = contract/lib/foundry-huff 6 | url = https://github.com/huff-language/foundry-huff 7 | [submodule "contract/lib/v3-core"] 8 | path = contract/lib/v3-core 9 | url = https://github.com/Uniswap/v3-core 10 | [submodule "contract/lib/v2-core"] 11 | path = contract/lib/v2-core 12 | url = https://github.com/Uniswap/v2-core 13 | [submodule "contract/lib/v2-periphery"] 14 | path = contract/lib/v2-periphery 15 | url = https://github.com/Uniswap/v2-periphery 16 | [submodule "contract/lib/solmate"] 17 | path = contract/lib/solmate 18 | url = https://github.com/transmissions11/solmate 19 | [submodule "contract/lib/v3-periphery"] 20 | path = contract/lib/v3-periphery 21 | url = https://github.com/Uniswap/v3-periphery 22 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/mod.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::abigen; 2 | 3 | abigen!( 4 | UniswapV2Factory, 5 | "src/abi/IUniswapV2Factory.abi", 6 | event_derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | abigen!( 9 | UniswapV3Factory, 10 | "src/abi/IUniswapV3Factory.abi", 11 | event_derives(serde::Deserialize, serde::Serialize) 12 | ); 13 | abigen!( 14 | UniswapV3Pool, 15 | "src/abi/IUniswapV3Pool.abi", 16 | event_derives(serde::Deserialize, serde::Serialize) 17 | ); 18 | abigen!( 19 | UniswapV2Pair, 20 | "src/abi/IUniswapV2Pair.abi", 21 | event_derives(serde::Deserialize, serde::Serialize) 22 | ); 23 | abigen!( 24 | UniswapV2Router, 25 | "src/abi/IUniswapV2Router.abi", 26 | event_derives(serde::Deserialize, serde::Serialize) 27 | ); 28 | abigen!( 29 | Erc20, 30 | "src/abi/IERC20.abi", 31 | event_derives(serde::Deserialize, serde::Serialize) 32 | ); 33 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/common/mod.rs: -------------------------------------------------------------------------------- 1 | /// This Module file holds common methods used for both v2 and v3 methods 2 | 3 | /// Utils to encode (and decode) 32 bytes to 5 bytes of calldata 4 | pub mod five_byte_encoder; 5 | 6 | /// Utils to encode (and decode) weth to `tx.value` 7 | pub mod weth_encoder; 8 | 9 | // Declare the array as static 10 | static FUNCTION_NAMES: [&str; 8] = [ 11 | "v2_backrun0", 12 | "v2_frontrun0", 13 | "v2_backrun1", 14 | "v2_frontrun1", 15 | "v3_backrun0", 16 | "v3_frontrun0", 17 | "v3_backrun1", 18 | "v3_frontrun1", 19 | ]; 20 | 21 | pub fn get_jump_dest_from_sig(function_name: &str) -> u8 { 22 | let starting_index = 0x05; 23 | 24 | // find index of associated JUMPDEST (sig) 25 | for (i, &name) in FUNCTION_NAMES.iter().enumerate() { 26 | if name == function_name { 27 | return (i as u8 * 5) + starting_index; 28 | } 29 | } 30 | 31 | // not found (force jump to invalid JUMPDEST) 32 | 0x00 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 0xmouseless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot/crates/strategy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strategy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # Misc 8 | hashbrown = "0.14.0" 9 | tokio = { version = "1.29.0", features = ["full"] } 10 | dashmap = "5.4.0" 11 | thiserror = "1.0.37" 12 | futures = "0.3.5" 13 | async-trait = "0.1.64" 14 | anyhow = "1.0.70" 15 | serde = "1.0.145" 16 | 17 | # EVM based crates 18 | cfmms = { git = "https://github.com/mouseless-eth/cfmms-rs.git", branch = "fix-serialize-dex-fee"} 19 | ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots"} 20 | ethers = {version = "2.0.7", features = ["abigen", "ws"]} 21 | foundry-evm = { git = "https://github.com/mouseless-eth/foundry.git", branch = "ethers-version-change" } 22 | anvil = { git = "https://github.com/mouseless-eth/foundry.git", branch = "ethers-version-change" } 23 | eth-encode-packed = "0.1.0" 24 | 25 | # Logging 26 | colored = "2.0.0" 27 | log = "0.4.17" 28 | indicatif = "0.17.5" 29 | 30 | # Artemis 31 | artemis-core = { path = "../artemis-core" } 32 | 33 | [dev-dependencies] 34 | fern = {version = "0.6.2", features = ["colored"]} 35 | 36 | [features] 37 | debug = [] 38 | -------------------------------------------------------------------------------- /contract/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 0xmouseless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot/sando-bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-sando" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Optimized sandwich bot written using Rust and Huff" 7 | readme = "README.md" 8 | homepage = "https://github.com/mouseless-eth/rusty-sando" 9 | repository = "https://github.com/mouseless-eth/rusty-sando" 10 | keywords = ["Ethereum", "Mev", "Dex", "Sandwich"] 11 | authors = ["0xmouseless "] 12 | 13 | [dependencies] 14 | dotenv = "0.15.0" 15 | hashbrown = "0.14.0" 16 | tokio = { version = "1.29.0", features = ["full"] } 17 | log = "0.4.17" 18 | url = "2.3.1" 19 | dashmap = "5.4.0" 20 | hex = "0.4.3" 21 | serde = "1.0.145" 22 | anyhow = "1.0.71" 23 | reqwest = "0.11.12" 24 | thiserror = "1.0.37" 25 | futures = "0.3.5" 26 | 27 | # EVM based crates 28 | cfmms = "0.6.2" 29 | ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots" } 30 | ethers = {version = "2.0.7", features = ["abigen", "ws"]} 31 | revm = "3.3.0" 32 | 33 | # logging 34 | indoc = "2" 35 | fern = {version = "0.6.2", features = ["colored"]} 36 | chrono = "0.4.23" 37 | colored = "2.0.0" 38 | 39 | # artemis related 40 | artemis-core = { path = "../crates/artemis-core"} 41 | strategy = { path = "../crates/strategy" } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | tests: 7 | name: Tests with Foundry 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | submodules: recursive 14 | 15 | - name: Install Foundry 16 | uses: foundry-rs/foundry-toolchain@v1 17 | with: 18 | version: nightly 19 | 20 | - name: Install Huff 21 | uses: huff-language/huff-toolchain@v2 22 | with: 23 | version: nightly 24 | 25 | - name: Run Tests 26 | run: forge test -vvv 27 | 28 | scripts: 29 | strategy: 30 | fail-fast: true 31 | name: Run Scripts 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | with: 36 | submodules: recursive 37 | 38 | - name: Install Foundry 39 | uses: foundry-rs/foundry-toolchain@v1 40 | with: 41 | version: nightly 42 | 43 | - name: Install Huff 44 | uses: huff-language/huff-toolchain@v2 45 | with: 46 | version: nightly 47 | 48 | - name: Run Forge build 49 | run: | 50 | forge --version 51 | forge build --sizes 52 | id: build 53 | continue-on-error: true 54 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/collectors/mempool_collector.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use ethers::{prelude::Middleware, providers::PubsubClient, types::Transaction}; 4 | use std::sync::Arc; 5 | 6 | use crate::types::{Collector, CollectorStream}; 7 | use anyhow::Result; 8 | 9 | /// A collector that listens for new transactions in the mempool, and generates a stream of 10 | /// [events](Transaction) which contain the transaction. 11 | pub struct MempoolCollector { 12 | provider: Arc, 13 | } 14 | 15 | impl MempoolCollector { 16 | pub fn new(provider: Arc) -> Self { 17 | Self { provider } 18 | } 19 | } 20 | 21 | /// Implementation of the [Collector](Collector) trait for the [MempoolCollector](MempoolCollector). 22 | /// This implementation uses the [PubsubClient](PubsubClient) to subscribe to new transactions. 23 | #[async_trait] 24 | impl Collector for MempoolCollector 25 | where 26 | M: Middleware, 27 | M::Provider: PubsubClient, 28 | M::Error: 'static, 29 | { 30 | async fn get_event_stream(&self) -> Result> { 31 | let stream = self 32 | .provider 33 | .subscribe(["newPendingTransactionsWithBody"]) 34 | .await 35 | .map_err(|_| anyhow::anyhow!("Failed to create mempool stream"))?; 36 | Ok(Box::pin(stream)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/managers/block_manager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ethers::{providers::Middleware, types::BlockNumber}; 3 | use log::info; 4 | use std::sync::Arc; 5 | 6 | use colored::Colorize; 7 | 8 | use crate::{startup_info_log, types::BlockInfo}; 9 | 10 | pub struct BlockManager { 11 | latest_block: BlockInfo, 12 | next_block: BlockInfo, 13 | } 14 | 15 | impl BlockManager { 16 | pub fn new() -> Self { 17 | Self { 18 | latest_block: BlockInfo::default(), 19 | next_block: BlockInfo::default(), 20 | } 21 | } 22 | 23 | pub async fn setup(&mut self, provider: Arc) -> Result<()> { 24 | let latest_block = provider 25 | .get_block(BlockNumber::Latest) 26 | .await 27 | .map_err(|_| anyhow!("Failed to get current block"))? 28 | .ok_or(anyhow!("Failed to get current block"))?; 29 | 30 | let latest_block: BlockInfo = latest_block.try_into()?; 31 | self.update_block_info(latest_block); 32 | 33 | startup_info_log!("latest block synced: {}", latest_block.number); 34 | Ok(()) 35 | } 36 | 37 | /// Return info for the next block 38 | pub fn get_next_block(&self) -> BlockInfo { 39 | self.next_block 40 | } 41 | 42 | /// Return info for the next block 43 | pub fn get_latest_block(&self) -> BlockInfo { 44 | self.latest_block 45 | } 46 | 47 | /// Updates internal state with the latest mined block and next block 48 | pub fn update_block_info>(&mut self, latest_block: T) { 49 | let latest_block: BlockInfo = latest_block.into(); 50 | self.latest_block = latest_block; 51 | self.next_block = latest_block.get_next_block(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/collectors/block_collector.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Collector, CollectorStream}; 2 | use anyhow::Result; 3 | use async_trait::async_trait; 4 | use ethers::{ 5 | prelude::Middleware, 6 | providers::PubsubClient, 7 | types::{U256, U64}, 8 | }; 9 | use std::sync::Arc; 10 | use tokio_stream::StreamExt; 11 | 12 | /// A collector that listens for new blocks, and generates a stream of 13 | /// [events](NewBlock) which contain the block number and hash. 14 | pub struct BlockCollector { 15 | provider: Arc, 16 | } 17 | 18 | /// A new block event, containing the block number and hash. 19 | #[derive(Debug, Clone)] 20 | pub struct NewBlock { 21 | pub number: U64, 22 | pub gas_used: U256, 23 | pub gas_limit: U256, 24 | pub base_fee_per_gas: U256, 25 | pub timestamp: U256, 26 | } 27 | 28 | impl BlockCollector { 29 | pub fn new(provider: Arc) -> Self { 30 | Self { provider } 31 | } 32 | } 33 | 34 | /// Implementation of the [Collector](Collector) trait for the [BlockCollector](BlockCollector). 35 | /// This implementation uses the [PubsubClient](PubsubClient) to subscribe to new blocks. 36 | #[async_trait] 37 | impl Collector for BlockCollector 38 | where 39 | M: Middleware, 40 | M::Provider: PubsubClient, 41 | M::Error: 'static, 42 | { 43 | async fn get_event_stream(&self) -> Result> { 44 | let stream = self.provider.subscribe_blocks().await?; 45 | let stream = stream.filter_map(|block| match block.number { 46 | Some(number) => Some(NewBlock { 47 | number, 48 | gas_limit: block.gas_limit, 49 | gas_used: block.gas_used, 50 | base_fee_per_gas: block.base_fee_per_gas.unwrap_or_default(), 51 | timestamp: block.timestamp, 52 | }), 53 | None => None, 54 | }); 55 | Ok(Box::pin(stream)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(unused_crate_dependencies)] 2 | #![deny(unused_must_use, rust_2018_idioms)] 3 | #![doc(test( 4 | no_crate_inject, 5 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 6 | ))] 7 | 8 | //! A library for writing MEV bots, designed to be simple, modular, and fast. 9 | //! 10 | //! At its core, Artemis is architected as an event processing pipeline. The 11 | //! library is made up of three main components: 12 | //! 13 | //! 1. [Collectors](types::Collector): *Collectors* take in external events (such as pending txs, 14 | //! new blocks, marketplace orders, etc. ) and turn them into an internal 15 | //! *event* representation. 16 | //! 17 | //! 2. [Strategies](types::Strategy): *Strategies* contain the core logic required for each MEV 18 | //! opportunity. They take in *events* as inputs, and compute whether any 19 | //! opportunities are available (for example, a strategy might listen to a stream 20 | //! of marketplace orders to see if there are any cross-exchange arbs). *Strategies* 21 | //! produce *actions*. 22 | //! 23 | //! 3. [Executors](types::Executor): *Executors* process *actions*, and are responsible for executing 24 | //! them in different domains (for example, submitting txs, posting off-chain orders, etc.). 25 | //! 26 | //! These components are tied together by the [Engine](engine::Engine), which is responsible for 27 | //! orchestrating the flow of data between them. 28 | 29 | /// This module contains [collector](types::Collector) implementations. 30 | pub mod collectors; 31 | /// This module contains the [Engine](engine::Engine) struct, which is responsible 32 | /// for orchestrating data flows between components 33 | pub mod engine; 34 | /// This module contains [executor](types::Executor) implementations. 35 | pub mod executors; 36 | /// This module contains the core type definitions for Artemis. 37 | pub mod types; 38 | /// This module contains utilities for working with Artemis. 39 | pub mod utilities; 40 | -------------------------------------------------------------------------------- /bot/sando-bin/src/config.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use reqwest::Url; 3 | use std::{env, str::FromStr}; 4 | 5 | use anyhow::{anyhow, Result}; 6 | use ethers::{ 7 | signers::LocalWallet, 8 | types::{Address, U64}, 9 | }; 10 | 11 | pub struct Config { 12 | pub searcher_signer: LocalWallet, 13 | pub sando_inception_block: U64, 14 | pub sando_address: Address, 15 | pub bundle_signer: LocalWallet, 16 | pub wss_rpc: Url, 17 | pub discord_webhook: String, 18 | } 19 | 20 | impl Config { 21 | pub async fn read_from_dotenv() -> Result { 22 | dotenv().ok(); 23 | 24 | let get_env = |var| { 25 | env::var(var).map_err(|_| anyhow!("Required environment variable \"{}\" not set", var)) 26 | }; 27 | 28 | let searcher_signer = get_env("SEARCHER_PRIVATE_KEY")? 29 | .parse::() 30 | .map_err(|_| anyhow!("Failed to parse \"SEARCHER_PRIVATE_KEY\""))?; 31 | 32 | let sando_inception_block = get_env("SANDWICH_INCEPTION_BLOCK")? 33 | .parse::() 34 | .map(U64::from) 35 | .map_err(|_| anyhow!("Failed to parse \"SANDWICH_INCEPTION_BLOCK\" into u64"))?; 36 | 37 | let sando_address = Address::from_str(&get_env("SANDWICH_CONTRACT")?) 38 | .map_err(|_| anyhow!("Failed to parse \"SANDWICH_CONTRACT\""))?; 39 | 40 | let bundle_signer = get_env("FLASHBOTS_AUTH_KEY")? 41 | .parse::() 42 | .map_err(|_| anyhow!("Failed to parse \"FLASHBOTS_AUTH_KEY\""))?; 43 | 44 | let wss_rpc = get_env("WSS_RPC")? 45 | .parse() 46 | .map_err(|_| anyhow!("Failed to parse \"WSS_RPC\""))?; 47 | 48 | let discord_webhook = get_env("DISCORD_WEBHOOK")?; 49 | 50 | Ok(Self { 51 | searcher_signer, 52 | sando_inception_block, 53 | sando_address, 54 | bundle_signer, 55 | wss_rpc, 56 | discord_webhook, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/executors/flashbots_executor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use ethers::{providers::Middleware, signers::Signer}; 6 | use ethers_flashbots::{BundleRequest, FlashbotsMiddleware}; 7 | use reqwest::Url; 8 | use tracing::{error, info}; 9 | 10 | use crate::types::Executor; 11 | 12 | /// A Flashbots executor that sends transactions to the Flashbots relay. 13 | pub struct FlashbotsExecutor { 14 | /// The Flashbots middleware. 15 | fb_client: FlashbotsMiddleware, S>, 16 | } 17 | 18 | /// A bundle of transactions to send to the Flashbots relay. 19 | /// Sending vec of bundle request because multiple actions per event not supported 20 | /// See issue: https://github.com/paradigmxyz/artemis/issues/34 21 | pub type FlashbotsBundle = Vec; 22 | 23 | impl FlashbotsExecutor { 24 | pub fn new(client: Arc, relay_signer: S, relay_url: impl Into) -> Self { 25 | let fb_client = FlashbotsMiddleware::new(client, relay_url, relay_signer); 26 | Self { fb_client } 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl Executor for FlashbotsExecutor 32 | where 33 | M: Middleware + 'static, 34 | M::Error: 'static, 35 | S: Signer + 'static, 36 | { 37 | /// Send a bundle to transactions to the Flashbots relay. 38 | async fn execute(&self, action: FlashbotsBundle) -> Result<()> { 39 | for bundle in action { 40 | //// Simulate bundle. 41 | //let simulated_bundle = self.fb_client.simulate_bundle(&bundle).await; 42 | 43 | //match simulated_bundle { 44 | // Ok(res) => info!("Simulation Result: {:?}", res), 45 | // Err(simulate_error) => error!("Error simulating bundle: {:?}", simulate_error), 46 | //} 47 | 48 | // Send bundle. 49 | let pending_bundle = self.fb_client.send_bundle(&bundle).await; 50 | 51 | match pending_bundle { 52 | Ok(res) => info!("Simulation Result: {:?}", res.await), 53 | Err(send_error) => error!("Error sending bundle: {:?}", send_error), 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bot/sando-bin/src/initialization.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use colored::Colorize; 3 | use fern::colors::{Color, ColoredLevelConfig}; 4 | use indoc::indoc; 5 | use log::LevelFilter; 6 | 7 | pub fn print_banner() { 8 | let banner = indoc! { 9 | r#" 10 | 11 | _____ ___ _ _ ______ _____ ______ _____ 12 | / ___| / _ \ | \ | || _ \| _ | | ___ \/ ___| 13 | \ `--. / /_\ \| \| || | | || | | | ______ | |_/ /\ `--. 14 | `--. \| _ || . ` || | | || | | ||______|| / `--. \ 15 | /\__/ /| | | || |\ || |/ / \ \_/ / | |\ \ /\__/ / 16 | \____/ \_| |_/\_| \_/|___/ \___/ \_| \_|\____/ 17 | 18 | ______ __ __ _____ ___ ___ _____ _ _ _____ _____ _ _____ _____ _____ 19 | | ___ \\ \ / / _ | _ | | \/ || _ || | | |/ ___|| ___|| | | ___|/ ___|/ ___| 20 | | |_/ / \ V / (_) | |/' |__ __| . . || | | || | | |\ `--. | |__ | | | |__ \ `--. \ `--. 21 | | ___ \ \ / | /| |\ \/ /| |\/| || | | || | | | `--. \| __| | | | __| `--. \ `--. \ 22 | | |_/ / | | _ \ |_/ / > < | | | |\ \_/ /| |_| |/\__/ /| |___ | |____| |___ /\__/ //\__/ / 23 | \____/ \_/ (_) \___/ /_/\_\\_| |_/ \___/ \___/ \____/ \____/ \_____/\____/ \____/ \____/ 24 | "#}; 25 | 26 | log::info!("{}", format!("{}", banner.green().bold())); 27 | } 28 | 29 | pub fn setup_logger() -> Result<()> { 30 | let colors = ColoredLevelConfig { 31 | trace: Color::Cyan, 32 | debug: Color::Magenta, 33 | info: Color::Green, 34 | warn: Color::Red, 35 | error: Color::BrightRed, 36 | ..ColoredLevelConfig::new() 37 | }; 38 | 39 | fern::Dispatch::new() 40 | .format(move |out, message, record| { 41 | out.finish(format_args!( 42 | "{}[{}] {}", 43 | chrono::Local::now().format("[%H:%M:%S]"), 44 | colors.color(record.level()), 45 | message 46 | )) 47 | }) 48 | .chain(std::io::stdout()) 49 | .chain(fern::log_file("output.log")?) 50 | .level(log::LevelFilter::Error) 51 | .level_for("rusty_sando", LevelFilter::Info) 52 | .level_for("strategy", LevelFilter::Info) 53 | .level_for("artemis_core", LevelFilter::Info) 54 | .apply()?; 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/v2.rs: -------------------------------------------------------------------------------- 1 | use cfmms::pool::UniswapV2Pool; 2 | use eth_encode_packed::{SolidityDataType, TakeLastXBytes}; 3 | use ethers::types::{Address, U256}; 4 | 5 | use crate::constants::WETH_ADDRESS; 6 | 7 | use super::common::{ 8 | five_byte_encoder::FiveByteMetaData, get_jump_dest_from_sig, weth_encoder::WethEncoder, 9 | }; 10 | 11 | pub fn v2_create_frontrun_payload( 12 | pool: UniswapV2Pool, 13 | output_token: Address, 14 | amount_in: U256, 15 | amount_out: U256, // amount_out is needed to be passed due to taxed tokens 16 | ) -> (Vec, U256) { 17 | let jump_dest = get_jump_dest_from_sig(if *WETH_ADDRESS < output_token { 18 | "v2_frontrun0" 19 | } else { 20 | "v2_frontrun1" 21 | }); 22 | 23 | let five_bytes = 24 | FiveByteMetaData::encode(amount_out, if *WETH_ADDRESS < output_token { 1 } else { 0 }); 25 | 26 | let (payload, _) = eth_encode_packed::abi::encode_packed(&[ 27 | SolidityDataType::NumberWithShift(jump_dest.into(), TakeLastXBytes(8)), 28 | SolidityDataType::Address(pool.address().0.into()), 29 | SolidityDataType::Bytes(&five_bytes.finalize_to_bytes()), 30 | ]); 31 | 32 | let encoded_call_value = WethEncoder::encode(amount_in); 33 | 34 | (payload, encoded_call_value) 35 | } 36 | 37 | /// dev: amount_out is needed to be passed due to taxed tokens 38 | pub fn v2_create_backrun_payload( 39 | pool: UniswapV2Pool, 40 | input_token: Address, 41 | amount_in: U256, 42 | amount_out: U256, // amount_out is needed to be passed due to taxed tokens 43 | ) -> (Vec, U256) { 44 | let jump_dest = get_jump_dest_from_sig(if *WETH_ADDRESS < input_token { 45 | "v2_backrun0" 46 | } else { 47 | "v2_backrun1" 48 | }); 49 | 50 | let five_bytes = FiveByteMetaData::encode(amount_in, 1); 51 | 52 | let (payload, _) = eth_encode_packed::abi::encode_packed(&[ 53 | SolidityDataType::NumberWithShift(jump_dest.into(), TakeLastXBytes(8)), 54 | SolidityDataType::Address(pool.address().0.into()), 55 | SolidityDataType::Address(input_token.0.into()), 56 | SolidityDataType::Bytes(&five_bytes.finalize_to_bytes()), 57 | ]); 58 | 59 | let encoded_call_value = WethEncoder::encode(amount_out); 60 | 61 | (payload, encoded_call_value) 62 | } 63 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/tests/main.rs: -------------------------------------------------------------------------------- 1 | use artemis_core::{ 2 | collectors::{block_collector::BlockCollector, mempool_collector::MempoolCollector}, 3 | types::Collector, 4 | }; 5 | use ethers::providers::StreamExt; 6 | use ethers::{ 7 | providers::{Middleware, Provider, Ws}, 8 | types::{BlockNumber, TransactionRequest, U256}, 9 | utils::{Anvil, AnvilInstance}, 10 | }; 11 | use std::{sync::Arc, time::Duration}; 12 | 13 | /// Spawns Anvil and instantiates an Http provider. 14 | pub async fn spawn_anvil() -> (Provider, AnvilInstance) { 15 | let anvil = Anvil::new().block_time(1u64).spawn(); 16 | let provider = Provider::::connect(anvil.ws_endpoint()) 17 | .await 18 | .unwrap() 19 | .interval(Duration::from_millis(50u64)); 20 | (provider, anvil) 21 | } 22 | 23 | /// Test that block collector correctly emits blocks. 24 | #[tokio::test] 25 | async fn test_block_collector_sends_blocks() { 26 | let (provider, _anvil) = spawn_anvil().await; 27 | let provider = Arc::new(provider); 28 | let block_collector = BlockCollector::new(provider.clone()); 29 | let block_stream = block_collector.get_event_stream().await.unwrap(); 30 | let block_a = block_stream.into_future().await.0.unwrap(); 31 | let block_b = provider 32 | .get_block(BlockNumber::Latest) 33 | .await 34 | .unwrap() 35 | .unwrap(); 36 | assert_eq!(block_a.number, block_b.number.unwrap()); 37 | } 38 | 39 | /// Test that mempool collector correctly emits blocks. 40 | #[tokio::test] 41 | async fn test_mempool_collector_sends_txs() { 42 | let (provider, _anvil) = spawn_anvil().await; 43 | let provider = Arc::new(provider); 44 | let mempool_collector = MempoolCollector::new(provider.clone()); 45 | let mempool_stream = mempool_collector.get_event_stream().await.unwrap(); 46 | 47 | let account = provider.get_accounts().await.unwrap()[0]; 48 | let value: u64 = 42; 49 | let gas_price = U256::from_dec_str("100000000000000000").unwrap(); 50 | let tx = TransactionRequest::new() 51 | .to(account) 52 | .from(account) 53 | .value(value) 54 | .gas_price(gas_price); 55 | 56 | provider.send_transaction(tx, None).await.unwrap(); 57 | let tx = mempool_stream.into_future().await.0.unwrap(); 58 | assert_eq!(tx.value, value.into()); 59 | } 60 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/IUniswapV2Factory.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":false,"internalType":"address","name":"pair","type":"address"},{"indexed":false,"internalType":"uint256","name":"","type":"uint256"}],"name":"PairCreated","type":"event"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"allPairs","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"allPairsLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"}],"name":"createPair","outputs":[{"internalType":"address","name":"pair","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feeTo","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeToSetter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"getPair","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"migrator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pairCodeHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"_feeTo","type":"address"}],"name":"setFeeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_feeToSetter","type":"address"}],"name":"setFeeToSetter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_migrator","type":"address"}],"name":"setMigrator","outputs":[],"stateMutability":"nonpayable","type":"function"}] 2 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/v3.rs: -------------------------------------------------------------------------------- 1 | use cfmms::pool::UniswapV3Pool; 2 | use eth_encode_packed::{SolidityDataType, TakeLastXBytes}; 3 | use ethers::{ 4 | abi::{encode, Token}, 5 | types::{Address, U256}, 6 | }; 7 | 8 | use crate::constants::WETH_ADDRESS; 9 | 10 | use super::common::{ 11 | five_byte_encoder::FiveByteMetaData, get_jump_dest_from_sig, weth_encoder::WethEncoder, 12 | }; 13 | 14 | pub fn v3_create_frontrun_payload( 15 | pool: UniswapV3Pool, 16 | output_token: Address, 17 | amount_in: U256, 18 | ) -> (Vec, U256) { 19 | let (payload, _) = eth_encode_packed::abi::encode_packed(&[ 20 | SolidityDataType::NumberWithShift( 21 | get_jump_dest_from_sig(if *WETH_ADDRESS < output_token { 22 | "v3_frontrun0" 23 | } else { 24 | "v3_frontrun1" 25 | }) 26 | .into(), 27 | TakeLastXBytes(8), 28 | ), 29 | SolidityDataType::Address(pool.address().0.into()), 30 | SolidityDataType::Bytes(&get_pool_key_hash(pool).to_vec()), 31 | ]); 32 | 33 | let encoded_value = WethEncoder::encode(amount_in); 34 | 35 | (payload, encoded_value) 36 | } 37 | 38 | pub fn v3_create_backrun_payload( 39 | pool: UniswapV3Pool, 40 | input_token: Address, 41 | amount_in: U256, 42 | ) -> Vec { 43 | let five_bytes = FiveByteMetaData::encode(U256::from(amount_in), 2); 44 | 45 | let (payload, _) = eth_encode_packed::abi::encode_packed(&[ 46 | SolidityDataType::NumberWithShift( 47 | get_jump_dest_from_sig(if *WETH_ADDRESS < input_token { 48 | "v3_backrun0" 49 | } else { 50 | "v3_backrun1" 51 | }) 52 | .into(), 53 | TakeLastXBytes(8), 54 | ), 55 | SolidityDataType::Address(pool.address().0.into()), 56 | SolidityDataType::Address(input_token.0.into()), 57 | SolidityDataType::Bytes(&get_pool_key_hash(pool).to_vec()), 58 | SolidityDataType::Bytes(&five_bytes.finalize_to_bytes()), 59 | ]); 60 | 61 | payload 62 | } 63 | 64 | /// https://github.com/Uniswap/v3-periphery/blob/6cce88e63e176af1ddb6cc56e029110289622317/contracts/libraries/PoolAddress.sol#L41C80-L41C80 65 | fn get_pool_key_hash(pool: UniswapV3Pool) -> [u8; 32] { 66 | ethers::utils::keccak256(encode(&[ 67 | Token::Address(pool.token_a), 68 | Token::Address(pool.token_b), 69 | Token::Uint(pool.fee.into()), 70 | ])) 71 | } 72 | -------------------------------------------------------------------------------- /contract/test/misc/GeneralHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "v2-core/interfaces/IUniswapV2Pair.sol"; 5 | import "v2-core/interfaces/IUniswapV2Factory.sol"; 6 | import "v2-periphery/interfaces/IUniswapV2Router02.sol"; 7 | import "forge-std/console.sol"; 8 | 9 | library GeneralHelper { 10 | function getAmountOut(address inputToken, address outputToken, uint256 amountIn) 11 | public 12 | view 13 | returns (uint256 amountOut) 14 | { 15 | IUniswapV2Router02 univ2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 16 | 17 | (uint256 reserveToken0, uint256 reserveToken1,) = 18 | IUniswapV2Pair(_getUniswapPair(inputToken, outputToken)).getReserves(); 19 | 20 | uint256 reserveIn; 21 | uint256 reserveOut; 22 | 23 | if (inputToken < outputToken) { 24 | // inputToken is token0 25 | reserveIn = reserveToken0; 26 | reserveOut = reserveToken1; 27 | } else { 28 | // inputToken is token1 29 | reserveIn = reserveToken1; 30 | reserveOut = reserveToken0; 31 | } 32 | 33 | amountOut = univ2Router.getAmountOut(amountIn, reserveIn, reserveOut); 34 | } 35 | 36 | function getAmountIn(address inputToken, address outputToken, uint256 amountOut) 37 | public 38 | view 39 | returns (uint256 amountIn) 40 | { 41 | IUniswapV2Router02 univ2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 42 | 43 | (uint256 reserveToken0, uint256 reserveToken1,) = 44 | IUniswapV2Pair(_getUniswapPair(inputToken, outputToken)).getReserves(); 45 | 46 | uint256 reserveIn; 47 | uint256 reserveOut; 48 | 49 | if (inputToken < outputToken) { 50 | // inputToken is token0 51 | reserveIn = reserveToken0; 52 | reserveOut = reserveToken1; 53 | } else { 54 | // inputToken is token1 55 | reserveIn = reserveToken1; 56 | reserveOut = reserveToken0; 57 | } 58 | 59 | amountIn = univ2Router.getAmountIn(amountOut, reserveIn, reserveOut); 60 | } 61 | 62 | function _getUniswapPair(address tokenA, address tokenB) private view returns (address pair) { 63 | IUniswapV2Factory univ2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); 64 | pair = address(IUniswapV2Pair(univ2Factory.getPair(address(tokenA), address(tokenB)))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/IUniswapV3Factory.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":true,"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"FeeAmountEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":false,"internalType":"int24","name":"tickSpacing","type":"int24"},{"indexed":false,"internalType":"address","name":"pool","type":"address"}],"name":"PoolCreated","type":"event"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"}],"name":"createPool","outputs":[{"internalType":"address","name":"pool","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"enableFeeAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"","type":"uint24"}],"name":"feeAmountTickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"parameters","outputs":[{"internalType":"address","name":"factory","type":"address"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"}] 2 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/huff_sando_interface/common/five_byte_encoder.rs: -------------------------------------------------------------------------------- 1 | use eth_encode_packed::{SolidityDataType, TakeLastXBytes}; 2 | use ethers::types::U256; 3 | 4 | /// A struct that contains the metadata for the five byte encoding 5 | pub struct FiveByteMetaData { 6 | /// TargetValue squashed down to four bytes 7 | four_bytes: u32, 8 | /// How many byte shifts to apply on our four bytes 9 | byte_shift: u8, 10 | /// Where should the value be stored (which param index in abi schema during func call) 11 | param_index: u8, 12 | } 13 | 14 | impl FiveByteMetaData { 15 | // Encodes a value to 5 bytes of calldata (used to represent other token value) 16 | pub fn encode(amount: U256, param_index: u8) -> Self { 17 | let mut byte_shift: u8 = 0; 18 | let mut four_bytes: u32 = 0; 19 | 20 | while byte_shift < 32 { 21 | // lossy encoding as we lose bits due to division 22 | let encoded_amount = amount / 2u128.pow(8 * byte_shift as u32); 23 | 24 | // if we can fit the value in 4 bytes, we can encode it 25 | if encoded_amount <= U256::from(2).pow((4 * 8).into()) - 1 { 26 | four_bytes = encoded_amount.as_u32(); 27 | break; 28 | } 29 | 30 | byte_shift += 1; 31 | } 32 | 33 | Self { 34 | byte_shift, 35 | four_bytes, 36 | param_index, 37 | } 38 | } 39 | 40 | /// Decrement the four bytes by one (used for when we want to keep dust on contract) 41 | pub fn decrement_four_bytes(&mut self) { 42 | self.four_bytes -= 1; 43 | } 44 | 45 | /// Decodes the 5 bytes back to a 32 byte value (lossy) 46 | pub fn decode(&self) -> U256 { 47 | let value: u128 = (self.four_bytes as u128) << (self.byte_shift as u32 * 8); 48 | return U256::from(value); 49 | } 50 | 51 | /// Finalize by encoding into five bytes for a specific param index 52 | /// Find memoffset for param index such that when stored it is shifted by `self.byte_shifts` 53 | pub fn finalize_to_bytes(self) -> Vec { 54 | // first +4 value is used for function selector, -4 because we account for the 4 bytes used 55 | // for encoding the value to be shifted 56 | let mem_offset = 4 + 32 + (self.param_index * 32) - 4 - self.byte_shift; 57 | 58 | let (encoded, _) = eth_encode_packed::abi::encode_packed(&vec![ 59 | SolidityDataType::NumberWithShift(mem_offset.into(), TakeLastXBytes(8)), 60 | SolidityDataType::NumberWithShift(self.four_bytes.into(), TakeLastXBytes(32)), 61 | ]); 62 | 63 | encoded 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bot/sando-bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use artemis_core::{ 5 | collectors::{block_collector::BlockCollector, mempool_collector::MempoolCollector}, 6 | engine::Engine, 7 | executors::flashbots_executor::FlashbotsExecutor, 8 | types::{CollectorMap, ExecutorMap}, 9 | }; 10 | use ethers::providers::{Provider, Ws}; 11 | use log::info; 12 | use reqwest::Url; 13 | use rusty_sando::{ 14 | config::Config, 15 | initialization::{print_banner, setup_logger}, 16 | }; 17 | use strategy::{ 18 | bot::SandoBot, 19 | types::{Action, Event, StratConfig}, 20 | }; 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<()> { 24 | // Setup 25 | setup_logger()?; 26 | print_banner(); 27 | let config = Config::read_from_dotenv().await?; 28 | 29 | // Setup ethers provider 30 | let ws = Ws::connect(config.wss_rpc).await?; 31 | let provider = Arc::new(Provider::new(ws)); 32 | 33 | // Setup signers 34 | let flashbots_signer = config.bundle_signer; 35 | let searcher_signer = config.searcher_signer; 36 | 37 | // Create engine 38 | let mut engine: Engine = Engine::default(); 39 | 40 | // Setup block collector 41 | let block_collector = Box::new(BlockCollector::new(provider.clone())); 42 | let block_collector = CollectorMap::new(block_collector, Event::NewBlock); 43 | engine.add_collector(Box::new(block_collector)); 44 | 45 | // Setup mempool collector 46 | let mempool_collector = Box::new(MempoolCollector::new(provider.clone())); 47 | let mempool_collector = CollectorMap::new(mempool_collector, Event::NewTransaction); 48 | engine.add_collector(Box::new(mempool_collector)); 49 | 50 | // Setup strategy 51 | let configs = StratConfig { 52 | sando_address: config.sando_address, 53 | sando_inception_block: config.sando_inception_block, 54 | searcher_signer, 55 | }; 56 | let strategy = SandoBot::new(provider.clone(), configs); 57 | engine.add_strategy(Box::new(strategy)); 58 | 59 | // Setup flashbots executor 60 | let executor = Box::new(FlashbotsExecutor::new( 61 | provider.clone(), 62 | flashbots_signer, 63 | Url::parse("https://relay.flashbots.net")?, 64 | )); 65 | let executor = ExecutorMap::new(executor, |action| match action { 66 | Action::SubmitToFlashbots(bundle) => Some(bundle), 67 | }); 68 | engine.add_executor(Box::new(executor)); 69 | 70 | // Start engine 71 | if let Ok(mut set) = engine.run().await { 72 | while let Some(res) = set.join_next().await { 73 | info!("res: {:?}", res) 74 | } 75 | } 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /contract/test/misc/V3SandoUtility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "./GeneralHelper.sol"; 5 | import "./SandoCommon.sol"; 6 | import "v3-core/interfaces/IUniswapV3Pool.sol"; 7 | 8 | /// @title V3SandoUtility 9 | /// @author 0xmouseless 10 | /// @notice Functions for interacting with sando contract's v3 methdos 11 | library V3SandoUtility { 12 | /** 13 | * @notice Utility function to create payload for our v3 frontruns 14 | * @return payload Calldata bytes to execute frontrun 15 | * @return encodedValue Encoded `tx.value` indicating WETH amount to send 16 | */ 17 | function v3CreateFrontrunPayload(IUniswapV3Pool pool, address outputToken, int256 amountIn) 18 | public 19 | view 20 | returns (bytes memory payload, uint256 encodedValue) 21 | { 22 | address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 23 | 24 | (address token0, address token1) = weth < outputToken ? (weth, outputToken) : (outputToken, weth); 25 | bytes32 poolKeyHash = keccak256(abi.encode(token0, token1, pool.fee())); 26 | 27 | string memory functionSignature = weth < outputToken ? "v3_frontrun0" : "v3_frontrun1"; 28 | uint8 jumpDest = SandoCommon.getJumpDestFromSig(functionSignature); 29 | payload = abi.encodePacked(jumpDest, address(pool), poolKeyHash); 30 | 31 | encodedValue = WethEncodingUtils.encode(uint256(amountIn)); 32 | } 33 | 34 | /** 35 | * @notice Utility function to create payload for our v3 backruns 36 | * @return payload Calldata bytes to execute backruns (empty tx.value because pool optimistically sends weth to sando contract) 37 | */ 38 | function v3CreateBackrunPayload(IUniswapV3Pool pool, address inputToken, int256 amountIn) 39 | public 40 | view 41 | returns (bytes memory payload) 42 | { 43 | address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 44 | (address token0, address token1) = inputToken < weth ? (inputToken, weth) : (weth, inputToken); 45 | bytes32 poolKeyHash = keccak256(abi.encode(token0, token1, pool.fee())); 46 | 47 | string memory functionSignature = weth < inputToken ? "v3_backrun0" : "v3_backrun1"; 48 | uint8 jumpDest = SandoCommon.getJumpDestFromSig(functionSignature); 49 | 50 | FiveBytesEncodingUtils.EncodingMetaData memory fiveByteParams = FiveBytesEncodingUtils.encode(uint256(amountIn)); 51 | 52 | payload = abi.encodePacked( 53 | jumpDest, 54 | address(pool), 55 | address(inputToken), 56 | poolKeyHash, 57 | FiveBytesEncodingUtils.finalzeForParamIndex(fiveByteParams, 2) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/utilities/state_override_middleware.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use ethers::{ 3 | core::types::{transaction::eip2718::TypedTransaction, BlockId}, 4 | providers::{spoof, CallBuilder, Middleware, MiddlewareError, RawCall}, 5 | types::{Address, Bytes}, 6 | }; 7 | use thiserror::Error; 8 | 9 | /// This custom middleware performs an ephemeral state override prior to executoring calls. 10 | #[derive(Debug)] 11 | pub struct StateOverrideMiddleware { 12 | /// The inner middleware 13 | inner: M, 14 | /// The state override set we use for calls 15 | state: spoof::State, 16 | } 17 | 18 | impl StateOverrideMiddleware 19 | where 20 | M: Middleware, 21 | { 22 | /// Creates an instance of StateOverrideMiddleware 23 | /// `ìnner` the inner Middleware 24 | pub fn new(inner: M) -> StateOverrideMiddleware { 25 | Self { 26 | inner, 27 | state: spoof::state(), 28 | } 29 | } 30 | } 31 | 32 | #[async_trait] 33 | impl Middleware for StateOverrideMiddleware 34 | where 35 | M: Middleware, 36 | { 37 | type Error = StateOverrideMiddlewareError; 38 | type Provider = M::Provider; 39 | type Inner = M; 40 | 41 | fn inner(&self) -> &M { 42 | &self.inner 43 | } 44 | 45 | /// Performs a call with the state override. 46 | async fn call( 47 | &self, 48 | tx: &TypedTransaction, 49 | block: Option, 50 | ) -> Result { 51 | let call_builder = CallBuilder::new(self.inner.provider(), tx); 52 | let call_builder = match block { 53 | Some(block) => call_builder.block(block), 54 | None => call_builder, 55 | }; 56 | let call_builder = call_builder.state(&self.state); 57 | call_builder 58 | .await 59 | .map_err(StateOverrideMiddlewareError::from_provider_err) 60 | } 61 | } 62 | 63 | impl StateOverrideMiddleware { 64 | /// Adds a code override at a given address. 65 | pub fn add_code_to_address(&mut self, address: Address, code: Bytes) { 66 | self.state.account(address).code(code); 67 | } 68 | 69 | /// Adds a code override at a random address, returning the address. 70 | pub fn add_code(&mut self, code: Bytes) -> Address { 71 | let address = Address::random(); 72 | self.state.account(address).code(code); 73 | address 74 | } 75 | } 76 | 77 | #[derive(Error, Debug)] 78 | pub enum StateOverrideMiddlewareError { 79 | /// Thrown when the internal middleware errors 80 | #[error("{0}")] 81 | MiddlewareError(M::Error), 82 | } 83 | 84 | impl MiddlewareError for StateOverrideMiddlewareError { 85 | type Inner = M::Error; 86 | 87 | fn from_err(src: M::Error) -> Self { 88 | StateOverrideMiddlewareError::MiddlewareError(src) 89 | } 90 | 91 | fn as_inner(&self) -> Option<&Self::Inner> { 92 | match self { 93 | StateOverrideMiddlewareError::MiddlewareError(e) => Some(e), 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/tx_utils/lil_router_interface.rs: -------------------------------------------------------------------------------- 1 | use cfmms::pool::{UniswapV2Pool, UniswapV3Pool}; 2 | use ethers::{abi::parse_abi, prelude::*}; 3 | 4 | use crate::constants::WETH_ADDRESS; 5 | 6 | // Build the data for the lil_router contract's calculateSwapV2 function 7 | pub fn build_swap_v2_data(amount_in: U256, pool: UniswapV2Pool, is_frontrun: bool) -> Bytes { 8 | let lil_router_contract = BaseContract::from(parse_abi(&[ 9 | "function calculateSwapV2(uint amountIn, address targetPair, address inputToken, address outputToken) external returns (uint amountOut, uint realAfterBalance)", 10 | ]).unwrap()); 11 | 12 | let other_token = [pool.token_a, pool.token_b] 13 | .into_iter() 14 | .find(|&t| t != *WETH_ADDRESS) 15 | .unwrap(); 16 | 17 | let (input_token, output_token) = if is_frontrun { 18 | // if frontrun we trade WETH -> TOKEN 19 | (*WETH_ADDRESS, other_token) 20 | } else { 21 | // if backrun we trade TOKEN -> WETH 22 | (other_token, *WETH_ADDRESS) 23 | }; 24 | 25 | lil_router_contract 26 | .encode( 27 | "calculateSwapV2", 28 | (amount_in, pool.address, input_token, output_token), 29 | ) 30 | .unwrap() 31 | } 32 | 33 | // Build the data for the lil_router contract's calculateSwapV3 function 34 | pub fn build_swap_v3_data(amount_in: I256, pool: UniswapV3Pool, is_frontrun: bool) -> Bytes { 35 | let lil_router_contract = BaseContract::from(parse_abi(&[ 36 | "function calculateSwapV3(int amountIn, address targetPoolAddress, address inputToken, address outputToken) public returns (uint amountOut, uint realAfterBalance)", 37 | ]).unwrap()); 38 | 39 | let other_token = [pool.token_a, pool.token_b] 40 | .into_iter() 41 | .find(|&t| t != *WETH_ADDRESS) 42 | .unwrap(); 43 | 44 | let (input_token, output_token) = if is_frontrun { 45 | // if frontrun we trade WETH -> TOKEN 46 | (*WETH_ADDRESS, other_token) 47 | } else { 48 | // if backrun we trade TOKEN -> WETH 49 | (other_token, *WETH_ADDRESS) 50 | }; 51 | 52 | lil_router_contract 53 | .encode( 54 | "calculateSwapV3", 55 | (amount_in, pool.address, input_token, output_token), 56 | ) 57 | .unwrap() 58 | } 59 | 60 | // Decode the result of the lil_router contract's calculateSwapV2 function 61 | pub fn decode_swap_v2_result(output: Bytes) -> Result<(U256, U256), AbiError> { 62 | let lil_router_contract = BaseContract::from(parse_abi(&[ 63 | "function calculateSwapV2(uint amountIn, address targetPair, address inputToken, address outputToken) external returns (uint amountOut, uint realAfterBalance)", 64 | ]).unwrap()); 65 | 66 | lil_router_contract.decode_output("calculateSwapV2", output) 67 | } 68 | 69 | // Decode the result of the lil_router contract's calculateSwapV3 function 70 | pub fn decode_swap_v3_result(output: Bytes) -> Result<(U256, U256), AbiError> { 71 | let lil_router_contract = BaseContract::from(parse_abi(&[ 72 | "function calculateSwapV3(int amountIn, address targetPoolAddress, address inputToken, address outputToken) public returns (uint amountOut, uint realAfterBalance)", 73 | ]).unwrap()); 74 | 75 | lil_router_contract.decode_output("calculateSwapV3", output) 76 | } 77 | -------------------------------------------------------------------------------- /contract/test/misc/V2SandoUtility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "./GeneralHelper.sol"; 5 | import "./SandoCommon.sol"; 6 | 7 | /// @title V2SandoUtility 8 | /// @author 0xmouseless 9 | /// @notice Functions for interacting with sando contract's v2 methods 10 | library V2SandoUtility { 11 | /** 12 | * @notice Utility function to create payload for our v2 backruns 13 | * @return payload Calldata bytes to execute backruns 14 | * @return encodedValue Encoded `tx.value` indicating WETH amount to send 15 | */ 16 | function v2CreateBackrunPayload(address otherToken, uint256 amountIn) 17 | public 18 | view 19 | returns (bytes memory payload, uint256 encodedValue) 20 | { 21 | // Declare uniswapv2 types 22 | address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 23 | IUniswapV2Factory univ2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); 24 | address pair = address(IUniswapV2Pair(univ2Factory.getPair(weth, address(otherToken)))); 25 | 26 | // encode amountIn 27 | FiveBytesEncodingUtils.EncodingMetaData memory fiveByteParams = FiveBytesEncodingUtils.encode(amountIn); 28 | uint256 amountInActual = FiveBytesEncodingUtils.decode(fiveByteParams); 29 | 30 | string memory functionSignature = weth < otherToken ? "v2_backrun0" : "v2_backrun1"; 31 | uint8 jumpDest = SandoCommon.getJumpDestFromSig(functionSignature); 32 | 33 | payload = abi.encodePacked( 34 | jumpDest, 35 | address(pair), // univ2 pair 36 | address(otherToken), // inputToken 37 | FiveBytesEncodingUtils.finalzeForParamIndex(fiveByteParams, 1) 38 | ); 39 | 40 | uint256 amountOut = GeneralHelper.getAmountOut(otherToken, weth, amountInActual); 41 | encodedValue = WethEncodingUtils.encode(amountOut); 42 | } 43 | 44 | /** 45 | * @notice Utility function to create payload for our v2 frontruns 46 | * @return payload Calldata bytes to execute frontruns 47 | * @return encodedValue Encoded `tx.value` indicating WETH amount to send 48 | */ 49 | function v2CreateFrontrunPayload(address outputToken, uint256 amountIn) 50 | public 51 | view 52 | returns (bytes memory payload, uint256 encodedValue) 53 | { 54 | // Declare uniswapv2 types 55 | address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 56 | IUniswapV2Factory univ2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); 57 | address pair = address(IUniswapV2Pair(univ2Factory.getPair(weth, address(outputToken)))); 58 | 59 | // Encode amountIn here (so we can use it for next step) 60 | uint256 amountInActual = WethEncodingUtils.decode(WethEncodingUtils.encode(amountIn)); 61 | 62 | // Get amounts out and encode it 63 | FiveBytesEncodingUtils.EncodingMetaData memory fiveByteParams = 64 | FiveBytesEncodingUtils.encode(GeneralHelper.getAmountOut(weth, outputToken, amountInActual)); 65 | 66 | string memory functionSignature = weth < outputToken ? "v2_frontrun0" : "v2_frontrun1"; 67 | uint8 jumpDest = SandoCommon.getJumpDestFromSig(functionSignature); 68 | 69 | payload = abi.encodePacked( 70 | jumpDest, // type of swap to make 71 | address(pair), // univ2 pair 72 | FiveBytesEncodingUtils.finalzeForParamIndex(fiveByteParams, weth < outputToken ? 1 : 0) 73 | ); 74 | 75 | encodedValue = WethEncodingUtils.encode(amountIn); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/types.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use ethers::types::Transaction; 4 | use std::pin::Pin; 5 | use tokio_stream::Stream; 6 | use tokio_stream::StreamExt; 7 | 8 | use crate::collectors::block_collector::NewBlock; 9 | use crate::executors::flashbots_executor::FlashbotsBundle; 10 | 11 | /// A stream of events emitted by a [Collector](Collector). 12 | pub type CollectorStream<'a, E> = Pin + Send + 'a>>; 13 | 14 | /// Collector trait, which defines a source of events. 15 | #[async_trait] 16 | pub trait Collector: Send + Sync { 17 | /// Returns the core event stream for the collector. 18 | async fn get_event_stream(&self) -> Result>; 19 | } 20 | 21 | /// Strategy trait, which defines the core logic for each opportunity. 22 | #[async_trait] 23 | pub trait Strategy: Send + Sync { 24 | /// Sync the initial state of the strategy if needed, usually by fetching 25 | /// onchain data. 26 | async fn sync_state(&mut self) -> Result<()>; 27 | 28 | /// Process an event, and return an action if needed. 29 | async fn process_event(&mut self, event: E) -> Option; 30 | } 31 | 32 | /// Executor trait, responsible for executing actions returned by strategies. 33 | #[async_trait] 34 | pub trait Executor: Send + Sync { 35 | /// Execute an action. 36 | async fn execute(&self, action: A) -> Result<()>; 37 | } 38 | 39 | /// CollectorMap is a wrapper around a [Collector](Collector) that maps outgoing 40 | /// events to a different type. 41 | pub struct CollectorMap { 42 | collector: Box>, 43 | f: F, 44 | } 45 | impl CollectorMap { 46 | pub fn new(collector: Box>, f: F) -> Self { 47 | Self { collector, f } 48 | } 49 | } 50 | 51 | #[async_trait] 52 | impl Collector for CollectorMap 53 | where 54 | E1: Send + Sync + 'static, 55 | E2: Send + Sync + 'static, 56 | F: Fn(E1) -> E2 + Send + Sync + Clone + 'static, 57 | { 58 | async fn get_event_stream(&self) -> Result> { 59 | let stream = self.collector.get_event_stream().await?; 60 | let f = self.f.clone(); 61 | let stream = stream.map(f); 62 | Ok(Box::pin(stream)) 63 | } 64 | } 65 | 66 | /// ExecutorMap is a wrapper around an [Executor](Executor) that maps incoming 67 | /// actions to a different type. 68 | pub struct ExecutorMap { 69 | executor: Box>, 70 | f: F, 71 | } 72 | 73 | impl ExecutorMap { 74 | pub fn new(executor: Box>, f: F) -> Self { 75 | Self { executor, f } 76 | } 77 | } 78 | 79 | #[async_trait] 80 | impl Executor for ExecutorMap 81 | where 82 | A1: Send + Sync + 'static, 83 | A2: Send + Sync + 'static, 84 | F: Fn(A1) -> Option + Send + Sync + Clone + 'static, 85 | { 86 | async fn execute(&self, action: A1) -> Result<()> { 87 | let action = (self.f)(action); 88 | match action { 89 | Some(action) => self.executor.execute(action).await, 90 | None => Ok(()), 91 | } 92 | } 93 | } 94 | 95 | /// Convenience enum containing all the events that can be emitted by collectors. 96 | pub enum Events { 97 | NewBlock(NewBlock), 98 | Transaction(Transaction), 99 | } 100 | 101 | /// Convenience enum containing all the actions that can be executed by executors. 102 | pub enum Actions { 103 | FlashbotsBundle(FlashbotsBundle), 104 | } 105 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/managers/sando_state_manager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use colored::Colorize; 3 | use ethers::{ 4 | providers::Middleware, 5 | signers::{LocalWallet, Signer}, 6 | types::{Address, BlockNumber, Filter, U256, U64}, 7 | }; 8 | use log::info; 9 | use std::sync::Arc; 10 | 11 | use crate::{ 12 | abi::Erc20, 13 | constants::{ERC20_TRANSFER_EVENT_SIG, WETH_ADDRESS}, 14 | startup_info_log, 15 | }; 16 | 17 | pub struct SandoStateManager { 18 | sando_contract: Address, 19 | sando_inception_block: U64, 20 | searcher_signer: LocalWallet, 21 | weth_inventory: U256, 22 | token_dust: Vec
, 23 | } 24 | 25 | impl SandoStateManager { 26 | pub fn new( 27 | sando_contract: Address, 28 | searcher_signer: LocalWallet, 29 | sando_inception_block: U64, 30 | ) -> Self { 31 | Self { 32 | sando_contract, 33 | sando_inception_block, 34 | searcher_signer, 35 | weth_inventory: Default::default(), 36 | token_dust: Default::default(), 37 | } 38 | } 39 | 40 | pub async fn setup(&mut self, provider: Arc) -> Result<()> { 41 | // find weth inventory 42 | let weth = Erc20::new(*WETH_ADDRESS, provider.clone()); 43 | let weth_balance = weth.balance_of(self.sando_contract).call().await?; 44 | startup_info_log!("weth inventory : {}", weth_balance); 45 | self.weth_inventory = weth_balance; 46 | 47 | // find weth dust 48 | let step = 10000; 49 | 50 | let latest_block = provider 51 | .get_block(BlockNumber::Latest) 52 | .await 53 | .map_err(|_| anyhow!("Failed to get latest block"))? 54 | .ok_or(anyhow!("Failed to get latest block"))? 55 | .number 56 | .ok_or(anyhow!("Field block number does not exist on latest block"))? 57 | .as_u64(); 58 | 59 | let mut token_dust = vec![]; 60 | 61 | let start_block = self.sando_inception_block.as_u64(); 62 | 63 | // for each block within the range, get all transfer events asynchronously 64 | for from_block in (start_block..=latest_block).step_by(step) { 65 | let to_block = from_block + step as u64; 66 | 67 | // check for all incoming and outgoing txs within step range 68 | let transfer_logs = provider 69 | .get_logs( 70 | &Filter::new() 71 | .topic0(*ERC20_TRANSFER_EVENT_SIG) 72 | .topic1(self.sando_contract) 73 | .from_block(BlockNumber::Number(U64([from_block]))) 74 | .to_block(BlockNumber::Number(U64([to_block]))), 75 | ) 76 | .await?; 77 | 78 | for log in transfer_logs { 79 | token_dust.push(log.address); 80 | } 81 | } 82 | 83 | startup_info_log!("token dust found : {}", token_dust.len()); 84 | self.token_dust = token_dust; 85 | 86 | Ok(()) 87 | } 88 | 89 | pub fn get_sando_address(&self) -> Address { 90 | self.sando_contract 91 | } 92 | 93 | pub fn get_searcher_address(&self) -> Address { 94 | self.searcher_signer.address() 95 | } 96 | 97 | pub fn get_searcher_signer(&self) -> &LocalWallet { 98 | &self.searcher_signer 99 | } 100 | 101 | pub fn get_weth_inventory(&self) -> U256 { 102 | self.weth_inventory 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rusty-Sando (re-write) ![license](https://img.shields.io/badge/License-MIT-green.svg?label=license) 2 | 3 | This repo was originally a fork of https://github.com/mouseless-eth/rusty-sando by mouseless 4 | 5 | ![twitter](https://img.shields.io/twitter/follow/0xMouseless?style=social) 6 | 7 | I am truly grateful to mouseless for his contribution and for introducing us to state of the art MEV & Sandwich bots. 8 | 9 | ------------------------------------------------------------------------------------------------------------------------------------ 10 | 11 | # Please note that I will not be actively fixing bugs and addressing issues here. 12 | 13 | I might push some critical fixes/improvements if I find time and that is it. 14 | 15 | If you desperately need help and are willing to pay for my time (very expensive), you can contact me at MevSando@ifiva.com 16 | 17 | ------------------------------------------------------------------------------------------------------------------------------------- 18 | 19 | 20 | A practical example on how to perform V2/V3 and multi-meat sandwich attacks written using Rust and Huff. 21 | 22 | The goal of this repo is to act as reference material for aspiring searchers. 23 | 24 | > **This codebase has been cleaned up and rewritten using the [`Artemis`](https://github.com/paradigmxyz/artemis) framework. (Further details in [pull request #31](https://github.com/mouseless-eth/rusty-sando/pull/31#issue-1818492576))** 25 | 26 | ## Demo 27 | https://user-images.githubusercontent.com/97399882/226269539-afedced0-e070-4d12-9853-dfbafbcefa49.mp4 28 | 29 | ## Brief Explanation 30 | 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. 31 | 32 | **Bot Logic Breakdown** can be found under [bot/README.md](https://github.com/mouseless-eth/rusty-sando/tree/master/bot) 33 | 34 | **Contract Logic Breakdown** can be found under [contract/README.md](https://github.com/mouseless-eth/rusty-sando/tree/master/contract) 35 | 36 | ## Features 37 | - **Fully Generalized**: Sandwich any tx that introduces slippage. 38 | - **V2 and V3 Logic**: Logic to handle Uniswap V2/V3 pools. 39 | - **Multi-Meat**: Build and send multi-meat sandwiches. 40 | - **Gas Optimized**: Contract written in Huff using unconventional gas optimizations. 41 | - **Local Simulations**: Fast concurrent EVM simulations to find sandwich opportunities. 42 | - **Token Dust**: Stores dust at the end of every bundle for lower gas usage the next time the token is traded. 43 | - **Salmonella Checks**: Detect if erc20's transfer function uses any unusual opcodes that may produce different mainnet results. 44 | 45 | ## Notice 46 | If any bugs or optimizations are found, feel free to create a pull request. **All pull requests are welcome!** 47 | 48 | > **Warning** 49 | > 50 | > **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. 51 | 52 | ## Acknowledgments 53 | - [subway](https://github.com/libevm/subway) 54 | - [subway-rs](https://github.com/refcell/subway-rs) 55 | - [cfmms-rs](https://github.com/0xKitsune/cfmms-rs) 56 | - [revm](https://github.com/bluealloy/revm) 57 | - [artemis](https://github.com/paradigmxyz/artemis) 58 | - [huff-language](https://github.com/huff-language/huff-rs) 59 | - [foundry](https://github.com/foundry-rs/foundry) 60 | - [reth](https://github.com/paradigmxyz/reth) 61 | - [ethers-rs](https://github.com/gakonst/ethers-rs) 62 | - [ethers-flashbots](https://github.com/onbjerg/ethers-flashbots) 63 | - [mev-template-rs](https://github.com/degatchi/mev-template-rs) 64 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ethers::{ 3 | signers::{LocalWallet, Signer}, 4 | types::{ 5 | transaction::{ 6 | eip2718::TypedTransaction, 7 | eip2930::{AccessList, AccessListItem}, 8 | }, 9 | BigEndianHash, Bytes, Eip1559TransactionRequest, H256, 10 | }, 11 | }; 12 | use foundry_evm::{ 13 | executor::{rU256, B160}, 14 | utils::{b160_to_h160, h160_to_b160, ru256_to_u256, u256_to_ru256}, 15 | }; 16 | 17 | /// Sign eip1559 transactions 18 | pub async fn sign_eip1559( 19 | tx: Eip1559TransactionRequest, 20 | signer_wallet: &LocalWallet, 21 | ) -> Result { 22 | let tx_typed = TypedTransaction::Eip1559(tx); 23 | let signed_frontrun_tx_sig = signer_wallet 24 | .sign_transaction(&tx_typed) 25 | .await 26 | .map_err(|e| anyhow!("Failed to sign eip1559 request: {:?}", e))?; 27 | 28 | Ok(tx_typed.rlp_signed(&signed_frontrun_tx_sig)) 29 | } 30 | 31 | /// convert revm access list to ethers access list 32 | pub fn access_list_to_ethers(access_list: Vec<(B160, Vec)>) -> AccessList { 33 | AccessList::from( 34 | access_list 35 | .into_iter() 36 | .map(|(address, slots)| AccessListItem { 37 | address: b160_to_h160(address), 38 | storage_keys: slots 39 | .into_iter() 40 | .map(|y| H256::from_uint(&ru256_to_u256(y))) 41 | .collect(), 42 | }) 43 | .collect::>(), 44 | ) 45 | } 46 | 47 | /// convert ethers access list to revm access list 48 | pub fn access_list_to_revm(access_list: AccessList) -> Vec<(B160, Vec)> { 49 | access_list 50 | .0 51 | .into_iter() 52 | .map(|x| { 53 | ( 54 | h160_to_b160(x.address), 55 | x.storage_keys 56 | .into_iter() 57 | .map(|y| u256_to_ru256(y.0.into())) 58 | .collect(), 59 | ) 60 | }) 61 | .collect() 62 | } 63 | 64 | // 65 | // -- Logging Macros -- 66 | // 67 | #[macro_export] 68 | macro_rules! log_info_cyan { 69 | ($($arg:tt)*) => { 70 | info!("{}", format_args!($($arg)*).to_string().cyan()); 71 | }; 72 | } 73 | 74 | #[macro_export] 75 | macro_rules! log_not_sandwichable { 76 | ($($arg:tt)*) => { 77 | info!("{}", format_args!($($arg)*).to_string().yellow()) 78 | }; 79 | } 80 | 81 | #[macro_export] 82 | macro_rules! log_opportunity { 83 | ($meats:expr, $optimal_input:expr, $revenue:expr) => {{ 84 | info!("\n{}", "[OPPORTUNITY DETECTED]".green().on_black().bold()); 85 | info!( 86 | "{}", 87 | format!("meats: {}", $meats.to_string().green().on_black()).bold() 88 | ); 89 | info!( 90 | "{}", 91 | format!( 92 | "optimal_input: {} wETH", 93 | $optimal_input.to_string().green().on_black() 94 | ) 95 | .bold() 96 | ); 97 | info!( 98 | "{}", 99 | format!( 100 | "revenue : {} wETH", 101 | $revenue.to_string().green().on_black() 102 | ) 103 | .bold() 104 | ); 105 | }}; 106 | } 107 | 108 | #[macro_export] 109 | macro_rules! startup_info_log { 110 | ($($arg:tt)*) => { 111 | info!("{}", format_args!($($arg)*).to_string().on_black().yellow().bold()); 112 | }; 113 | } 114 | 115 | #[macro_export] 116 | macro_rules! log_error { 117 | ($($arg:tt)*) => { 118 | error!("{}", format_args!($($arg)*).to_string().red()); 119 | }; 120 | } 121 | 122 | #[macro_export] 123 | macro_rules! log_new_block_info { 124 | ($new_block:expr) => { 125 | log::info!( 126 | "{}", 127 | format!( 128 | "\nFound New Block\nLatest Block: (number:{:?}, timestamp:{:?}, basefee:{:?})", 129 | $new_block.number, $new_block.timestamp, $new_block.base_fee_per_gas, 130 | ) 131 | .bright_purple() 132 | .on_black() 133 | ); 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/simulator/salmonella_inspector.rs: -------------------------------------------------------------------------------- 1 | use foundry_evm::{ 2 | executor::InstructionResult, 3 | revm::{ 4 | interpreter::{opcode, Interpreter}, 5 | Database, EVMData, Inspector, 6 | }, 7 | }; 8 | 9 | pub enum IsSandoSafu { 10 | Safu, 11 | NotSafu(Vec), 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct OpCode { 16 | name: String, 17 | code: u8, 18 | } 19 | 20 | impl OpCode { 21 | // creat a new opcode instance from numeric opcode 22 | // 23 | // Arguments: 24 | // * `code`: numberic opcode 25 | // 26 | // Returns: 27 | // `OpCode`: new opcode instance 28 | fn new_from_code(code: u8) -> Self { 29 | let name = match opcode::OPCODE_JUMPMAP[code as usize] { 30 | Some(name) => name.to_string(), 31 | None => "UNKNOWN".to_string(), 32 | }; 33 | 34 | OpCode { code, name } 35 | } 36 | } 37 | 38 | pub struct SalmonellaInspectoooor { 39 | suspicious_opcodes: Vec, 40 | gas_opcode_counter: u64, 41 | call_opcode_counter: u64, 42 | } 43 | 44 | impl SalmonellaInspectoooor { 45 | // create new salmonella inspector 46 | pub fn new() -> Self { 47 | Self { 48 | suspicious_opcodes: Vec::new(), 49 | gas_opcode_counter: 0, 50 | call_opcode_counter: 0, 51 | } 52 | } 53 | 54 | // checks if opportunity is safu 55 | // 56 | // Arguments: 57 | // `self`: consumes self during calculation 58 | // 59 | // Returns: 60 | // IsSandoSafu: enum that is either Safu or NotSafu 61 | pub fn is_sando_safu(self) -> IsSandoSafu { 62 | // if more gas opcodes used then call then we know that the contract is checking gas_used 63 | let mut suspicious_opcodes = self.suspicious_opcodes.clone(); 64 | if self.gas_opcode_counter < self.call_opcode_counter { 65 | let gas_opcode = OpCode::new_from_code(opcode::GAS); 66 | suspicious_opcodes.insert(0, gas_opcode); 67 | } 68 | 69 | match self.suspicious_opcodes.len() == 0 { 70 | true => IsSandoSafu::Safu, 71 | false => IsSandoSafu::NotSafu(suspicious_opcodes), 72 | } 73 | } 74 | } 75 | 76 | impl Inspector for SalmonellaInspectoooor { 77 | // get opcode by calling `interp.contract.opcode(interp.program_counter())`. 78 | // all other information can be obtained from interp. 79 | fn step( 80 | &mut self, 81 | interp: &mut Interpreter, 82 | _data: &mut EVMData<'_, DB>, 83 | _is_static: bool, 84 | ) -> InstructionResult { 85 | let executed_opcode = interp.current_opcode(); 86 | 87 | let mut add_suspicious = |opcode: OpCode| self.suspicious_opcodes.push(opcode); 88 | let mut increment_call_counter = || self.call_opcode_counter += 1; 89 | 90 | let executed_opcode = OpCode::new_from_code(executed_opcode); 91 | 92 | match executed_opcode.code { 93 | // these opcodes can be used to divert execution flow when ran locally vs on mainnet 94 | // extra safe version, can easily ignore half of these checks if ur up for it 95 | opcode::BALANCE => add_suspicious(executed_opcode.clone()), 96 | opcode::GASPRICE => add_suspicious(executed_opcode.clone()), 97 | opcode::EXTCODEHASH => add_suspicious(executed_opcode.clone()), 98 | opcode::BLOCKHASH => add_suspicious(executed_opcode.clone()), 99 | opcode::COINBASE => add_suspicious(executed_opcode.clone()), 100 | opcode::DIFFICULTY => add_suspicious(executed_opcode.clone()), 101 | opcode::GASLIMIT => add_suspicious(executed_opcode.clone()), 102 | opcode::SELFBALANCE => add_suspicious(executed_opcode.clone()), 103 | opcode::BASEFEE => add_suspicious(executed_opcode.clone()), 104 | opcode::CREATE => add_suspicious(executed_opcode.clone()), 105 | opcode::CREATE2 => add_suspicious(executed_opcode.clone()), 106 | opcode::SELFDESTRUCT => add_suspicious(executed_opcode.clone()), 107 | // add one to call counter 108 | opcode::CALL => increment_call_counter(), 109 | opcode::DELEGATECALL => increment_call_counter(), 110 | opcode::STATICCALL => increment_call_counter(), 111 | // add one to gas opcode counter 112 | opcode::GAS => self.gas_opcode_counter += 1, 113 | _ => { /* this opcode is safu */ } 114 | } 115 | 116 | if &executed_opcode.name == "UNKNOWN" { 117 | add_suspicious(executed_opcode); 118 | } 119 | 120 | InstructionResult::Continue 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /bot/crates/artemis-core/src/engine.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::broadcast::{self, Sender}; 2 | use tokio::task::JoinSet; 3 | use tokio_stream::StreamExt; 4 | use tracing::{error, info}; 5 | 6 | use crate::types::{Collector, Executor, Strategy}; 7 | 8 | /// The main engine of Artemis. This struct is responsible for orchestrating the 9 | /// data flow between collectors, strategies, and executors. 10 | pub struct Engine { 11 | /// The set of collectors that the engine will use to collect events. 12 | collectors: Vec>>, 13 | 14 | /// The set of strategies that the engine will use to process events. 15 | strategies: Vec>>, 16 | 17 | /// The set of executors that the engine will use to execute actions. 18 | executors: Vec>>, 19 | } 20 | 21 | impl Engine { 22 | pub fn new() -> Self { 23 | Self { 24 | collectors: vec![], 25 | strategies: vec![], 26 | executors: vec![], 27 | } 28 | } 29 | } 30 | 31 | impl Default for Engine { 32 | fn default() -> Self { 33 | Self::new() 34 | } 35 | } 36 | 37 | impl Engine 38 | where 39 | E: Send + Clone + 'static + std::fmt::Debug, 40 | A: Send + Clone + 'static + std::fmt::Debug, 41 | { 42 | /// Adds a collector to be used by the engine. 43 | pub fn add_collector(&mut self, collector: Box>) { 44 | self.collectors.push(collector); 45 | } 46 | 47 | /// Adds a strategy to be used by the engine. 48 | pub fn add_strategy(&mut self, strategy: Box>) { 49 | self.strategies.push(strategy); 50 | } 51 | 52 | /// Adds an executor to be used by the engine. 53 | pub fn add_executor(&mut self, executor: Box>) { 54 | self.executors.push(executor); 55 | } 56 | 57 | /// The core run loop of the engine. This function will spawn a thread for 58 | /// each collector, strategy, and executor. It will then orchestrate the 59 | /// data flow between them. 60 | pub async fn run(self) -> Result, Box> { 61 | let (event_sender, _): (Sender, _) = broadcast::channel(512); 62 | let (action_sender, _): (Sender, _) = broadcast::channel(512); 63 | 64 | let mut set = JoinSet::new(); 65 | 66 | // Spawn executors in separate threads. 67 | for executor in self.executors { 68 | let mut receiver = action_sender.subscribe(); 69 | set.spawn(async move { 70 | info!("starting executor... "); 71 | loop { 72 | match receiver.recv().await { 73 | Ok(action) => match executor.execute(action).await { 74 | Ok(_) => {} 75 | Err(e) => error!("error executing action: {}", e), 76 | }, 77 | Err(e) => error!("error receiving action: {}", e), 78 | } 79 | } 80 | }); 81 | } 82 | 83 | // Spawn strategies in separate threads. 84 | for mut strategy in self.strategies { 85 | let mut event_receiver = event_sender.subscribe(); 86 | let action_sender = action_sender.clone(); 87 | strategy.sync_state().await?; 88 | 89 | set.spawn(async move { 90 | info!("starting strategy... "); 91 | loop { 92 | match event_receiver.recv().await { 93 | Ok(event) => { 94 | if let Some(action) = strategy.process_event(event).await { 95 | match action_sender.send(action) { 96 | Ok(_) => {} 97 | Err(e) => error!("error sending action: {}", e), 98 | } 99 | } 100 | } 101 | Err(e) => error!("error receiving event: {}", e), 102 | } 103 | } 104 | }); 105 | } 106 | 107 | // Spawn collectors in separate threads. 108 | for collector in self.collectors { 109 | let event_sender = event_sender.clone(); 110 | set.spawn(async move { 111 | info!("starting collector... "); 112 | let mut event_stream = collector.get_event_stream().await.unwrap(); 113 | while let Some(event) = event_stream.next().await { 114 | match event_sender.send(event) { 115 | Ok(_) => {} 116 | Err(e) => error!("error sending event: {}", e), 117 | } 118 | } 119 | }); 120 | } 121 | 122 | Ok(set) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /bot/README.md: -------------------------------------------------------------------------------- 1 | # Rusty-Sando/Bot ![license](https://img.shields.io/badge/License-MIT-green.svg?label=license) 2 | 3 | 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/mouseless-eth/rusty-sando/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. 4 | 5 | Performing EVM simulations in this way allows the bot to detect sandwichable opportunities against any tx that introduces slippage. 6 | 7 | ## Logic Breakdown 8 | - At startup, index all pools from a specific factory by parsing the `PairCreated` event. And fetch all token dust stored on sando addy. 9 | - Read and decode tx from mempool. 10 | - Send tx to [`trace_call`](https://openethereum.github.io/JSONRPC-trace-module#trace_call) to obtain `stateDiff`. 11 | - Check if `statediff` contains keys that equal to indexed pool addresses. 12 | - For each pool that tx touches: 13 | - Find the optimal amount in for a sandwich attack by performing a concurrent binary search. 14 | - Check for salmonella by checking if tx uses unconventional opcodes. 15 | - If profitable after gas calculations, send the sando bundle to relays. 16 | 17 | ## Usage 18 | 19 | 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. 20 | 21 | 2. [Install Rust](https://www.rust-lang.org/tools/install) if you haven't already. 22 | 23 | 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. 24 | > 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). 25 | 26 | 4. Copy `.env.example` into `.env` and fill out values. 27 | 28 | ```console 29 | cp .env.example .env 30 | ``` 31 | 32 | ``` 33 | WSS_RPC=ws://localhost:8545 34 | SEARCHER_PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000001 35 | FLASHBOTS_AUTH_KEY=0000000000000000000000000000000000000000000000000000000000000002 36 | SANDWICH_CONTRACT=0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa 37 | SANDWICH_INCEPTION_BLOCK=... 38 | ``` 39 | 40 | 5. Run the integration tests 41 | 42 | ```console 43 | cargo test -p strategy --release --features debug 44 | ``` 45 | 46 | 6. Run the bot in `debug mode` 47 | Test bot's sandwich finding functionality without a deployed or funded Sando contract (no bundles will be sent) 48 | 49 | ``` 50 | cargo run --release --features debug 51 | ``` 52 | 53 | 7. Running the bot 54 | 55 | ```console 56 | cargo run --release 57 | ``` 58 | > **Warning** 59 | > 60 | > **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. 61 | 62 | ## Improvements 63 | 64 | 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: 65 | 66 | - Stable coin pair sandwiches. 67 | - 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). 68 | - 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). 69 | - 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). 70 | - Longtail sandwiches to target TOKEN->(WETH or STABLE) swaps by pre buying and holding token. 71 | - Sandwiches that include a user's token approval tx + swap tx in one bundle. 72 | - Sandwiches that include a user's pending tx/s + swap tx in one bundle if swap tx nonce is higher than pending tx. 73 | -------------------------------------------------------------------------------- /contract/test/misc/SandoCommon.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "./GeneralHelper.sol"; 5 | 6 | /// @title FiveByteUtils 7 | /// @author 0xmouseless 8 | /// @notice Holds data and functions related to five byte encoding 9 | /// @dev This is a lossy encoding system however the wei lost in encoding is minamal and can be ignored 10 | /// @dev Encoding schema: fits any uint256 (32 byte value) into 5 bytes. 4 bytes reserved for a value, 1 byte reserved for storage slot to store the 4 byte value in. 11 | library FiveBytesEncodingUtils { 12 | struct EncodingMetaData { 13 | /// @notice TargetValue squashed down to four bytes 14 | uint32 fourBytes; 15 | /// @notice How many byte shifts to apply on our four bytes 16 | uint8 byteShift; 17 | } 18 | 19 | /** 20 | * @notice Encodes a value to 5 bytes of calldata (used for other token value) 21 | * 22 | * @param amount The amount to be encoded 23 | * @return encodingParams Parameters used for encoding the given input 24 | */ 25 | function encode(uint256 amount) public pure returns (EncodingMetaData memory encodingParams) { 26 | uint8 byteShift = 0; // how many byte shifts are needed to store value into four bytes? 27 | uint32 fourByteValue = 0; 28 | 29 | while (byteShift < 32) { 30 | uint256 _encodedAmount = amount / 2 ** (8 * byteShift); 31 | 32 | // If we can fit the value in 4 bytes, we can encode it 33 | if (_encodedAmount <= 2 ** (4 * (8)) - 1) { 34 | fourByteValue = uint32(_encodedAmount); 35 | break; 36 | } 37 | 38 | byteShift++; 39 | } 40 | 41 | encodingParams = EncodingMetaData({fourBytes: fourByteValue, byteShift: byteShift}); 42 | } 43 | 44 | /** 45 | * @notice Decodes the 5 bytes back to a 32 byte value (lossy) 46 | * 47 | * @param params Parameters used for the encoded value 48 | * @return decodedValue The decoded value after applying the byte shifts 49 | */ 50 | function decode(EncodingMetaData memory params) public pure returns (uint256 decodedValue) { 51 | decodedValue = uint256(params.fourBytes) << (uint256(params.byteShift) * 8); 52 | } 53 | 54 | /** 55 | * @notice Finalize by encoding for a specific param index 56 | * 57 | * @param encodingParams Metadata used for the encoded value 58 | * @param paramIndex Which param index should we encode to 59 | * @return fiveBytes The final five bytes used in calldata 60 | */ 61 | function finalzeForParamIndex(EncodingMetaData calldata encodingParams, uint8 paramIndex) 62 | public 63 | pure 64 | returns (uint40 fiveBytes) 65 | { 66 | // 4 for function selector 67 | uint8 memLocation = 4 + 32 + (paramIndex * 32) - 4 - encodingParams.byteShift; 68 | 69 | bytes memory encodedBytes = abi.encodePacked(memLocation, encodingParams.fourBytes); 70 | assembly { 71 | fiveBytes := mload(add(encodedBytes, 0x5)) 72 | } 73 | } 74 | } 75 | 76 | /// @title WethEncodingUtils 77 | /// @author 0xmouseless 78 | /// @notice Holds data and functions related to encoding weth for use in `tx.value` 79 | /// @dev lossy encoding but it is okay to leave a small amount of wei in pool contract 80 | library WethEncodingUtils { 81 | /** 82 | * @notice Constant used for encoding WETH amount 83 | */ 84 | function encodeMultiple() public pure returns (uint256) { 85 | return 1e5; 86 | } 87 | 88 | /** 89 | * @notice Encodes a value 90 | */ 91 | function encode(uint256 amount) public pure returns (uint256) { 92 | return amount / encodeMultiple(); 93 | } 94 | 95 | /** 96 | * @notice decode by multiplying amount by weth constant 97 | */ 98 | function decode(uint256 amount) public pure returns (uint256 amountOut) { 99 | amountOut = amount * encodeMultiple(); 100 | } 101 | } 102 | 103 | /// @title SandoCommon 104 | /// @author 0xmouseless 105 | /// @notice Holds common methods between v2 and v3 sandos 106 | library SandoCommon { 107 | /** 108 | * @notice This function is used to look up the JUMPDEST for a given function name 109 | * @param functionName The name of the function we want to jump to 110 | * @return JUMPDEST location in bytecode 111 | */ 112 | function getJumpDestFromSig(string memory functionName) public pure returns (uint8) { 113 | uint8 startingIndex = 0x05; 114 | 115 | // array mapped in same order as on sando contract 116 | string[11] memory functionNames = [ 117 | "v2_backrun0", 118 | "v2_frontrun0", 119 | "v2_backrun1", 120 | "v2_frontrun1", 121 | "v3_backrun0", 122 | "v3_frontrun0", 123 | "v3_backrun1", 124 | "v3_frontrun1", 125 | "seppuku", 126 | "recoverEth", 127 | "recoverWeth" 128 | ]; 129 | 130 | // find index of associated JUMPDEST (sig) 131 | for (uint256 i = 0; i < functionNames.length; i++) { 132 | if (keccak256(abi.encodePacked(functionNames[i])) == keccak256(abi.encodePacked(functionName))) { 133 | return (uint8(i) * 5) + startingIndex; 134 | } 135 | } 136 | 137 | // not found (force jump to invalid JUMPDEST) 138 | return 0x00; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /contract/README.md: -------------------------------------------------------------------------------- 1 | # Rusty-Sando/Contract ![license](https://img.shields.io/badge/License-MIT-green.svg?label=license) 2 | 3 | Gas-optimized sando contract written in Huff to make use of unconventional gas optimizations. 4 | 5 | > Why not Yul? Yul does not give access to the stack or jump instructions. 6 | 7 | ## Gas Optimizations 8 | 9 | ### JUMPDEST Function Sig 10 | Instead of reserving 4 bytes for a function selector, store a JUMPDEST in the first byte of calldata and jump to it at the beginning of execution. Doing so allows us to jump to the code range 0x00-0xFF, so we fill that range with place holder JUMPDEST that point to the location of the associated function body. 11 | 12 | Example: 13 | ```as 14 | #define macro MAIN() = takes (0) returns (0) { 15 | // extract function selector (JUMPDEST encoding) 16 | push0 // [0x00] 17 | calldataload // [calldata] 18 | push0 // [0x00, calldata] 19 | byte // [jumplabel] 20 | jump // [] 21 | ``` 22 | 23 | > **Note** 24 | > JUMPDEST 0xfa is reserved to handle [UniswapV3 callback](https://docs.uniswap.org/contracts/v3/reference/core/interfaces/callback/IUniswapV3SwapCallback). 25 | 26 | ### Encoding WETH Value Using tx.value 27 | When dealing with WETH amounts, the amount is encoded by first dividing the value by 100000, and setting the divided value as `tx.value` when calling the contract. The contract then multiplies `tx.value` by 100000 to get the original amount. 28 | 29 | > The last 5 digits of the original value are lost after encoding, however, it is a small amount of wei and can be ignored. 30 | 31 | Example: 32 | ```as 33 | // setup calldata for swap(wethOut, 0, address(this), "") 34 | [V2_Swap_Sig] 0x00 mstore 35 | 0x0186a0 callvalue mul 0x04 mstore // original weth value is decoded here by doing `100000 * callvalue` 36 | 0x00 0x24 mstore 37 | address 0x44 mstore 38 | 0x80 0x64 mstore 39 | ``` 40 | 41 | ### Encoding Other Token Value Using 5 Bytes Of Calldata 42 | When dealing with the other token amount, the values can range significantly depending on the token decimal and total supply. To account for the full range, we encode by fitting the value into 4 bytes of calldata plus a byte shift. To decode, we byteshift the 4bytes to the left. 43 | 44 | We use byte shifts instead of bitshifts because we perform a byteshift by storing the 4bytes in memory N bytes to the left of its memory slot. 45 | 46 | To optimize further, instead of encoding the byteshift into our calldata, we encode the offset in memory such that when the 4bytes are stored, it will be N bytes from the left of its storage slot. [more details](https://github.com/mouseless-eth/rusty-sando/blob/3b17b30340f6ef3558be5e505e55a1eb2fe8ca36/contract/test/misc/SandoCommon.sol#L11). 47 | 48 | ### Hardcoded values 49 | Weth address is hardcoded into the contract and there are individual methods to handle when Weth is token0 or token1. 50 | 51 | ### Encode Packed 52 | All calldata is encoded by packing the values together. 53 | 54 | > **Note** 55 | > Free alfa: Might be able to optimize contract by eliminating unnecessary [memory expansions](https://www.evm.codes/about#memoryexpansion) by changing order that params are stored in memory. I did not account for this when writing the contract. 56 | 57 | ## Interface 58 | 59 | | JUMPDEST | Function Name | 60 | | :-------------: | :------------- | 61 | | 0x05 | V2 Backrun, Weth is Token0 and Output | 62 | | 0x0A | V2 Frontrun, Weth is Token0 and Input | 63 | | 0x0F | V2 Backrun, Weth is Token1 and Output | 64 | | 0x14 | V2 Frontrun, Weth is Token1 and Input | 65 | | 0x19 | V3 Backrun, Weth is Token0 and Output | 66 | | 0x1E | V3 Frontrun, Weth is Token0 and Input | 67 | | 0x23 | V3 Backrun (Weth is Token1 and Output) | 68 | | 0x28 | V3 Frontrun, Weth is Token1 and Input | 69 | | 0x2D | Seppuku (self-destruct) | 70 | | 0x32 | Recover Eth | 71 | | 0x37 | Recover Weth | 72 | | ... | ... | 73 | | 0xFA | UniswapV3 Callback | 74 | 75 | 76 | ## Calldata Encoding (Interface) 77 | ### Uniswap V2 Calldata Encoding Format 78 | 79 | #### Frontrun (weth is input) 80 | | Byte Length | Variable | 81 | | :-------------: | :------------- | 82 | | 1 | JUMPDEST | 83 | | 20 | PairAddress | 84 | | 1 | Where to store AmountOut | 85 | | 4 | EncodedAmountOut | 86 | 87 | #### Backrun(weth is output) 88 | | Byte Length | Variable | 89 | | :-------------: | :------------- | 90 | | 1 | JUMPDEST | 91 | | 20 | PairAddress | 92 | | 20 | TokenInAddress | 93 | | 1 | Where to store AmountIn | 94 | | 4 | EncodedAmountIn | 95 | 96 | ### Uniswap V3 Calldata Encoding Format 97 | 98 | #### Frontrun (weth is input) 99 | | Byte Length | Variable | 100 | | :-------------: | :------------- | 101 | | 1 | JUMPDEST | 102 | | 20 | PairAddress | 103 | | 32 | PoolKeyHash | 104 | > PoolKeyHash used to verify that msg.sender is a uniswawp v3 pool in callback (protection) 105 | 106 | #### Backrun (weth is output) 107 | | Byte Length | Variable | 108 | | :-------------: | :------------- | 109 | | 1 | JUMPDEST | 110 | | 20 | PairAddress | 111 | | 20 | TokenInAddress | 112 | | 32 | PoolKeyHash | 113 | | 1 | Where to store AmountIn | 114 | | 4 | EncodedAmountIn | 115 | 116 | > **Note** 117 | > PairAddress can be omitted from calldata because it can be derived from PoolKeyHash 118 | 119 | ## Running Tests 120 | ```console 121 | forge install 122 | forge test 123 | ``` 124 | -------------------------------------------------------------------------------- /contract/test/LilRouter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console2.sol"; 6 | import "v2-core/interfaces/IUniswapV2Pair.sol"; 7 | import "v2-core/interfaces/IUniswapV2Factory.sol"; 8 | import "v2-periphery/interfaces/IUniswapV2Router02.sol"; 9 | import "v3-periphery/interfaces/IQuoter.sol"; 10 | import "v3-core/interfaces/IUniswapV3Pool.sol"; 11 | import "solmate/tokens/WETH.sol"; 12 | 13 | import "../src/LilRouter.sol"; 14 | 15 | /// @title LilRouterTest 16 | /// @author 0xmouseless 17 | /// @notice Test suite for the LilRouter contract 18 | contract LilRouterTest is Test { 19 | address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 20 | IUniswapV2Factory uniV2Factory; 21 | IUniswapV2Router02 uniV2Router; 22 | IQuoter uniV3Quoter; 23 | LilRouter lilRouter; 24 | 25 | /// @notice Set up the testing suite 26 | function setUp() public { 27 | lilRouter = new LilRouter(); 28 | 29 | uniV2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); 30 | uniV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); 31 | uniV3Quoter = IQuoter(0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6); 32 | WETH wrappedEther = WETH(payable(weth)); 33 | 34 | wrappedEther.deposit{value: 10e18}(); 35 | wrappedEther.transfer(address(lilRouter), 10e18); 36 | } 37 | 38 | /// @notice Test swapping weth to usdc and back 39 | function testUniswapV3() public { 40 | address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 41 | address usdcWethPool = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 500 fee pool 42 | 43 | // swapping 2 weth to usdc 44 | int256 amountIn = 2 ether; 45 | uint256 amountOutExpected = _quoteV3Swap(amountIn, usdcWethPool, weth, usdc); 46 | (uint256 amountOut,) = lilRouter.calculateSwapV3(amountIn, usdcWethPool, weth, usdc); 47 | console2.log("swapped %d WETH for %d USDC", uint256(amountIn), amountOut); 48 | assertEq( 49 | amountOutExpected, amountOut, "WETH->USDC swap failed: received USDC deviates from expected router output." 50 | ); 51 | 52 | // swapping received usdc back to weth 53 | amountIn = int256(amountOut); 54 | amountOutExpected = _quoteV3Swap(amountIn, usdcWethPool, usdc, weth); 55 | (amountOut,) = lilRouter.calculateSwapV3(amountIn, usdcWethPool, usdc, weth); 56 | console2.log("swapped %d USDC for %d WETH", uint256(amountIn), amountOut); 57 | assertEq( 58 | amountOutExpected, amountOut, "USDC->WETH swap failed: received WETH deviates from expected router output." 59 | ); 60 | } 61 | 62 | /// @notice Test swapping weth to usdc and back 63 | function testUniswapV2() public { 64 | address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 65 | address usdcWethPair = _getPairUniV2(usdc, address(weth)); 66 | 67 | // swapping 2 weth to usdc 68 | uint256 amountIn = 2 ether; 69 | uint256 amountOutExpected = _quoteV2Swap(amountIn, usdcWethPair, weth < usdc); 70 | (uint256 amountOut,) = lilRouter.calculateSwapV2(amountIn, usdcWethPair, weth, usdc); 71 | console2.log("swapped %d WETH for %d USDC", amountIn, amountOut); 72 | assertEq( 73 | amountOutExpected, amountOut, "WETH->USDC swap failed: received USDC deviates from expected router output." 74 | ); 75 | 76 | // swapping received usdc back to weth 77 | amountIn = amountOut; 78 | amountOutExpected = _quoteV2Swap(amountIn, usdcWethPair, usdc < weth); 79 | (amountOut,) = lilRouter.calculateSwapV2(amountIn, usdcWethPair, usdc, weth); 80 | console2.log("swapped %d USDC for %d WETH", amountIn, amountOut); 81 | assertEq( 82 | amountOutExpected, amountOut, "USDC->WETH swap failed: received WETH deviates from expected router output." 83 | ); 84 | } 85 | 86 | /// @notice Get the deployed LilRouter bytecode (we inject this into evm instances for simulations) 87 | function testGetLilRouterCode() public { 88 | bytes memory code = address(lilRouter).code; 89 | emit log_bytes(code); 90 | } 91 | 92 | // ------------- 93 | // -- HELPERS -- 94 | // ------------- 95 | function _quoteV3Swap(int256 amountIn, address _pool, address tokenIn, address tokenOut) 96 | private 97 | returns (uint256 amountOut) 98 | { 99 | IUniswapV3Pool pool = IUniswapV3Pool(_pool); 100 | 101 | // wether tokenIn is token0 or token1 102 | bool zeroForOne = tokenIn < tokenOut; 103 | // From docs: The Q64.96 sqrt price limit. If zero for one, 104 | // The price cannot be less than this value after the swap. 105 | // If one for zero, the price cannot be greater than this value after the swap 106 | uint160 sqrtPriceLimitX96 = (zeroForOne ? 4295128749 : 1461446703485210103287273052203988822378723970341); 107 | 108 | amountOut = 109 | uniV3Quoter.quoteExactInputSingle(tokenIn, tokenOut, pool.fee(), uint256(amountIn), sqrtPriceLimitX96); 110 | } 111 | 112 | function _quoteV2Swap(uint256 amountIn, address pair, bool isInputToken0) 113 | private 114 | view 115 | returns (uint256 amountOut) 116 | { 117 | (uint256 reserveIn, uint256 reserveOut,) = IUniswapV2Pair(pair).getReserves(); 118 | 119 | if (!isInputToken0) { 120 | // reserveIn is token1 121 | (reserveIn, reserveOut) = (reserveOut, reserveIn); 122 | } 123 | 124 | amountOut = uniV2Router.getAmountOut(amountIn, reserveIn, reserveOut); 125 | } 126 | 127 | function _getPairUniV2(address tokenA, address tokenB) private view returns (address pair) { 128 | pair = uniV2Factory.getPair(tokenA, tokenB); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/IERC20.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /contract/src/LilRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "v2-core/interfaces/IUniswapV2Pair.sol"; 5 | import "v2-periphery/interfaces/IUniswapV2Router02.sol"; 6 | import "v3-core/interfaces/IUniswapV3Pool.sol"; 7 | import "solmate/tokens/ERC20.sol"; 8 | 9 | /// @title LilRouter 10 | /// @author 0xmouseless 11 | /// @notice Minimal swap router used to sim V2/V3 swaps (and account for taxed tokens) 12 | contract LilRouter { 13 | /** 14 | * @notice Performs a token swap on a v2 pool 15 | * @return amountOut Expected output tokens from the swap 16 | * @return realAfterBalance Post-swap balance, accounting for token tax 17 | */ 18 | function calculateSwapV2(uint256 amountIn, address targetPair, address inputToken, address outputToken) 19 | external 20 | returns (uint256 amountOut, uint256 realAfterBalance) 21 | { 22 | ////////////////////////////////////// 23 | // SETUP // 24 | ////////////////////////////////////// 25 | 26 | // Optimistically send amountIn of inputToken to targetPair 27 | ERC20(inputToken).transfer(targetPair, amountIn); 28 | 29 | ////////////////////////////////////// 30 | // CALCULATING OUR EXPECTED OUTPUT // 31 | ////////////////////////////////////// 32 | 33 | // Prepare variables for calculating expected amount out 34 | uint256 reserveIn; 35 | uint256 reserveOut; 36 | 37 | { 38 | // Avoid stack too deep error 39 | (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(targetPair).getReserves(); 40 | 41 | // sort reserves 42 | if (inputToken < outputToken) { 43 | // Token0 is equal to inputToken 44 | // Token1 is equal to outputToken 45 | reserveIn = reserve0; 46 | reserveOut = reserve1; 47 | } else { 48 | // Token0 is equal to outputToken 49 | // Token1 is equal to inputToken 50 | reserveIn = reserve1; 51 | reserveOut = reserve0; 52 | } 53 | } 54 | 55 | ////////////////////////////////////// 56 | // PERFORMING SWAP // 57 | ////////////////////////////////////// 58 | 59 | // Find the actual amountIn sent to pair (accounts for tax if any) and amountOut 60 | uint256 actualAmountIn = ERC20(inputToken).balanceOf(address(targetPair)) - reserveIn; 61 | amountOut = _getAmountOut(actualAmountIn, reserveIn, reserveOut); 62 | 63 | // Prepare swap variables and call pair.swap() 64 | (uint256 amount0Out, uint256 amount1Out) = 65 | inputToken < outputToken ? (uint256(0), amountOut) : (amountOut, uint256(0)); 66 | IUniswapV2Pair(targetPair).swap(amount0Out, amount1Out, address(this), new bytes(0)); 67 | 68 | // Find real balance after (accounts for taxed tokens) 69 | realAfterBalance = ERC20(outputToken).balanceOf(address(this)); 70 | } 71 | 72 | /** 73 | * @notice Performs a token swap on a v3 pool 74 | * @return amountOut Expected output tokens from the swap 75 | * @return realAfterBalance Post-swap balance, accounting for token tax 76 | */ 77 | function calculateSwapV3(int256 amountIn, address targetPoolAddress, address inputToken, address outputToken) 78 | public 79 | returns (uint256 amountOut, uint256 realAfterBalance) 80 | { 81 | IUniswapV3Pool targetPool = IUniswapV3Pool(targetPoolAddress); 82 | // wether tokenIn is token0 or token1 83 | bool zeroForOne = inputToken < outputToken; 84 | // From docs: The Q64.96 sqrt price limit. If zero for one, 85 | // The price cannot be less than this value after the swap. 86 | // If one for zero, the price cannot be greater than this value after the swap 87 | uint160 sqrtPriceLimitX96 = (zeroForOne ? 4295128749 : 1461446703485210103287273052203988822378723970341); 88 | 89 | // Data used for callback 90 | bytes memory data = abi.encode(zeroForOne, inputToken); 91 | 92 | // Make swap and calc amountOut 93 | (int256 amount0, int256 amount1) = targetPool.swap(address(this), zeroForOne, amountIn, sqrtPriceLimitX96, data); 94 | amountOut = uint256(-(zeroForOne ? amount1 : amount0)); 95 | 96 | // Find real balance after (accounts for taxed tokens) 97 | realAfterBalance = ERC20(outputToken).balanceOf(address(this)); 98 | } 99 | 100 | /** 101 | * @notice Post swap callback to sends amount of input token to v3 pool 102 | */ 103 | function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external { 104 | require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported 105 | (bool isZeroForOne, address inputToken) = abi.decode(_data, (bool, address)); 106 | 107 | if (isZeroForOne) { 108 | ERC20(inputToken).transfer(msg.sender, uint256(amount0Delta)); 109 | } else { 110 | ERC20(inputToken).transfer(msg.sender, uint256(amount1Delta)); 111 | } 112 | } 113 | 114 | /** 115 | * @notice Helper to find output amount from xy=k 116 | * @dev Note that fees are hardcoded to 0.3% (default for sushi and uni) 117 | * @return amountOut Output tokens expected from swap 118 | */ 119 | function _getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) 120 | internal 121 | pure 122 | returns (uint256 amountOut) 123 | { 124 | require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT"); 125 | require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY"); 126 | uint256 amountInWithFee = amountIn * 997; 127 | uint256 numerator = amountInWithFee * reserveOut; 128 | uint256 denominator = reserveIn * 1000 + amountInWithFee; 129 | amountOut = numerator / denominator; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/managers/pool_manager.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use cfmms::{ 3 | checkpoint::sync_pools_from_checkpoint, 4 | dex::{Dex, DexVariant}, 5 | pool::Pool, 6 | sync::sync_pairs, 7 | }; 8 | use colored::Colorize; 9 | use dashmap::DashMap; 10 | use ethers::{ 11 | abi, 12 | providers::Middleware, 13 | types::{Address, BlockNumber, Diff, TraceType, Transaction, H160, H256, U256}, 14 | }; 15 | use log::info; 16 | use std::{path::Path, str::FromStr, sync::Arc}; 17 | 18 | use crate::{constants::WETH_ADDRESS, startup_info_log}; 19 | 20 | pub(crate) struct PoolManager { 21 | /// Provider 22 | provider: Arc, 23 | /// Sandwichable pools 24 | pools: DashMap, 25 | /// Which dexes to monitor 26 | dexes: Vec, 27 | } 28 | 29 | impl PoolManager { 30 | /// Gets state of all pools 31 | pub async fn setup(&mut self) -> Result<()> { 32 | let checkpoint_path = ".cfmms-checkpoint.json"; 33 | 34 | let checkpoint_exists = Path::new(checkpoint_path).exists(); 35 | 36 | let pools = if checkpoint_exists { 37 | let (_, pools) = 38 | sync_pools_from_checkpoint(checkpoint_path, 100000, self.provider.clone()).await?; 39 | pools 40 | } else { 41 | sync_pairs( 42 | self.dexes.clone(), 43 | self.provider.clone(), 44 | Some(checkpoint_path), 45 | ) 46 | .await? 47 | }; 48 | 49 | for pool in pools { 50 | self.pools.insert(pool.address(), pool); 51 | } 52 | 53 | startup_info_log!("pools synced: {}", self.pools.len()); 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Return a tx's touched pools 59 | // enhancement: record stable coin pairs to sandwich as well here 60 | pub async fn get_touched_sandwichable_pools( 61 | &self, 62 | victim_tx: &Transaction, 63 | latest_block: BlockNumber, 64 | provider: Arc, 65 | ) -> Result> { 66 | // get victim tx state diffs 67 | let state_diffs = provider 68 | .trace_call(victim_tx, vec![TraceType::StateDiff], Some(latest_block)) 69 | .await? 70 | .state_diff 71 | .ok_or(anyhow!("not sandwichable, no state diffs produced"))? 72 | .0; 73 | 74 | // capture all addresses that have a state change and are also a `WETH` pool 75 | let touched_pools: Vec = state_diffs 76 | .keys() 77 | .filter_map(|e| self.pools.get(e).map(|p| (*p.value()).clone())) 78 | .filter(|e| match e { 79 | Pool::UniswapV2(p) => vec![p.token_a, p.token_b].contains(&WETH_ADDRESS), 80 | Pool::UniswapV3(p) => vec![p.token_a, p.token_b].contains(&WETH_ADDRESS), 81 | }) 82 | .collect(); 83 | 84 | // nothing to sandwich 85 | if touched_pools.is_empty() { 86 | return Ok(vec![]); 87 | } 88 | 89 | // find trade direction 90 | let weth_state_diff = &state_diffs 91 | .get(&WETH_ADDRESS) 92 | .ok_or(anyhow!("Missing WETH state diffs"))? 93 | .storage; 94 | 95 | let mut sandwichable_pools = vec![]; 96 | 97 | for pool in touched_pools { 98 | // find pool mapping location on WETH contract 99 | let storage_key = H256::from(ethers::utils::keccak256(abi::encode(&[ 100 | abi::Token::Address(pool.address()), 101 | abi::Token::Uint(U256::from(3)), // WETH balanceOf mapping is at index 3 102 | ]))); 103 | 104 | // in reality we also want to check stable coin pools 105 | if let Some(Diff::Changed(c)) = weth_state_diff.get(&storage_key) { 106 | let from = U256::from(c.from.to_fixed_bytes()); 107 | let to = U256::from(c.to.to_fixed_bytes()); 108 | 109 | // right now bot can only sandwich `weth->token` trades 110 | // enhancement: add support for `token->weth` trades (using longtail or flashswaps sandos) 111 | if to > from { 112 | sandwichable_pools.push(pool); 113 | } 114 | } 115 | } 116 | 117 | Ok(sandwichable_pools) 118 | } 119 | 120 | pub fn new(provider: Arc) -> Self { 121 | let dexes_data = [ 122 | ( 123 | // Uniswap v2 124 | "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", 125 | DexVariant::UniswapV2, 126 | 10000835u64, 127 | ), 128 | ( 129 | // Sushiswap 130 | "0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac", 131 | DexVariant::UniswapV2, 132 | 10794229u64, 133 | ), 134 | ( 135 | // Crypto.com swap 136 | "0x9DEB29c9a4c7A88a3C0257393b7f3335338D9A9D", 137 | DexVariant::UniswapV2, 138 | 10828414u64, 139 | ), 140 | ( 141 | // Convergence swap 142 | "0x4eef5746ED22A2fD368629C1852365bf5dcb79f1", 143 | DexVariant::UniswapV2, 144 | 12385067u64, 145 | ), 146 | ( 147 | // Pancakeswap 148 | "0x1097053Fd2ea711dad45caCcc45EfF7548fCB362", 149 | DexVariant::UniswapV2, 150 | 15614590u64, 151 | ), 152 | ( 153 | // ShibaSwap 154 | "0x115934131916C8b277DD010Ee02de363c09d037c", 155 | DexVariant::UniswapV2, 156 | 12771526u64, 157 | ), 158 | ( 159 | // Saitaswap 160 | "0x35113a300ca0D7621374890ABFEAC30E88f214b1", 161 | DexVariant::UniswapV2, 162 | 15210780u64, 163 | ), 164 | ( 165 | // Uniswap v3 166 | "0x1F98431c8aD98523631AE4a59f267346ea31F984", 167 | DexVariant::UniswapV3, 168 | 12369621u64, 169 | ), 170 | ]; 171 | 172 | let dexes = dexes_data 173 | .into_iter() 174 | .map(|(address, variant, number)| { 175 | Dex::new(H160::from_str(address).unwrap(), variant, number, Some(300)) 176 | }) 177 | .collect(); 178 | 179 | Self { 180 | pools: DashMap::new(), 181 | provider, 182 | dexes, 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /bot/crates/strategy/tests/main.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc}; 2 | 3 | use cfmms::pool::{Pool, UniswapV2Pool, UniswapV3Pool}; 4 | use ethers::{ 5 | prelude::Lazy, 6 | providers::{Middleware, Provider, Ws}, 7 | types::{Address, Transaction, TxHash, U64}, 8 | }; 9 | use strategy::{ 10 | bot::SandoBot, 11 | types::{BlockInfo, RawIngredients, StratConfig}, 12 | }; 13 | 14 | // -- consts -- 15 | static WSS_RPC: &str = "ws://localhost:8545"; 16 | pub static WETH_ADDRESS: Lazy
= Lazy::new(|| { 17 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 18 | .parse() 19 | .unwrap() 20 | }); 21 | 22 | // -- utils -- 23 | fn setup_logger() { 24 | let _ = fern::Dispatch::new() 25 | .level(log::LevelFilter::Error) 26 | .level_for("strategy", log::LevelFilter::Info) 27 | .chain(std::io::stdout()) 28 | .apply(); 29 | } 30 | 31 | async fn setup_bot(provider: Arc>) -> SandoBot> { 32 | setup_logger(); 33 | 34 | let strat_config = StratConfig { 35 | sando_address: "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa" 36 | .parse() 37 | .unwrap(), 38 | sando_inception_block: U64::from(17700000), 39 | searcher_signer: "0x0000000000000000000000000000000000000000000000000000000000000001" 40 | .parse() 41 | .unwrap(), 42 | }; 43 | 44 | SandoBot::new(provider, strat_config) 45 | } 46 | 47 | async fn block_num_to_info(block_num: u64, provider: Arc>) -> BlockInfo { 48 | let block = provider.get_block(block_num).await.unwrap().unwrap(); 49 | block.try_into().unwrap() 50 | } 51 | 52 | fn hex_to_address(hex: &str) -> Address { 53 | hex.parse().unwrap() 54 | } 55 | 56 | async fn hex_to_univ2_pool(hex: &str, provider: Arc>) -> Pool { 57 | let pair_address = hex_to_address(hex); 58 | let pool = UniswapV2Pool::new_from_address(pair_address, provider) 59 | .await 60 | .unwrap(); 61 | Pool::UniswapV2(pool) 62 | } 63 | 64 | async fn hex_to_univ3_pool(hex: &str, provider: Arc>) -> Pool { 65 | let pair_address = hex_to_address(hex); 66 | let pool = UniswapV3Pool::new_from_address(pair_address, provider) 67 | .await 68 | .unwrap(); 69 | Pool::UniswapV3(pool) 70 | } 71 | 72 | async fn victim_tx_hash(tx: &str, provider: Arc>) -> Transaction { 73 | let tx_hash: TxHash = TxHash::from_str(tx).unwrap(); 74 | provider.get_transaction(tx_hash).await.unwrap().unwrap() 75 | } 76 | 77 | /// testing against: https://eigenphi.io/mev/ethereum/tx/0x292156c07794bc50952673bf948b90ab71148b81938b6ab4904096adb654d99a 78 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 79 | async fn can_sandwich_uni_v2() { 80 | let client = Arc::new(Provider::new(Ws::connect(WSS_RPC).await.unwrap())); 81 | 82 | let bot = setup_bot(client.clone()).await; 83 | 84 | let ingredients = RawIngredients::new( 85 | vec![ 86 | victim_tx_hash( 87 | "0xfecf2c78d1418e6905c18a6a6301c9d39b14e5320e345adce52baaecf805580d", 88 | client.clone(), 89 | ) 90 | .await, 91 | ], 92 | *WETH_ADDRESS, 93 | hex_to_address("0x3642Cf76c5894B4aB51c1080B2c4F5B9eA734106"), 94 | hex_to_univ2_pool("0x5d1dd0661E1D22697943C1F50Cc726eA3143329b", client.clone()).await, 95 | ); 96 | 97 | let target_block = block_num_to_info(17754167, client.clone()).await; 98 | 99 | let _ = bot 100 | .is_sandwichable(ingredients, target_block) 101 | .await 102 | .unwrap(); 103 | } 104 | 105 | /// testing against: https://eigenphi.io/mev/ethereum/tx/0x056ede919e31be59b7e1e8676b3be1272ce2bbd3d18f42317a26a3d1f2951fc8 106 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 107 | async fn can_sandwich_sushi_swap() { 108 | let client = Arc::new(Provider::new(Ws::connect(WSS_RPC).await.unwrap())); 109 | 110 | let bot = setup_bot(client.clone()).await; 111 | 112 | let ingredients = RawIngredients::new( 113 | vec![ 114 | victim_tx_hash( 115 | "0xb344fdc6a3b7c65c5dd971cb113567e2ee6d0636f261c3b8d624627b90694cdb", 116 | client.clone(), 117 | ) 118 | .await, 119 | ], 120 | *WETH_ADDRESS, 121 | hex_to_address("0x3b484b82567a09e2588A13D54D032153f0c0aEe0"), 122 | hex_to_univ2_pool("0xB84C45174Bfc6b8F3EaeCBae11deE63114f5c1b2", client.clone()).await, 123 | ); 124 | 125 | let target_block = block_num_to_info(16873148, client.clone()).await; 126 | 127 | let _ = bot 128 | .is_sandwichable(ingredients, target_block) 129 | .await 130 | .unwrap(); 131 | } 132 | 133 | /// testing against: https://eigenphi.io/mev/ethereum/tx/0xc132e351e8c7d3d8763a894512bd8a33e4ca60f56c0516f7a6cafd3128bd59bb 134 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 135 | async fn can_sandwich_multi_v2_swaps() { 136 | let client = Arc::new(Provider::new(Ws::connect(WSS_RPC).await.unwrap())); 137 | 138 | let bot = setup_bot(client.clone()).await; 139 | 140 | let ingredients = RawIngredients::new( 141 | vec![ 142 | victim_tx_hash( 143 | "0x4791d05bdd6765f036ff4ae44fc27099997417e3bdb053ecb52182bbfc7767c5", 144 | client.clone(), 145 | ) 146 | .await, 147 | victim_tx_hash( 148 | "0x923c9ba97fea8d72e60c14d1cc360a8e7d99dd4b31274928d6a79704a8546eda", 149 | client.clone(), 150 | ) 151 | .await, 152 | ], 153 | *WETH_ADDRESS, 154 | hex_to_address("0x31b16Ff7823096a227Aac78F1C094525A84ab64F"), 155 | hex_to_univ2_pool("0x657c6a08d49B4F0778f9cce1Dc49d196cFCe9d08", client.clone()).await, 156 | ); 157 | 158 | let target_block = block_num_to_info(16780625, client.clone()).await; 159 | 160 | let _ = bot 161 | .is_sandwichable(ingredients, target_block) 162 | .await 163 | .unwrap(); 164 | } 165 | 166 | /// testing against: https://eigenphi.io/mev/ethereum/tx/0x64158690880d053adc2c42fbadd1838bc6d726cb81982443be00f83b51d8c25d 167 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 168 | async fn can_sandwich_uni_v3() { 169 | let client = Arc::new(Provider::new(Ws::connect(WSS_RPC).await.unwrap())); 170 | 171 | let bot = setup_bot(client.clone()).await; 172 | 173 | let ingredients = RawIngredients::new( 174 | vec![ 175 | victim_tx_hash( 176 | "0x90dfe56814821e7f76f2e4970a7b35948670a968abffebb7be69fe528283e6d8", 177 | client.clone(), 178 | ) 179 | .await, 180 | ], 181 | *WETH_ADDRESS, 182 | hex_to_address("0x24C19F7101c1731b85F1127EaA0407732E36EcDD"), 183 | hex_to_univ3_pool("0x62CBac19051b130746Ec4CF96113aF5618F3A212", client.clone()).await, 184 | ); 185 | 186 | let target_block = block_num_to_info(16863225, client.clone()).await; 187 | 188 | let _ = bot 189 | .is_sandwichable(ingredients, target_block) 190 | .await 191 | .unwrap(); 192 | } 193 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/constants.rs: -------------------------------------------------------------------------------- 1 | use ethers::{ 2 | prelude::Lazy, 3 | types::{Address, Bytes, H256, U256}, 4 | }; 5 | use foundry_evm::revm::primitives::{B160 as rAddress, U256 as rU256}; 6 | 7 | pub static ONE_ETHER_IN_WEI: Lazy = Lazy::new(|| rU256::from(1000000000000000000_u128)); 8 | pub static WETH_FUND_AMT: Lazy = Lazy::new(|| rU256::from(69) * *ONE_ETHER_IN_WEI); 9 | 10 | // could generate random address to use at runtime 11 | pub static LIL_ROUTER_CONTROLLER: Lazy = Lazy::new(|| { 12 | "0xC0ff33C0ffeeC0ff33C0ffeeC0ff33C0ff33C0ff" 13 | .parse() 14 | .unwrap() 15 | }); 16 | 17 | // could generate random address to use at runtime 18 | pub static LIL_ROUTER_ADDRESS: Lazy = Lazy::new(|| { 19 | "0xDecafC0ffee15BadDecafC0ffee15BadDecafC0f" 20 | .parse() 21 | .unwrap() 22 | }); 23 | 24 | // could compile from `../contract` at runtime instead of parsing from string 25 | pub static LIL_ROUTER_CODE: Lazy = Lazy::new(|| { 26 | "0x608060405234801561001057600080fd5b50600436106100415760003560e01c80634b588d401461004657806381eeb93c14610072578063fa461e3314610085575b600080fd5b610059610054366004610743565b61009a565b6040805192835260208301919091520160405180910390f35b610059610080366004610743565b61021f565b610098610093366004610796565b6104e3565b005b600080846001600160a01b038085169086161082816100cd5773fffd8963efd1fc6a506488495d951d5263988d256100d4565b6401000276ad5b9050600082886040516020016100ff92919091151582526001600160a01b0316602082015260400190565b6040516020818303038152906040529050600080856001600160a01b031663128acb0830878f88886040518663ffffffff1660e01b8152600401610147959493929190610863565b60408051808303816000875af1158015610165573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610189919061089e565b9150915084610198578161019a565b805b6101a3906108d8565b6040516370a0823160e01b81523060048201529098506001600160a01b038a16906370a0823190602401602060405180830381865afa1580156101ea573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020e91906108f4565b965050505050505094509492505050565b60405163a9059cbb60e01b81526001600160a01b03848116600483015260248201869052600091829185169063a9059cbb906044016020604051808303816000875af1158015610273573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610297919061091b565b50600080600080886001600160a01b0316630902f1ac6040518163ffffffff1660e01b8152600401606060405180830381865afa1580156102dc573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610300919061095b565b506001600160701b031691506001600160701b03169150866001600160a01b0316886001600160a01b0316101561033c57819350809250610343565b8093508192505b50506040516370a0823160e01b81526001600160a01b03888116600483015260009184918916906370a0823190602401602060405180830381865afa158015610390573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103b491906108f4565b6103be91906109ab565b90506103cb818484610606565b9450600080876001600160a01b0316896001600160a01b0316106103f1578660006103f5565b6000875b6040805160008152602081019182905263022c0d9f60e01b90915291935091506001600160a01b038b169063022c0d9f9061043990859085903090602481016109c2565b600060405180830381600087803b15801561045357600080fd5b505af1158015610467573d6000803e3d6000fd5b50506040516370a0823160e01b81523060048201526001600160a01b038b1692506370a082319150602401602060405180830381865afa1580156104af573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104d391906108f4565b9550505050505094509492505050565b60008413806104f25750600083135b6104fb57600080fd5b60008061050a838501856109f9565b91509150811561058b5760405163a9059cbb60e01b8152336004820152602481018790526001600160a01b0382169063a9059cbb906044016020604051808303816000875af1158015610561573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610585919061091b565b506105fe565b60405163a9059cbb60e01b8152336004820152602481018690526001600160a01b0382169063a9059cbb906044016020604051808303816000875af11580156105d8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105fc919061091b565b505b505050505050565b60008084116106705760405162461bcd60e51b815260206004820152602b60248201527f556e697377617056324c6962726172793a20494e53554646494349454e545f4960448201526a1394155517d05353d5539560aa1b60648201526084015b60405180910390fd5b6000831180156106805750600082115b6106dd5760405162461bcd60e51b815260206004820152602860248201527f556e697377617056324c6962726172793a20494e53554646494349454e545f4c604482015267495155494449545960c01b6064820152608401610667565b60006106eb856103e5610a32565b905060006106f98483610a32565b905060008261070a876103e8610a32565b6107149190610a51565b90506107208183610a69565b979650505050505050565b6001600160a01b038116811461074057600080fd5b50565b6000806000806080858703121561075957600080fd5b84359350602085013561076b8161072b565b9250604085013561077b8161072b565b9150606085013561078b8161072b565b939692955090935050565b600080600080606085870312156107ac57600080fd5b8435935060208501359250604085013567ffffffffffffffff808211156107d257600080fd5b818701915087601f8301126107e657600080fd5b8135818111156107f557600080fd5b88602082850101111561080757600080fd5b95989497505060200194505050565b6000815180845260005b8181101561083c57602081850181015186830182015201610820565b8181111561084e576000602083870101525b50601f01601f19169290920160200192915050565b6001600160a01b0386811682528515156020830152604082018590528316606082015260a06080820181905260009061072090830184610816565b600080604083850312156108b157600080fd5b505080516020909101519092909150565b634e487b7160e01b600052601160045260246000fd5b6000600160ff1b82016108ed576108ed6108c2565b5060000390565b60006020828403121561090657600080fd5b5051919050565b801515811461074057600080fd5b60006020828403121561092d57600080fd5b81516109388161090d565b9392505050565b80516001600160701b038116811461095657600080fd5b919050565b60008060006060848603121561097057600080fd5b6109798461093f565b92506109876020850161093f565b9150604084015163ffffffff811681146109a057600080fd5b809150509250925092565b6000828210156109bd576109bd6108c2565b500390565b84815283602082015260018060a01b03831660408201526080606082015260006109ef6080830184610816565b9695505050505050565b60008060408385031215610a0c57600080fd5b8235610a178161090d565b91506020830135610a278161072b565b809150509250929050565b6000816000190483118215151615610a4c57610a4c6108c2565b500290565b60008219821115610a6457610a646108c2565b500190565b600082610a8657634e487b7160e01b600052601260045260246000fd5b50049056fea2646970667358221220a3830fddb415d84a0f9225a1e9bbeef724e1b5a2dc0efc456635debefba7af2c64736f6c634300080f0033" 27 | .parse() 28 | .unwrap() 29 | }); 30 | 31 | // funciton signature for getting reserves 32 | pub static GET_RESERVES_SIG: Lazy = Lazy::new(|| "0x0902f1ac".parse().unwrap()); 33 | 34 | pub static ERC20_TRANSFER_EVENT_SIG: Lazy = Lazy::new(|| { 35 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 36 | .parse() 37 | .unwrap() 38 | }); 39 | 40 | pub static WETH_ADDRESS: Lazy
= Lazy::new(|| { 41 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 42 | .parse() 43 | .unwrap() 44 | }); 45 | 46 | // when we need an address with a lot of eth 47 | pub static SUGAR_DADDY: Lazy
= Lazy::new(|| { 48 | "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" 49 | .parse() 50 | .unwrap() 51 | }); 52 | 53 | // could generate random address to use at runtime 54 | pub static COINBASE: Lazy = Lazy::new(|| { 55 | "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990" 56 | .parse() 57 | .unwrap() 58 | }); 59 | 60 | pub static DUST_OVERPAY: Lazy = Lazy::new(|| ethers::utils::parse_ether("0.00015").unwrap()); 61 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/IUniswapV2Pair.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0In","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1In","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount0Out","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1Out","type":"uint256"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"Swap","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint112","name":"reserve0","type":"uint112"},{"indexed":false,"internalType":"uint112","name":"reserve1","type":"uint112"}],"name":"Sync","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MINIMUM_LIQUIDITY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"burn","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_token0","type":"address"},{"internalType":"address","name":"_token1","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"kLast","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"mint","outputs":[{"internalType":"uint256","name":"liquidity","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"price0CumulativeLast","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"price1CumulativeLast","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"skim","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"amount0Out","type":"uint256"},{"internalType":"uint256","name":"amount1Out","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"swap","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"sync","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}] 2 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/bot/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use artemis_core::{collectors::block_collector::NewBlock, types::Strategy}; 3 | use async_trait::async_trait; 4 | use cfmms::pool::Pool::{UniswapV2, UniswapV3}; 5 | use colored::Colorize; 6 | use ethers::{providers::Middleware, types::Transaction}; 7 | use foundry_evm::executor::fork::{BlockchainDb, BlockchainDbMeta, SharedBackend}; 8 | use log::{error, info}; 9 | use std::{collections::BTreeSet, sync::Arc}; 10 | 11 | use crate::{ 12 | constants::WETH_ADDRESS, 13 | log_error, log_info_cyan, log_new_block_info, log_not_sandwichable, log_opportunity, 14 | managers::{ 15 | block_manager::BlockManager, pool_manager::PoolManager, 16 | sando_state_manager::SandoStateManager, 17 | }, 18 | simulator::{huff_sando::create_recipe, lil_router::find_optimal_input}, 19 | types::{Action, BlockInfo, Event, RawIngredients, SandoRecipe, StratConfig}, 20 | }; 21 | 22 | pub struct SandoBot { 23 | /// Ethers client 24 | provider: Arc, 25 | /// Keeps track of onchain pools 26 | pool_manager: PoolManager, 27 | /// Block manager 28 | block_manager: BlockManager, 29 | /// Keeps track of weth inventory & token dust 30 | sando_state_manager: SandoStateManager, 31 | } 32 | 33 | impl SandoBot { 34 | /// Create a new instance 35 | pub fn new(client: Arc, config: StratConfig) -> Self { 36 | Self { 37 | pool_manager: PoolManager::new(client.clone()), 38 | provider: client, 39 | block_manager: BlockManager::new(), 40 | sando_state_manager: SandoStateManager::new( 41 | config.sando_address, 42 | config.searcher_signer, 43 | config.sando_inception_block, 44 | ), 45 | } 46 | } 47 | 48 | /// Main logic for the strategy 49 | /// Checks if the passed `RawIngredients` is sandwichable 50 | pub async fn is_sandwichable( 51 | &self, 52 | ingredients: RawIngredients, 53 | target_block: BlockInfo, 54 | ) -> Result { 55 | // setup shared backend 56 | let shared_backend = SharedBackend::spawn_backend_thread( 57 | self.provider.clone(), 58 | BlockchainDb::new( 59 | BlockchainDbMeta { 60 | cfg_env: Default::default(), 61 | block_env: Default::default(), 62 | hosts: BTreeSet::from(["".to_string()]), 63 | }, 64 | None, 65 | ), /* default because not accounting for this atm */ 66 | Some((target_block.number - 1).into()), 67 | ); 68 | 69 | let weth_inventory = if cfg!(feature = "debug") { 70 | // spoof weth balance when the debug feature is active 71 | (*crate::constants::WETH_FUND_AMT).into() 72 | } else { 73 | self.sando_state_manager.get_weth_inventory() 74 | }; 75 | 76 | let optimal_input = find_optimal_input( 77 | &ingredients, 78 | &target_block, 79 | weth_inventory, 80 | shared_backend.clone(), 81 | ) 82 | .await?; 83 | 84 | let recipe = create_recipe( 85 | &ingredients, 86 | &target_block, 87 | optimal_input, 88 | weth_inventory, 89 | self.sando_state_manager.get_searcher_address(), 90 | self.sando_state_manager.get_sando_address(), 91 | shared_backend, 92 | )?; 93 | 94 | log_opportunity!( 95 | ingredients.print_meats(), 96 | optimal_input.as_u128() as f64 / 1e18, 97 | recipe.get_revenue().as_u128() as f64 / 1e18 98 | ); 99 | 100 | Ok(recipe) 101 | } 102 | } 103 | 104 | #[async_trait] 105 | impl Strategy for SandoBot { 106 | /// Setup by getting all pools to monitor for swaps 107 | async fn sync_state(&mut self) -> Result<()> { 108 | self.pool_manager.setup().await?; 109 | self.sando_state_manager 110 | .setup(self.provider.clone()) 111 | .await?; 112 | self.block_manager.setup(self.provider.clone()).await?; 113 | Ok(()) 114 | } 115 | 116 | /// Process incoming events 117 | async fn process_event(&mut self, event: Event) -> Option { 118 | match event { 119 | Event::NewBlock(block) => match self.process_new_block(block).await { 120 | Ok(_) => None, 121 | Err(e) => { 122 | panic!("strategy is out of sync {}", e); 123 | } 124 | }, 125 | Event::NewTransaction(tx) => self.process_new_tx(tx).await, 126 | } 127 | } 128 | } 129 | 130 | impl SandoBot { 131 | /// Process new blocks as they come in 132 | async fn process_new_block(&mut self, event: NewBlock) -> Result<()> { 133 | log_new_block_info!(event); 134 | self.block_manager.update_block_info(event); 135 | Ok(()) 136 | } 137 | 138 | /// Process new txs as they come in 139 | #[allow(unused_mut)] 140 | async fn process_new_tx(&mut self, victim_tx: Transaction) -> Option { 141 | // setup variables for processing tx 142 | let next_block = self.block_manager.get_next_block(); 143 | let latest_block = self.block_manager.get_latest_block(); 144 | 145 | // ignore txs that we can't include in next block 146 | // enhancement: simulate all txs regardless, store result, and use result when tx can included 147 | if victim_tx.max_fee_per_gas.unwrap_or_default() < next_block.base_fee_per_gas { 148 | log_info_cyan!("{:?} mf (p.token_a, p.token_b), 178 | UniswapV3(p) => (p.token_a, p.token_b), 179 | }; 180 | 181 | if token_a != *WETH_ADDRESS && token_b != *WETH_ADDRESS { 182 | // contract can only sandwich weth pools 183 | continue; 184 | } 185 | 186 | // token that we use as frontrun input and backrun output 187 | let start_end_token = *WETH_ADDRESS; 188 | 189 | // token that we use as frontrun output and backrun input 190 | let intermediary_token = if token_a == start_end_token { 191 | token_b 192 | } else { 193 | token_a 194 | }; 195 | 196 | let ingredients = RawIngredients::new( 197 | vec![victim_tx.clone()], 198 | start_end_token, 199 | intermediary_token, 200 | pool, 201 | ); 202 | 203 | match self.is_sandwichable(ingredients, next_block.clone()).await { 204 | Ok(s) => { 205 | let _bundle = match s 206 | .to_fb_bundle( 207 | self.sando_state_manager.get_sando_address(), 208 | self.sando_state_manager.get_searcher_signer(), 209 | false, 210 | self.provider.clone(), 211 | ) 212 | .await 213 | { 214 | Ok(b) => b, 215 | Err(e) => { 216 | log_not_sandwichable!("{:?}", e); 217 | continue; 218 | } 219 | }; 220 | 221 | #[cfg(not(feature = "debug"))] 222 | { 223 | sando_bundles.push(_bundle); 224 | } 225 | } 226 | Err(e) => { 227 | log_not_sandwichable!("{:?} {:?}", victim_tx.hash, e) 228 | } 229 | }; 230 | } 231 | 232 | Some(Action::SubmitToFlashbots(sando_bundles)) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::ensure; 4 | use anyhow::{anyhow, Result}; 5 | use artemis_core::{ 6 | collectors::block_collector::NewBlock, executors::flashbots_executor::FlashbotsBundle, 7 | }; 8 | use cfmms::pool::Pool; 9 | use ethers::providers::Middleware; 10 | use ethers::signers::LocalWallet; 11 | use ethers::signers::Signer; 12 | use ethers::types::{ 13 | Address, Block, Bytes, Eip1559TransactionRequest, Transaction, H256, U256, U64, 14 | }; 15 | use ethers_flashbots::BundleRequest; 16 | use foundry_evm::executor::TxEnv; 17 | 18 | use crate::constants::DUST_OVERPAY; 19 | use crate::helpers::access_list_to_ethers; 20 | use crate::helpers::sign_eip1559; 21 | 22 | /// Core Event enum for current strategy 23 | #[derive(Debug, Clone)] 24 | pub enum Event { 25 | NewBlock(NewBlock), 26 | NewTransaction(Transaction), 27 | } 28 | 29 | /// Core Action enum for current strategy 30 | #[derive(Debug, Clone)] 31 | pub enum Action { 32 | SubmitToFlashbots(FlashbotsBundle), 33 | } 34 | 35 | /// Configuration for variables needed for sandwiches 36 | #[derive(Debug, Clone)] 37 | pub struct StratConfig { 38 | pub sando_address: Address, 39 | pub sando_inception_block: U64, 40 | pub searcher_signer: LocalWallet, 41 | } 42 | 43 | /// Information on potential sandwichable opportunity 44 | #[derive(Clone)] 45 | pub struct RawIngredients { 46 | /// Victim tx/s to be used in sandwich 47 | meats: Vec, 48 | /// Which token do start and end sandwich with 49 | start_end_token: Address, 50 | /// Which token do we hold for duration of sandwich 51 | intermediary_token: Address, 52 | /// Which pool are we targetting 53 | target_pool: Pool, 54 | } 55 | 56 | impl RawIngredients { 57 | pub fn new( 58 | meats: Vec, 59 | start_end_token: Address, 60 | intermediary_token: Address, 61 | target_pool: Pool, 62 | ) -> Self { 63 | Self { 64 | meats, 65 | start_end_token, 66 | intermediary_token, 67 | target_pool, 68 | } 69 | } 70 | 71 | pub fn get_start_end_token(&self) -> Address { 72 | self.start_end_token 73 | } 74 | 75 | pub fn get_intermediary_token(&self) -> Address { 76 | self.intermediary_token 77 | } 78 | 79 | pub fn get_meats_ref(&self) -> &Vec { 80 | &self.meats 81 | } 82 | 83 | pub fn get_target_pool(&self) -> Pool { 84 | self.target_pool 85 | } 86 | 87 | // Used for logging 88 | pub fn print_meats(&self) -> String { 89 | let mut s = String::new(); 90 | s.push('['); 91 | for (i, x) in self.meats.iter().enumerate() { 92 | s.push_str(&format!("{:?}", x.hash)); 93 | if i != self.meats.len() - 1 { 94 | s.push_str(","); 95 | } 96 | } 97 | s.push(']'); 98 | s 99 | } 100 | } 101 | 102 | #[derive(Default, Clone, Copy)] 103 | pub struct BlockInfo { 104 | pub number: U64, 105 | pub base_fee_per_gas: U256, 106 | pub timestamp: U256, 107 | // These are optional because we don't know these values for `next_block` 108 | pub gas_used: Option, 109 | pub gas_limit: Option, 110 | } 111 | 112 | impl BlockInfo { 113 | /// Returns block info for next block 114 | pub fn get_next_block(&self) -> BlockInfo { 115 | BlockInfo { 116 | number: self.number + 1, 117 | base_fee_per_gas: calculate_next_block_base_fee(&self), 118 | timestamp: self.timestamp + 12, 119 | gas_used: None, 120 | gas_limit: None, 121 | } 122 | } 123 | } 124 | 125 | impl TryFrom> for BlockInfo { 126 | type Error = anyhow::Error; 127 | 128 | fn try_from(value: Block) -> std::result::Result { 129 | Ok(BlockInfo { 130 | number: value.number.ok_or(anyhow!( 131 | "could not parse block.number when setting up `block_manager`" 132 | ))?, 133 | gas_used: Some(value.gas_used), 134 | gas_limit: Some(value.gas_limit), 135 | base_fee_per_gas: value.base_fee_per_gas.ok_or(anyhow!( 136 | "could not parse base fee when setting up `block_manager`" 137 | ))?, 138 | timestamp: value.timestamp, 139 | }) 140 | } 141 | } 142 | 143 | impl From for BlockInfo { 144 | fn from(value: NewBlock) -> Self { 145 | Self { 146 | number: value.number, 147 | base_fee_per_gas: value.base_fee_per_gas, 148 | timestamp: value.timestamp, 149 | gas_used: Some(value.gas_used), 150 | gas_limit: Some(value.gas_limit), 151 | } 152 | } 153 | } 154 | 155 | /// Calculate the next block base fee 156 | // based on math provided here: https://ethereum.stackexchange.com/questions/107173/how-is-the-base-fee-per-gas-computed-for-a-new-block 157 | fn calculate_next_block_base_fee(block: &BlockInfo) -> U256 { 158 | // Get the block base fee per gas 159 | let current_base_fee_per_gas = block.base_fee_per_gas; 160 | 161 | let current_gas_used = block 162 | .gas_used 163 | .expect("can't calculate base fee from unmined block \"next_block\""); 164 | 165 | let current_gas_target = block 166 | .gas_limit 167 | .expect("can't calculate base fee from unmined block \"next_block\"") 168 | / 2; 169 | 170 | if current_gas_used == current_gas_target { 171 | current_base_fee_per_gas 172 | } else if current_gas_used > current_gas_target { 173 | let gas_used_delta = current_gas_used - current_gas_target; 174 | let base_fee_per_gas_delta = 175 | current_base_fee_per_gas * gas_used_delta / current_gas_target / 8; 176 | 177 | return current_base_fee_per_gas + base_fee_per_gas_delta; 178 | } else { 179 | let gas_used_delta = current_gas_target - current_gas_used; 180 | let base_fee_per_gas_delta = 181 | current_base_fee_per_gas * gas_used_delta / current_gas_target / 8; 182 | 183 | return current_base_fee_per_gas - base_fee_per_gas_delta; 184 | } 185 | } 186 | 187 | /// All details for capturing a sando opp 188 | pub struct SandoRecipe { 189 | frontrun: TxEnv, 190 | frontrun_gas_used: u64, 191 | meats: Vec, 192 | backrun: TxEnv, 193 | backrun_gas_used: u64, 194 | revenue: U256, 195 | target_block: BlockInfo, 196 | } 197 | 198 | impl SandoRecipe { 199 | pub fn new( 200 | frontrun: TxEnv, 201 | frontrun_gas_used: u64, 202 | meats: Vec, 203 | backrun: TxEnv, 204 | backrun_gas_used: u64, 205 | revenue: U256, 206 | target_block: BlockInfo, 207 | ) -> Self { 208 | Self { 209 | frontrun, 210 | frontrun_gas_used, 211 | meats, 212 | backrun, 213 | backrun_gas_used, 214 | revenue, 215 | target_block, 216 | } 217 | } 218 | 219 | pub fn get_revenue(&self) -> U256 { 220 | self.revenue 221 | } 222 | 223 | /// turn recipe into a signed bundle that can be sumbitted to flashbots 224 | pub async fn to_fb_bundle( 225 | self, 226 | sando_address: Address, 227 | searcher: &LocalWallet, 228 | has_dust: bool, 229 | provider: Arc, 230 | ) -> Result { 231 | let nonce = provider 232 | .get_transaction_count(searcher.address(), Some(self.target_block.number.into())) 233 | .await 234 | .map_err(|e| anyhow!("FAILED TO CREATE BUNDLE: Failed to get nonce {:?}", e))?; 235 | 236 | let frontrun_tx = Eip1559TransactionRequest { 237 | to: Some(sando_address.into()), 238 | gas: Some((U256::from(self.frontrun_gas_used) * 10) / 7), 239 | value: Some(self.frontrun.value.into()), 240 | data: Some(self.frontrun.data.into()), 241 | nonce: Some(nonce), 242 | access_list: access_list_to_ethers(self.frontrun.access_list), 243 | max_fee_per_gas: Some(self.target_block.base_fee_per_gas.into()), 244 | ..Default::default() 245 | }; 246 | let signed_frontrun = sign_eip1559(frontrun_tx, &searcher).await?; 247 | 248 | let signed_meat_txs: Vec = self.meats.into_iter().map(|meat| meat.rlp()).collect(); 249 | 250 | // calc bribe (bribes paid in backrun) 251 | let revenue_minus_frontrun_tx_fee = self 252 | .revenue 253 | .checked_sub(U256::from(self.frontrun_gas_used) * self.target_block.base_fee_per_gas) 254 | .ok_or_else(|| { 255 | anyhow!("[FAILED TO CREATE BUNDLE] revenue doesn't cover frontrun basefee") 256 | })?; 257 | 258 | // eat a loss (overpay) to get dust onto the sando contract (more: https://twitter.com/libevm/status/1474870661373779969) 259 | let bribe_amount = if !has_dust { 260 | revenue_minus_frontrun_tx_fee + *DUST_OVERPAY 261 | } else { 262 | // bribe away 99.9999999% of revenue lmeow 263 | revenue_minus_frontrun_tx_fee * 999999999 / 1000000000 264 | }; 265 | 266 | let max_fee = bribe_amount / self.backrun_gas_used; 267 | 268 | ensure!( 269 | max_fee >= self.target_block.base_fee_per_gas, 270 | "[FAILED TO CREATE BUNDLE] backrun maxfee less than basefee" 271 | ); 272 | 273 | let effective_miner_tip = max_fee.checked_sub(self.target_block.base_fee_per_gas); 274 | 275 | ensure!( 276 | effective_miner_tip.is_none(), 277 | "[FAILED TO CREATE BUNDLE] negative miner tip" 278 | ); 279 | 280 | let backrun_tx = Eip1559TransactionRequest { 281 | to: Some(sando_address.into()), 282 | gas: Some((U256::from(self.backrun_gas_used) * 10) / 7), 283 | value: Some(self.backrun.value.into()), 284 | data: Some(self.backrun.data.into()), 285 | nonce: Some(nonce+1), 286 | access_list: access_list_to_ethers(self.backrun.access_list), 287 | max_priority_fee_per_gas: Some(max_fee), 288 | max_fee_per_gas: Some(max_fee), 289 | ..Default::default() 290 | }; 291 | let signed_backrun = sign_eip1559(backrun_tx, &searcher).await?; 292 | 293 | // construct bundle 294 | let mut bundled_transactions: Vec = vec![signed_frontrun]; 295 | bundled_transactions.append(&mut signed_meat_txs.clone()); 296 | bundled_transactions.push(signed_backrun); 297 | 298 | let mut bundle_request = BundleRequest::new(); 299 | for tx in bundled_transactions { 300 | bundle_request = bundle_request.push_transaction(tx); 301 | } 302 | 303 | bundle_request = bundle_request 304 | .set_block(self.target_block.number) 305 | .set_simulation_block(self.target_block.number - 1) 306 | .set_simulation_timestamp(self.target_block.timestamp.as_u64()); 307 | 308 | Ok(bundle_request) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/abi/IUniswapV2Router.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"amountADesired","type":"uint256"},{"internalType":"uint256","name":"amountBDesired","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountTokenDesired","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"addLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"},{"internalType":"uint256","name":"liquidity","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"reserveIn","type":"uint256"},{"internalType":"uint256","name":"reserveOut","type":"uint256"}],"name":"getAmountOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsIn","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"reserveA","type":"uint256"},{"internalType":"uint256","name":"reserveB","type":"uint256"}],"name":"quote","outputs":[{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidity","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETH","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"removeLiquidityETHSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermit","outputs":[{"internalType":"uint256","name":"amountToken","type":"uint256"},{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountTokenMin","type":"uint256"},{"internalType":"uint256","name":"amountETHMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityETHWithPermitSupportingFeeOnTransferTokens","outputs":[{"internalType":"uint256","name":"amountETH","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint256","name":"liquidity","type":"uint256"},{"internalType":"uint256","name":"amountAMin","type":"uint256"},{"internalType":"uint256","name":"amountBMin","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bool","name":"approveMax","type":"bool"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"removeLiquidityWithPermit","outputs":[{"internalType":"uint256","name":"amountA","type":"uint256"},{"internalType":"uint256","name":"amountB","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapETHForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactETH","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMax","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapTokensForExactTokens","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 2 | -------------------------------------------------------------------------------- /bot/crates/strategy/src/simulator/lil_router.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use cfmms::pool::Pool::{UniswapV2, UniswapV3}; 3 | use ethers::{abi, types::U256}; 4 | use foundry_evm::{ 5 | executor::{fork::SharedBackend, Bytecode, ExecutionResult, Output, TransactTo}, 6 | revm::{ 7 | db::CacheDB, 8 | primitives::{keccak256, AccountInfo, Address as rAddress, U256 as rU256}, 9 | EVM, 10 | }, 11 | }; 12 | 13 | use crate::{ 14 | constants::{ 15 | LIL_ROUTER_ADDRESS, LIL_ROUTER_CODE, LIL_ROUTER_CONTROLLER, WETH_ADDRESS, WETH_FUND_AMT, 16 | }, 17 | tx_utils::lil_router_interface::{ 18 | build_swap_v2_data, build_swap_v3_data, decode_swap_v2_result, decode_swap_v3_result, 19 | }, 20 | types::{BlockInfo, RawIngredients}, 21 | }; 22 | 23 | use super::{eth_to_wei, setup_block_state}; 24 | 25 | // Juiced implementation of https://research.ijcaonline.org/volume65/number14/pxc3886165.pdf 26 | // splits range in more intervals, search intervals concurrently, compare, repeat till termination 27 | pub async fn find_optimal_input( 28 | ingredients: &RawIngredients, 29 | target_block: &BlockInfo, 30 | weth_inventory: U256, 31 | shared_backend: SharedBackend, 32 | ) -> Result { 33 | // 34 | // [EXAMPLE WITH 10 BOUND INTERVALS] 35 | // 36 | // (first) (mid) (last) 37 | // ▼ ▼ ▼ 38 | // +---+---+---+---+---+---+---+---+---+---+ 39 | // | | | | | | | | | | | 40 | // +---+---+---+---+---+---+---+---+---+---+ 41 | // ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ 42 | // 0 1 2 3 4 5 6 7 8 9 X 43 | // 44 | // * [0, X] = search range 45 | // * Find revenue at each interval 46 | // * Find index of interval with highest revenue 47 | // * Search again with bounds set to adjacent index of highest 48 | 49 | // setup values for search termination 50 | let base = U256::from(1000000u64); 51 | let tolerance = U256::from(1u64); 52 | 53 | let mut lower_bound = U256::zero(); 54 | let mut upper_bound = weth_inventory; 55 | 56 | let tolerance = (tolerance * ((upper_bound + lower_bound) / rU256::from(2))) / base; 57 | 58 | // initialize variables for search 59 | let l_interval_lower = |i: usize, intervals: &Vec| intervals[i - 1].clone() + 1; 60 | let r_interval_upper = |i: usize, intervals: &Vec| { 61 | intervals[i + 1] 62 | .clone() 63 | .checked_sub(1.into()) 64 | .ok_or(anyhow!("r_interval - 1 underflowed")) 65 | }; 66 | let should_loop_terminate = |lower_bound: U256, upper_bound: U256| -> bool { 67 | let search_range = match upper_bound.checked_sub(lower_bound) { 68 | Some(range) => range, 69 | None => return true, 70 | }; 71 | // produces negative result 72 | if lower_bound > upper_bound { 73 | return true; 74 | } 75 | // tolerance condition not met 76 | if search_range < tolerance { 77 | return true; 78 | } 79 | false 80 | }; 81 | let mut highest_sando_input = U256::zero(); 82 | let number_of_intervals = 15; 83 | let mut counter = 0; 84 | 85 | // continue search until termination condition is met (no point seraching down to closest wei) 86 | loop { 87 | counter += 1; 88 | if should_loop_terminate(lower_bound, upper_bound) { 89 | break; 90 | } 91 | 92 | // split search range into intervals 93 | let mut intervals = Vec::new(); 94 | for i in 0..=number_of_intervals { 95 | let diff = upper_bound 96 | .checked_sub(lower_bound) 97 | .ok_or(anyhow!("upper_bound - lower_bound resulted in underflow"))?; 98 | 99 | let fraction = diff * i; 100 | let divisor = U256::from(number_of_intervals); 101 | let interval = lower_bound + (fraction / divisor); 102 | 103 | intervals.push(interval); 104 | } 105 | 106 | // calculate revenue at each interval concurrently 107 | let mut revenues = Vec::new(); 108 | for bound in &intervals { 109 | let sim = tokio::task::spawn(evaluate_sandwich_revenue( 110 | *bound, 111 | target_block.clone(), 112 | shared_backend.clone(), 113 | ingredients.clone(), 114 | )); 115 | revenues.push(sim); 116 | } 117 | 118 | let revenues = futures::future::join_all(revenues).await; 119 | 120 | let revenues = revenues 121 | .into_iter() 122 | .map(|r| r.unwrap().unwrap_or_default()) 123 | .collect::>(); 124 | 125 | // find interval that produces highest revenue 126 | let (highest_revenue_index, _highest_revenue) = revenues 127 | .iter() 128 | .enumerate() 129 | .max_by(|(_, a), (_, b)| a.cmp(&b)) 130 | .unwrap(); 131 | 132 | highest_sando_input = intervals[highest_revenue_index]; 133 | 134 | // enhancement: find better way to increase finding opps incase of all rev=0 135 | if revenues[highest_revenue_index] == U256::zero() { 136 | // most likely there is no sandwich possibility 137 | if counter == 10 { 138 | return Ok(U256::zero()); 139 | } 140 | // no revenue found, most likely small optimal so decrease range 141 | upper_bound = intervals[intervals.len() / 3] 142 | .checked_sub(1.into()) 143 | .ok_or(anyhow!("intervals[intervals.len()/3] - 1 underflowed"))?; 144 | continue; 145 | } 146 | 147 | // if highest revenue is produced at last interval (upper bound stays fixed) 148 | if highest_revenue_index == intervals.len() - 1 { 149 | lower_bound = l_interval_lower(highest_revenue_index, &intervals); 150 | continue; 151 | } 152 | 153 | // if highest revenue is produced at first interval (lower bound stays fixed) 154 | if highest_revenue_index == 0 { 155 | upper_bound = r_interval_upper(highest_revenue_index, &intervals)?; 156 | continue; 157 | } 158 | 159 | // set bounds to intervals adjacent to highest revenue index and search again 160 | lower_bound = l_interval_lower(highest_revenue_index, &intervals); 161 | upper_bound = r_interval_upper(highest_revenue_index, &intervals)?; 162 | } 163 | 164 | Ok(highest_sando_input) 165 | } 166 | 167 | async fn evaluate_sandwich_revenue( 168 | frontrun_in: U256, 169 | next_block: BlockInfo, 170 | shared_backend: SharedBackend, 171 | ingredients: RawIngredients, 172 | ) -> Result { 173 | let mut fork_db = CacheDB::new(shared_backend); 174 | inject_lil_router_code(&mut fork_db); 175 | 176 | let mut evm = EVM::new(); 177 | evm.database(fork_db); 178 | setup_block_state(&mut evm, &next_block); 179 | 180 | /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ 181 | /* FRONTRUN TRANSACTION */ 182 | /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ 183 | let frontrun_data = match ingredients.get_target_pool() { 184 | UniswapV2(pool) => build_swap_v2_data(frontrun_in, pool, true), 185 | UniswapV3(pool) => build_swap_v3_data(frontrun_in.as_u128().into(), pool, true), 186 | }; 187 | 188 | evm.env.tx.caller = *LIL_ROUTER_CONTROLLER; 189 | evm.env.tx.transact_to = TransactTo::Call(*LIL_ROUTER_ADDRESS); 190 | evm.env.tx.data = frontrun_data.0; 191 | evm.env.tx.gas_limit = 700000; 192 | evm.env.tx.gas_price = next_block.base_fee_per_gas.into(); 193 | evm.env.tx.value = rU256::ZERO; 194 | 195 | let result = match evm.transact_commit() { 196 | Ok(result) => result, 197 | Err(e) => return Err(anyhow!("[lilRouter: EVM ERROR] frontrun: {:?}", e)), 198 | }; 199 | let output = match result { 200 | ExecutionResult::Success { output, .. } => match output { 201 | Output::Call(o) => o, 202 | Output::Create(o, _) => o, 203 | }, 204 | ExecutionResult::Revert { output, .. } => { 205 | return Err(anyhow!("[lilRouter: REVERT] frontrun: {:?}", output)) 206 | } 207 | ExecutionResult::Halt { reason, .. } => { 208 | return Err(anyhow!("[lilRouter: HALT] frontrun: {:?}", reason)) 209 | } 210 | }; 211 | let (_frontrun_out, backrun_in) = match ingredients.get_target_pool() { 212 | UniswapV2(_) => match decode_swap_v2_result(output.into()) { 213 | Ok(output) => output, 214 | Err(e) => { 215 | return Err(anyhow!( 216 | "[lilRouter: FailedToDecodeOutput] frontrun: {:?}", 217 | e 218 | )) 219 | } 220 | }, 221 | UniswapV3(_) => match decode_swap_v3_result(output.into()) { 222 | Ok(output) => output, 223 | Err(e) => return Err(anyhow!("lilRouter: FailedToDecodeOutput: {:?}", e)), 224 | }, 225 | }; 226 | 227 | /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ 228 | /* MEAT TRANSACTION/s */ 229 | /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ 230 | for meat in ingredients.get_meats_ref().iter() { 231 | evm.env.tx.caller = rAddress::from_slice(&meat.from.0); 232 | evm.env.tx.transact_to = 233 | TransactTo::Call(rAddress::from_slice(&meat.to.unwrap_or_default().0)); 234 | evm.env.tx.data = meat.input.0.clone(); 235 | evm.env.tx.value = meat.value.into(); 236 | evm.env.tx.chain_id = meat.chain_id.map(|id| id.as_u64()); 237 | // evm.env.tx.nonce = Some(meat.nonce.as_u64()); /** ignore nonce check for now **/ 238 | evm.env.tx.gas_limit = meat.gas.as_u64(); 239 | match meat.transaction_type { 240 | Some(ethers::types::U64([0])) => { 241 | // legacy tx 242 | evm.env.tx.gas_price = meat.gas_price.unwrap_or_default().into(); 243 | } 244 | Some(_) => { 245 | // type 2 tx 246 | evm.env.tx.gas_priority_fee = meat.max_priority_fee_per_gas.map(|mpf| mpf.into()); 247 | evm.env.tx.gas_price = meat.max_fee_per_gas.unwrap_or_default().into(); 248 | } 249 | None => { 250 | // legacy tx 251 | evm.env.tx.gas_price = meat.gas_price.unwrap_or_default().into(); 252 | } 253 | } 254 | 255 | let _res = evm.transact_commit(); 256 | } 257 | 258 | /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ 259 | /* BACKRUN TRANSACTION */ 260 | /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ 261 | let backrun_data = match ingredients.get_target_pool() { 262 | UniswapV2(pool) => build_swap_v2_data(backrun_in, pool, false), 263 | UniswapV3(pool) => build_swap_v3_data(backrun_in.as_u128().into(), pool, false), 264 | }; 265 | 266 | evm.env.tx.caller = *LIL_ROUTER_CONTROLLER; 267 | evm.env.tx.transact_to = TransactTo::Call(*LIL_ROUTER_ADDRESS); 268 | evm.env.tx.data = backrun_data.0; 269 | evm.env.tx.gas_limit = 700000; 270 | evm.env.tx.gas_price = next_block.base_fee_per_gas.into(); 271 | evm.env.tx.value = rU256::ZERO; 272 | 273 | let result = match evm.transact_commit() { 274 | Ok(result) => result, 275 | Err(e) => return Err(anyhow!("[lilRouter: EVM ERROR] backrun: {:?}", e)), 276 | }; 277 | let output = match result { 278 | ExecutionResult::Success { output, .. } => match output { 279 | Output::Call(o) => o, 280 | Output::Create(o, _) => o, 281 | }, 282 | ExecutionResult::Revert { output, .. } => { 283 | return Err(anyhow!("[lilRouter: REVERT] backrun: {:?}", output)) 284 | } 285 | ExecutionResult::Halt { reason, .. } => { 286 | return Err(anyhow!("[lilRouter: HALT] backrun: {:?}", reason)) 287 | } 288 | }; 289 | let (_backrun_out, post_sandwich_balance) = match ingredients.get_target_pool() { 290 | UniswapV2(_) => match decode_swap_v2_result(output.into()) { 291 | Ok(output) => output, 292 | Err(e) => return Err(anyhow!("[lilRouter: FailedToDecodeOutput] {:?}", e)), 293 | }, 294 | UniswapV3(_) => match decode_swap_v3_result(output.into()) { 295 | Ok(output) => output, 296 | Err(e) => return Err(anyhow!("[lilRouter: FailedToDecodeOutput] {:?}", e)), 297 | }, 298 | }; 299 | 300 | let revenue = post_sandwich_balance 301 | .checked_sub((*WETH_FUND_AMT).into()) 302 | .unwrap_or_default(); 303 | 304 | Ok(revenue) 305 | } 306 | 307 | /// Inserts custom minimal router contract into evm instance for simulations 308 | fn inject_lil_router_code(db: &mut CacheDB) { 309 | // insert lilRouter bytecode 310 | let lil_router_info = AccountInfo::new( 311 | rU256::ZERO, 312 | 0, 313 | Bytecode::new_raw((*LIL_ROUTER_CODE.0).into()), 314 | ); 315 | db.insert_account_info(*LIL_ROUTER_ADDRESS, lil_router_info); 316 | 317 | // insert and fund lilRouter controller (so we can spoof) 318 | let controller_info = AccountInfo::new(*WETH_FUND_AMT, 0, Bytecode::default()); 319 | db.insert_account_info(*LIL_ROUTER_CONTROLLER, controller_info); 320 | 321 | // fund lilRouter with 200 weth 322 | let slot = keccak256(&abi::encode(&[ 323 | abi::Token::Address((*LIL_ROUTER_ADDRESS).into()), 324 | abi::Token::Uint(U256::from(3)), 325 | ])); 326 | 327 | db.insert_account_storage((*WETH_ADDRESS).into(), slot.into(), eth_to_wei(200)) 328 | .unwrap(); 329 | } 330 | -------------------------------------------------------------------------------- /contract/test/Sando.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import "forge-std/Test.sol"; 5 | import "foundry-huff/HuffDeployer.sol"; 6 | import "v3-core/interfaces/IUniswapV3Pool.sol"; 7 | import "solmate/tokens/ERC20.sol"; 8 | import "solmate/tokens/WETH.sol"; 9 | 10 | import "./misc/GeneralHelper.sol"; 11 | import "./misc/V2SandoUtility.sol"; 12 | import "./misc/V3SandoUtility.sol"; 13 | import "./misc/SandoCommon.sol"; 14 | 15 | // Need custom interface cause USDT does not return a bool after swap 16 | // see more here: https://github.com/d-xo/weird-erc20#missing-return-values 17 | interface IUSDT { 18 | function transfer(address to, uint256 value) external; 19 | } 20 | 21 | /// @title SandoTest 22 | /// @author 0xmouseless 23 | /// @notice Test suite for the huff sando contract 24 | contract SandoTest is Test { 25 | // wallet associated with private key 0x1 26 | address constant searcher = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; 27 | WETH weth = WETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); 28 | uint256 wethFundAmount = 100 ether; 29 | address sando; 30 | 31 | function setUp() public { 32 | // change this if ur node isn't hosted on localhost:8545 33 | vm.createSelectFork("http://localhost:8545", 17401879); 34 | sando = HuffDeployer.deploy("sando"); 35 | 36 | // fund sando 37 | weth.deposit{value: wethFundAmount}(); 38 | weth.transfer(sando, wethFundAmount); 39 | 40 | payable(searcher).transfer(100 ether); 41 | } 42 | 43 | function testRecoverEth() public { 44 | vm.startPrank(searcher); 45 | 46 | uint256 sandoBalanceBefore = address(sando).balance; 47 | uint256 eoaBalanceBefore = address(searcher).balance; 48 | 49 | (bool s,) = sando.call(abi.encodePacked(SandoCommon.getJumpDestFromSig("recoverEth"))); 50 | assertTrue(s, "calling recoverEth failed"); 51 | 52 | assertTrue(address(sando).balance == 0, "sando ETH balance should be zero after calling recover eth"); 53 | assertTrue( 54 | address(searcher).balance == eoaBalanceBefore + sandoBalanceBefore, 55 | "searcher should gain all eth from sando" 56 | ); 57 | } 58 | 59 | function testSepukku() public { 60 | vm.startPrank(searcher); 61 | (bool s,) = sando.call(abi.encodePacked(SandoCommon.getJumpDestFromSig("seppuku"))); 62 | assertTrue(s, "calling seppuku failed"); 63 | } 64 | 65 | function testRecoverWeth() public { 66 | vm.startPrank(searcher); 67 | 68 | uint256 sandoBalanceBefore = weth.balanceOf(sando); 69 | uint256 searcherBalanceBefore = weth.balanceOf(searcher); 70 | 71 | (bool s,) = sando.call(abi.encodePacked(SandoCommon.getJumpDestFromSig("recoverWeth"), sandoBalanceBefore)); 72 | assertTrue(s, "failed to call recoverWeth"); 73 | 74 | assertTrue(weth.balanceOf(sando) == 0, "sando WETH balance should be zero after calling recoverWeth"); 75 | assertTrue( 76 | weth.balanceOf(searcher) == searcherBalanceBefore + sandoBalanceBefore, 77 | "searcher should gain all weth from sando after calling recoverWeth" 78 | ); 79 | } 80 | 81 | function testUnauthorizedAccessToCallback(address trespasser, bytes32 fakePoolKeyHash) public { 82 | vm.startPrank(trespasser); 83 | vm.deal(address(trespasser), 5 ether); 84 | /* 85 | function uniswapV3SwapCallback( 86 | int256 amount0Delta, 87 | int256 amount1Delta, 88 | bytes data 89 | ) external 90 | 91 | custom data = abi.encodePacked(isZeroForOne, input_token, pool_key_hash) 92 | */ 93 | bytes memory payload = 94 | abi.encodePacked(uint8(250), uint256(5 ether), uint256(5 ether), uint8(1), address(weth), fakePoolKeyHash); // 0xfa = 250 95 | (bool s,) = sando.call(payload); 96 | assertFalse(s, "only pools should be able to call callback"); 97 | } 98 | 99 | function testV3FrontrunWeth1(uint256 inputWethAmount) public { 100 | IUniswapV3Pool pool = IUniswapV3Pool(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); // USDC - WETH 101 | (, address outputToken) = (pool.token1(), pool.token0()); 102 | 103 | // make sure fuzzed value is within bounds 104 | inputWethAmount = bound(inputWethAmount, WethEncodingUtils.encodeMultiple(), weth.balanceOf(sando)); 105 | 106 | (bytes memory payload, uint256 encodedValue) = 107 | V3SandoUtility.v3CreateFrontrunPayload(pool, outputToken, int256(inputWethAmount)); 108 | 109 | vm.prank(searcher, searcher); 110 | (bool s,) = address(sando).call{value: encodedValue}(payload); 111 | 112 | assertTrue(s, "calling swap failed"); 113 | } 114 | 115 | function testV3FrontrunWeth0(uint256 inputWethAmount) public { 116 | IUniswapV3Pool pool = IUniswapV3Pool(0x7379e81228514a1D2a6Cf7559203998E20598346); // ETH - STETH 117 | (address outputToken,) = (pool.token1(), pool.token0()); 118 | 119 | // make sure fuzzed value is within bounds 120 | inputWethAmount = bound(inputWethAmount, WethEncodingUtils.encodeMultiple(), weth.balanceOf(sando)); 121 | 122 | (bytes memory payload, uint256 encodedValue) = 123 | V3SandoUtility.v3CreateFrontrunPayload(pool, outputToken, int256(inputWethAmount)); 124 | 125 | vm.prank(searcher, searcher); 126 | (bool s,) = address(sando).call{value: encodedValue}(payload); 127 | 128 | assertTrue(s, "calling swap failed"); 129 | } 130 | 131 | function testV3BackrunWeth0(uint256 inputBttAmount) public { 132 | IUniswapV3Pool pool = IUniswapV3Pool(0x64A078926AD9F9E88016c199017aea196e3899E1); 133 | (address inputToken,) = (pool.token1(), pool.token0()); 134 | 135 | // make sure fuzzed value is within bounds 136 | address sugarDaddy = 0x9277a463A508F45115FdEaf22FfeDA1B16352433; 137 | inputBttAmount = bound(inputBttAmount, 1, ERC20(inputToken).balanceOf(sugarDaddy)); 138 | 139 | // fund sando contract 140 | vm.startPrank(sugarDaddy); 141 | IUSDT(inputToken).transfer(sando, uint256(inputBttAmount)); 142 | 143 | bytes memory payload = V3SandoUtility.v3CreateBackrunPayload(pool, inputToken, int256(inputBttAmount)); 144 | 145 | changePrank(searcher, searcher); 146 | (bool s,) = address(sando).call(payload); 147 | assertTrue(s, "calling swap failed"); 148 | } 149 | 150 | function testV3BackrunWeth1(uint256 inputDaiAmount) public { 151 | IUniswapV3Pool pool = IUniswapV3Pool(0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8); 152 | (address inputToken,) = (pool.token0(), pool.token1()); 153 | 154 | // make sure fuzzed value is within bounds 155 | address sugarDaddy = 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643; 156 | inputDaiAmount = bound(inputDaiAmount, 1, ERC20(inputToken).balanceOf(sugarDaddy)); 157 | 158 | // fund sando contract 159 | vm.startPrank(sugarDaddy); 160 | ERC20(inputToken).transfer(sando, uint256(inputDaiAmount)); 161 | 162 | bytes memory payload = V3SandoUtility.v3CreateBackrunPayload(pool, inputToken, int256(inputDaiAmount)); 163 | 164 | changePrank(searcher, searcher); 165 | (bool s,) = address(sando).call(payload); 166 | assertTrue(s, "calling swap failed"); 167 | } 168 | 169 | // +-------------------------------+ 170 | // | Generic Tests | 171 | // +-------------------------------+ 172 | // could decompose further but ran into issues with vm.assume/vm.bound when fuzzing 173 | function testV2FrontrunWeth0(uint256 inputWethAmount) public { 174 | address usdtAddress = 0xdAC17F958D2ee523a2206206994597C13D831ec7; 175 | 176 | // make sure fuzzed value is within bounds 177 | inputWethAmount = bound(inputWethAmount, WethEncodingUtils.encodeMultiple(), weth.balanceOf(sando)); 178 | 179 | // capture pre swap state 180 | uint256 preSwapWethBalance = weth.balanceOf(sando); 181 | uint256 preSwapUsdtBalance = ERC20(usdtAddress).balanceOf(sando); 182 | 183 | // calculate expected values 184 | uint256 actualWethInput = WethEncodingUtils.decode(WethEncodingUtils.encode(inputWethAmount)); 185 | uint256 actualUsdtOutput = GeneralHelper.getAmountOut(address(weth), usdtAddress, actualWethInput); 186 | uint256 expectedUsdtOutput = FiveBytesEncodingUtils.decode(FiveBytesEncodingUtils.encode(actualUsdtOutput)); 187 | 188 | // need this to pass because: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L160 189 | vm.assume(expectedUsdtOutput > 0); 190 | 191 | (bytes memory calldataPayload, uint256 wethEncodedValue) = 192 | V2SandoUtility.v2CreateFrontrunPayload(usdtAddress, inputWethAmount); 193 | vm.prank(searcher); 194 | (bool s,) = address(sando).call{value: wethEncodedValue}(calldataPayload); 195 | assertTrue(s); 196 | 197 | // check values after swap 198 | assertEq( 199 | ERC20(usdtAddress).balanceOf(sando) - preSwapUsdtBalance, 200 | expectedUsdtOutput, 201 | "did not get expected usdt amount out from swap" 202 | ); 203 | assertEq(preSwapWethBalance - weth.balanceOf(sando), actualWethInput, "unexpected amount of weth used in swap"); 204 | } 205 | 206 | function testV2FrontrunWeth1(uint256 inputWethAmount) public { 207 | address usdcAddress = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 208 | 209 | // make sure fuzzed value is within bounds 210 | inputWethAmount = bound(inputWethAmount, WethEncodingUtils.encodeMultiple(), weth.balanceOf(sando)); 211 | 212 | // capture pre swap state 213 | uint256 preSwapWethBalance = weth.balanceOf(sando); 214 | uint256 preSwapUsdcBalance = ERC20(usdcAddress).balanceOf(sando); 215 | 216 | // calculate expected values 217 | uint256 actualWethInput = WethEncodingUtils.decode(WethEncodingUtils.encode(inputWethAmount)); 218 | uint256 actualUsdcOutput = GeneralHelper.getAmountOut(address(weth), usdcAddress, actualWethInput); 219 | uint256 expectedUsdcOutput = FiveBytesEncodingUtils.decode(FiveBytesEncodingUtils.encode(actualUsdcOutput)); 220 | 221 | // need this to pass because: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L160 222 | vm.assume(expectedUsdcOutput > 0); 223 | 224 | (bytes memory calldataPayload, uint256 wethEncodedValue) = 225 | V2SandoUtility.v2CreateFrontrunPayload(usdcAddress, inputWethAmount); 226 | vm.prank(searcher); 227 | (bool s,) = address(sando).call{value: wethEncodedValue}(calldataPayload); 228 | assertTrue(s); 229 | 230 | // check values after swap 231 | assertEq( 232 | ERC20(usdcAddress).balanceOf(sando) - preSwapUsdcBalance, 233 | expectedUsdcOutput, 234 | "did not get expected usdc amount out from swap" 235 | ); 236 | assertEq(preSwapWethBalance - weth.balanceOf(sando), actualWethInput, "unexpected amount of weth used in swap"); 237 | } 238 | 239 | function testV2BackrunWeth0(uint256 inputSuperAmount) public { 240 | address superAddress = 0xe53EC727dbDEB9E2d5456c3be40cFF031AB40A55; // superfarm token 241 | address sugarDaddy = 0xF977814e90dA44bFA03b6295A0616a897441aceC; 242 | 243 | // make sure fuzzed value is within bounds 244 | inputSuperAmount = bound(inputSuperAmount, 1, ERC20(superAddress).balanceOf(sugarDaddy)); 245 | 246 | // fund sando 247 | vm.prank(sugarDaddy); 248 | IUSDT(superAddress).transfer(sando, inputSuperAmount); 249 | 250 | // capture pre swap state 251 | uint256 preSwapWethBalance = weth.balanceOf(sando); 252 | uint256 preSwapSuperBalance = ERC20(superAddress).balanceOf(sando); 253 | 254 | // calculate expected values 255 | uint256 actualFarmInput = FiveBytesEncodingUtils.decode(FiveBytesEncodingUtils.encode(preSwapSuperBalance)); 256 | uint256 actualWethOutput = GeneralHelper.getAmountOut(superAddress, address(weth), actualFarmInput); 257 | uint256 expectedWethOutput = WethEncodingUtils.decode(WethEncodingUtils.encode(actualWethOutput)); 258 | 259 | // need this to pass because: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L160 260 | vm.assume(expectedWethOutput > 0); 261 | 262 | // perform swap 263 | (bytes memory calldataPayload, uint256 wethEncodedValue) = 264 | V2SandoUtility.v2CreateBackrunPayload(superAddress, inputSuperAmount); 265 | vm.prank(searcher); 266 | (bool s,) = address(sando).call{value: wethEncodedValue}(calldataPayload); 267 | assertTrue(s, "swap failed"); 268 | 269 | // check values after swap 270 | assertEq( 271 | weth.balanceOf(sando) - preSwapWethBalance, 272 | expectedWethOutput, 273 | "did not get expected weth amount out from swap" 274 | ); 275 | assertEq( 276 | preSwapSuperBalance - ERC20(superAddress).balanceOf(sando), 277 | actualFarmInput, 278 | "unexpected amount of superFarm used in swap" 279 | ); 280 | } 281 | 282 | function testV2BackrunWeth1(uint256 inputDaiAmount) public { 283 | address daiAddress = 0x6B175474E89094C44Da98b954EedeAC495271d0F; // DAI 284 | address sugarDaddy = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; 285 | 286 | // make sure fuzzed value is within bounds 287 | inputDaiAmount = bound(inputDaiAmount, 1, ERC20(daiAddress).balanceOf(sugarDaddy)); 288 | 289 | // fund sando 290 | vm.prank(sugarDaddy); 291 | ERC20(daiAddress).transfer(sando, inputDaiAmount); 292 | 293 | // capture pre swap state 294 | uint256 preSwapWethBalance = weth.balanceOf(sando); 295 | uint256 preSwapDaiBalance = ERC20(daiAddress).balanceOf(sando); 296 | 297 | // calculate expected values 298 | uint256 actualDaiInput = FiveBytesEncodingUtils.decode(FiveBytesEncodingUtils.encode(preSwapDaiBalance)); 299 | uint256 actualWethOutput = GeneralHelper.getAmountOut(daiAddress, address(weth), actualDaiInput); 300 | uint256 expectedWethOutput = WethEncodingUtils.decode(WethEncodingUtils.encode(actualWethOutput)); 301 | 302 | // need this to pass because: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L160 303 | vm.assume(expectedWethOutput > 0); 304 | 305 | // perform swap 306 | (bytes memory calldataPayload, uint256 wethEncodedValue) = 307 | V2SandoUtility.v2CreateBackrunPayload(daiAddress, inputDaiAmount); 308 | vm.prank(searcher); 309 | (bool s,) = address(sando).call{value: wethEncodedValue}(calldataPayload); 310 | assertTrue(s, "swap failed"); 311 | 312 | // check values after swap 313 | assertEq( 314 | weth.balanceOf(sando) - preSwapWethBalance, 315 | expectedWethOutput, 316 | "did not get expected weth amount out from swap" 317 | ); 318 | assertEq( 319 | preSwapDaiBalance - ERC20(daiAddress).balanceOf(sando), 320 | actualDaiInput, 321 | "unexpected amount of dai used in swap" 322 | ); 323 | } 324 | } 325 | --------------------------------------------------------------------------------