├── .env.example ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── README.md ├── contracts ├── .github │ └── workflows │ │ └── test.yml ├── foundry.toml ├── src │ ├── Request.sol │ └── Sandooo.sol └── test │ └── Sandooo.t.sol └── src ├── common ├── abi.rs ├── alert.rs ├── bytecode.rs ├── constants.rs ├── evm.rs ├── execution.rs ├── mod.rs ├── pools.rs ├── streams.rs ├── tokens.rs └── utils.rs ├── lib.rs ├── main.rs └── sandwich ├── appetizer.rs ├── main_dish.rs ├── mod.rs ├── simulation.rs └── strategy.rs /.env.example: -------------------------------------------------------------------------------- 1 | HTTPS_URL=http://localhost:8545 2 | WSS_URL=ws://localhost:8546 3 | BOT_ADDRESS= 4 | PRIVATE_KEY= 5 | IDENTITY_KEY= 6 | TELEGRAM_TOKEN= 7 | TELEGRAM_CHAT_ID= 8 | USE_ALERT=false 9 | DEBUG=true 10 | RUST_BACKTRACE=1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target 3 | 4 | # Compiler files 5 | cache/ 6 | out/ 7 | 8 | # Ignores development broadcast logs 9 | !/broadcast 10 | /broadcast/*/31337/ 11 | /broadcast/**/dry-run/ 12 | 13 | # Docs 14 | docs/ 15 | 16 | # Dotenv file 17 | .env 18 | 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/forge-std"] 2 | path = contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandooo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | url = "2.3.1" 8 | dotenv = "0.15.0" 9 | anyhow = "1.0.70" 10 | itertools = "0.11.0" 11 | serde = "1.0.188" 12 | serde_json = "1.0.107" 13 | bounded-vec-deque = "0.1.1" 14 | 15 | # Telegram 16 | teloxide = { version = "0.12", features = ["macros"] } 17 | 18 | futures = "0.3.5" 19 | futures-util = "*" 20 | tokio = { version = "1.29.0", features = ["full"] } 21 | tokio-stream = { version = "0.1", features = ['sync'] } 22 | tokio-tungstenite = "*" 23 | async-trait = "0.1.74" 24 | 25 | ethers-core = "2.0" 26 | ethers-providers = "2.0" 27 | ethers-contract = "2.0" 28 | ethers = { version = "2.0", features = ["abigen", "ws", "ipc"] } 29 | 30 | ethers-flashbots = { git = "https://github.com/onbjerg/ethers-flashbots" } 31 | 32 | eth-encode-packed = "0.1.0" 33 | rlp = { version = "0.5", features = ["derive"] } 34 | 35 | foundry-evm-mini = { git = "https://github.com/solidquant/foundry-evm-mini.git" } 36 | 37 | revm = { version = "3", default-features = false, features = [ 38 | "std", 39 | "serde", 40 | "memory_limit", 41 | "optional_eip3607", 42 | "optional_block_gas_limit", 43 | "optional_no_base_fee", 44 | ] } 45 | 46 | csv = "1.2.2" 47 | colored = "2.0.0" 48 | log = "0.4.17" 49 | fern = { version = "0.6.2", features = ["colored"] } 50 | chrono = "0.4.23" 51 | indicatif = "0.17.5" 52 | 53 | [patch.crates-io] 54 | revm = { git = "https://github.com/bluealloy/revm/", rev = "80c909d6f242886cb26e6103a01d1a4bf9468426" } 55 | 56 | [profile.release] 57 | codegen-units = 1 58 | lto = "fat" 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sandooo 2 | 3 | A sandwich bot 4 | 5 | Medium articles WIP: 6 | https://medium.com/@solidquant 7 | 8 | Roadmap + expected release dates: 9 | 1. Opportunity detection (~1/28) 10 | [100 Hours of Building a Sandwich Bot](https://medium.com/@solidquant/100-hours-of-building-a-sandwich-bot-a89235281da3) 11 | 2. Execution (~2/4) 12 | [Let's See If our Sandwich Bot Really Works](https://medium.com/@solidquant/lets-see-if-our-sandwich-bot-really-works-9546c49059bd) 13 | 3. Update #1: Stablecoin sandwich (~2/18) 14 | 4. Update #2: Multiple sandwich bundling (~2/18) 15 | [Adding Stablecoin Sandwiches and Group Bundling to improve our sandwich bot](https://medium.com/@solidquant/adding-stablecoin-sandwiches-and-group-bundling-to-improve-our-sandwich-bot-2037cf741f77) 16 | 5. Update #3: V3 implementation (~2/25) 17 | 18 | ☕ Follow me on Twitter: 19 | https://twitter.com/solidquant 20 | 21 | ⚡️ Come join our Discord community to take this journey together: 22 | 23 | [👨‍👩‍👦‍👦 Join the Solid Quant Discord Server!](https://discord.com/invite/e6KpjTQP98) -------------------------------------------------------------------------------- /contracts/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc_version = '0.8.20' 3 | auto_detect_solc = false 4 | viaIR = true 5 | optimizer = true 6 | optimizer_runs = 200 7 | remappings = [ 8 | "forge-std/=lib/forge-std/src/", 9 | ] -------------------------------------------------------------------------------- /contracts/src/Request.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | interface IERC20 { 5 | function name() external view returns (string memory); 6 | 7 | function symbol() external view returns (string memory); 8 | 9 | function decimals() external view returns (uint8); 10 | 11 | function totalSupply() external view returns (uint256); 12 | } 13 | 14 | contract Request { 15 | function getTokenInfo( 16 | address targetToken 17 | ) 18 | external 19 | view 20 | returns ( 21 | string memory name, 22 | string memory symbol, 23 | uint8 decimals, 24 | uint256 totalSupply 25 | ) 26 | { 27 | IERC20 t = IERC20(targetToken); 28 | 29 | name = t.name(); 30 | symbol = t.symbol(); 31 | decimals = t.decimals(); 32 | totalSupply = t.totalSupply(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/src/Sandooo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | contract Sandooo { 5 | address public owner; 6 | 7 | bytes4 internal constant TOKEN_TRANSFER_ID = 0xa9059cbb; 8 | bytes4 internal constant V2_SWAP_ID = 0x022c0d9f; 9 | 10 | constructor() { 11 | owner = msg.sender; 12 | } 13 | 14 | function recoverToken(address token, uint256 amount) public { 15 | require(msg.sender == owner, "NOT_OWNER"); 16 | 17 | assembly { 18 | switch eq(token, 0) 19 | case 0 { 20 | let ptr := mload(0x40) 21 | mstore(ptr, TOKEN_TRANSFER_ID) 22 | mstore(add(ptr, 4), caller()) 23 | mstore(add(ptr, 36), amount) 24 | if iszero(call(gas(), token, 0, ptr, 68, 0, 0)) { 25 | revert(0, 0) 26 | } 27 | } 28 | case 1 { 29 | if iszero(call(gas(), caller(), amount, 0, 0, 0, 0)) { 30 | revert(0, 0) 31 | } 32 | } 33 | } 34 | } 35 | 36 | receive() external payable {} 37 | 38 | fallback() external payable { 39 | require(msg.sender == owner, "NOT_OWNER"); 40 | 41 | assembly { 42 | let ptr := mload(0x40) 43 | let end := calldatasize() 44 | 45 | let block_number := shr(192, calldataload(0)) 46 | if iszero(eq(block_number, number())) { 47 | revert(0, 0) 48 | } 49 | 50 | for { 51 | let offset := 8 52 | } lt(offset, end) { 53 | 54 | } { 55 | let zeroForOne := shr(248, calldataload(offset)) 56 | let pair := shr(96, calldataload(add(offset, 1))) 57 | let tokenIn := shr(96, calldataload(add(offset, 21))) 58 | let amountIn := calldataload(add(offset, 41)) 59 | let amountOut := calldataload(add(offset, 73)) 60 | offset := add(offset, 105) 61 | 62 | mstore(ptr, TOKEN_TRANSFER_ID) 63 | mstore(add(ptr, 4), pair) 64 | mstore(add(ptr, 36), amountIn) 65 | 66 | if iszero(call(gas(), tokenIn, 0, ptr, 68, 0, 0)) { 67 | revert(0, 0) 68 | } 69 | 70 | mstore(ptr, V2_SWAP_ID) 71 | switch zeroForOne 72 | case 0 { 73 | mstore(add(ptr, 4), amountOut) 74 | mstore(add(ptr, 36), 0) 75 | } 76 | case 1 { 77 | mstore(add(ptr, 4), 0) 78 | mstore(add(ptr, 36), amountOut) 79 | } 80 | mstore(add(ptr, 68), address()) 81 | mstore(add(ptr, 100), 0x80) 82 | 83 | if iszero(call(gas(), pair, 0, ptr, 164, 0, 0)) { 84 | revert(0, 0) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contracts/test/Sandooo.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.20; 2 | 3 | import "forge-std/Test.sol"; 4 | import "forge-std/console.sol"; 5 | 6 | import "../src/Sandooo.sol"; 7 | 8 | interface IERC20 { 9 | event Transfer(address indexed from, address indexed to, uint256 value); 10 | event Approval( 11 | address indexed owner, 12 | address indexed spender, 13 | uint256 value 14 | ); 15 | 16 | function name() external view returns (string memory); 17 | 18 | function symbol() external view returns (string memory); 19 | 20 | function decimals() external view returns (uint8); 21 | 22 | function totalSupply() external view returns (uint256); 23 | 24 | function balanceOf(address account) external view returns (uint256); 25 | 26 | function transfer(address to, uint256 value) external returns (bool); 27 | 28 | function allowance( 29 | address owner, 30 | address spender 31 | ) external view returns (uint256); 32 | 33 | function approve(address spender, uint256 value) external returns (bool); 34 | 35 | function transferFrom( 36 | address from, 37 | address to, 38 | uint256 value 39 | ) external returns (bool); 40 | } 41 | 42 | interface IWETH is IERC20 { 43 | function deposit() external payable; 44 | 45 | function withdraw(uint amount) external; 46 | } 47 | 48 | interface IUniswapV2Pair { 49 | function token0() external returns (address); 50 | 51 | function token1() external returns (address); 52 | 53 | function getReserves() 54 | external 55 | view 56 | returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 57 | } 58 | 59 | // anvil --fork-url http://localhost:8545 --port 2000 60 | // forge test --fork-url http://localhost:2000 --match-contract SandoooTest -vv 61 | contract SandoooTest is Test { 62 | Sandooo bot; 63 | IWETH weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 64 | 65 | receive() external payable {} 66 | 67 | function test() public { 68 | console.log("Sandooo bot test starting"); 69 | 70 | // Create Sandooo instance 71 | bot = new Sandooo(); 72 | 73 | uint256 amountIn = 100000000000000000; // 0.1 ETH 74 | 75 | // Wrap 0.1 ETH to 0.1 WETH and send to Sandooo contract 76 | weth.deposit{value: amountIn}(); 77 | weth.transfer(address(bot), amountIn); 78 | 79 | // Check if WETH is properly sent 80 | uint256 botBalance = weth.balanceOf(address(bot)); 81 | console.log("Bot WETH balance: %s", botBalance); 82 | 83 | // Check if we can recover WETH 84 | bot.recoverToken(address(weth), botBalance); 85 | uint256 botBalanceAfterRecover = weth.balanceOf(address(bot)); 86 | console.log( 87 | "Bot WETH balance after recover: %s", 88 | botBalanceAfterRecover 89 | ); // should be 0 90 | 91 | // Check if we can recover ETH 92 | (bool s, ) = address(bot).call{value: amountIn}(""); 93 | console.log("ETH transfer: %s", s); 94 | uint256 testEthBal = address(this).balance; 95 | uint256 botEthBal = address(bot).balance; 96 | console.log("Curr ETH balance: %s", testEthBal); 97 | console.log("Bot ETH balance: %s", botEthBal); 98 | 99 | // Send zero address to retrieve ETH 100 | bot.recoverToken(address(0), botEthBal); 101 | 102 | uint256 testEthBalAfterRecover = address(this).balance; 103 | uint256 botEthBalAfterRecover = address(bot).balance; 104 | console.log("ETH balance after recover: %s", testEthBalAfterRecover); 105 | console.log("Bot ETH balance after recover: %s", botEthBalAfterRecover); 106 | 107 | console.log("============================"); 108 | 109 | // Transfer WETH to contract again 110 | weth.transfer(address(bot), amountIn); 111 | uint256 startingWethBalance = weth.balanceOf(address(bot)); 112 | console.log("Starting WETH balance: %s", startingWethBalance); 113 | 114 | address usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; 115 | address wethUsdtV2 = 0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852; 116 | 117 | IUniswapV2Pair pair = IUniswapV2Pair(wethUsdtV2); 118 | address token0 = pair.token0(); 119 | address token1 = pair.token1(); 120 | 121 | // We will be testing WETH --> USDT 122 | // So it's zeroForOne if WETH is token0 123 | uint8 zeroForOne = address(weth) == token0 ? 1 : 0; 124 | 125 | // Calculate the amountOut using reserves 126 | (uint112 reserve0, uint112 reserve1, ) = IUniswapV2Pair(address(pair)) 127 | .getReserves(); 128 | 129 | uint256 reserveIn; 130 | uint256 reserveOut; 131 | 132 | if (zeroForOne == 1) { 133 | reserveIn = reserve0; 134 | reserveOut = reserve1; 135 | } else { 136 | reserveIn = reserve1; 137 | reserveOut = reserve0; 138 | } 139 | 140 | uint256 amountInWithFee = amountIn * 997; 141 | uint256 numerator = amountInWithFee * reserveOut; 142 | uint256 denominator = reserveIn * 1000 + amountInWithFee; 143 | uint256 targetAmountOut = numerator / denominator; 144 | 145 | console.log("Amount in: %s", amountIn); 146 | console.log("Target amount out: %s", targetAmountOut); 147 | 148 | bytes memory data = abi.encodePacked( 149 | uint64(block.number), // blockNumber 150 | uint8(zeroForOne), // zeroForOne 151 | address(pair), // pair 152 | address(weth), // tokenIn 153 | uint256(amountIn), // amountIn 154 | uint256(targetAmountOut) // amountOut 155 | ); 156 | console.log("Calldata:"); 157 | console.logBytes(data); 158 | 159 | uint gasBefore = gasleft(); 160 | (bool success, ) = address(bot).call(data); 161 | uint gasAfter = gasleft(); 162 | uint gasUsed = gasBefore - gasAfter; 163 | console.log("Swap success: %s", success); 164 | console.log("Gas used: %s", gasUsed); 165 | 166 | uint256 usdtBalance = IERC20(usdt).balanceOf(address(bot)); 167 | console.log("Bot USDT balance: %s", usdtBalance); 168 | 169 | require(success, "FAILED"); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/common/abi.rs: -------------------------------------------------------------------------------- 1 | use ethers::abi::parse_abi; 2 | use ethers::prelude::BaseContract; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct Abi { 6 | pub factory: BaseContract, 7 | pub pair: BaseContract, 8 | pub token: BaseContract, 9 | pub sando_bot: BaseContract, 10 | } 11 | 12 | impl Abi { 13 | pub fn new() -> Self { 14 | let factory = BaseContract::from( 15 | parse_abi(&["function getPair(address,address) external view returns (address)"]) 16 | .unwrap(), 17 | ); 18 | 19 | let pair = BaseContract::from( 20 | parse_abi(&[ 21 | "function token0() external view returns (address)", 22 | "function token1() external view returns (address)", 23 | "function getReserves() external view returns (uint112,uint112,uint32)", 24 | ]) 25 | .unwrap(), 26 | ); 27 | 28 | let token = BaseContract::from( 29 | parse_abi(&[ 30 | "function owner() external view returns (address)", 31 | "function name() external view returns (string)", 32 | "function symbol() external view returns (string)", 33 | "function decimals() external view returns (uint8)", 34 | "function totalSupply() external view returns (uint256)", 35 | "function balanceOf(address) external view returns (uint256)", 36 | "function approve(address,uint256) external view returns (bool)", 37 | "function transfer(address,uint256) external returns (bool)", 38 | "function allowance(address,address) external view returns (uint256)", 39 | ]) 40 | .unwrap(), 41 | ); 42 | 43 | let sando_bot = BaseContract::from( 44 | parse_abi(&["function recoverToken(address,uint256) public"]).unwrap(), 45 | ); 46 | 47 | Self { 48 | factory, 49 | pair, 50 | token, 51 | sando_bot, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/common/alert.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ethers::types::{H256, U64}; 3 | use teloxide::prelude::*; 4 | use teloxide::types::ChatId; 5 | 6 | use crate::common::constants::Env; 7 | 8 | pub struct Alert { 9 | pub bot: Option, 10 | pub chat_id: Option, 11 | } 12 | 13 | impl Alert { 14 | pub fn new() -> Self { 15 | let env = Env::new(); 16 | if env.use_alert { 17 | let bot = Bot::from_env(); 18 | let chat_id = ChatId(env.telegram_chat_id.parse::().unwrap()); 19 | Self { 20 | bot: Some(bot), 21 | chat_id: Some(chat_id), 22 | } 23 | } else { 24 | Self { 25 | bot: None, 26 | chat_id: None, 27 | } 28 | } 29 | } 30 | 31 | pub async fn send(&self, message: &str) -> Result<()> { 32 | match &self.bot { 33 | Some(bot) => { 34 | bot.send_message(self.chat_id.unwrap(), message).await?; 35 | } 36 | _ => {} 37 | } 38 | Ok(()) 39 | } 40 | 41 | pub async fn send_bundle_sent( 42 | &self, 43 | block_number: U64, 44 | tx_hash: H256, 45 | gambit_hash: H256, 46 | ) -> Result<()> { 47 | let eigenphi_url = format!("https://eigenphi.io/mev/eigentx/{:?}", tx_hash); 48 | let gambit_url = format!("https://gmbit-co.vercel.app/auction?txHash={:?}", tx_hash); 49 | let mut message = format!("[Block #{:?}] Bundle sent: {:?}", block_number, tx_hash); 50 | message = format!("{}\n-Eigenphi: {}", message, eigenphi_url); 51 | message = format!("{}\n-Gambit: {}", message, gambit_url); 52 | message = format!("{}\n-Gambit bundle hash: {:?}", message, gambit_hash); 53 | self.send(&message).await?; 54 | Ok(()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/common/bytecode.rs: -------------------------------------------------------------------------------- 1 | use ethers::{prelude::Lazy, types::Bytes}; 2 | 3 | pub static REQUEST_BYTECODE: Lazy = Lazy::new(|| { 4 | "0x608060409080825260048036101561001657600080fd5b6000803560e01c631f69565f1461002c57600080fd5b3461014857602093846003193601126102035782356001600160a01b03811693908490036101ff576306fdde0360e01b855282858281875afa9485156101f35783956101d7575b5081516395d89b4160e01b81529083828281885afa9182156101ca5784926101a6575b50825163313ce56760e01b81529487868381845afa95861561019c57908891869761015d575b5084516318160ddd60e01b815292839182905afa938415610152578094610119575b505061010a6100fd9660ff92845198899860808a5260808a019061022a565b918883039089015261022a565b93169084015260608301520390f35b909193508682813d831161014b575b610132818361024f565b810103126101485750519161010a6100fd6100de565b80fd5b503d610128565b8351903d90823e3d90fd5b8281939298503d8311610195575b610175818361024f565b81010312610191575160ff8116810361019157879095386100bc565b8480fd5b503d61016b565b84513d87823e3d90fd5b6101c39192503d8086833e6101bb818361024f565b810190610287565b9038610096565b50505051903d90823e3d90fd5b6101ec9195503d8085833e6101bb818361024f565b9338610073565b505051903d90823e3d90fd5b8280fd5b5080fd5b60005b83811061021a5750506000910152565b818101518382015260200161020a565b9060209161024381518092818552858086019101610207565b601f01601f1916010190565b90601f8019910116810190811067ffffffffffffffff82111761027157604052565b634e487b7160e01b600052604160045260246000fd5b6020818303126102f357805167ffffffffffffffff918282116102f357019082601f830112156102f357815190811161027157604051926102d2601f8301601f19166020018561024f565b818452602082840101116102f3576102f09160208085019101610207565b90565b600080fdfea264697066735822122004fbd047c788ee9f88c1adbdb92195b7b34a4454850b26610c1bca2cfdee742264736f6c63430008140033".parse().unwrap() 5 | }); 6 | 7 | pub static SANDOOO_BYTECODE: Lazy = Lazy::new(|| { 8 | "0x6080604052600436101561001e575b361561001c5761001c61012d565b005b6000803560e01c80638da5cb5b146100d05763b29a814014610040575061000e565b3461009e57604036600319011261009e57806001600160a01b0360043581811681036100cc576100776024359284541633146100f5565b82811591826000146100a157505060011461008f5750f35b81808092335af11561009e5780f35b80fd5b60449250908093916040519263a9059cbb60e01b845233600485015260248401525af11561009e5780f35b5050fd5b503461009e578060031936011261009e57546001600160a01b03166080908152602090f35b156100fc57565b60405162461bcd60e51b81526020600482015260096024820152682727aa2fa7aba722a960b91b6044820152606490fd5b60008054610145906001600160a01b031633146100f5565b60405143823560c01c03610209576008600482019160248101925b36831061016e575050505050565b823560f81c926060906001810135821c916015820135901c9487806044878260298701359a6069604989013598019b63a9059cbb60e01b8452898b528d525af1156102055784888094819460a49463022c0d9f60e01b8552806000146101f9576001146101ee575b50306044840152608060648401525af1610160578480fd5b8288528a52386101d6565b508752818a52386101d6565b8780fd5b5080fdfea264697066735822122070cd8d8a51fe625e0f10f1ea26f94679859661cf1936f171d337a6616cfb19ad64736f6c63430008140033".parse().unwrap() 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/constants.rs: -------------------------------------------------------------------------------- 1 | pub static PROJECT_NAME: &str = "sandooo"; 2 | 3 | pub fn get_env(key: &str) -> String { 4 | std::env::var(key).unwrap_or(String::from("")) 5 | } 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Env { 9 | pub https_url: String, 10 | pub wss_url: String, 11 | pub bot_address: String, 12 | pub private_key: String, 13 | pub identity_key: String, 14 | pub telegram_token: String, 15 | pub telegram_chat_id: String, 16 | pub use_alert: bool, 17 | pub debug: bool, 18 | } 19 | 20 | impl Env { 21 | pub fn new() -> Self { 22 | Env { 23 | https_url: get_env("HTTPS_URL"), 24 | wss_url: get_env("WSS_URL"), 25 | bot_address: get_env("BOT_ADDRESS"), 26 | private_key: get_env("PRIVATE_KEY"), 27 | identity_key: get_env("IDENTITY_KEY"), 28 | telegram_token: get_env("TELEGRAM_TOKEN"), 29 | telegram_chat_id: get_env("TELEGRAM_CHAT_ID"), 30 | use_alert: get_env("USE_ALERT").parse::().unwrap(), 31 | debug: get_env("DEBUG").parse::().unwrap(), 32 | } 33 | } 34 | } 35 | 36 | pub static COINBASE: &str = "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"; // Flashbots Builder 37 | 38 | pub static WETH: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; 39 | pub static USDT: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; 40 | pub static USDC: &str = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; 41 | 42 | /* 43 | Can figure out the balance slot of ERC-20 tokens using the: 44 | EvmSimulator::get_balance_slot method 45 | 46 | However, note that this does not work for all tokens. 47 | Especially tokens that are using proxy patterns. 48 | */ 49 | pub static WETH_BALANCE_SLOT: i32 = 3; 50 | pub static USDT_BALANCE_SLOT: i32 = 2; 51 | pub static USDC_BALANCE_SLOT: i32 = 9; 52 | 53 | pub static WETH_DECIMALS: u8 = 18; 54 | pub static USDT_DECIMALS: u8 = 6; 55 | pub static USDC_DECIMALS: u8 = 6; 56 | -------------------------------------------------------------------------------- /src/common/evm.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ethers::prelude::abi; 3 | use ethers::providers::Middleware; 4 | use ethers::types::{transaction::eip2930::AccessList, H160, H256, U256, U64}; 5 | use foundry_evm_mini::evm::executor::fork::{BlockchainDb, BlockchainDbMeta, SharedBackend}; 6 | use foundry_evm_mini::evm::executor::inspector::{get_precompiles_for, AccessListTracer}; 7 | use revm::primitives::bytes::Bytes as rBytes; 8 | use revm::primitives::{Bytes, Log, B160}; 9 | use revm::{ 10 | db::{CacheDB, Database}, 11 | primitives::{ 12 | keccak256, AccountInfo, Bytecode, ExecutionResult, Output, TransactTo, B256, U256 as rU256, 13 | }, 14 | EVM, 15 | }; 16 | use std::{collections::BTreeSet, default::Default, str::FromStr, sync::Arc}; 17 | 18 | use crate::common::abi::Abi; 19 | use crate::common::constants::COINBASE; 20 | use crate::common::utils::{access_list_to_revm, create_new_wallet}; 21 | 22 | #[derive(Debug, Clone, Default)] 23 | pub struct VictimTx { 24 | pub tx_hash: H256, 25 | pub from: H160, 26 | pub to: H160, 27 | pub data: Bytes, 28 | pub value: U256, 29 | pub gas_price: U256, 30 | pub gas_limit: Option, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct Tx { 35 | pub caller: H160, 36 | pub transact_to: H160, 37 | pub data: rBytes, 38 | pub value: U256, 39 | pub gas_price: U256, 40 | pub gas_limit: u64, 41 | } 42 | 43 | impl Tx { 44 | pub fn from(tx: VictimTx) -> Self { 45 | let gas_limit = match tx.gas_limit { 46 | Some(gas_limit) => gas_limit, 47 | None => 5000000, 48 | }; 49 | Self { 50 | caller: tx.from, 51 | transact_to: tx.to, 52 | data: tx.data, 53 | value: tx.value, 54 | gas_price: tx.gas_price, 55 | gas_limit, 56 | } 57 | } 58 | } 59 | 60 | #[derive(Debug, Clone)] 61 | pub struct TxResult { 62 | pub output: rBytes, 63 | pub logs: Option>, 64 | pub gas_used: u64, 65 | pub gas_refunded: u64, 66 | } 67 | 68 | #[derive(Clone)] 69 | pub struct EvmSimulator { 70 | pub provider: Arc, 71 | pub owner: H160, 72 | pub evm: EVM>, 73 | pub block_number: U64, 74 | pub abi: Abi, 75 | } 76 | 77 | impl EvmSimulator { 78 | pub fn new(provider: Arc, owner: Option, block_number: U64) -> Self { 79 | let shared_backend = SharedBackend::spawn_backend_thread( 80 | provider.clone(), 81 | BlockchainDb::new( 82 | BlockchainDbMeta { 83 | cfg_env: Default::default(), 84 | block_env: Default::default(), 85 | hosts: BTreeSet::from(["".to_string()]), 86 | }, 87 | None, 88 | ), 89 | Some(block_number.into()), 90 | ); 91 | let db = CacheDB::new(shared_backend); 92 | EvmSimulator::new_with_db(provider, owner, block_number, db) 93 | } 94 | 95 | pub fn new_with_db( 96 | provider: Arc, 97 | owner: Option, 98 | block_number: U64, 99 | db: CacheDB, 100 | ) -> Self { 101 | let owner = match owner { 102 | Some(owner) => owner, 103 | None => create_new_wallet().1, 104 | }; 105 | 106 | let mut evm = EVM::new(); 107 | evm.database(db); 108 | 109 | evm.env.block.number = rU256::from(block_number.as_u64() + 1); 110 | evm.env.block.coinbase = H160::from_str(COINBASE).unwrap().into(); 111 | 112 | Self { 113 | provider, 114 | owner, 115 | evm, 116 | block_number, 117 | abi: Abi::new(), 118 | } 119 | } 120 | 121 | pub fn clone_db(&mut self) -> CacheDB { 122 | self.evm.db.as_mut().unwrap().clone() 123 | } 124 | 125 | pub fn insert_db(&mut self, db: CacheDB) { 126 | let mut evm = EVM::new(); 127 | evm.database(db); 128 | 129 | self.evm = evm; 130 | } 131 | 132 | pub fn get_block_number(&mut self) -> U256 { 133 | self.evm.env.block.number.into() 134 | } 135 | 136 | pub fn get_coinbase(&mut self) -> H160 { 137 | self.evm.env.block.coinbase.into() 138 | } 139 | 140 | pub fn get_base_fee(&mut self) -> U256 { 141 | self.evm.env.block.basefee.into() 142 | } 143 | 144 | pub fn set_base_fee(&mut self, base_fee: U256) { 145 | self.evm.env.block.basefee = base_fee.into(); 146 | } 147 | 148 | pub fn get_access_list(&mut self, tx: Tx) -> Result { 149 | self.evm.env.tx.caller = tx.caller.into(); 150 | self.evm.env.tx.transact_to = TransactTo::Call(tx.transact_to.into()); 151 | self.evm.env.tx.data = tx.data; 152 | self.evm.env.tx.value = tx.value.into(); 153 | self.evm.env.tx.gas_price = tx.gas_price.into(); 154 | self.evm.env.tx.gas_limit = tx.gas_limit; 155 | let mut access_list_tracer = AccessListTracer::new( 156 | Default::default(), 157 | tx.caller.into(), 158 | tx.transact_to.into(), 159 | get_precompiles_for(self.evm.env.cfg.spec_id), 160 | ); 161 | let access_list = match self.evm.inspect_ref(&mut access_list_tracer) { 162 | Ok(_) => access_list_tracer.access_list(), 163 | Err(_) => AccessList::default(), 164 | }; 165 | Ok(access_list) 166 | } 167 | 168 | pub fn set_access_list(&mut self, access_list: AccessList) { 169 | self.evm.env.tx.access_list = access_list_to_revm(access_list); 170 | } 171 | 172 | pub fn staticcall(&mut self, tx: Tx) -> Result { 173 | self._call(tx, false) 174 | } 175 | 176 | pub fn call(&mut self, tx: Tx) -> Result { 177 | self._call(tx, true) 178 | } 179 | 180 | pub fn _call(&mut self, tx: Tx, commit: bool) -> Result { 181 | self.evm.env.tx.caller = tx.caller.into(); 182 | self.evm.env.tx.transact_to = TransactTo::Call(tx.transact_to.into()); 183 | self.evm.env.tx.data = tx.data; 184 | self.evm.env.tx.value = tx.value.into(); 185 | self.evm.env.tx.gas_price = tx.gas_price.into(); 186 | self.evm.env.tx.gas_limit = tx.gas_limit; 187 | 188 | let result; 189 | 190 | if commit { 191 | result = match self.evm.transact_commit() { 192 | Ok(result) => result, 193 | Err(e) => return Err(anyhow!("EVM call failed: {:?}", e)), 194 | }; 195 | } else { 196 | let ref_tx = self 197 | .evm 198 | .transact_ref() 199 | .map_err(|e| anyhow!("EVM staticcall failed: {:?}", e))?; 200 | result = ref_tx.result; 201 | } 202 | 203 | let output = match result { 204 | ExecutionResult::Success { 205 | gas_used, 206 | gas_refunded, 207 | output, 208 | logs, 209 | .. 210 | } => match output { 211 | Output::Call(o) => TxResult { 212 | output: o, 213 | logs: Some(logs), 214 | gas_used, 215 | gas_refunded, 216 | }, 217 | Output::Create(o, _) => TxResult { 218 | output: o, 219 | logs: Some(logs), 220 | gas_used, 221 | gas_refunded, 222 | }, 223 | }, 224 | ExecutionResult::Revert { gas_used, output } => { 225 | return Err(anyhow!( 226 | "EVM REVERT: {:?} / Gas used: {:?}", 227 | output, 228 | gas_used 229 | )) 230 | } 231 | ExecutionResult::Halt { reason, .. } => return Err(anyhow!("EVM HALT: {:?}", reason)), 232 | }; 233 | 234 | Ok(output) 235 | } 236 | 237 | pub fn basic(&mut self, target: H160) -> Result> { 238 | self.evm 239 | .db 240 | .as_mut() 241 | .unwrap() 242 | .basic(target.into()) 243 | .map_err(|e| anyhow!("Basic error: {e:?}")) 244 | } 245 | 246 | pub fn insert_account_info(&mut self, target: H160, account_info: AccountInfo) { 247 | self.evm 248 | .db 249 | .as_mut() 250 | .unwrap() 251 | .insert_account_info(target.into(), account_info); 252 | } 253 | 254 | pub fn insert_account_storage( 255 | &mut self, 256 | target: H160, 257 | slot: rU256, 258 | value: rU256, 259 | ) -> Result<()> { 260 | self.evm 261 | .db 262 | .as_mut() 263 | .unwrap() 264 | .insert_account_storage(target.into(), slot, value)?; 265 | Ok(()) 266 | } 267 | 268 | pub fn deploy(&mut self, target: H160, bytecode: Bytecode) { 269 | let contract_info = AccountInfo::new(rU256::ZERO, 0, B256::zero(), bytecode); 270 | self.insert_account_info(target, contract_info); 271 | } 272 | 273 | pub fn get_eth_balance_of(&mut self, target: H160) -> U256 { 274 | let acc = self.basic(target).unwrap().unwrap(); 275 | acc.balance.into() 276 | } 277 | 278 | pub fn set_eth_balance(&mut self, target: H160, amount: U256) { 279 | let user_balance = amount.into(); 280 | let user_info = AccountInfo::new(user_balance, 0, B256::zero(), Bytecode::default()); 281 | self.insert_account_info(target.into(), user_info); 282 | } 283 | 284 | pub fn get_token_balance(&mut self, token_address: H160, owner: H160) -> Result { 285 | let calldata = self.abi.token.encode("balanceOf", owner)?; 286 | let value = self.staticcall(Tx { 287 | caller: self.owner, 288 | transact_to: token_address, 289 | data: calldata.0, 290 | value: U256::zero(), 291 | gas_price: U256::zero(), 292 | gas_limit: 5000000, 293 | })?; 294 | let out = self.abi.token.decode_output("balanceOf", value.output)?; 295 | Ok(out) 296 | } 297 | 298 | pub fn set_token_balance( 299 | &mut self, 300 | token_address: H160, 301 | to: H160, 302 | slot: i32, 303 | amount: rU256, 304 | ) -> Result<()> { 305 | let balance_slot = keccak256(&abi::encode(&[ 306 | abi::Token::Address(to.into()), 307 | abi::Token::Uint(U256::from(slot)), 308 | ])); 309 | self.insert_account_storage(token_address, balance_slot.into(), amount)?; 310 | Ok(()) 311 | } 312 | 313 | pub fn get_pair_reserves(&mut self, pair_address: H160) -> Result<(U256, U256)> { 314 | let calldata = self.abi.pair.encode("getReserves", ())?; 315 | let value = self.staticcall(Tx { 316 | caller: self.owner, 317 | transact_to: pair_address, 318 | data: calldata.0, 319 | value: U256::zero(), 320 | gas_price: U256::zero(), 321 | gas_limit: 5000000, 322 | })?; 323 | let out: (U256, U256, U256) = self.abi.pair.decode_output("getReserves", value.output)?; 324 | Ok((out.0, out.1)) 325 | } 326 | 327 | pub fn get_balance_slot(&mut self, token_address: H160) -> Result { 328 | let calldata = self.abi.token.encode("balanceOf", token_address)?; 329 | self.evm.env.tx.caller = self.owner.into(); 330 | self.evm.env.tx.transact_to = TransactTo::Call(token_address.into()); 331 | self.evm.env.tx.data = calldata.0; 332 | let result = match self.evm.transact_ref() { 333 | Ok(result) => result, 334 | Err(e) => return Err(anyhow!("EVM ref call failed: {e:?}")), 335 | }; 336 | let token_b160: B160 = token_address.into(); 337 | let token_acc = result.state.get(&token_b160).unwrap(); 338 | let token_touched_storage = token_acc.storage.clone(); 339 | for i in 0..30 { 340 | let slot = keccak256(&abi::encode(&[ 341 | abi::Token::Address(token_address.into()), 342 | abi::Token::Uint(U256::from(i)), 343 | ])); 344 | let slot: rU256 = U256::from(slot).into(); 345 | match token_touched_storage.get(&slot) { 346 | Some(_) => { 347 | return Ok(i); 348 | } 349 | None => {} 350 | } 351 | } 352 | 353 | Ok(-1) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/common/execution.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ethers::prelude::*; 3 | use ethers::providers::{Middleware, Provider}; 4 | use ethers::signers::{LocalWallet, Signer}; 5 | use ethers::types::transaction::{eip2718::TypedTransaction, eip2930::AccessList}; 6 | use ethers_flashbots::*; 7 | use serde::Deserialize; 8 | use std::collections::HashMap; 9 | use std::str::FromStr; 10 | use std::sync::Arc; 11 | use url::Url; 12 | 13 | use crate::common::abi::Abi; 14 | use crate::common::constants::Env; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct SandoBundle { 18 | pub frontrun_tx: TypedTransaction, 19 | pub victim_txs: Vec, 20 | pub backrun_tx: TypedTransaction, 21 | } 22 | 23 | #[derive(Debug, Deserialize, Clone, Default)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct SendBundleResponse { 26 | pub bundle_hash: BundleHash, 27 | } 28 | 29 | pub async fn send_bundle( 30 | builder: String, 31 | relay_url: Url, 32 | identity: LocalWallet, 33 | bundle: BundleRequest, 34 | ) -> Result<(String, Option)> { 35 | let relay = Relay::new(relay_url, Some(identity.clone())); 36 | let result: Option = relay.request("eth_sendBundle", [bundle]).await?; 37 | Ok((builder, result)) 38 | } 39 | 40 | pub struct Executor { 41 | pub provider: Arc>, 42 | pub abi: Abi, 43 | pub owner: LocalWallet, 44 | pub identity: LocalWallet, 45 | pub bot_address: H160, 46 | pub builder_urls: HashMap, 47 | pub client: SignerMiddleware>, LocalWallet>, LocalWallet>, 48 | } 49 | 50 | impl Executor { 51 | pub fn new(provider: Arc>) -> Self { 52 | let env = Env::new(); 53 | let abi = Abi::new(); 54 | let bot_address = H160::from_str(&env.bot_address).unwrap(); 55 | 56 | let owner = env 57 | .private_key 58 | .parse::() 59 | .unwrap() 60 | .with_chain_id(1 as u64); 61 | 62 | let identity = env 63 | .identity_key 64 | .parse::() 65 | .unwrap() 66 | .with_chain_id(1 as u64); 67 | 68 | let relay_url = Url::parse("https://relay.flashbots.net").unwrap(); 69 | 70 | let client = SignerMiddleware::new( 71 | FlashbotsMiddleware::new(provider.clone(), relay_url.clone(), identity.clone()), 72 | owner.clone(), 73 | ); 74 | 75 | // The endpoints here will gracefully fail if it doesn't work 76 | let mut builder_urls = HashMap::new(); 77 | builder_urls.insert( 78 | "flashbots".to_string(), 79 | Url::parse("https://relay.flashbots.net").unwrap(), 80 | ); 81 | builder_urls.insert( 82 | "beaverbuild".to_string(), 83 | Url::parse("https://rpc.beaverbuild.org").unwrap(), 84 | ); 85 | builder_urls.insert( 86 | "rsync".to_string(), 87 | Url::parse("https://rsync-builder.xyz").unwrap(), 88 | ); 89 | builder_urls.insert( 90 | "titanbuilder".to_string(), 91 | Url::parse("https://rpc.titanbuilder.xyz").unwrap(), 92 | ); 93 | builder_urls.insert( 94 | "builder0x69".to_string(), 95 | Url::parse("https://builder0x69.io").unwrap(), 96 | ); 97 | builder_urls.insert("f1b".to_string(), Url::parse("https://rpc.f1b.io").unwrap()); 98 | builder_urls.insert( 99 | "lokibuilder".to_string(), 100 | Url::parse("https://rpc.lokibuilder.xyz").unwrap(), 101 | ); 102 | builder_urls.insert( 103 | "eden".to_string(), 104 | Url::parse("https://api.edennetwork.io/v1/rpc").unwrap(), 105 | ); 106 | builder_urls.insert( 107 | "penguinbuild".to_string(), 108 | Url::parse("https://rpc.penguinbuild.org").unwrap(), 109 | ); 110 | builder_urls.insert( 111 | "gambit".to_string(), 112 | Url::parse("https://builder.gmbit.co/rpc").unwrap(), 113 | ); 114 | builder_urls.insert( 115 | "idcmev".to_string(), 116 | Url::parse("https://rpc.idcmev.xyz").unwrap(), 117 | ); 118 | 119 | Self { 120 | provider, 121 | abi, 122 | owner, 123 | identity, 124 | bot_address, 125 | builder_urls, 126 | client, 127 | } 128 | } 129 | 130 | pub async fn _common_fields(&self) -> Result<(H160, U256, U64)> { 131 | let nonce = self 132 | .provider 133 | .get_transaction_count(self.owner.address(), Some(BlockNumber::Latest.into())) 134 | .await?; 135 | Ok((self.owner.address(), U256::from(nonce), U64::from(1))) 136 | } 137 | 138 | pub async fn transfer_in_tx(&self, amount_in: U256) -> Result { 139 | let tx = { 140 | let mut inner: TypedTransaction = 141 | TransactionRequest::pay(self.bot_address, amount_in).into(); 142 | self.client 143 | .fill_transaction(&mut inner, None) 144 | .await 145 | .unwrap(); 146 | inner 147 | }; 148 | Ok(tx) 149 | } 150 | 151 | pub async fn transfer_out_tx( 152 | &self, 153 | token: H160, 154 | amount: U256, 155 | max_priority_fee_per_gas: U256, 156 | max_fee_per_gas: U256, 157 | ) -> Result { 158 | let common = self._common_fields().await?; 159 | let calldata = self.abi.sando_bot.encode("recoverToken", (token, amount))?; 160 | let to = NameOrAddress::Address(self.bot_address); 161 | Ok(TypedTransaction::Eip1559(Eip1559TransactionRequest { 162 | to: Some(to), 163 | from: Some(common.0), 164 | data: Some(calldata), 165 | value: Some(U256::zero()), 166 | chain_id: Some(common.2), 167 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 168 | max_fee_per_gas: Some(max_fee_per_gas), 169 | gas: Some(U256::from(600000)), 170 | nonce: Some(common.1), 171 | access_list: AccessList::default(), 172 | })) 173 | } 174 | 175 | pub async fn to_typed_transaction( 176 | &self, 177 | calldata: Bytes, 178 | access_list: AccessList, 179 | gas_limit: u64, 180 | nonce: U256, 181 | max_priority_fee_per_gas: U256, 182 | max_fee_per_gas: U256, 183 | ) -> Result { 184 | let common = self._common_fields().await?; 185 | let to = NameOrAddress::Address(self.bot_address); 186 | Ok(TypedTransaction::Eip1559(Eip1559TransactionRequest { 187 | to: Some(to.clone()), 188 | from: Some(common.0), 189 | data: Some(calldata), 190 | value: Some(U256::zero()), 191 | chain_id: Some(common.2), 192 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 193 | max_fee_per_gas: Some(max_fee_per_gas), 194 | gas: Some(U256::from(gas_limit)), 195 | nonce: Some(nonce), 196 | access_list, 197 | })) 198 | } 199 | 200 | pub async fn create_sando_bundle( 201 | &self, 202 | victim_txs: Vec, 203 | front_calldata: Bytes, 204 | back_calldata: Bytes, 205 | front_access_list: AccessList, 206 | back_access_list: AccessList, 207 | front_gas_limit: u64, 208 | back_gas_limit: u64, 209 | base_fee: U256, 210 | max_priority_fee_per_gas: U256, 211 | max_fee_per_gas: U256, 212 | ) -> Result { 213 | let common = self._common_fields().await?; 214 | let to = NameOrAddress::Address(self.bot_address); 215 | let front_nonce = common.1; 216 | let back_nonce = front_nonce + U256::from(1); // should increase nonce by 1 217 | let frontrun_tx = TypedTransaction::Eip1559(Eip1559TransactionRequest { 218 | to: Some(to.clone()), 219 | from: Some(common.0), 220 | data: Some(front_calldata), 221 | value: Some(U256::zero()), 222 | chain_id: Some(common.2), 223 | max_priority_fee_per_gas: Some(U256::zero()), 224 | max_fee_per_gas: Some(base_fee), 225 | gas: Some(U256::from(front_gas_limit)), 226 | nonce: Some(front_nonce), 227 | access_list: front_access_list, 228 | }); 229 | let backrun_tx = TypedTransaction::Eip1559(Eip1559TransactionRequest { 230 | to: Some(to), 231 | from: Some(common.0), 232 | data: Some(back_calldata), 233 | value: Some(U256::zero()), 234 | chain_id: Some(common.2), 235 | max_priority_fee_per_gas: Some(max_priority_fee_per_gas), 236 | max_fee_per_gas: Some(max_fee_per_gas), 237 | gas: Some(U256::from(back_gas_limit)), 238 | nonce: Some(back_nonce), 239 | access_list: back_access_list, 240 | }); 241 | Ok(SandoBundle { 242 | frontrun_tx, 243 | victim_txs, 244 | backrun_tx, 245 | }) 246 | } 247 | 248 | pub async fn to_bundle_request( 249 | &self, 250 | tx: TypedTransaction, 251 | block_number: U64, 252 | retries: usize, 253 | ) -> Result { 254 | let signature = self.client.signer().sign_transaction(&tx).await?; 255 | let bundle = BundleRequest::new() 256 | .push_transaction(tx.rlp_signed(&signature)) 257 | .set_block(block_number + U64::from(retries)) 258 | .set_simulation_block(block_number) 259 | .set_simulation_timestamp(0); 260 | Ok(bundle) 261 | } 262 | 263 | pub async fn to_sando_bundle_request( 264 | &self, 265 | sando_bundle: SandoBundle, 266 | block_number: U64, 267 | retries: usize, 268 | ) -> Result { 269 | let frontrun_signature = self 270 | .client 271 | .signer() 272 | .sign_transaction(&sando_bundle.frontrun_tx) 273 | .await?; 274 | let signed_frontrun_tx = sando_bundle.frontrun_tx.rlp_signed(&frontrun_signature); 275 | 276 | let backrun_signature = self 277 | .client 278 | .signer() 279 | .sign_transaction(&sando_bundle.backrun_tx) 280 | .await?; 281 | let signed_backrun_tx = sando_bundle.backrun_tx.rlp_signed(&backrun_signature); 282 | 283 | let mut bundle = BundleRequest::new() 284 | .set_block(block_number + U64::from(retries)) 285 | .set_simulation_block(block_number) 286 | .set_simulation_timestamp(0); 287 | 288 | bundle = bundle.push_transaction(signed_frontrun_tx); 289 | for victim_tx in &sando_bundle.victim_txs { 290 | let signed_victim_tx = victim_tx.rlp(); 291 | bundle = bundle.push_transaction(signed_victim_tx); 292 | } 293 | bundle = bundle.push_transaction(signed_backrun_tx); 294 | 295 | Ok(bundle) 296 | } 297 | 298 | pub async fn simulate_bundle(&self, bundle: &BundleRequest) { 299 | match self.client.inner().simulate_bundle(bundle).await { 300 | Ok(simulated) => { 301 | println!("{:?}", simulated); 302 | } 303 | Err(e) => { 304 | println!("Flashbots bundle simulation error: {e:?}"); 305 | } 306 | } 307 | } 308 | 309 | pub async fn broadcast_bundle( 310 | &self, 311 | bundle: BundleRequest, 312 | ) -> Result> { 313 | let mut requests = Vec::new(); 314 | for (builder, url) in &self.builder_urls { 315 | requests.push(tokio::task::spawn(send_bundle( 316 | builder.clone(), 317 | url.clone(), 318 | self.identity.clone(), 319 | bundle.clone(), 320 | ))); 321 | } 322 | let results = futures::future::join_all(requests).await; 323 | let mut response_map = HashMap::new(); 324 | for result in results { 325 | match result { 326 | Ok(response) => match response { 327 | Ok(bundle_response) => { 328 | let builder = bundle_response.0; 329 | let send_bundle_response = bundle_response.1.unwrap_or_default(); 330 | response_map.insert(builder.clone(), send_bundle_response); 331 | } 332 | Err(_) => {} 333 | }, 334 | Err(_) => {} 335 | } 336 | } 337 | 338 | Ok(response_map) 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod abi; 2 | pub mod alert; 3 | pub mod bytecode; 4 | pub mod constants; 5 | pub mod evm; 6 | pub mod execution; 7 | pub mod pools; 8 | pub mod streams; 9 | pub mod tokens; 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /src/common/pools.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use csv::StringRecord; 3 | use ethers::abi::{parse_abi, ParamType}; 4 | use ethers::prelude::*; 5 | use ethers::{ 6 | providers::{Provider, Ws}, 7 | types::{H160, H256}, 8 | }; 9 | use indicatif::{ProgressBar, ProgressStyle}; 10 | use itertools::Itertools; 11 | use log::info; 12 | use serde::{Deserialize, Serialize}; 13 | use std::{ 14 | collections::HashMap, 15 | fs::{create_dir_all, OpenOptions}, 16 | path::Path, 17 | str::FromStr, 18 | sync::Arc, 19 | }; 20 | 21 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] 22 | pub enum DexVariant { 23 | UniswapV2, // 2 24 | } 25 | 26 | impl DexVariant { 27 | pub fn num(&self) -> u8 { 28 | match self { 29 | DexVariant::UniswapV2 => 2, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 35 | pub struct Pool { 36 | pub id: i64, 37 | pub address: H160, 38 | pub version: DexVariant, 39 | pub token0: H160, 40 | pub token1: H160, 41 | pub fee: u32, // uniswap v3 specific 42 | pub block_number: u64, 43 | pub timestamp: u64, 44 | } 45 | 46 | impl From for Pool { 47 | fn from(record: StringRecord) -> Self { 48 | let version = match record.get(2).unwrap().parse().unwrap() { 49 | 2 => DexVariant::UniswapV2, 50 | _ => DexVariant::UniswapV2, 51 | }; 52 | Self { 53 | id: record.get(0).unwrap().parse().unwrap(), 54 | address: H160::from_str(record.get(1).unwrap()).unwrap(), 55 | version, 56 | token0: H160::from_str(record.get(3).unwrap()).unwrap(), 57 | token1: H160::from_str(record.get(4).unwrap()).unwrap(), 58 | fee: record.get(5).unwrap().parse().unwrap(), 59 | block_number: record.get(6).unwrap().parse().unwrap(), 60 | timestamp: record.get(7).unwrap().parse().unwrap(), 61 | } 62 | } 63 | } 64 | 65 | impl Pool { 66 | pub fn cache_row(&self) -> (i64, String, i32, String, String, u32, u64, u64) { 67 | ( 68 | self.id, 69 | format!("{:?}", self.address), 70 | self.version.num() as i32, 71 | format!("{:?}", self.token0), 72 | format!("{:?}", self.token1), 73 | self.fee, 74 | self.block_number, 75 | self.timestamp, 76 | ) 77 | } 78 | 79 | pub fn trades(&self, token_a: H160, token_b: H160) -> bool { 80 | let is_zero_for_one = self.token0 == token_a && self.token1 == token_b; 81 | let is_one_for_zero = self.token1 == token_a && self.token0 == token_b; 82 | is_zero_for_one || is_one_for_zero 83 | } 84 | 85 | pub fn pretty_msg(&self) -> String { 86 | format!( 87 | "[{:?}] {:?}: {:?} --> {:?}", 88 | self.version, self.address, self.token0, self.token1 89 | ) 90 | } 91 | 92 | pub fn pretty_print(&self) { 93 | info!("{}", self.pretty_msg()); 94 | } 95 | } 96 | 97 | pub async fn get_touched_pools( 98 | provider: &Arc>, 99 | block_number: U64, 100 | ) -> Result> { 101 | let v2_swap_event = "Swap(address,uint256,uint256,uint256,uint256,address)"; 102 | let event_filter = Filter::new() 103 | .from_block(block_number) 104 | .to_block(block_number) 105 | .events(vec![v2_swap_event]); 106 | let logs = provider.get_logs(&event_filter).await?; 107 | let touched_pools: Vec = logs.iter().map(|log| log.address).unique().collect(); 108 | Ok(touched_pools) 109 | } 110 | 111 | pub async fn load_all_pools( 112 | wss_url: String, 113 | from_block: u64, 114 | chunk: u64, 115 | ) -> Result<(Vec, i64)> { 116 | match create_dir_all("cache") { 117 | _ => {} 118 | } 119 | let cache_file = "cache/.cached-pools.csv"; 120 | let file_path = Path::new(cache_file); 121 | let file_exists = file_path.exists(); 122 | let file = OpenOptions::new() 123 | .write(true) 124 | .append(true) 125 | .create(true) 126 | .open(file_path) 127 | .unwrap(); 128 | let mut writer = csv::Writer::from_writer(file); 129 | 130 | let mut pools = Vec::new(); 131 | 132 | let mut v2_pool_cnt = 0; 133 | 134 | if file_exists { 135 | let mut reader = csv::Reader::from_path(file_path)?; 136 | 137 | for row in reader.records() { 138 | let row = row.unwrap(); 139 | let pool = Pool::from(row); 140 | match pool.version { 141 | DexVariant::UniswapV2 => v2_pool_cnt += 1, 142 | } 143 | pools.push(pool); 144 | } 145 | } else { 146 | writer.write_record(&[ 147 | "id", 148 | "address", 149 | "version", 150 | "token0", 151 | "token1", 152 | "fee", 153 | "block_number", 154 | "timestamp", 155 | ])?; 156 | } 157 | info!("Pools loaded: {:?}", pools.len()); 158 | info!("V2 pools: {:?}", v2_pool_cnt); 159 | 160 | let ws = Ws::connect(wss_url).await?; 161 | let provider = Arc::new(Provider::new(ws)); 162 | 163 | // Uniswap V2 164 | let pair_created_event = "PairCreated(address,address,address,uint256)"; 165 | 166 | let abi = parse_abi(&[&format!("event {}", pair_created_event)]).unwrap(); 167 | 168 | let pair_created_signature = abi.event("PairCreated").unwrap().signature(); 169 | 170 | let mut id = if pools.len() > 0 { 171 | pools.last().as_ref().unwrap().id as i64 172 | } else { 173 | -1 174 | }; 175 | let last_id = id as i64; 176 | 177 | let from_block = if id != -1 { 178 | pools.last().as_ref().unwrap().block_number + 1 179 | } else { 180 | from_block 181 | }; 182 | let to_block = provider.get_block_number().await.unwrap().as_u64(); 183 | let mut blocks_processed = 0; 184 | 185 | let mut block_range = Vec::new(); 186 | 187 | loop { 188 | let start_idx = from_block + blocks_processed; 189 | let mut end_idx = start_idx + chunk - 1; 190 | if end_idx > to_block { 191 | end_idx = to_block; 192 | block_range.push((start_idx, end_idx)); 193 | break; 194 | } 195 | block_range.push((start_idx, end_idx)); 196 | blocks_processed += chunk; 197 | } 198 | info!("Block range: {:?}", block_range); 199 | 200 | let pb = ProgressBar::new(block_range.len() as u64); 201 | pb.set_style( 202 | ProgressStyle::with_template( 203 | "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", 204 | ) 205 | .unwrap() 206 | .progress_chars("##-"), 207 | ); 208 | 209 | for range in block_range { 210 | let mut requests = Vec::new(); 211 | requests.push(tokio::task::spawn(load_uniswap_v2_pools( 212 | provider.clone(), 213 | range.0, 214 | range.1, 215 | pair_created_event, 216 | pair_created_signature, 217 | ))); 218 | let results = futures::future::join_all(requests).await; 219 | for result in results { 220 | match result { 221 | Ok(response) => match response { 222 | Ok(pools_response) => { 223 | pools.extend(pools_response); 224 | } 225 | _ => {} 226 | }, 227 | _ => {} 228 | } 229 | } 230 | 231 | pb.inc(1); 232 | } 233 | 234 | let mut added = 0; 235 | pools.sort_by_key(|p| p.block_number); 236 | for pool in pools.iter_mut() { 237 | if pool.id == -1 { 238 | id += 1; 239 | pool.id = id; 240 | } 241 | if (pool.id as i64) > last_id { 242 | writer.serialize(pool.cache_row())?; 243 | added += 1; 244 | } 245 | } 246 | writer.flush()?; 247 | info!("Added {:?} new pools", added); 248 | 249 | Ok((pools, last_id)) 250 | } 251 | 252 | pub async fn load_uniswap_v2_pools( 253 | provider: Arc>, 254 | from_block: u64, 255 | to_block: u64, 256 | event: &str, 257 | signature: H256, 258 | ) -> Result> { 259 | let mut pools = Vec::new(); 260 | let mut timestamp_map = HashMap::new(); 261 | 262 | let event_filter = Filter::new() 263 | .from_block(U64::from(from_block)) 264 | .to_block(U64::from(to_block)) 265 | .event(event); 266 | let logs = provider.get_logs(&event_filter).await?; 267 | 268 | for log in logs { 269 | let topic = log.topics[0]; 270 | let block_number = log.block_number.unwrap_or_default(); 271 | 272 | if topic != signature { 273 | continue; 274 | } 275 | 276 | let timestamp = if !timestamp_map.contains_key(&block_number) { 277 | let block = provider.get_block(block_number).await.unwrap().unwrap(); 278 | let timestamp = block.timestamp.as_u64(); 279 | timestamp_map.insert(block_number, timestamp); 280 | timestamp 281 | } else { 282 | let timestamp = *timestamp_map.get(&block_number).unwrap(); 283 | timestamp 284 | }; 285 | 286 | let token0 = H160::from(log.topics[1]); 287 | let token1 = H160::from(log.topics[2]); 288 | if let Ok(input) = 289 | ethers::abi::decode(&[ParamType::Address, ParamType::Uint(256)], &log.data) 290 | { 291 | let pair = input[0].to_owned().into_address().unwrap(); 292 | let pool_data = Pool { 293 | id: -1, 294 | address: pair, 295 | version: DexVariant::UniswapV2, 296 | token0, 297 | token1, 298 | fee: 300, 299 | block_number: block_number.as_u64(), 300 | timestamp, 301 | }; 302 | pools.push(pool_data); 303 | }; 304 | } 305 | 306 | Ok(pools) 307 | } 308 | -------------------------------------------------------------------------------- /src/common/streams.rs: -------------------------------------------------------------------------------- 1 | use ethers::{ 2 | providers::{Middleware, Provider, Ws}, 3 | types::*, 4 | }; 5 | use std::sync::Arc; 6 | use tokio::sync::broadcast::Sender; 7 | use tokio_stream::StreamExt; 8 | 9 | use crate::common::utils::calculate_next_block_base_fee; 10 | 11 | #[derive(Default, Debug, Clone)] 12 | pub struct NewBlock { 13 | pub block_number: U64, 14 | pub base_fee: U256, 15 | pub next_base_fee: U256, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct NewPendingTx { 20 | pub added_block: Option, 21 | pub tx: Transaction, 22 | } 23 | 24 | impl Default for NewPendingTx { 25 | fn default() -> Self { 26 | Self { 27 | added_block: None, 28 | tx: Transaction::default(), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub enum Event { 35 | Block(NewBlock), 36 | PendingTx(NewPendingTx), 37 | } 38 | 39 | pub async fn stream_new_blocks(provider: Arc>, event_sender: Sender) { 40 | let stream = provider.subscribe_blocks().await.unwrap(); 41 | let mut stream = stream.filter_map(|block| match block.number { 42 | Some(number) => Some(NewBlock { 43 | block_number: number, 44 | base_fee: block.base_fee_per_gas.unwrap_or_default(), 45 | next_base_fee: U256::from(calculate_next_block_base_fee( 46 | block.gas_used, 47 | block.gas_limit, 48 | block.base_fee_per_gas.unwrap_or_default(), 49 | )), 50 | }), 51 | None => None, 52 | }); 53 | 54 | while let Some(block) = stream.next().await { 55 | match event_sender.send(Event::Block(block)) { 56 | Ok(_) => {} 57 | Err(_) => {} 58 | } 59 | } 60 | } 61 | 62 | pub async fn stream_pending_transactions(provider: Arc>, event_sender: Sender) { 63 | let stream = provider.subscribe_pending_txs().await.unwrap(); 64 | let mut stream = stream.transactions_unordered(256).fuse(); 65 | 66 | while let Some(result) = stream.next().await { 67 | match result { 68 | Ok(tx) => match event_sender.send(Event::PendingTx(NewPendingTx { 69 | added_block: None, 70 | tx, 71 | })) { 72 | Ok(_) => {} 73 | Err(_) => {} 74 | }, 75 | Err(_) => {} 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/common/tokens.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use csv::StringRecord; 3 | use ethers::abi::parse_abi; 4 | use ethers::prelude::BaseContract; 5 | use ethers::providers::{call_raw::RawCall, Provider, Ws}; 6 | use ethers::types::{spoof, BlockNumber, TransactionRequest, H160, U256, U64}; 7 | use indicatif::{ProgressBar, ProgressStyle}; 8 | use log::info; 9 | use std::{collections::HashMap, fs::OpenOptions, path::Path, str::FromStr, sync::Arc}; 10 | 11 | use crate::common::bytecode::REQUEST_BYTECODE; 12 | use crate::common::pools::Pool; 13 | use crate::common::utils::create_new_wallet; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Token { 17 | pub id: i64, 18 | pub address: H160, 19 | pub name: String, 20 | pub symbol: String, 21 | pub decimals: u8, 22 | pub pool_ids: Vec, // refers to the "id" field of Pool struct 23 | } 24 | 25 | impl From for Token { 26 | fn from(record: StringRecord) -> Self { 27 | Self { 28 | id: record.get(0).unwrap().parse().unwrap(), 29 | address: H160::from_str(record.get(1).unwrap()).unwrap(), 30 | name: String::from(record.get(2).unwrap()), 31 | symbol: String::from(record.get(3).unwrap()), 32 | decimals: record.get(4).unwrap().parse().unwrap(), 33 | pool_ids: Vec::new(), 34 | } 35 | } 36 | } 37 | 38 | impl Token { 39 | pub fn cache_row(&self) -> (i64, String, String, String, u8) { 40 | ( 41 | self.id, 42 | format!("{:?}", self.address), 43 | self.name.clone(), 44 | self.symbol.clone(), 45 | self.decimals, 46 | ) 47 | } 48 | } 49 | 50 | // for eth_call response 51 | #[derive(Debug, Clone)] 52 | pub struct TokenInfo { 53 | pub address: H160, 54 | pub name: String, 55 | pub symbol: String, 56 | pub decimals: u8, 57 | } 58 | 59 | pub async fn load_all_tokens( 60 | provider: &Arc>, 61 | block_number: U64, 62 | pools: &Vec, 63 | prev_pool_id: i64, 64 | ) -> Result> { 65 | let cache_file = "cache/.cached-tokens.csv"; 66 | let file_path = Path::new(cache_file); 67 | let file_exists = file_path.exists(); 68 | let file = OpenOptions::new() 69 | .write(true) 70 | .append(true) 71 | .create(true) 72 | .open(file_path) 73 | .unwrap(); 74 | let mut writer = csv::Writer::from_writer(file); 75 | 76 | let mut tokens_map: HashMap = HashMap::new(); 77 | let mut token_id = 0; 78 | 79 | if file_exists { 80 | let mut reader = csv::Reader::from_path(file_path)?; 81 | 82 | for row in reader.records() { 83 | let row = row.unwrap(); 84 | let token = Token::from(row); 85 | tokens_map.insert(token.address, token); 86 | token_id += 1; 87 | } 88 | } else { 89 | writer.write_record(&["id", "address", "name", "symbol", "decimals"])?; 90 | } 91 | 92 | let pb = ProgressBar::new(pools.len() as u64); 93 | pb.set_style( 94 | ProgressStyle::with_template( 95 | "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}", 96 | ) 97 | .unwrap() 98 | .progress_chars("##-"), 99 | ); 100 | 101 | let new_token_id = token_id; 102 | 103 | for pool in pools { 104 | let pool_id = pool.id; 105 | if pool_id < prev_pool_id - 50 { 106 | continue; 107 | } 108 | 109 | let token0 = pool.token0; 110 | let token1 = pool.token1; 111 | 112 | for token in vec![token0, token1] { 113 | if !tokens_map.contains_key(&token) { 114 | match get_token_info(provider, block_number.into(), token).await { 115 | Ok(token_info) => { 116 | tokens_map.insert( 117 | token, 118 | Token { 119 | id: token_id, 120 | address: token, 121 | name: token_info.name, 122 | symbol: token_info.symbol, 123 | decimals: token_info.decimals, 124 | pool_ids: Vec::new(), 125 | }, 126 | ); 127 | token_id += 1; 128 | } 129 | Err(_) => {} 130 | } 131 | } 132 | } 133 | 134 | pb.inc(1); 135 | } 136 | 137 | for pool in pools { 138 | let pool_id = pool.id; 139 | 140 | let token0 = pool.token0; 141 | let token1 = pool.token1; 142 | for token in vec![token0, token1] { 143 | if let Some(token_map) = tokens_map.get_mut(&token) { 144 | token_map.pool_ids.push(pool_id); 145 | } 146 | } 147 | } 148 | 149 | info!("Token count: {:?}", tokens_map.len()); 150 | 151 | let mut added = 0; 152 | let mut tokens_vec: Vec<&Token> = tokens_map.values().collect(); 153 | tokens_vec.sort_by_key(|t| t.id); 154 | for token in tokens_vec.iter() { 155 | if token.id >= new_token_id { 156 | writer.serialize(token.cache_row())?; 157 | added += 1; 158 | } 159 | } 160 | writer.flush()?; 161 | info!("Added {:?} new tokens", added); 162 | 163 | Ok(tokens_map) 164 | } 165 | 166 | pub async fn get_token_info( 167 | provider: &Arc>, 168 | block_number: BlockNumber, 169 | token_address: H160, 170 | ) -> Result { 171 | let owner = create_new_wallet().1; 172 | 173 | let mut state = spoof::state(); 174 | state.account(owner).balance(U256::MAX).nonce(0.into()); 175 | 176 | let request_address = create_new_wallet().1; 177 | state 178 | .account(request_address) 179 | .code((*REQUEST_BYTECODE).clone()); 180 | 181 | let request_abi = BaseContract::from(parse_abi(&[ 182 | "function getTokenInfo(address) external returns (string,string,uint8,uint256)", 183 | ])?); 184 | let calldata = request_abi.encode("getTokenInfo", token_address)?; 185 | 186 | let gas_price = U256::from(1000) 187 | .checked_mul(U256::from(10).pow(U256::from(9))) 188 | .unwrap(); 189 | let tx = TransactionRequest::default() 190 | .from(owner) 191 | .to(request_address) 192 | .value(U256::zero()) 193 | .data(calldata.0) 194 | .nonce(U256::zero()) 195 | .gas(5000000) 196 | .gas_price(gas_price) 197 | .chain_id(1) 198 | .into(); 199 | let result = provider 200 | .call_raw(&tx) 201 | .state(&state) 202 | .block(block_number.into()) 203 | .await?; 204 | let out: (String, String, u8, U256) = request_abi.decode_output("getTokenInfo", result)?; 205 | let token_info = TokenInfo { 206 | address: token_address, 207 | name: out.0, 208 | symbol: out.1, 209 | decimals: out.2, 210 | }; 211 | Ok(token_info) 212 | } 213 | 214 | pub async fn get_token_info_wrapper( 215 | provider: Arc>, 216 | block: BlockNumber, 217 | token_address: H160, 218 | ) -> Result { 219 | get_token_info(&provider, block, token_address).await 220 | } 221 | 222 | pub async fn get_token_info_multi( 223 | provider: Arc>, 224 | block: BlockNumber, 225 | tokens: &Vec, 226 | ) -> Result> { 227 | let mut requests = Vec::new(); 228 | for token in tokens { 229 | let req = tokio::task::spawn(get_token_info_wrapper( 230 | provider.clone(), 231 | block.clone(), 232 | *token, 233 | )); 234 | requests.push(req); 235 | } 236 | let results = futures::future::join_all(requests).await; 237 | 238 | let mut token_info = HashMap::new(); 239 | for i in 0..tokens.len() { 240 | let token = tokens[i]; 241 | let result = &results[i]; 242 | match result { 243 | Ok(result) => { 244 | if let Ok(info) = result { 245 | token_info.insert(token, info.clone()); 246 | } 247 | } 248 | _ => {} 249 | }; 250 | } 251 | 252 | Ok(token_info) 253 | } 254 | -------------------------------------------------------------------------------- /src/common/utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ethers::core::rand::thread_rng; 3 | use ethers::prelude::*; 4 | use ethers::{ 5 | self, 6 | types::{ 7 | transaction::eip2930::{AccessList, AccessListItem}, 8 | U256, 9 | }, 10 | }; 11 | use fern::colors::{Color, ColoredLevelConfig}; 12 | use foundry_evm_mini::evm::utils::{b160_to_h160, h160_to_b160, ru256_to_u256, u256_to_ru256}; 13 | use log::LevelFilter; 14 | use rand::Rng; 15 | use revm::primitives::{B160, U256 as rU256}; 16 | use std::str::FromStr; 17 | use std::sync::Arc; 18 | 19 | use crate::common::constants::*; 20 | 21 | pub fn setup_logger() -> Result<()> { 22 | let colors = ColoredLevelConfig { 23 | trace: Color::Cyan, 24 | debug: Color::Magenta, 25 | info: Color::Green, 26 | warn: Color::Red, 27 | error: Color::BrightRed, 28 | ..ColoredLevelConfig::new() 29 | }; 30 | 31 | fern::Dispatch::new() 32 | .format(move |out, message, record| { 33 | out.finish(format_args!( 34 | "{}[{}] {}", 35 | chrono::Local::now().format("[%H:%M:%S]"), 36 | colors.color(record.level()), 37 | message 38 | )) 39 | }) 40 | .chain(std::io::stdout()) 41 | .level(log::LevelFilter::Error) 42 | .level_for(PROJECT_NAME, LevelFilter::Info) 43 | .apply()?; 44 | 45 | Ok(()) 46 | } 47 | 48 | pub fn calculate_next_block_base_fee( 49 | gas_used: U256, 50 | gas_limit: U256, 51 | base_fee_per_gas: U256, 52 | ) -> U256 { 53 | let gas_used = gas_used; 54 | 55 | let mut target_gas_used = gas_limit / 2; 56 | target_gas_used = if target_gas_used == U256::zero() { 57 | U256::one() 58 | } else { 59 | target_gas_used 60 | }; 61 | 62 | let new_base_fee = { 63 | if gas_used > target_gas_used { 64 | base_fee_per_gas 65 | + ((base_fee_per_gas * (gas_used - target_gas_used)) / target_gas_used) 66 | / U256::from(8u64) 67 | } else { 68 | base_fee_per_gas 69 | - ((base_fee_per_gas * (target_gas_used - gas_used)) / target_gas_used) 70 | / U256::from(8u64) 71 | } 72 | }; 73 | 74 | let seed = rand::thread_rng().gen_range(0..9); 75 | new_base_fee + seed 76 | } 77 | 78 | pub fn access_list_to_ethers(access_list: Vec<(B160, Vec)>) -> AccessList { 79 | AccessList::from( 80 | access_list 81 | .into_iter() 82 | .map(|(address, slots)| AccessListItem { 83 | address: b160_to_h160(address), 84 | storage_keys: slots 85 | .into_iter() 86 | .map(|y| H256::from_uint(&ru256_to_u256(y))) 87 | .collect(), 88 | }) 89 | .collect::>(), 90 | ) 91 | } 92 | 93 | pub fn access_list_to_revm(access_list: AccessList) -> Vec<(B160, Vec)> { 94 | access_list 95 | .0 96 | .into_iter() 97 | .map(|x| { 98 | ( 99 | h160_to_b160(x.address), 100 | x.storage_keys 101 | .into_iter() 102 | .map(|y| u256_to_ru256(y.0.into())) 103 | .collect(), 104 | ) 105 | }) 106 | .collect() 107 | } 108 | 109 | abigen!( 110 | IERC20, 111 | r#"[ 112 | function balanceOf(address) external view returns (uint256) 113 | ]"#, 114 | ); 115 | 116 | pub async fn get_token_balance( 117 | provider: Arc>, 118 | owner: H160, 119 | token: H160, 120 | ) -> Result { 121 | let contract = IERC20::new(token, provider); 122 | let token_balance = contract.balance_of(owner).call().await?; 123 | Ok(token_balance) 124 | } 125 | 126 | pub fn create_new_wallet() -> (LocalWallet, H160) { 127 | let wallet = LocalWallet::new(&mut thread_rng()); 128 | let address = wallet.address(); 129 | (wallet, address) 130 | } 131 | 132 | pub fn to_h160(str_address: &'static str) -> H160 { 133 | H160::from_str(str_address).unwrap() 134 | } 135 | 136 | pub fn is_weth(token_address: H160) -> bool { 137 | token_address == to_h160(WETH) 138 | } 139 | 140 | pub fn is_main_currency(token_address: H160) -> bool { 141 | let main_currencies = vec![to_h160(WETH), to_h160(USDT), to_h160(USDC)]; 142 | main_currencies.contains(&token_address) 143 | } 144 | 145 | #[derive(Debug, Clone)] 146 | pub enum MainCurrency { 147 | WETH, 148 | USDT, 149 | USDC, 150 | 151 | Default, // Pairs that aren't WETH/Stable pairs. Default to WETH for now 152 | } 153 | 154 | impl MainCurrency { 155 | pub fn new(address: H160) -> Self { 156 | if address == to_h160(WETH) { 157 | MainCurrency::WETH 158 | } else if address == to_h160(USDT) { 159 | MainCurrency::USDT 160 | } else if address == to_h160(USDC) { 161 | MainCurrency::USDC 162 | } else { 163 | MainCurrency::Default 164 | } 165 | } 166 | 167 | pub fn decimals(&self) -> u8 { 168 | match self { 169 | MainCurrency::WETH => WETH_DECIMALS, 170 | MainCurrency::USDT => USDC_DECIMALS, 171 | MainCurrency::USDC => USDC_DECIMALS, 172 | MainCurrency::Default => WETH_DECIMALS, 173 | } 174 | } 175 | 176 | pub fn balance_slot(&self) -> i32 { 177 | match self { 178 | MainCurrency::WETH => WETH_BALANCE_SLOT, 179 | MainCurrency::USDT => USDT_BALANCE_SLOT, 180 | MainCurrency::USDC => USDC_BALANCE_SLOT, 181 | MainCurrency::Default => WETH_BALANCE_SLOT, 182 | } 183 | } 184 | 185 | /* 186 | We score the currencies by importance 187 | WETH has the highest importance, and USDT, USDC in the following order 188 | */ 189 | pub fn weight(&self) -> u8 { 190 | match self { 191 | MainCurrency::WETH => 3, 192 | MainCurrency::USDT => 2, 193 | MainCurrency::USDC => 1, 194 | MainCurrency::Default => 3, // default is WETH 195 | } 196 | } 197 | } 198 | 199 | pub fn return_main_and_target_currency(token0: H160, token1: H160) -> Option<(H160, H160)> { 200 | let token0_supported = is_main_currency(token0); 201 | let token1_supported = is_main_currency(token1); 202 | 203 | if !token0_supported && !token1_supported { 204 | return None; 205 | } 206 | 207 | if token0_supported && token1_supported { 208 | let mc0 = MainCurrency::new(token0); 209 | let mc1 = MainCurrency::new(token1); 210 | 211 | let token0_weight = mc0.weight(); 212 | let token1_weight = mc1.weight(); 213 | 214 | if token0_weight > token1_weight { 215 | return Some((token0, token1)); 216 | } else { 217 | return Some((token1, token0)); 218 | } 219 | } 220 | 221 | if token0_supported { 222 | return Some((token0, token1)); 223 | } else { 224 | return Some((token1, token0)); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod sandwich; 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ethers::providers::{Provider, Ws}; 3 | use log::info; 4 | use std::sync::Arc; 5 | use tokio::sync::broadcast::{self, Sender}; 6 | use tokio::task::JoinSet; 7 | 8 | use sandooo::common::constants::Env; 9 | use sandooo::common::streams::{stream_new_blocks, stream_pending_transactions, Event}; 10 | use sandooo::common::utils::setup_logger; 11 | use sandooo::sandwich::strategy::run_sandwich_strategy; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<()> { 15 | dotenv::dotenv().ok(); 16 | setup_logger().unwrap(); 17 | 18 | info!("Starting Sandooo"); 19 | 20 | let env = Env::new(); 21 | 22 | let ws = Ws::connect(env.wss_url.clone()).await.unwrap(); 23 | let provider = Arc::new(Provider::new(ws)); 24 | 25 | let (event_sender, _): (Sender, _) = broadcast::channel(512); 26 | 27 | let mut set = JoinSet::new(); 28 | 29 | set.spawn(stream_new_blocks(provider.clone(), event_sender.clone())); 30 | set.spawn(stream_pending_transactions( 31 | provider.clone(), 32 | event_sender.clone(), 33 | )); 34 | 35 | set.spawn(run_sandwich_strategy( 36 | provider.clone(), 37 | event_sender.clone(), 38 | )); 39 | 40 | while let Some(res) = set.join_next().await { 41 | info!("{:?}", res); 42 | } 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/sandwich/appetizer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ethers::{ 3 | providers::{Provider, Ws}, 4 | types::{H256, U256}, 5 | }; 6 | use log::warn; 7 | use std::{collections::HashMap, sync::Arc}; 8 | 9 | use crate::common::evm::VictimTx; 10 | use crate::common::streams::NewBlock; 11 | use crate::common::utils::{is_weth, MainCurrency}; 12 | use crate::sandwich::simulation::{BatchSandwich, PendingTxInfo, Sandwich, SwapDirection}; 13 | 14 | pub async fn appetizer( 15 | provider: &Arc>, 16 | new_block: &NewBlock, 17 | tx_hash: H256, 18 | victim_gas_price: U256, 19 | pending_txs: &HashMap, 20 | promising_sandwiches: &mut HashMap>, 21 | ) -> Result<()> { 22 | let pending_tx_info = pending_txs.get(&tx_hash).unwrap(); 23 | let pending_tx = &pending_tx_info.pending_tx; 24 | // make sandwiches and simulate 25 | let victim_tx = VictimTx { 26 | tx_hash, 27 | from: pending_tx.tx.from, 28 | to: pending_tx.tx.to.unwrap_or_default(), 29 | data: pending_tx.tx.input.0.clone().into(), 30 | value: pending_tx.tx.value, 31 | gas_price: victim_gas_price, 32 | gas_limit: Some(pending_tx.tx.gas.as_u64()), 33 | }; 34 | 35 | let swap_info = &pending_tx_info.touched_pairs; 36 | 37 | /* 38 | For now, we focus on the buys: 39 | 1. Frontrun: Buy 40 | 2. Victim: Buy 41 | 3. Backrun: Sell 42 | */ 43 | for info in swap_info { 44 | match info.direction { 45 | SwapDirection::Sell => continue, 46 | _ => {} 47 | } 48 | 49 | let main_currency = info.main_currency; 50 | let mc = MainCurrency::new(main_currency); 51 | let decimals = mc.decimals(); 52 | 53 | let small_amount_in = if is_weth(main_currency) { 54 | U256::from(10).pow(U256::from(decimals - 2)) // 0.01 WETH 55 | } else { 56 | U256::from(10) * U256::from(10).pow(U256::from(decimals)) // 10 USDT, 10 USDC 57 | }; 58 | let base_fee = new_block.next_base_fee; 59 | let max_fee = base_fee; 60 | 61 | let mut sandwich = Sandwich { 62 | amount_in: small_amount_in, 63 | swap_info: info.clone(), 64 | victim_tx: victim_tx.clone(), 65 | optimized_sandwich: None, 66 | }; 67 | 68 | let batch_sandwich = BatchSandwich { 69 | sandwiches: vec![sandwich.clone()], 70 | }; 71 | 72 | let simulated_sandwich = batch_sandwich 73 | .simulate( 74 | provider.clone(), 75 | None, 76 | new_block.block_number, 77 | base_fee, 78 | max_fee, 79 | None, 80 | None, 81 | None, 82 | ) 83 | .await; 84 | if simulated_sandwich.is_err() { 85 | let e = simulated_sandwich.as_ref().err().unwrap(); 86 | warn!("BatchSandwich.simulate error: {e:?}"); 87 | continue; 88 | } 89 | let simulated_sandwich = simulated_sandwich.unwrap(); 90 | // profit should be greater than 0 to simulate/optimize any further 91 | if simulated_sandwich.profit <= 0 { 92 | continue; 93 | } 94 | let ceiling_amount_in = if is_weth(main_currency) { 95 | U256::from(100) * U256::from(10).pow(U256::from(18)) // 100 ETH 96 | } else { 97 | U256::from(300000) * U256::from(10).pow(U256::from(decimals)) // 300000 USDT/USDC (both 6 decimals) 98 | }; 99 | let optimized_sandwich = sandwich 100 | .optimize( 101 | provider.clone(), 102 | new_block.block_number, 103 | ceiling_amount_in, 104 | base_fee, 105 | max_fee, 106 | simulated_sandwich.front_access_list.clone(), 107 | simulated_sandwich.back_access_list.clone(), 108 | ) 109 | .await; 110 | if optimized_sandwich.is_err() { 111 | let e = optimized_sandwich.as_ref().err().unwrap(); 112 | warn!("Sandwich.optimize error: {e:?}"); 113 | continue; 114 | } 115 | let optimized_sandwich = optimized_sandwich.unwrap(); 116 | if optimized_sandwich.max_revenue > U256::zero() { 117 | // add optimized sandwiches to promising_sandwiches 118 | if !promising_sandwiches.contains_key(&tx_hash) { 119 | promising_sandwiches.insert(tx_hash, vec![sandwich.clone()]); 120 | } else { 121 | let sandwiches = promising_sandwiches.get_mut(&tx_hash).unwrap(); 122 | sandwiches.push(sandwich.clone()); 123 | } 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /src/sandwich/main_dish.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bounded_vec_deque::BoundedVecDeque; 3 | use ethers::{ 4 | providers::{Provider, Ws}, 5 | types::{H160, H256, U256, U64}, 6 | }; 7 | use log::{info, warn}; 8 | use std::str::FromStr; 9 | use std::{collections::HashMap, sync::Arc}; 10 | 11 | use crate::common::alert::Alert; 12 | use crate::common::constants::*; 13 | use crate::common::execution::{Executor, SandoBundle}; 14 | use crate::common::streams::NewBlock; 15 | use crate::common::utils::get_token_balance; 16 | use crate::sandwich::simulation::{BatchSandwich, PendingTxInfo, Sandwich}; 17 | 18 | pub async fn get_token_balances( 19 | provider: &Arc>, 20 | owner: H160, 21 | tokens: &Vec, 22 | ) -> HashMap { 23 | let mut token_balances = HashMap::new(); 24 | for token in tokens { 25 | let balance = get_token_balance(provider.clone(), owner, *token) 26 | .await 27 | .unwrap_or_default(); 28 | token_balances.insert(*token, balance); 29 | } 30 | token_balances 31 | } 32 | 33 | pub async fn send_sando_bundle_request( 34 | executor: &Executor, 35 | sando_bundle: SandoBundle, 36 | block_number: U64, 37 | alert: &Alert, 38 | ) -> Result<()> { 39 | let bundle_request = executor 40 | .to_sando_bundle_request(sando_bundle, block_number, 1) 41 | .await?; 42 | // If you want to check the simulation results provided by Flashbots 43 | // run the following code, but this will take something like 0.1 ~ 0.3 seconds 44 | // executor.simulate_bundle(&bundle_request).await; 45 | let response = executor.broadcast_bundle(bundle_request).await?; 46 | info!("Bundle sent: {:?}", response); 47 | match alert 48 | .send(&format!("[{:?}] Bundle sent", block_number)) 49 | .await 50 | { 51 | _ => {} 52 | } 53 | Ok(()) 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | pub struct Ingredients { 58 | pub tx_hash: H256, 59 | pub pair: H160, 60 | pub main_currency: H160, 61 | pub amount_in: U256, 62 | pub max_revenue: U256, 63 | pub score: f64, 64 | pub sandwich: Sandwich, 65 | } 66 | 67 | pub async fn main_dish( 68 | provider: &Arc>, 69 | alert: &Alert, 70 | executor: &Executor, 71 | new_block: &NewBlock, 72 | owner: H160, 73 | bot_address: H160, 74 | bribe_pct: U256, 75 | promising_sandwiches: &HashMap>, 76 | simulated_bundle_ids: &mut BoundedVecDeque, 77 | pending_txs: &HashMap, 78 | ) -> Result<()> { 79 | let env = Env::new(); 80 | 81 | let weth = H160::from_str(WETH).unwrap(); 82 | let usdt = H160::from_str(USDT).unwrap(); 83 | let usdc = H160::from_str(USDC).unwrap(); 84 | 85 | let bot_balances = if env.debug { 86 | // assume you have infinite funds when debugging 87 | let mut bot_balances = HashMap::new(); 88 | bot_balances.insert(weth, U256::MAX); 89 | bot_balances.insert(usdt, U256::MAX); 90 | bot_balances.insert(usdc, U256::MAX); 91 | bot_balances 92 | } else { 93 | let bot_balances = 94 | get_token_balances(&provider, bot_address, &vec![weth, usdt, usdc]).await; 95 | bot_balances 96 | }; 97 | 98 | let mut plate = Vec::new(); 99 | for (promising_tx_hash, sandwiches) in promising_sandwiches { 100 | for sandwich in sandwiches { 101 | let optimized_sandwich = sandwich.optimized_sandwich.as_ref().unwrap(); 102 | let amount_in = optimized_sandwich.amount_in; 103 | let max_revenue = optimized_sandwich.max_revenue; 104 | let score = (max_revenue.as_u128() as f64) / (amount_in.as_u128() as f64); 105 | let clean_sandwich = Sandwich { 106 | amount_in, 107 | swap_info: sandwich.swap_info.clone(), 108 | victim_tx: sandwich.victim_tx.clone(), 109 | optimized_sandwich: None, 110 | }; 111 | let ingredients = Ingredients { 112 | tx_hash: *promising_tx_hash, 113 | pair: sandwich.swap_info.target_pair, 114 | main_currency: sandwich.swap_info.main_currency, 115 | amount_in, 116 | max_revenue, 117 | score, 118 | sandwich: clean_sandwich, 119 | }; 120 | plate.push(ingredients); 121 | } 122 | } 123 | 124 | /* 125 | [Multi-sandwich algorithm] Sorting by score. 126 | 127 | Score is calculated as: revenue / amount_in 128 | Sorting by score in descending order will place the sandwich opportunities with higer scores at the top 129 | This is good because we want to invest in opportunities that have the greatest return over cost ratios 130 | 131 | * Note: 132 | Score for WETH pairs and USDT/USDC pairs will be different in scale. 133 | USDT/USDC pairs will always have bigger scores, because amount_in is represented as stable amounts (decimals = 6) 134 | and max_revenue is represented as WETH amount (decimals = 18) 135 | However, this is good, because we can pick up stable sandwiches first (where there's less competition) 136 | 137 | After we've go through all stable pair sandwiches, we next pick up WETH pairs by score order 138 | */ 139 | plate.sort_by(|x, y| y.score.partial_cmp(&x.score).unwrap()); 140 | 141 | /* 142 | Say you have: [sando1, sando2, sando3] on your plate 143 | We then want to send bundles as such: 144 | - 145 | - 146 | - 147 | 3 bundles in total. This way you can optimize your profits. 148 | However, if you have infinite funds, you can always group all of the sandwich opportunities. 149 | */ 150 | for i in 0..plate.len() { 151 | let mut balances = bot_balances.clone(); 152 | let mut sandwiches = Vec::new(); 153 | 154 | for j in 0..(i + 1) { 155 | let ingredient = &plate[j]; 156 | let main_currency = ingredient.main_currency; 157 | let balance = *balances.get(&main_currency).unwrap(); 158 | let optimized = ingredient.amount_in; 159 | let amount_in = std::cmp::min(balance, optimized); 160 | 161 | let mut final_sandwich = ingredient.sandwich.clone(); 162 | final_sandwich.amount_in = amount_in; 163 | 164 | let new_balance = balance - amount_in; 165 | balances.insert(main_currency, new_balance); 166 | 167 | sandwiches.push(final_sandwich); 168 | } 169 | 170 | let final_batch_sandwich = BatchSandwich { sandwiches }; 171 | 172 | let bundle_id = final_batch_sandwich.bundle_id(); 173 | 174 | if simulated_bundle_ids.contains(&bundle_id) { 175 | continue; 176 | } 177 | 178 | simulated_bundle_ids.push_back(bundle_id.clone()); 179 | 180 | let base_fee = new_block.next_base_fee; 181 | let max_fee = base_fee; 182 | 183 | let (owner, bot_address) = if env.debug { 184 | (None, None) 185 | } else { 186 | (Some(owner), Some(bot_address)) 187 | }; 188 | 189 | // set bribe amount as 1 initially, just so we can add the bribe operation gas usage 190 | // we'll figure out the priority fee and the bribe amount after this simulation 191 | let (bribe_amount, front_access_list, back_access_list) = match final_batch_sandwich 192 | .simulate( 193 | provider.clone(), 194 | owner, 195 | new_block.block_number, 196 | base_fee, 197 | max_fee, 198 | None, 199 | None, 200 | bot_address, 201 | ) 202 | .await 203 | { 204 | Ok(simulated_sandwich) => { 205 | if simulated_sandwich.revenue > 0 { 206 | let bribe_amount = 207 | (U256::from(simulated_sandwich.revenue) * bribe_pct) / U256::from(10000); 208 | ( 209 | bribe_amount, 210 | Some(simulated_sandwich.front_access_list), 211 | Some(simulated_sandwich.back_access_list), 212 | ) 213 | } else { 214 | (U256::zero(), None, None) 215 | } 216 | } 217 | Err(e) => { 218 | warn!("bribe_amount simulated failed: {e:?}"); 219 | (U256::zero(), None, None) 220 | } 221 | }; 222 | 223 | if bribe_amount.is_zero() { 224 | continue; 225 | } 226 | 227 | // final simulation 228 | let simulated_sandwich = final_batch_sandwich 229 | .simulate( 230 | provider.clone(), 231 | owner, 232 | new_block.block_number, 233 | base_fee, 234 | max_fee, 235 | front_access_list, 236 | back_access_list, 237 | bot_address, 238 | ) 239 | .await; 240 | if simulated_sandwich.is_err() { 241 | let e = simulated_sandwich.as_ref().err().unwrap(); 242 | warn!("BatchSandwich.simulate error: {e:?}"); 243 | continue; 244 | } 245 | let simulated_sandwich = simulated_sandwich.unwrap(); 246 | if simulated_sandwich.revenue <= 0 { 247 | continue; 248 | } 249 | // set limit as 30% above what we simulated 250 | let front_gas_limit = (simulated_sandwich.front_gas_used * 13) / 10; 251 | let back_gas_limit = (simulated_sandwich.back_gas_used * 13) / 10; 252 | 253 | let realistic_back_gas_limit = (simulated_sandwich.back_gas_used * 105) / 100; 254 | let max_priority_fee_per_gas = bribe_amount / U256::from(realistic_back_gas_limit); 255 | let max_fee_per_gas = base_fee + max_priority_fee_per_gas; 256 | 257 | info!( 258 | "🥪🥪🥪 Sandwiches: {:?} ({})", 259 | final_batch_sandwich.sandwiches.len(), 260 | bundle_id 261 | ); 262 | info!( 263 | "> Base fee: {:?} / Priority fee: {:?} / Max fee: {:?} / Bribe: {:?}", 264 | base_fee, max_priority_fee_per_gas, max_fee_per_gas, bribe_amount 265 | ); 266 | info!( 267 | "> Revenue: {:?} / Profit: {:?} / Gas cost: {:?}", 268 | simulated_sandwich.revenue, simulated_sandwich.profit, simulated_sandwich.gas_cost 269 | ); 270 | info!( 271 | "> Front gas: {:?} / Back gas: {:?}", 272 | simulated_sandwich.front_gas_used, simulated_sandwich.back_gas_used 273 | ); 274 | 275 | let message = format!( 276 | "[{:?}] Front: {:?} / Back: {:?} / Bribe: {:?}", 277 | bundle_id, 278 | simulated_sandwich.front_gas_used, 279 | simulated_sandwich.back_gas_used, 280 | bribe_amount, 281 | ); 282 | match alert.send(&message).await { 283 | Err(e) => warn!("Telegram error: {e:?}"), 284 | _ => {} 285 | } 286 | 287 | let victim_tx_hashes = final_batch_sandwich.victim_tx_hashes(); 288 | let mut victim_txs = Vec::new(); 289 | for tx_hash in victim_tx_hashes { 290 | if let Some(tx_info) = pending_txs.get(&tx_hash) { 291 | let tx = tx_info.pending_tx.tx.clone(); 292 | victim_txs.push(tx); 293 | } 294 | } 295 | 296 | let sando_bundle = executor 297 | .create_sando_bundle( 298 | victim_txs, 299 | simulated_sandwich.front_calldata, 300 | simulated_sandwich.back_calldata, 301 | simulated_sandwich.front_access_list, 302 | simulated_sandwich.back_access_list, 303 | front_gas_limit, 304 | back_gas_limit, 305 | base_fee, 306 | max_priority_fee_per_gas, 307 | max_fee_per_gas, 308 | ) 309 | .await; 310 | if sando_bundle.is_err() { 311 | let e = sando_bundle.as_ref().err().unwrap(); 312 | warn!("Executor.create_sando_bundle error: {e:?}"); 313 | continue; 314 | } 315 | let sando_bundle = sando_bundle.unwrap(); 316 | match send_sando_bundle_request(&executor, sando_bundle, new_block.block_number, &alert) 317 | .await 318 | { 319 | Err(e) => warn!("send_sando_bundle_request error: {e:?}"), 320 | _ => {} 321 | } 322 | } 323 | 324 | Ok(()) 325 | } 326 | -------------------------------------------------------------------------------- /src/sandwich/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod appetizer; 2 | pub mod main_dish; 3 | pub mod simulation; 4 | pub mod strategy; 5 | -------------------------------------------------------------------------------- /src/sandwich/simulation.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use eth_encode_packed::ethabi::ethereum_types::{H160 as eH160, U256 as eU256}; 3 | use eth_encode_packed::{SolidityDataType, TakeLastXBytes}; 4 | use ethers::abi::ParamType; 5 | use ethers::prelude::*; 6 | use ethers::providers::{Provider, Ws}; 7 | use ethers::types::{transaction::eip2930::AccessList, Bytes, H160, H256, I256, U256, U64}; 8 | use log::info; 9 | use revm::primitives::{Bytecode, U256 as rU256}; 10 | use std::{collections::HashMap, default::Default, str::FromStr, sync::Arc}; 11 | 12 | use crate::common::bytecode::SANDOOO_BYTECODE; 13 | use crate::common::constants::{USDC, USDT}; 14 | use crate::common::evm::{EvmSimulator, Tx, VictimTx}; 15 | use crate::common::pools::Pool; 16 | use crate::common::streams::{NewBlock, NewPendingTx}; 17 | use crate::common::utils::{ 18 | create_new_wallet, is_weth, return_main_and_target_currency, MainCurrency, 19 | }; 20 | 21 | #[derive(Debug, Clone, Default)] 22 | pub struct PendingTxInfo { 23 | pub pending_tx: NewPendingTx, 24 | pub touched_pairs: Vec, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub enum SwapDirection { 29 | Buy, 30 | Sell, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct SwapInfo { 35 | pub tx_hash: H256, 36 | pub target_pair: H160, 37 | pub main_currency: H160, 38 | pub target_token: H160, 39 | pub version: u8, 40 | pub token0_is_main: bool, 41 | pub direction: SwapDirection, 42 | } 43 | 44 | #[derive(Debug, Clone)] 45 | pub struct Sandwich { 46 | pub amount_in: U256, 47 | pub swap_info: SwapInfo, 48 | pub victim_tx: VictimTx, 49 | pub optimized_sandwich: Option, 50 | } 51 | 52 | #[derive(Debug, Default, Clone)] 53 | pub struct BatchSandwich { 54 | pub sandwiches: Vec, 55 | } 56 | 57 | #[derive(Debug, Default, Clone)] 58 | pub struct SimulatedSandwich { 59 | pub revenue: i128, 60 | pub profit: i128, 61 | pub gas_cost: i128, 62 | pub front_gas_used: u64, 63 | pub back_gas_used: u64, 64 | pub front_access_list: AccessList, 65 | pub back_access_list: AccessList, 66 | pub front_calldata: Bytes, 67 | pub back_calldata: Bytes, 68 | } 69 | 70 | #[derive(Debug, Default, Clone)] 71 | pub struct OptimizedSandwich { 72 | pub amount_in: U256, 73 | pub max_revenue: U256, 74 | pub front_gas_used: u64, 75 | pub back_gas_used: u64, 76 | pub front_access_list: AccessList, 77 | pub back_access_list: AccessList, 78 | pub front_calldata: Bytes, 79 | pub back_calldata: Bytes, 80 | } 81 | 82 | pub static V2_SWAP_EVENT_ID: &str = "0xd78ad95f"; 83 | 84 | pub async fn debug_trace_call( 85 | provider: &Arc>, 86 | new_block: &NewBlock, 87 | pending_tx: &NewPendingTx, 88 | ) -> Result> { 89 | let mut opts = GethDebugTracingCallOptions::default(); 90 | let mut call_config = CallConfig::default(); 91 | call_config.with_log = Some(true); 92 | 93 | opts.tracing_options.tracer = Some(GethDebugTracerType::BuiltInTracer( 94 | GethDebugBuiltInTracerType::CallTracer, 95 | )); 96 | opts.tracing_options.tracer_config = Some(GethDebugTracerConfig::BuiltInTracer( 97 | GethDebugBuiltInTracerConfig::CallTracer(call_config), 98 | )); 99 | 100 | let block_number = new_block.block_number; 101 | let mut tx = pending_tx.tx.clone(); 102 | let nonce = provider 103 | .get_transaction_count(tx.from, Some(block_number.into())) 104 | .await 105 | .unwrap_or_default(); 106 | tx.nonce = nonce; 107 | 108 | let trace = provider 109 | .debug_trace_call(&tx, Some(block_number.into()), opts) 110 | .await; 111 | 112 | match trace { 113 | Ok(trace) => match trace { 114 | GethTrace::Known(call_tracer) => match call_tracer { 115 | GethTraceFrame::CallTracer(frame) => Ok(Some(frame)), 116 | _ => Ok(None), 117 | }, 118 | _ => Ok(None), 119 | }, 120 | _ => Ok(None), 121 | } 122 | } 123 | 124 | pub async fn extract_swap_info( 125 | provider: &Arc>, 126 | new_block: &NewBlock, 127 | pending_tx: &NewPendingTx, 128 | pools_map: &HashMap, 129 | ) -> Result> { 130 | let tx_hash = pending_tx.tx.hash; 131 | let mut swap_info_vec = Vec::new(); 132 | 133 | let frame = debug_trace_call(provider, new_block, pending_tx).await?; 134 | if frame.is_none() { 135 | return Ok(swap_info_vec); 136 | } 137 | let frame = frame.unwrap(); 138 | 139 | let mut logs = Vec::new(); 140 | extract_logs(&frame, &mut logs); 141 | 142 | for log in &logs { 143 | match &log.topics { 144 | Some(topics) => { 145 | if topics.len() > 1 { 146 | let selector = &format!("{:?}", topics[0])[0..10]; 147 | let is_v2_swap = selector == V2_SWAP_EVENT_ID; 148 | if is_v2_swap { 149 | let pair_address = log.address.unwrap(); 150 | 151 | // filter out the pools we have in memory only 152 | let pool = pools_map.get(&pair_address); 153 | if pool.is_none() { 154 | continue; 155 | } 156 | let pool = pool.unwrap(); 157 | 158 | let token0 = pool.token0; 159 | let token1 = pool.token1; 160 | 161 | let (main_currency, target_token, token0_is_main) = 162 | match return_main_and_target_currency(token0, token1) { 163 | Some(out) => (out.0, out.1, out.0 == token0), 164 | None => continue, 165 | }; 166 | 167 | let (in0, _, _, out1) = match ethers::abi::decode( 168 | &[ 169 | ParamType::Uint(256), 170 | ParamType::Uint(256), 171 | ParamType::Uint(256), 172 | ParamType::Uint(256), 173 | ], 174 | log.data.as_ref().unwrap(), 175 | ) { 176 | Ok(input) => { 177 | let uints: Vec = input 178 | .into_iter() 179 | .map(|i| i.to_owned().into_uint().unwrap()) 180 | .collect(); 181 | (uints[0], uints[1], uints[2], uints[3]) 182 | } 183 | _ => { 184 | let zero = U256::zero(); 185 | (zero, zero, zero, zero) 186 | } 187 | }; 188 | 189 | let zero_for_one = (in0 > U256::zero()) && (out1 > U256::zero()); 190 | 191 | let direction = if token0_is_main { 192 | if zero_for_one { 193 | SwapDirection::Buy 194 | } else { 195 | SwapDirection::Sell 196 | } 197 | } else { 198 | if zero_for_one { 199 | SwapDirection::Sell 200 | } else { 201 | SwapDirection::Buy 202 | } 203 | }; 204 | 205 | let swap_info = SwapInfo { 206 | tx_hash, 207 | target_pair: pair_address, 208 | main_currency, 209 | target_token, 210 | version: 2, 211 | token0_is_main, 212 | direction, 213 | }; 214 | swap_info_vec.push(swap_info); 215 | } 216 | } 217 | } 218 | _ => {} 219 | } 220 | } 221 | 222 | Ok(swap_info_vec) 223 | } 224 | 225 | pub fn extract_logs(call_frame: &CallFrame, logs: &mut Vec) { 226 | if let Some(ref logs_vec) = call_frame.logs { 227 | logs.extend(logs_vec.iter().cloned()); 228 | } 229 | 230 | if let Some(ref calls_vec) = call_frame.calls { 231 | for call in calls_vec { 232 | extract_logs(call, logs); 233 | } 234 | } 235 | } 236 | 237 | pub fn get_v2_amount_out(amount_in: U256, reserve_in: U256, reserve_out: U256) -> U256 { 238 | let amount_in_with_fee = amount_in * U256::from(997); 239 | let numerator = amount_in_with_fee * reserve_out; 240 | let denominator = (reserve_in * U256::from(1000)) + amount_in_with_fee; 241 | let amount_out = numerator.checked_div(denominator); 242 | amount_out.unwrap_or_default() 243 | } 244 | 245 | pub fn convert_usdt_to_weth( 246 | simulator: &mut EvmSimulator>, 247 | amount: U256, 248 | ) -> Result { 249 | let conversion_pair = H160::from_str("0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852").unwrap(); 250 | // token0: WETH / token1: USDT 251 | let reserves = simulator.get_pair_reserves(conversion_pair)?; 252 | let (reserve_in, reserve_out) = (reserves.1, reserves.0); 253 | let weth_out = get_v2_amount_out(amount, reserve_in, reserve_out); 254 | Ok(weth_out) 255 | } 256 | 257 | pub fn convert_usdc_to_weth( 258 | simulator: &mut EvmSimulator>, 259 | amount: U256, 260 | ) -> Result { 261 | let conversion_pair = H160::from_str("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc").unwrap(); 262 | // token0: USDC / token1: WETH 263 | let reserves = simulator.get_pair_reserves(conversion_pair)?; 264 | let (reserve_in, reserve_out) = (reserves.0, reserves.1); 265 | let weth_out = get_v2_amount_out(amount, reserve_in, reserve_out); 266 | Ok(weth_out) 267 | } 268 | 269 | impl BatchSandwich { 270 | pub fn bundle_id(&self) -> String { 271 | let mut tx_hashes = Vec::new(); 272 | for sandwich in &self.sandwiches { 273 | let tx_hash = sandwich.victim_tx.tx_hash; 274 | let tx_hash_4_bytes = &format!("{:?}", tx_hash)[0..10]; 275 | tx_hashes.push(String::from_str(tx_hash_4_bytes).unwrap()); 276 | } 277 | tx_hashes.sort(); 278 | tx_hashes.dedup(); 279 | tx_hashes.join("-") 280 | } 281 | 282 | pub fn victim_tx_hashes(&self) -> Vec { 283 | self.sandwiches 284 | .iter() 285 | .map(|s| s.victim_tx.tx_hash) 286 | .collect() 287 | } 288 | 289 | pub fn target_tokens(&self) -> Vec { 290 | self.sandwiches 291 | .iter() 292 | .map(|s| s.swap_info.target_token) 293 | .collect() 294 | } 295 | 296 | pub fn target_v2_pairs(&self) -> Vec { 297 | self.sandwiches 298 | .iter() 299 | .filter(|s| s.swap_info.version == 2) 300 | .map(|s| s.swap_info.target_pair) 301 | .collect() 302 | } 303 | 304 | pub fn encode_frontrun_tx( 305 | &self, 306 | block_number: U256, 307 | pair_reserves: &HashMap, 308 | ) -> Result<(Bytes, Vec, HashMap)> { 309 | let mut starting_mc_values = HashMap::new(); 310 | 311 | let mut added_tx_hash = HashMap::new(); 312 | let mut victim_txs = Vec::new(); 313 | 314 | let mut frontrun_swap_params = Vec::new(); 315 | 316 | let block_number_u256 = eU256::from_dec_str(&block_number.to_string())?; 317 | frontrun_swap_params.push( 318 | SolidityDataType::NumberWithShift(block_number_u256, TakeLastXBytes(64)), // blockNumber (uint64) 319 | ); 320 | 321 | for sandwich in &self.sandwiches { 322 | let tx_hash = sandwich.victim_tx.tx_hash; 323 | if !added_tx_hash.contains_key(&tx_hash) { 324 | added_tx_hash.insert(tx_hash, true); 325 | victim_txs.push(Tx::from(sandwich.victim_tx.clone())); 326 | } 327 | 328 | // Token swap 0 -> 1 329 | // Frontrun tx is a main_currency -> target_token BUY tx 330 | // thus, if token0_is_main, then it is zero_for_one swap 331 | let zero_for_one = sandwich.swap_info.token0_is_main; 332 | 333 | let new_amount_in = sandwich 334 | .amount_in 335 | .checked_sub(U256::from(1)) 336 | .unwrap_or(U256::zero()); 337 | let amount_in_u256 = eU256::from_dec_str(&new_amount_in.to_string())?; 338 | let amount_out_u256 = if sandwich.swap_info.version == 2 { 339 | let reserves = pair_reserves.get(&sandwich.swap_info.target_pair).unwrap(); 340 | let (reserve_in, reserve_out) = if zero_for_one { 341 | (reserves.0, reserves.1) 342 | } else { 343 | (reserves.1, reserves.0) 344 | }; 345 | let amount_out = get_v2_amount_out(new_amount_in, reserve_in, reserve_out); 346 | eU256::from_dec_str(&amount_out.to_string())? 347 | } else { 348 | eU256::zero() 349 | }; 350 | 351 | let pair = eH160::from_str(&format!("{:?}", sandwich.swap_info.target_pair)).unwrap(); 352 | let token_in = 353 | eH160::from_str(&format!("{:?}", sandwich.swap_info.main_currency)).unwrap(); 354 | 355 | let main_currency = sandwich.swap_info.main_currency; 356 | if starting_mc_values.contains_key(&main_currency) { 357 | let prev_mc_value = *starting_mc_values.get(&main_currency).unwrap(); 358 | starting_mc_values.insert(main_currency, prev_mc_value + new_amount_in); 359 | } else { 360 | starting_mc_values.insert(main_currency, new_amount_in); 361 | } 362 | 363 | frontrun_swap_params.extend(vec![ 364 | SolidityDataType::NumberWithShift( 365 | eU256::from(zero_for_one as u8), 366 | TakeLastXBytes(8), 367 | ), // zeroForOne (uint8) 368 | SolidityDataType::Address(pair), // pair (address) 369 | SolidityDataType::Address(token_in), // tokenIn (address) 370 | SolidityDataType::NumberWithShift(amount_in_u256, TakeLastXBytes(256)), // amountIn (uint256) 371 | SolidityDataType::NumberWithShift(amount_out_u256, TakeLastXBytes(256)), // amountOut (uint256) 372 | ]); 373 | } 374 | 375 | let frontrun_calldata = eth_encode_packed::abi::encode_packed(&frontrun_swap_params); 376 | let frontrun_calldata_bytes = Bytes::from_str(&frontrun_calldata.1).unwrap_or_default(); 377 | 378 | Ok((frontrun_calldata_bytes, victim_txs, starting_mc_values)) 379 | } 380 | 381 | pub fn encode_backrun_tx( 382 | &self, 383 | block_number: U256, 384 | pair_reserves: &HashMap, 385 | token_balances: &HashMap, 386 | ) -> Result { 387 | let mut backrun_swap_params = Vec::new(); 388 | 389 | let block_number_u256 = eU256::from_dec_str(&block_number.to_string())?; 390 | backrun_swap_params.push( 391 | SolidityDataType::NumberWithShift(block_number_u256, TakeLastXBytes(64)), // blockNumber (uint64) 392 | ); 393 | 394 | for sandwich in &self.sandwiches { 395 | let amount_in = *token_balances 396 | .get(&sandwich.swap_info.target_token) 397 | .unwrap_or(&U256::zero()); 398 | let new_amount_in = amount_in.checked_sub(U256::from(1)).unwrap_or(U256::zero()); 399 | let amount_in_u256 = eU256::from_dec_str(&new_amount_in.to_string())?; 400 | 401 | // this means that the buy order is token0 -> token1 402 | let zero_for_one = sandwich.swap_info.token0_is_main; 403 | 404 | // in backrun tx we sell tokens we bought in our frontrun tx 405 | // so it's important to flip the boolean value of zero_for_one 406 | let amount_out_u256 = if sandwich.swap_info.version == 2 { 407 | let reserves = pair_reserves.get(&sandwich.swap_info.target_pair).unwrap(); 408 | let (reserve_in, reserve_out) = if zero_for_one { 409 | // token0 is main_currency 410 | (reserves.1, reserves.0) 411 | } else { 412 | // token1 is main_currency 413 | (reserves.0, reserves.1) 414 | }; 415 | let amount_out = get_v2_amount_out(new_amount_in, reserve_in, reserve_out); 416 | eU256::from_dec_str(&amount_out.to_string())? 417 | } else { 418 | eU256::zero() 419 | }; 420 | 421 | let pair = eH160::from_str(&format!("{:?}", sandwich.swap_info.target_pair)).unwrap(); 422 | let token_in = 423 | eH160::from_str(&format!("{:?}", sandwich.swap_info.target_token)).unwrap(); 424 | 425 | backrun_swap_params.extend(vec![ 426 | SolidityDataType::NumberWithShift( 427 | eU256::from(!zero_for_one as u8), // <-- make sure to flip boolean value (it's a sell now, not buy) 428 | TakeLastXBytes(8), 429 | ), // zeroForOne (uint8) 430 | SolidityDataType::Address(pair), // pair (address) 431 | SolidityDataType::Address(token_in), // tokenIn (address) 432 | SolidityDataType::NumberWithShift(amount_in_u256, TakeLastXBytes(256)), // amountIn (uint256) 433 | SolidityDataType::NumberWithShift(amount_out_u256, TakeLastXBytes(256)), // amountOut (uint256) 434 | ]); 435 | } 436 | 437 | let backrun_calldata = eth_encode_packed::abi::encode_packed(&backrun_swap_params); 438 | let backrun_calldata_bytes = Bytes::from_str(&backrun_calldata.1).unwrap_or_default(); 439 | 440 | Ok(backrun_calldata_bytes) 441 | } 442 | 443 | pub async fn simulate( 444 | &self, 445 | provider: Arc>, 446 | owner: Option, 447 | block_number: U64, 448 | base_fee: U256, 449 | max_fee: U256, 450 | front_access_list: Option, 451 | back_access_list: Option, 452 | bot_address: Option, 453 | ) -> Result { 454 | let mut simulator = EvmSimulator::new(provider.clone(), owner, block_number); 455 | 456 | // set ETH balance so that it's enough to cover gas fees 457 | match owner { 458 | None => { 459 | let initial_eth_balance = U256::from(100) * U256::from(10).pow(U256::from(18)); 460 | simulator.set_eth_balance(simulator.owner, initial_eth_balance); 461 | } 462 | _ => {} 463 | } 464 | 465 | // get reserves for v2 pairs and target tokens 466 | let target_v2_pairs = self.target_v2_pairs(); 467 | let target_tokens = self.target_tokens(); 468 | 469 | let mut reserves_before = HashMap::new(); 470 | 471 | for v2_pair in &target_v2_pairs { 472 | let reserves = simulator.get_pair_reserves(*v2_pair)?; 473 | reserves_before.insert(*v2_pair, reserves); 474 | } 475 | 476 | let next_block_number = simulator.get_block_number(); 477 | 478 | // create frontrun tx calldata and inject main_currency token balance to bot contract 479 | let (frontrun_calldata, victim_txs, starting_mc_values) = 480 | self.encode_frontrun_tx(next_block_number, &reserves_before)?; 481 | 482 | // deploy Sandooo bot 483 | let bot_address = match bot_address { 484 | Some(bot_address) => bot_address, 485 | None => { 486 | let bot_address = create_new_wallet().1; 487 | simulator.deploy(bot_address, Bytecode::new_raw((*SANDOOO_BYTECODE.0).into())); 488 | 489 | // override owner slot 490 | let owner_ru256 = rU256::from_str(&format!("{:?}", simulator.owner)).unwrap(); 491 | simulator.insert_account_storage(bot_address, rU256::from(0), owner_ru256)?; 492 | 493 | for (main_currency, starting_value) in &starting_mc_values { 494 | let mc = MainCurrency::new(*main_currency); 495 | let balance_slot = mc.balance_slot(); 496 | simulator.set_token_balance( 497 | *main_currency, 498 | bot_address, 499 | balance_slot, 500 | (*starting_value).into(), 501 | )?; 502 | } 503 | 504 | bot_address 505 | } 506 | }; 507 | 508 | // check ETH, MC balance before any txs are run 509 | let eth_balance_before = simulator.get_eth_balance_of(simulator.owner); 510 | let mut mc_balances_before = HashMap::new(); 511 | for (main_currency, _) in &starting_mc_values { 512 | let balance_before = simulator.get_token_balance(*main_currency, bot_address)?; 513 | mc_balances_before.insert(main_currency, balance_before); 514 | } 515 | 516 | // set base fee so that gas fees are taken into account 517 | simulator.set_base_fee(base_fee); 518 | 519 | // Frontrun 520 | let front_tx = Tx { 521 | caller: simulator.owner, 522 | transact_to: bot_address, 523 | data: frontrun_calldata.0.clone(), 524 | value: U256::zero(), 525 | gas_price: base_fee, 526 | gas_limit: 5000000, 527 | }; 528 | let front_access_list = match front_access_list { 529 | Some(access_list) => access_list, 530 | None => match simulator.get_access_list(front_tx.clone()) { 531 | Ok(access_list) => access_list, 532 | _ => AccessList::default(), 533 | }, 534 | }; 535 | simulator.set_access_list(front_access_list.clone()); 536 | let front_gas_used = match simulator.call(front_tx) { 537 | Ok(result) => result.gas_used, 538 | Err(_) => 0, 539 | }; 540 | 541 | // Victim Txs 542 | for victim_tx in victim_txs { 543 | match simulator.call(victim_tx) { 544 | _ => {} 545 | } 546 | } 547 | 548 | simulator.set_base_fee(U256::zero()); 549 | 550 | // get reserves after frontrun / victim tx 551 | let mut reserves_after = HashMap::new(); 552 | let mut token_balances = HashMap::new(); 553 | 554 | for v2_pair in &target_v2_pairs { 555 | let reserves = simulator 556 | .get_pair_reserves(*v2_pair) 557 | .unwrap_or((U256::zero(), U256::zero())); 558 | reserves_after.insert(*v2_pair, reserves); 559 | } 560 | 561 | for token in &target_tokens { 562 | let token_balance = simulator 563 | .get_token_balance(*token, bot_address) 564 | .unwrap_or_default(); 565 | token_balances.insert(*token, token_balance); 566 | } 567 | 568 | simulator.set_base_fee(base_fee); 569 | 570 | let backrun_calldata = 571 | self.encode_backrun_tx(next_block_number, &reserves_after, &token_balances)?; 572 | 573 | // Backrun 574 | let back_tx = Tx { 575 | caller: simulator.owner, 576 | transact_to: bot_address, 577 | data: backrun_calldata.0.clone(), 578 | value: U256::zero(), 579 | gas_price: max_fee, 580 | gas_limit: 5000000, 581 | }; 582 | let back_access_list = match back_access_list.clone() { 583 | Some(access_list) => access_list, 584 | None => match simulator.get_access_list(back_tx.clone()) { 585 | Ok(access_list) => access_list, 586 | _ => AccessList::default(), 587 | }, 588 | }; 589 | let back_access_list = back_access_list.clone(); 590 | simulator.set_access_list(back_access_list.clone()); 591 | let back_gas_used = match simulator.call(back_tx) { 592 | Ok(result) => result.gas_used, 593 | Err(_) => 0, 594 | }; 595 | 596 | simulator.set_base_fee(U256::zero()); 597 | 598 | let eth_balance_after = simulator.get_eth_balance_of(simulator.owner); 599 | let mut mc_balances_after = HashMap::new(); 600 | for (main_currency, _) in &starting_mc_values { 601 | let balance_after = simulator.get_token_balance(*main_currency, bot_address)?; 602 | mc_balances_after.insert(main_currency, balance_after); 603 | } 604 | 605 | let eth_used_as_gas = eth_balance_before 606 | .checked_sub(eth_balance_after) 607 | .unwrap_or(eth_balance_before); 608 | let eth_used_as_gas_i256 = I256::from_dec_str(ð_used_as_gas.to_string())?; 609 | 610 | let usdt = H160::from_str(USDT).unwrap(); 611 | let usdc = H160::from_str(USDC).unwrap(); 612 | 613 | let mut weth_before_i256 = I256::zero(); 614 | let mut weth_after_i256 = I256::zero(); 615 | 616 | for (main_currency, _) in &starting_mc_values { 617 | let mc_balance_before = *mc_balances_before.get(&main_currency).unwrap(); 618 | let mc_balance_after = *mc_balances_after.get(&main_currency).unwrap(); 619 | 620 | let (mc_balance_before, mc_balance_after) = if *main_currency == usdt { 621 | let before = 622 | convert_usdt_to_weth(&mut simulator, mc_balance_before).unwrap_or_default(); 623 | let after = 624 | convert_usdt_to_weth(&mut simulator, mc_balance_after).unwrap_or_default(); 625 | (before, after) 626 | } else if *main_currency == usdc { 627 | let before = 628 | convert_usdc_to_weth(&mut simulator, mc_balance_before).unwrap_or_default(); 629 | let after = 630 | convert_usdc_to_weth(&mut simulator, mc_balance_after).unwrap_or_default(); 631 | (before, after) 632 | } else { 633 | (mc_balance_before, mc_balance_after) 634 | }; 635 | 636 | let mc_balance_before_i256 = I256::from_dec_str(&mc_balance_before.to_string())?; 637 | let mc_balance_after_i256 = I256::from_dec_str(&mc_balance_after.to_string())?; 638 | 639 | weth_before_i256 += mc_balance_before_i256; 640 | weth_after_i256 += mc_balance_after_i256; 641 | } 642 | 643 | let profit = (weth_after_i256 - weth_before_i256).as_i128(); 644 | let gas_cost = eth_used_as_gas_i256.as_i128(); 645 | let revenue = profit - gas_cost; 646 | 647 | let simulated_sandwich = SimulatedSandwich { 648 | revenue, 649 | profit, 650 | gas_cost, 651 | front_gas_used, 652 | back_gas_used, 653 | front_access_list, 654 | back_access_list, 655 | front_calldata: frontrun_calldata, 656 | back_calldata: backrun_calldata, 657 | }; 658 | 659 | Ok(simulated_sandwich) 660 | } 661 | } 662 | 663 | impl Sandwich { 664 | pub fn is_optimized(&mut self) -> bool { 665 | self.optimized_sandwich.is_some() 666 | } 667 | 668 | pub fn pretty_print(&self) { 669 | println!("\n"); 670 | info!("🥪 SANDWICH: [{:?}]", self.victim_tx.tx_hash); 671 | info!("- Target token: {:?}", self.swap_info.target_token); 672 | info!( 673 | "- Target V{:?} pair: {:?}", 674 | self.swap_info.version, self.swap_info.target_pair 675 | ); 676 | 677 | match &self.optimized_sandwich { 678 | Some(optimized_sandwich) => { 679 | info!("----- Optimized -----"); 680 | info!("- Amount in: {:?}", optimized_sandwich.amount_in); 681 | info!("- Profit: {:?}", optimized_sandwich.max_revenue); 682 | info!( 683 | "- Front gas: {:?} / Back gas: {:?}", 684 | optimized_sandwich.front_gas_used, optimized_sandwich.back_gas_used 685 | ); 686 | } 687 | _ => {} 688 | } 689 | } 690 | 691 | pub async fn optimize( 692 | &mut self, 693 | provider: Arc>, 694 | block_number: U64, 695 | amount_in_ceiling: U256, 696 | base_fee: U256, 697 | max_fee: U256, 698 | front_access_list: AccessList, 699 | back_access_list: AccessList, 700 | ) -> Result { 701 | let main_currency = self.swap_info.main_currency; 702 | 703 | let mut min_amount_in = U256::zero(); 704 | let mut max_amount_in = amount_in_ceiling; 705 | let tolerance = if is_weth(main_currency) { 706 | U256::from(1) * U256::from(10).pow(U256::from(14)) 707 | } else { 708 | U256::from(1) * U256::from(10).pow(U256::from(3)) 709 | }; 710 | 711 | if max_amount_in < min_amount_in { 712 | return Ok(OptimizedSandwich { 713 | amount_in: U256::zero(), 714 | max_revenue: U256::zero(), 715 | front_gas_used: 0, 716 | back_gas_used: 0, 717 | front_access_list: AccessList::default(), 718 | back_access_list: AccessList::default(), 719 | front_calldata: Bytes::default(), 720 | back_calldata: Bytes::default(), 721 | }); 722 | } 723 | 724 | let mut optimized_in = U256::zero(); 725 | let mut max_revenue = U256::zero(); 726 | let mut max_front_gas_used = 0; 727 | let mut max_back_gas_used = 0; 728 | let mut max_front_calldata = Bytes::default(); 729 | let mut max_back_calldata = Bytes::default(); 730 | 731 | let intervals = U256::from(10); 732 | 733 | loop { 734 | let diff = max_amount_in - min_amount_in; 735 | let step = diff.checked_div(intervals).unwrap(); 736 | 737 | if step <= tolerance { 738 | break; 739 | } 740 | 741 | let mut inputs = Vec::new(); 742 | for i in 0..intervals.as_u64() + 1 { 743 | let _i = U256::from(i); 744 | let input = min_amount_in + (_i * step); 745 | inputs.push(input); 746 | } 747 | 748 | let mut simulations = Vec::new(); 749 | 750 | for (idx, input) in inputs.iter().enumerate() { 751 | let sim = tokio::task::spawn(simulate_sandwich( 752 | idx, 753 | provider.clone(), 754 | block_number, 755 | self.clone(), 756 | *input, 757 | base_fee, 758 | max_fee, 759 | front_access_list.clone(), 760 | back_access_list.clone(), 761 | )); 762 | simulations.push(sim); 763 | } 764 | 765 | let results = futures::future::join_all(simulations).await; 766 | let revenue: Vec<(usize, U256, i128, u64, u64, Bytes, Bytes)> = 767 | results.into_iter().map(|res| res.unwrap()).collect(); 768 | 769 | let mut max_idx = 0; 770 | 771 | for ( 772 | idx, 773 | amount_in, 774 | profit, 775 | front_gas_used, 776 | back_gas_used, 777 | front_calldata, 778 | back_calldata, 779 | ) in &revenue 780 | { 781 | if *profit > max_revenue.as_u128() as i128 { 782 | optimized_in = *amount_in; 783 | max_revenue = U256::from(*profit); 784 | max_front_gas_used = *front_gas_used; 785 | max_back_gas_used = *back_gas_used; 786 | max_front_calldata = front_calldata.clone(); 787 | max_back_calldata = back_calldata.clone(); 788 | 789 | max_idx = *idx; 790 | } 791 | } 792 | 793 | min_amount_in = if max_idx == 0 { 794 | U256::zero() 795 | } else { 796 | revenue[max_idx - 1].1 797 | }; 798 | max_amount_in = if max_idx == revenue.len() - 1 { 799 | revenue[max_idx].1 800 | } else { 801 | revenue[max_idx + 1].1 802 | }; 803 | } 804 | 805 | let optimized_sandwich = OptimizedSandwich { 806 | amount_in: optimized_in, 807 | max_revenue, 808 | front_gas_used: max_front_gas_used, 809 | back_gas_used: max_back_gas_used, 810 | front_access_list, 811 | back_access_list, 812 | front_calldata: max_front_calldata, 813 | back_calldata: max_back_calldata, 814 | }; 815 | 816 | self.optimized_sandwich = Some(optimized_sandwich.clone()); 817 | Ok(optimized_sandwich) 818 | } 819 | } 820 | 821 | pub async fn simulate_sandwich( 822 | idx: usize, 823 | provider: Arc>, 824 | block_number: U64, 825 | sandwich: Sandwich, 826 | amount_in: U256, 827 | base_fee: U256, 828 | max_fee: U256, 829 | front_access_list: AccessList, 830 | back_access_list: AccessList, 831 | ) -> (usize, U256, i128, u64, u64, Bytes, Bytes) { 832 | let mut sandwich = sandwich; 833 | sandwich.amount_in = amount_in; 834 | 835 | let batch_sandwich = BatchSandwich { 836 | sandwiches: vec![sandwich], 837 | }; 838 | match batch_sandwich 839 | .simulate( 840 | provider, 841 | None, 842 | block_number, 843 | base_fee, 844 | max_fee, 845 | Some(front_access_list), 846 | Some(back_access_list), 847 | None, 848 | ) 849 | .await 850 | { 851 | Ok(simulated_sandwich) => ( 852 | idx, 853 | amount_in, 854 | simulated_sandwich.revenue, 855 | simulated_sandwich.front_gas_used, 856 | simulated_sandwich.back_gas_used, 857 | simulated_sandwich.front_calldata, 858 | simulated_sandwich.back_calldata, 859 | ), 860 | _ => (idx, amount_in, 0, 0, 0, Bytes::default(), Bytes::default()), 861 | } 862 | } 863 | -------------------------------------------------------------------------------- /src/sandwich/strategy.rs: -------------------------------------------------------------------------------- 1 | use bounded_vec_deque::BoundedVecDeque; 2 | use ethers::signers::{LocalWallet, Signer}; 3 | use ethers::{ 4 | providers::{Middleware, Provider, Ws}, 5 | types::{BlockNumber, H160, H256, U256, U64}, 6 | }; 7 | use log::{info, warn}; 8 | use std::{collections::HashMap, str::FromStr, sync::Arc}; 9 | use tokio::sync::broadcast::Sender; 10 | 11 | use crate::common::alert::Alert; 12 | use crate::common::constants::Env; 13 | use crate::common::execution::Executor; 14 | use crate::common::pools::{load_all_pools, Pool}; 15 | use crate::common::streams::{Event, NewBlock}; 16 | use crate::common::tokens::load_all_tokens; 17 | use crate::common::utils::calculate_next_block_base_fee; 18 | use crate::sandwich::appetizer::appetizer; 19 | use crate::sandwich::main_dish::main_dish; 20 | use crate::sandwich::simulation::{extract_swap_info, PendingTxInfo, Sandwich}; 21 | 22 | pub async fn run_sandwich_strategy(provider: Arc>, event_sender: Sender) { 23 | let env = Env::new(); 24 | 25 | let (pools, prev_pool_id) = load_all_pools(env.wss_url.clone(), 10000000, 50000) 26 | .await 27 | .unwrap(); 28 | 29 | let block_number = provider.get_block_number().await.unwrap(); 30 | let tokens_map = load_all_tokens(&provider, block_number, &pools, prev_pool_id) 31 | .await 32 | .unwrap(); 33 | info!("Tokens map count: {:?}", tokens_map.len()); 34 | 35 | // filter pools that don't have both token0 / token1 info 36 | let pools_vec: Vec = pools 37 | .into_iter() 38 | .filter(|p| { 39 | let token0_exists = tokens_map.contains_key(&p.token0); 40 | let token1_exists = tokens_map.contains_key(&p.token1); 41 | token0_exists && token1_exists 42 | }) 43 | .collect(); 44 | info!("Filtered pools by tokens count: {:?}", pools_vec.len()); 45 | 46 | let pools_map: HashMap = pools_vec 47 | .clone() 48 | .into_iter() 49 | .map(|p| (p.address, p)) 50 | .collect(); 51 | 52 | let block = provider 53 | .get_block(BlockNumber::Latest) 54 | .await 55 | .unwrap() 56 | .unwrap(); 57 | let mut new_block = NewBlock { 58 | block_number: block.number.unwrap(), 59 | base_fee: block.base_fee_per_gas.unwrap(), 60 | next_base_fee: calculate_next_block_base_fee( 61 | block.gas_used, 62 | block.gas_limit, 63 | block.base_fee_per_gas.unwrap(), 64 | ), 65 | }; 66 | 67 | let alert = Alert::new(); 68 | let executor = Executor::new(provider.clone()); 69 | 70 | let bot_address = H160::from_str(&env.bot_address).unwrap(); 71 | let wallet = env 72 | .private_key 73 | .parse::() 74 | .unwrap() 75 | .with_chain_id(1 as u64); 76 | let owner = wallet.address(); 77 | 78 | let mut event_receiver = event_sender.subscribe(); 79 | 80 | let mut pending_txs: HashMap = HashMap::new(); 81 | let mut promising_sandwiches: HashMap> = HashMap::new(); 82 | let mut simulated_bundle_ids = BoundedVecDeque::new(30); 83 | 84 | loop { 85 | match event_receiver.recv().await { 86 | Ok(event) => match event { 87 | Event::Block(block) => { 88 | new_block = block; 89 | info!("[Block #{:?}]", new_block.block_number); 90 | 91 | // remove confirmed transactions 92 | let block_with_txs = provider 93 | .get_block_with_txs(new_block.block_number) 94 | .await 95 | .unwrap() 96 | .unwrap(); 97 | 98 | let txs: Vec = block_with_txs 99 | .transactions 100 | .into_iter() 101 | .map(|tx| tx.hash) 102 | .collect(); 103 | 104 | for tx_hash in &txs { 105 | if pending_txs.contains_key(tx_hash) { 106 | // Remove any pending txs that have been confirmed 107 | let removed = pending_txs.remove(tx_hash).unwrap(); 108 | promising_sandwiches.remove(tx_hash); 109 | // info!( 110 | // "⚪️ V{:?} TX REMOVED: {:?} / Pending txs: {:?}", 111 | // removed.touched_pairs.get(0).unwrap().version, 112 | // tx_hash, 113 | // pending_txs.len() 114 | // ); 115 | } 116 | } 117 | 118 | // remove pending txs older than 5 blocks 119 | pending_txs.retain(|_, v| { 120 | (new_block.block_number - v.pending_tx.added_block.unwrap()) < U64::from(3) 121 | }); 122 | promising_sandwiches.retain(|h, _| pending_txs.contains_key(h)); 123 | } 124 | Event::PendingTx(mut pending_tx) => { 125 | let tx_hash = pending_tx.tx.hash; 126 | let already_received = pending_txs.contains_key(&tx_hash); 127 | 128 | let mut should_add = false; 129 | 130 | if !already_received { 131 | let tx_receipt = provider.get_transaction_receipt(tx_hash).await; 132 | match tx_receipt { 133 | Ok(receipt) => match receipt { 134 | Some(_) => { 135 | // returning a receipt means that the tx is confirmed 136 | // should not be in pending_txs 137 | pending_txs.remove(&tx_hash); 138 | } 139 | None => { 140 | should_add = true; 141 | } 142 | }, 143 | _ => {} 144 | } 145 | } 146 | 147 | let mut victim_gas_price = U256::zero(); 148 | 149 | match pending_tx.tx.transaction_type { 150 | Some(tx_type) => { 151 | if tx_type == U64::zero() { 152 | victim_gas_price = pending_tx.tx.gas_price.unwrap_or_default(); 153 | should_add = victim_gas_price >= new_block.base_fee; 154 | } else if tx_type == U64::from(2) { 155 | victim_gas_price = 156 | pending_tx.tx.max_fee_per_gas.unwrap_or_default(); 157 | should_add = victim_gas_price >= new_block.base_fee; 158 | } 159 | } 160 | _ => {} 161 | } 162 | 163 | let swap_info = if should_add { 164 | match extract_swap_info(&provider, &new_block, &pending_tx, &pools_map) 165 | .await 166 | { 167 | Ok(swap_info) => swap_info, 168 | Err(e) => { 169 | warn!("extract_swap_info error: {e:?}"); 170 | Vec::new() 171 | } 172 | } 173 | } else { 174 | Vec::new() 175 | }; 176 | 177 | if swap_info.len() > 0 { 178 | pending_tx.added_block = Some(new_block.block_number); 179 | let pending_tx_info = PendingTxInfo { 180 | pending_tx: pending_tx.clone(), 181 | touched_pairs: swap_info.clone(), 182 | }; 183 | pending_txs.insert(tx_hash, pending_tx_info.clone()); 184 | // info!( 185 | // "🔴 V{:?} TX ADDED: {:?} / Pending txs: {:?}", 186 | // pending_tx_info.touched_pairs.get(0).unwrap().version, 187 | // tx_hash, 188 | // pending_txs.len() 189 | // ); 190 | 191 | match appetizer( 192 | &provider, 193 | &new_block, 194 | tx_hash, 195 | victim_gas_price, 196 | &pending_txs, 197 | &mut promising_sandwiches, 198 | ) 199 | .await 200 | { 201 | Err(e) => warn!("appetizer error: {e:?}"), 202 | _ => {} 203 | } 204 | 205 | if promising_sandwiches.len() > 0 { 206 | match main_dish( 207 | &provider, 208 | &alert, 209 | &executor, 210 | &new_block, 211 | owner, 212 | bot_address, 213 | U256::from(9900), // 99% 214 | &promising_sandwiches, 215 | &mut simulated_bundle_ids, 216 | &pending_txs, 217 | ) 218 | .await 219 | { 220 | Err(e) => warn!("main_dish error: {e:?}"), 221 | _ => {} 222 | } 223 | } 224 | } 225 | } 226 | }, 227 | _ => {} 228 | } 229 | } 230 | } 231 | --------------------------------------------------------------------------------