├── .gitignore ├── sandwich-finder ├── src │ ├── events │ │ ├── swaps │ │ │ ├── private.rs │ │ │ ├── mod.rs │ │ │ ├── meteora_damm_v2.rs │ │ │ ├── meteora_dbc.rs │ │ │ ├── fluxbeam.rs │ │ │ ├── aqua.rs │ │ │ ├── meteora.rs │ │ │ ├── onedex.rs │ │ │ ├── raydium_v4.rs │ │ │ ├── zerofi.rs │ │ │ ├── lifinity_v2.rs │ │ │ ├── stabble_weighted.rs │ │ │ ├── dooar.rs │ │ │ ├── pancake_swap.rs │ │ │ ├── heaven.rs │ │ │ ├── raydium_cl.rs │ │ │ ├── meteora_dlmm.rs │ │ │ ├── raydium_v5.rs │ │ │ ├── clearpool.rs │ │ │ ├── fusionamm.rs │ │ │ ├── sv2e.rs │ │ │ ├── solfi.rs │ │ │ ├── tessv.rs │ │ │ ├── alpha.rs │ │ │ ├── goonfi.rs │ │ │ ├── openbook_v2.rs │ │ │ ├── saros_dlmm.rs │ │ │ ├── apesu.rs │ │ │ ├── limo.rs │ │ │ ├── utils.rs │ │ │ ├── pumpamm.rs │ │ │ ├── jup_perps.rs │ │ │ ├── raydium_lp.rs │ │ │ ├── humidifi.rs │ │ │ ├── jup_order_engine.rs │ │ │ ├── discoverer.rs │ │ │ ├── pumpup.rs │ │ │ ├── pumpfun.rs │ │ │ ├── sugar.rs │ │ │ └── whirlpool.rs │ │ ├── transfers │ │ │ ├── private.rs │ │ │ ├── mod.rs │ │ │ ├── transfer_finder_ext.rs │ │ │ ├── stake.rs │ │ │ ├── system.rs │ │ │ └── token.rs │ │ ├── mod.rs │ │ ├── transaction.rs │ │ ├── transfer.rs │ │ ├── addresses.rs │ │ └── swap.rs │ ├── lib.rs │ ├── bin │ │ ├── indexer.rs │ │ ├── backfill.rs │ │ ├── rewards.rs │ │ ├── populate-leader-schedule.rs │ │ ├── detector-realtime.rs │ │ ├── detector.rs │ │ ├── populate-profits.rs │ │ └── report.rs │ └── detector.rs └── Cargo.toml ├── Cargo.toml ├── .gitmodules ├── .env.example ├── update.sh ├── sandwich.sql └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/private.rs: -------------------------------------------------------------------------------- 1 | pub trait Sealed {} -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/private.rs: -------------------------------------------------------------------------------- 1 | pub trait Sealed {} -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "sandwich-finder", 4 | ] 5 | -------------------------------------------------------------------------------- /sandwich-finder/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod detector; 2 | pub mod utils; 3 | pub mod events; -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "yellowstone-grpc"] 2 | path = yellowstone-grpc 3 | url = https://github.com/rpcpool/yellowstone-grpc 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL=http://127.0.0.1:8899 2 | GRPC_URL=http://127.0.0.1:10000 3 | MYSQL=mysql://root:password@localhost:3307/db_name 4 | API_PORT=11000 -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/mod.rs: -------------------------------------------------------------------------------- 1 | mod private; 2 | 3 | pub mod stake; 4 | pub mod system; 5 | pub mod token; 6 | pub mod transfer_finder_ext; -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl https://hanabi.so/api/sandwiching_report -F "filtered_report=@reports/$1/filtered_report.csv" -F "report_path=$1" > README.md 3 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod addresses; 2 | pub mod common; 3 | pub mod event; 4 | pub mod sandwich; 5 | pub mod swap; 6 | pub mod swaps; 7 | pub mod transaction; 8 | pub mod transfer; 9 | pub mod transfers; -------------------------------------------------------------------------------- /sandwich-finder/src/events/transaction.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use derive_getters::Getters; 4 | use serde::Serialize; 5 | 6 | #[derive(Clone, Debug, Serialize, Getters)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct TransactionV2 { 9 | slot: u64, 10 | inclusion_order: u32, 11 | sig: Arc, 12 | fee: u64, 13 | cu_actual: u64, 14 | dont_front: bool 15 | } 16 | 17 | impl TransactionV2 { 18 | pub fn new(slot: u64, inclusion_order: u32, sig: Arc, fee: u64, cu_actual: u64, dont_front: bool) -> Self { 19 | Self { 20 | slot, 21 | inclusion_order, 22 | sig, 23 | fee, 24 | cu_actual, 25 | dont_front, 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /sandwich-finder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandwich-finder" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | axum = { version = "0.8.1", features = ["ws"] } 8 | clap = "4.5.27" 9 | dashmap = "6.1.0" 10 | debug_print = "1.0.0" 11 | derive-getters = "0.5.0" 12 | dotenv = "0.15.0" 13 | futures = "0.3.31" 14 | mysql = "26.0.0" 15 | reqwest = { version = "0.12.12", features = ["json"] } 16 | serde = "1.0.217" 17 | serde_json = "1.0.137" 18 | sandwich-finder-derive = { path = "../sandwich-finder-derive" } 19 | solana-rpc-client = "2.1.9" 20 | solana-rpc-client-api = "2.1.9" 21 | solana-sdk = "2.1.9" 22 | tokio = "1.43.0" 23 | yellowstone-grpc-client = "4.1.0+solana.2.1.9" 24 | yellowstone-grpc-proto = "4.1.0+solana.2.1.9" 25 | sha2 = "0.10.9" 26 | convert_case = "0.8.0" 27 | hex = "0.4.3" 28 | thiserror = "2.0.17" 29 | uuid = { version = "1.18.1", features = ["v5"] } 30 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/mod.rs: -------------------------------------------------------------------------------- 1 | mod private; 2 | 3 | pub mod swap_finder_ext; 4 | pub mod utils; 5 | 6 | pub mod discoverer; 7 | 8 | pub mod alpha; 9 | pub mod apesu; 10 | pub mod aqua; 11 | pub mod clearpool; 12 | pub mod dooar; 13 | pub mod fluxbeam; 14 | pub mod fusionamm; 15 | pub mod goonfi; 16 | pub mod heaven; 17 | pub mod humidifi; 18 | pub mod jup_order_engine; 19 | pub mod jup_perps; 20 | pub mod meteora; 21 | pub mod meteora_dlmm; 22 | pub mod meteora_damm_v2; 23 | pub mod meteora_dbc; 24 | pub mod limo; 25 | pub mod lifinity_v2; 26 | pub mod onedex; 27 | pub mod openbook_v2; 28 | pub mod pancake_swap; 29 | pub mod pumpup; 30 | pub mod pumpamm; 31 | pub mod pumpfun; 32 | pub mod raydium_cl; 33 | pub mod raydium_v4; 34 | pub mod raydium_v5; 35 | pub mod raydium_lp; 36 | pub mod saros_dlmm; 37 | pub mod solfi; 38 | pub mod stabble_weighted; 39 | pub mod sugar; 40 | pub mod sv2e; 41 | pub mod tessv; 42 | pub mod whirlpool; 43 | pub mod zerofi; -------------------------------------------------------------------------------- /sandwich-finder/src/bin/indexer.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use sandwich_finder::{events::{common::Inserter, event::start_event_processor}, utils::create_db_pool}; 4 | use tokio::join; 5 | 6 | const CHUNK_SIZE: usize = 1000; 7 | 8 | async fn indexer_loop() { 9 | loop { 10 | indexer().await; 11 | // reconnect in 5secs 12 | tokio::time::sleep(std::time::Duration::from_secs(5)).await; 13 | } 14 | } 15 | 16 | async fn indexer() { 17 | let rpc_url = env::var("RPC_URL").expect("RPC_URL is not set"); 18 | let grpc_url = env::var("GRPC_URL").expect("GRPC_URL is not set"); 19 | let pool = create_db_pool(); 20 | let mut receiver = start_event_processor(grpc_url, rpc_url); 21 | let inserter = Inserter::new(pool.clone()); 22 | println!("Started event processor"); 23 | while let Some((_slot, event)) = receiver.recv().await { 24 | println!("Received batch: {:?}", event.len()); 25 | // process event here 26 | let mut inserter = inserter.clone(); 27 | tokio::spawn(async move { 28 | for chunk in event.chunks(CHUNK_SIZE) { 29 | inserter.insert_events(chunk).await; 30 | } 31 | }); 32 | } 33 | println!("Event processor disconnected"); 34 | } 35 | 36 | #[tokio::main] 37 | async fn main() { 38 | dotenv::dotenv().ok(); 39 | // let db_pool = create_db_pool(); 40 | join!( 41 | tokio::spawn(indexer_loop()), 42 | ).0.unwrap(); 43 | } 44 | -------------------------------------------------------------------------------- /sandwich-finder/src/bin/backfill.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use sandwich_finder::utils::create_db_pool; 4 | use solana_rpc_client_api::config::RpcBlockConfig; 5 | use solana_rpc_client::nonblocking::rpc_client::{RpcClient}; 6 | use solana_sdk::commitment_config::CommitmentConfig; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | dotenv::dotenv().ok(); 11 | // let pool = create_db_pool(); 12 | let rpc_url = env::var("RPC_URL").expect("RPC_URL is not set"); 13 | let rpc_client = RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::processed()); 14 | let mut args = env::args(); 15 | args.next(); // argv[0] 16 | let slot: u64 = args.next().unwrap().parse().unwrap(); 17 | let block = rpc_client.get_block_with_config( 18 | slot, 19 | RpcBlockConfig { 20 | encoding: None, 21 | // transaction_details: Some(TransactionDetails::Full), 22 | transaction_details: None, 23 | rewards: Some(true), 24 | commitment: Some(CommitmentConfig::finalized()), 25 | max_supported_transaction_version: Some(0) 26 | }).await; 27 | if let Ok(block) = block { 28 | println!("Block: {:?}", block); 29 | // Here you can add logic to process the block and backfill data into the database 30 | // For example, you might want to insert transactions or accounts into your database 31 | } else { 32 | println!("No block found for slot {} {}", slot, block.err().unwrap()); 33 | } 34 | } -------------------------------------------------------------------------------- /sandwich-finder/src/bin/rewards.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env}; 2 | 3 | use futures::StreamExt as _; 4 | use yellowstone_grpc_client::GeyserGrpcBuilder; 5 | use yellowstone_grpc_proto::{geyser::{subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, SubscribeRequestFilterBlocks}, tonic::transport::Endpoint}; 6 | 7 | #[tokio::main] 8 | pub async fn main() { 9 | let grpc_url = env::var("GRPC_URL").expect("GRPC_URL is not set"); 10 | let mut grpc_client = GeyserGrpcBuilder{ 11 | endpoint: Endpoint::from_shared(grpc_url.to_string()).unwrap(), 12 | x_token: None, 13 | x_request_snapshot: false, 14 | send_compressed: None, 15 | accept_compressed: None, 16 | max_decoding_message_size: Some(128 * 1024 * 1024), 17 | max_encoding_message_size: None, 18 | }.connect().await.expect("cannon connect to grpc server"); 19 | let mut blocks = HashMap::new(); 20 | blocks.insert("client".to_string(), SubscribeRequestFilterBlocks { 21 | account_include: vec![], 22 | include_transactions: Some(false), 23 | include_accounts: Some(false), 24 | include_entries: Some(false), 25 | }); 26 | let (mut sink, mut stream) = grpc_client.subscribe_with_request(Some(SubscribeRequest { 27 | blocks, 28 | commitment: Some(CommitmentLevel::Confirmed as i32), 29 | ..Default::default() 30 | })).await.expect("unable to subscribe"); 31 | while let Some(msg) = stream.next().await { 32 | if msg.is_err() { 33 | println!("grpc error: {:?}", msg.err()); 34 | break; 35 | } 36 | let msg = msg.unwrap(); 37 | match msg.update_oneof { 38 | Some(UpdateOneof::Block(block)) => { 39 | println!("Rewards: {:?}", block.rewards); 40 | }, 41 | _ => {} 42 | }; 43 | } 44 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/meteora_damm_v2.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::METEORA_DAMMV2_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for MeteoraDammV2Finder {} 7 | 8 | pub struct MeteoraDammV2Finder {} 9 | 10 | /// Meteora bonding curve swaps have one variant 11 | impl SwapFinder for MeteoraDammV2Finder { 12 | fn amm_ix(ix: &Instruction) -> Pubkey { 13 | ix.accounts[1].pubkey 14 | } 15 | 16 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 17 | account_keys[inner_ix.accounts[1] as usize] 18 | } 19 | 20 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 21 | ( 22 | ix.accounts[2].pubkey, 23 | ix.accounts[3].pubkey, 24 | ) 25 | } 26 | 27 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 28 | ( 29 | account_keys[inner_ix.accounts[2] as usize], 30 | account_keys[inner_ix.accounts[3] as usize], 31 | ) 32 | } 33 | 34 | fn blacklist_ata_indexs() -> Vec { 35 | vec![11] // referral 36 | } 37 | 38 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 39 | [ 40 | // swap 41 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DAMMV2_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 24), 42 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DAMMV2_PUBKEY, &[0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88], 0, 25), 43 | ].concat() 44 | } 45 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/meteora_dbc.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::METEORA_DBC_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for MeteoraDBCSwapFinder {} 7 | 8 | pub struct MeteoraDBCSwapFinder {} 9 | 10 | /// Meteora bonding curve swaps have two variants 11 | impl SwapFinder for MeteoraDBCSwapFinder { 12 | fn amm_ix(ix: &Instruction) -> Pubkey { 13 | ix.accounts[2].pubkey 14 | } 15 | 16 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 17 | account_keys[inner_ix.accounts[2] as usize] 18 | } 19 | 20 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 21 | ( 22 | ix.accounts[3].pubkey, 23 | ix.accounts[4].pubkey, 24 | ) 25 | } 26 | 27 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 28 | ( 29 | account_keys[inner_ix.accounts[3] as usize], 30 | account_keys[inner_ix.accounts[4] as usize], 31 | ) 32 | } 33 | 34 | fn blacklist_ata_indexs() -> Vec { 35 | vec![12] // referral 36 | } 37 | 38 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 39 | [ 40 | // swap 41 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DBC_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 24), 42 | // swap2 43 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DBC_PUBKEY, &[0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88], 0, 24), 44 | ].concat() 45 | } 46 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/fluxbeam.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::FLUXBEAM_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for FluxbeamSwapFinder {} 7 | 8 | pub struct FluxbeamSwapFinder {} 9 | 10 | /// Fluxbeam swaps have one variant 11 | impl SwapFinder for FluxbeamSwapFinder { 12 | fn amm_ix(ix: &Instruction) -> Pubkey { 13 | ix.accounts[0].pubkey 14 | } 15 | 16 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 17 | account_keys[inner_ix.accounts[0] as usize] 18 | } 19 | 20 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 21 | ( 22 | ix.accounts[3].pubkey, 23 | ix.accounts[6].pubkey, 24 | ) 25 | } 26 | 27 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 28 | ( 29 | account_keys[inner_ix.accounts[3] as usize], 30 | account_keys[inner_ix.accounts[6] as usize], 31 | ) 32 | } 33 | 34 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 35 | ( 36 | ix.accounts[5].pubkey, 37 | ix.accounts[4].pubkey, 38 | ) 39 | } 40 | 41 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 42 | ( 43 | account_keys[inner_ix.accounts[5] as usize], 44 | account_keys[inner_ix.accounts[4] as usize], 45 | ) 46 | } 47 | 48 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 49 | [ 50 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &FLUXBEAM_PUBKEY, &[0x01], 0, 17), 51 | ].concat() 52 | } 53 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/aqua.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::AQUA_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for AquaSwapFinder {} 7 | 8 | pub struct AquaSwapFinder {} 9 | 10 | /// Aqu1... a single swap instruction 11 | /// user a/b: 6/3, pool a/b: 15/13 12 | 13 | impl SwapFinder for AquaSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[9].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[9] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[6].pubkey, 25 | ix.accounts[3].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[6] as usize], 32 | account_keys[inner_ix.accounts[3] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[13].pubkey, 39 | ix.accounts[15].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[13] as usize], // quote 46 | account_keys[inner_ix.accounts[15] as usize], // base 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &AQUA_PUBKEY, &[0x01], 0, 9), 54 | ].concat() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/meteora.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::METEORA_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for MeteoraSwapFinder {} 7 | 8 | pub struct MeteoraSwapFinder {} 9 | 10 | /// Ray v4 swaps have the discriminant [0x09], followed by the input amount and the min amount out 11 | /// Swap direction is determined the input/output token accounts ([-3], [-2] respectively) 12 | /// The pool's ATA are at [-12] and [-13] but due to the ordering the order can't be reliably determined 13 | impl SwapFinder for MeteoraSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[0].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[0] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[1].pubkey, 25 | ix.accounts[2].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[1] as usize], 32 | account_keys[inner_ix.accounts[2] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(_ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | Pubkey::default(), 39 | Pubkey::default(), 40 | ) 41 | } 42 | 43 | // The 1st inner ix is either a transfer for the fee or the "vault deposit" 44 | fn ixs_to_skip() -> usize { 45 | 1 46 | } 47 | 48 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 49 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 17) 50 | } 51 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/onedex.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::ONEDEX_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for OneDexSwapFinder {} 7 | 8 | pub struct OneDexSwapFinder {} 9 | 10 | /// 1DEX a single swap instruction 11 | /// user a/b: 6/7, pool a/b: 3/4 12 | 13 | impl SwapFinder for OneDexSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[1].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[1] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[6].pubkey, 25 | ix.accounts[7].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[6] as usize], 32 | account_keys[inner_ix.accounts[7] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[4].pubkey, 39 | ix.accounts[3].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[4] as usize], // base 46 | account_keys[inner_ix.accounts[3] as usize], // quote 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &ONEDEX_PUBKEY, &[0x08, 0x97, 0xf5, 0x4c, 0xac, 0xcb, 0x90, 0x27], 0, 24), 54 | ].concat() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/raydium_v4.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::RAYDIUM_V4_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for RaydiumV4SwapFinder {} 7 | 8 | pub struct RaydiumV4SwapFinder {} 9 | 10 | /// Ray v4 swaps have the discriminant [0x09], followed by the input amount and the min amount out 11 | /// Swap direction is determined the input/output token accounts ([-3], [-2] respectively) 12 | /// The pool's ATA are at [-12] and [-13] but due to the ordering the order can't be reliably determined 13 | impl SwapFinder for RaydiumV4SwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[1].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[1] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[ix.accounts.len() - 3].pubkey, 25 | ix.accounts[ix.accounts.len() - 2].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[inner_ix.accounts.len() - 3] as usize], 32 | account_keys[inner_ix.accounts[inner_ix.accounts.len() - 2] as usize], 33 | ) 34 | } 35 | 36 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 37 | [ 38 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_V4_PUBKEY, &[0x09], 0, 17), 39 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_V4_PUBKEY, &[0x0b], 0, 17), 40 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_V4_PUBKEY, &[0x10], 0, 17), 41 | ].concat() 42 | } 43 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/zerofi.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::ZEROFI_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for ZeroFiSwapFinder {} 7 | 8 | pub struct ZeroFiSwapFinder {} 9 | 10 | /// ZeroFi doesn't have any published IDL so it's guesswork from solscan/jup txs 11 | /// swap: [0x06, input: u64, min_output: u64] 12 | 13 | impl SwapFinder for ZeroFiSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[0].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[0] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[5].pubkey, 25 | ix.accounts[6].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[5] as usize], 32 | account_keys[inner_ix.accounts[6] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[4].pubkey, 39 | ix.accounts[2].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[4] as usize], 46 | account_keys[inner_ix.accounts[2] as usize], 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &ZEROFI_PUBKEY, &[0x06], 0, 17), 54 | ].concat() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/lifinity_v2.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::LIFINITY_V2_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt as _}}; 5 | 6 | impl Sealed for LifinityV2SwapFinder {} 7 | 8 | pub struct LifinityV2SwapFinder {} 9 | 10 | /// LifinityV2 has a single swap instruction 11 | /// user a/b is 3/4, pool a/b is 5/6 12 | 13 | impl SwapFinder for LifinityV2SwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[1].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[1] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[3].pubkey, 25 | ix.accounts[4].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[3] as usize], 32 | account_keys[inner_ix.accounts[4] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[6].pubkey, 39 | ix.accounts[5].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[6] as usize], 46 | account_keys[inner_ix.accounts[5] as usize], 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &LIFINITY_V2_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 24), 54 | ].concat() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/stabble_weighted.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::STABBLE_WEIGHTED_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for StabbleWeightedSwapFinder {} 7 | 8 | pub struct StabbleWeightedSwapFinder {} 9 | 10 | /// Aqu1... a single swap instruction 11 | /// user a/b: 6/3, pool a/b: 15/13 12 | 13 | impl SwapFinder for StabbleWeightedSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[6].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[9] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[1].pubkey, 25 | ix.accounts[2].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[1] as usize], 32 | account_keys[inner_ix.accounts[2] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[4].pubkey, 39 | ix.accounts[3].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[4] as usize], 46 | account_keys[inner_ix.accounts[3] as usize], 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &STABBLE_WEIGHTED_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 25), 54 | ].concat() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/dooar.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::DOOAR_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for DooarSwapFinder {} 7 | 8 | pub struct DooarSwapFinder {} 9 | 10 | /// Dooar swaps have one variant 11 | /// there are 6 writable accounts in the ix, corresponding to user/pool in/out atas, lp mint and fee receiver ata 12 | /// fees are accrued by minting additional lp tokens to the fee receiver ata 13 | /// [0] and [1] are both readonly but the "correct" amm was identified by checking ownership 14 | impl SwapFinder for DooarSwapFinder { 15 | fn amm_ix(ix: &Instruction) -> Pubkey { 16 | ix.accounts[0].pubkey 17 | } 18 | 19 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 20 | account_keys[inner_ix.accounts[0] as usize] 21 | } 22 | 23 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 24 | ( 25 | ix.accounts[3].pubkey, 26 | ix.accounts[6].pubkey, 27 | ) 28 | } 29 | 30 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 31 | ( 32 | account_keys[inner_ix.accounts[3] as usize], 33 | account_keys[inner_ix.accounts[6] as usize], 34 | ) 35 | } 36 | 37 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 38 | ( 39 | ix.accounts[5].pubkey, 40 | ix.accounts[4].pubkey, 41 | ) 42 | } 43 | 44 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 45 | ( 46 | account_keys[inner_ix.accounts[5] as usize], 47 | account_keys[inner_ix.accounts[4] as usize], 48 | ) 49 | } 50 | 51 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 52 | [ 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &DOOAR_PUBKEY, &[0x01], 0, 17), 54 | ].concat() 55 | } 56 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/pancake_swap.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::PANCAKE_SWAP_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for PancakeSwapSwapFinder {} 7 | 8 | pub struct PancakeSwapSwapFinder {} 9 | 10 | /// PancakeSwap swaps have two variants, swap and swap_v2, both share the same account list 11 | impl SwapFinder for PancakeSwapSwapFinder { 12 | fn amm_ix(ix: &Instruction) -> Pubkey { 13 | ix.accounts[1].pubkey 14 | } 15 | 16 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 17 | account_keys[inner_ix.accounts[1] as usize] 18 | } 19 | 20 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 21 | ( 22 | ix.accounts[3].pubkey, 23 | ix.accounts[4].pubkey, 24 | ) 25 | } 26 | 27 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 28 | ( 29 | account_keys[inner_ix.accounts[3] as usize], 30 | account_keys[inner_ix.accounts[4] as usize], 31 | ) 32 | } 33 | 34 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 35 | ( 36 | ix.accounts[6].pubkey, 37 | ix.accounts[5].pubkey, 38 | ) 39 | } 40 | 41 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 42 | ( 43 | account_keys[inner_ix.accounts[6] as usize], 44 | account_keys[inner_ix.accounts[5] as usize], 45 | ) 46 | } 47 | 48 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 49 | [ 50 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &PANCAKE_SWAP_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 41), 51 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &PANCAKE_SWAP_PUBKEY, &[0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62], 0, 41), 52 | ].concat() 53 | } 54 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/heaven.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::DOOAR_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for HeavenSwapFinder {} 7 | 8 | pub struct HeavenSwapFinder {} 9 | 10 | /// Dooar swaps have one variant 11 | /// there are 6 writable accounts in the ix, corresponding to user/pool in/out atas, lp mint and fee receiver ata 12 | /// fees are accrued by minting additional lp tokens to the fee receiver ata 13 | /// [0] and [1] are both readonly but the "correct" amm was identified by checking ownership 14 | impl SwapFinder for HeavenSwapFinder { 15 | fn amm_ix(ix: &Instruction) -> Pubkey { 16 | ix.accounts[0].pubkey 17 | } 18 | 19 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 20 | account_keys[inner_ix.accounts[0] as usize] 21 | } 22 | 23 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 24 | ( 25 | ix.accounts[3].pubkey, 26 | ix.accounts[6].pubkey, 27 | ) 28 | } 29 | 30 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 31 | ( 32 | account_keys[inner_ix.accounts[3] as usize], 33 | account_keys[inner_ix.accounts[6] as usize], 34 | ) 35 | } 36 | 37 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 38 | ( 39 | ix.accounts[5].pubkey, 40 | ix.accounts[4].pubkey, 41 | ) 42 | } 43 | 44 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 45 | ( 46 | account_keys[inner_ix.accounts[5] as usize], 47 | account_keys[inner_ix.accounts[4] as usize], 48 | ) 49 | } 50 | 51 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 52 | [ 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &DOOAR_PUBKEY, &[0x01], 0, 17), 54 | ].concat() 55 | } 56 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/raydium_cl.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::RAYDIUM_CL_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for RaydiumCLSwapFinder {} 7 | 8 | pub struct RaydiumCLSwapFinder {} 9 | 10 | /// Ray concentrated liquidity has 2 variants: 11 | /// 1. swap [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8] 12 | /// 2. swapV2 [0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62] 13 | impl SwapFinder for RaydiumCLSwapFinder { 14 | fn amm_ix(ix: &Instruction) -> Pubkey { 15 | ix.accounts[2].pubkey 16 | } 17 | 18 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 19 | account_keys[inner_ix.accounts[2] as usize] 20 | } 21 | 22 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 23 | ( 24 | ix.accounts[3].pubkey, 25 | ix.accounts[4].pubkey, 26 | ) 27 | } 28 | 29 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 30 | ( 31 | account_keys[inner_ix.accounts[3] as usize], 32 | account_keys[inner_ix.accounts[4] as usize], 33 | ) 34 | } 35 | 36 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 37 | ( 38 | ix.accounts[6].pubkey, 39 | ix.accounts[5].pubkey, 40 | ) 41 | } 42 | 43 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | ( 45 | account_keys[inner_ix.accounts[6] as usize], 46 | account_keys[inner_ix.accounts[5] as usize], 47 | ) 48 | } 49 | 50 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 51 | [ 52 | // swap 53 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_CL_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 41), 54 | // swap_v2 55 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_CL_PUBKEY, &[0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62], 0, 41), 56 | ].concat() 57 | } 58 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/transfer_finder_ext.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::{geyser::SubscribeUpdateTransactionInfo, prelude::InnerInstructions}; 3 | 4 | use crate::events::{transfer::{TransferFinder, TransferV2}, transfers::private}; 5 | 6 | 7 | /// This trait contains helper methods not meant to be overridden by the implementors of [`TransferFinder`]. 8 | pub trait TransferFinderExt: private::Sealed { 9 | /// Finds transfer in this tx utilising the provided program id by iterating through the ixs. 10 | fn find_transfers_in_tx(slot: u64, raw_tx: &SubscribeUpdateTransactionInfo, ixs: &Vec, account_keys: &Vec) -> Vec; 11 | } 12 | 13 | impl TransferFinderExt for T { 14 | fn find_transfers_in_tx(slot: u64, raw_tx: &SubscribeUpdateTransactionInfo, ixs: &Vec, account_keys: &Vec) -> Vec { 15 | if let Some(meta) = &raw_tx.meta { 16 | let mut transfers = vec![]; 17 | ixs.iter().enumerate().for_each(|(i, ix)| { 18 | let inner_ixs = meta.inner_instructions.iter().find(|x| x.index == i as u32); 19 | let default = InnerInstructions { index: i as u32, instructions: vec![] }; 20 | let inner_ixs = inner_ixs.unwrap_or(&default); 21 | // We want to index events here even if there's no inner ixs since that's how plain transfers work 22 | Self::find_transfers(ix, inner_ixs, account_keys, meta).iter().for_each(|transfer| { 23 | let transfer = TransferV2::new( 24 | transfer.outer_program().clone(), 25 | transfer.program().clone(), 26 | transfer.authority().clone(), 27 | transfer.mint().clone(), 28 | *transfer.amount(), 29 | transfer.input_ata().clone(), 30 | transfer.output_ata().clone(), 31 | slot, 32 | raw_tx.index as u32, 33 | i as u32, 34 | *transfer.inner_ix_index(), 35 | *transfer.id(), 36 | ); 37 | transfers.push(transfer); 38 | }); 39 | }); 40 | return transfers; 41 | } 42 | vec![] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sandwich-finder/src/bin/populate-leader-schedule.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::{HashMap, HashSet}, env}; 2 | 3 | use mysql::{prelude::Queryable, Pool}; 4 | use solana_rpc_client::nonblocking::rpc_client::RpcClient; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | dotenv::dotenv().ok(); 9 | let rpc_client = RpcClient::new(env::var("RPC_URL").unwrap()); 10 | let args = std::env::args().collect::>(); 11 | let epoch = if args.len() >= 2 { 12 | args[1].parse::().expect("Invalid epoch") 13 | } else { 14 | rpc_client.get_epoch_info().await.unwrap().epoch 15 | }; 16 | let leader_schedule = rpc_client.get_leader_schedule(Some(epoch * 432000)).await.unwrap(); 17 | let leader_schedule = leader_schedule.unwrap(); 18 | let mysql_url = env::var("MYSQL").unwrap(); 19 | let pool = Pool::new(mysql_url.as_str()).unwrap(); 20 | let mut conn = pool.get_conn().unwrap(); 21 | let leader_set: HashSet<_> = leader_schedule.keys().collect(); 22 | let stmt = format!("insert ignore into address_lookup_table (address) values {}", leader_set.iter().map(|_| "(?)").collect::>().join(",")); 23 | conn.exec_drop(stmt, leader_set.iter().collect::>()).unwrap(); 24 | let stmt = format!("select id, address from address_lookup_table where address in ({})", leader_set.iter().map(|_| "(?)").collect::>().join(",")); 25 | let leader_map: HashMap = HashMap::from_iter(conn.exec_map(stmt, leader_set.iter().collect::>(), |(id, leader)| (leader, id)).unwrap()); 26 | println!("{:#?}", leader_map); 27 | 28 | let rev_leader_schedule: HashMap = leader_schedule.iter().fold(HashMap::new(), |mut acc, (k, v)| { 29 | v.iter().for_each(|v| { 30 | acc.insert(*v as u64 + 432000 * epoch, *leader_map.get(k).unwrap()); 31 | }); 32 | acc 33 | }); 34 | // insert in batches of 1600 rows 35 | let stmt = "INSERT INTO leader_schedule (slot, leader_id) VALUES "; 36 | let mut query = String::from(stmt); 37 | let mut count = 0; 38 | let mut cum_count = 0; 39 | for (slot, leader) in rev_leader_schedule.iter() { 40 | query.push_str(&format!("({}, '{}'),", slot, leader)); 41 | count += 1; 42 | cum_count += 1; 43 | if count == 1600 { 44 | query.pop(); 45 | conn.exec_drop(query, ()).unwrap(); 46 | query = String::from(stmt); 47 | count = 0; 48 | println!("inserted {}/{}", cum_count, rev_leader_schedule.len()); 49 | } 50 | } 51 | if count > 0 { 52 | query.pop(); 53 | conn.exec_drop(query, ()).unwrap(); 54 | } 55 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/meteora_dlmm.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::METEORA_DLMM_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for MeteoraDLMMSwapFinder {} 7 | 8 | pub struct MeteoraDLMMSwapFinder {} 9 | 10 | /// There's a grand total of 6 swap variants for DLMM 11 | /// But all 6 of them have user_token_{in,out} at the [4] and [5] respectively 12 | impl SwapFinder for MeteoraDLMMSwapFinder { 13 | fn amm_ix(ix: &Instruction) -> Pubkey { 14 | return ix.accounts[0].pubkey; 15 | } 16 | 17 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 18 | return account_keys[inner_ix.accounts[0] as usize]; 19 | } 20 | 21 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 22 | return ( 23 | ix.accounts[4].pubkey, 24 | ix.accounts[5].pubkey, 25 | ); 26 | } 27 | 28 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 29 | return ( 30 | account_keys[inner_ix.accounts[4] as usize], 31 | account_keys[inner_ix.accounts[5] as usize], 32 | ); 33 | } 34 | 35 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 36 | [ 37 | // swap 38 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 24), 39 | // swap2 40 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88], 0, 24), 41 | // swap_exact_out 42 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8], 0, 24), 43 | // swap_exact_out2 44 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51], 0, 24), 45 | // swap_with_price_impact 46 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd], 0, 24), 47 | // swap_with_price_impact2 48 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &METEORA_DLMM_PUBKEY, &[0x4a, 0x62, 0xc0, 0xd6, 0xb1, 0x33, 0x4b, 0x33], 0, 24), 49 | ].concat() 50 | } 51 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/raydium_v5.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::RAYDIUM_V5_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for RaydiumV5SwapFinder {} 7 | 8 | pub struct RaydiumV5SwapFinder {} 9 | 10 | /// Ray v5 swaps have two variants: 11 | /// 1. swap_base_input [0x8f, 0xbe, 0x5a, 0xda, 0xc4, 0x1e, 0x33, 0xde] 12 | /// 2. swap_base_output [0x37, 0xd9, 0x62, 0x56, 0xa3, 0x4a, 0xb4, 0xad] 13 | /// In/out amounts follows the discriminant, with one being exact and the other being the worst acceptable value. 14 | /// Swap direction is determined by the input/output token accounts ([4], [5] respectively) 15 | /// Unlike v4, the ordering of the pool's ATA also depends on the swap direction. 16 | impl SwapFinder for RaydiumV5SwapFinder { 17 | fn amm_ix(ix: &Instruction) -> Pubkey { 18 | ix.accounts[3].pubkey 19 | } 20 | 21 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 22 | account_keys[inner_ix.accounts[3] as usize] 23 | } 24 | 25 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 26 | ( 27 | ix.accounts[4].pubkey, 28 | ix.accounts[5].pubkey, 29 | ) 30 | } 31 | 32 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 33 | ( 34 | account_keys[inner_ix.accounts[4] as usize], 35 | account_keys[inner_ix.accounts[5] as usize], 36 | ) 37 | } 38 | 39 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 40 | ( 41 | ix.accounts[7].pubkey, 42 | ix.accounts[6].pubkey, 43 | ) 44 | } 45 | 46 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 47 | ( 48 | account_keys[inner_ix.accounts[7] as usize], 49 | account_keys[inner_ix.accounts[6] as usize], 50 | ) 51 | } 52 | 53 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 54 | [ 55 | // swap_base_input 56 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_V5_PUBKEY, &[0x8f, 0xbe, 0x5a, 0xda, 0xc4, 0x1e, 0x33, 0xde], 0, 24), 57 | // swap_base_output 58 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_V5_PUBKEY, &[0x37, 0xd9, 0x62, 0x56, 0xa3, 0x4a, 0xb4, 0xad], 0, 24), 59 | ].concat() 60 | } 61 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/clearpool.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::CLEARPOOL_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for ClearpoolSwapFinder {} 7 | 8 | pub struct ClearpoolSwapFinder {} 9 | 10 | impl ClearpoolSwapFinder { 11 | fn is_a_to_b(data: &[u8]) -> bool { 12 | data[41] == 1 13 | } 14 | 15 | fn user_ata_indexes(data: &[u8]) -> (usize, usize) { 16 | if Self::is_a_to_b(data) { 17 | (3, 5) 18 | } else { 19 | (5, 3) 20 | } 21 | } 22 | 23 | fn pool_ata_indexes(data: &[u8]) -> (usize, usize) { 24 | if Self::is_a_to_b(data) { 25 | (6, 4) 26 | } else { 27 | (4, 6) 28 | } 29 | } 30 | } 31 | 32 | impl SwapFinder for ClearpoolSwapFinder { 33 | fn amm_ix(ix: &Instruction) -> Pubkey { 34 | ix.accounts[2].pubkey 35 | } 36 | 37 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 38 | account_keys[inner_ix.accounts[2] as usize] 39 | } 40 | 41 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 42 | let (in_idx, out_idx) = Self::user_ata_indexes(&ix.data); 43 | ( 44 | ix.accounts[in_idx].pubkey, 45 | ix.accounts[out_idx].pubkey, 46 | ) 47 | } 48 | 49 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 50 | let (in_idx, out_idx) = Self::user_ata_indexes(&inner_ix.data); 51 | ( 52 | account_keys[inner_ix.accounts[in_idx] as usize], 53 | account_keys[inner_ix.accounts[out_idx] as usize], 54 | ) 55 | } 56 | 57 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 58 | let (in_idx, out_idx) = Self::pool_ata_indexes(&ix.data); 59 | ( 60 | ix.accounts[in_idx].pubkey, 61 | ix.accounts[out_idx].pubkey, 62 | ) 63 | } 64 | 65 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 66 | let (in_idx, out_idx) = Self::pool_ata_indexes(&inner_ix.data); 67 | ( 68 | account_keys[inner_ix.accounts[in_idx] as usize], 69 | account_keys[inner_ix.accounts[out_idx] as usize], 70 | ) 71 | } 72 | 73 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 74 | [ 75 | // swap 76 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &CLEARPOOL_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 42), 77 | ].concat() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/fusionamm.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::FUSIONAMM_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for FusionAmmSwapFinder {} 7 | 8 | pub struct FusionAmmSwapFinder {} 9 | 10 | impl FusionAmmSwapFinder { 11 | fn is_a_to_b(data: &[u8]) -> bool { 12 | data[41] == 1 13 | } 14 | } 15 | 16 | impl SwapFinder for FusionAmmSwapFinder { 17 | fn amm_ix(ix: &Instruction) -> Pubkey { 18 | ix.accounts[4].pubkey 19 | } 20 | 21 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 22 | account_keys[inner_ix.accounts[4] as usize] 23 | } 24 | 25 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 26 | if Self::is_a_to_b(&ix.data) { 27 | ( 28 | ix.accounts[7].pubkey, 29 | ix.accounts[8].pubkey, 30 | ) 31 | } else { 32 | ( 33 | ix.accounts[8].pubkey, 34 | ix.accounts[7].pubkey, 35 | ) 36 | } 37 | } 38 | 39 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 40 | if Self::is_a_to_b(&inner_ix.data) { 41 | ( 42 | account_keys[inner_ix.accounts[7] as usize], 43 | account_keys[inner_ix.accounts[8] as usize], 44 | ) 45 | } else { 46 | ( 47 | account_keys[inner_ix.accounts[8] as usize], 48 | account_keys[inner_ix.accounts[7] as usize], 49 | ) 50 | } 51 | } 52 | 53 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 54 | if Self::is_a_to_b(&ix.data) { 55 | ( 56 | ix.accounts[10].pubkey, 57 | ix.accounts[9].pubkey, 58 | ) 59 | } else { 60 | ( 61 | ix.accounts[9].pubkey, 62 | ix.accounts[10].pubkey, 63 | ) 64 | } 65 | } 66 | 67 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 68 | if Self::is_a_to_b(&inner_ix.data) { 69 | ( 70 | account_keys[inner_ix.accounts[10] as usize], 71 | account_keys[inner_ix.accounts[9] as usize], 72 | ) 73 | } else { 74 | ( 75 | account_keys[inner_ix.accounts[9] as usize], 76 | account_keys[inner_ix.accounts[10] as usize], 77 | ) 78 | } 79 | } 80 | 81 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 82 | [ 83 | // swap 84 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &FUSIONAMM_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 42), 85 | ].concat() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/sv2e.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::SV2E_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for Sv2eSwapFinder {} 7 | 8 | pub struct Sv2eSwapFinder {} 9 | 10 | /// SV2E... a single swap instruction 11 | /// [17] is a_to_b 12 | /// user a/b: 6/7, pool a/b: 4/5 13 | impl Sv2eSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[17] == 1 16 | } 17 | } 18 | 19 | impl SwapFinder for Sv2eSwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[1].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[1] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_a_to_b(&ix.data) { 30 | ( 31 | ix.accounts[7].pubkey, 32 | ix.accounts[6].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[6].pubkey, 37 | ix.accounts[7].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[7] as usize], 46 | account_keys[inner_ix.accounts[6] as usize], 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[6] as usize], 51 | account_keys[inner_ix.accounts[7] as usize], 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[4].pubkey, 60 | ix.accounts[5].pubkey, 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[5].pubkey, 65 | ix.accounts[4].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[4] as usize], // base 74 | account_keys[inner_ix.accounts[5] as usize], // quote 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[5] as usize], // quote 79 | account_keys[inner_ix.accounts[4] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &SV2E_PUBKEY, &[0x07], 0, 18), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/solfi.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::SOLFI_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for SolFiSwapFinder {} 7 | 8 | pub struct SolFiSwapFinder {} 9 | 10 | /// SolFi a single swap instruction 11 | /// [17] is !a_to_b 12 | /// user a/b: 4/5, pool a/b: 2/3 13 | impl SolFiSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[17] == 0 16 | } 17 | } 18 | 19 | impl SwapFinder for SolFiSwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[1].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[1] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_a_to_b(&ix.data) { 30 | ( 31 | ix.accounts[4].pubkey, 32 | ix.accounts[5].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[5].pubkey, 37 | ix.accounts[4].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[4] as usize], // base 46 | account_keys[inner_ix.accounts[5] as usize], // quote 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[5] as usize], // quote 51 | account_keys[inner_ix.accounts[4] as usize], // base 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[3].pubkey, 60 | ix.accounts[2].pubkey, 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[2].pubkey, 65 | ix.accounts[3].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[3] as usize], // base 74 | account_keys[inner_ix.accounts[2] as usize], // quote 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[2] as usize], // quote 79 | account_keys[inner_ix.accounts[3] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &SOLFI_PUBKEY, &[0x07], 0, 18), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/tessv.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::TESS_V_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for TessVSwapFinder {} 7 | 8 | pub struct TessVSwapFinder {} 9 | 10 | /// TessV a single swap instruction 11 | /// [1] is a_to_b 12 | /// user a/b: 5/6, pool a/b: 3/4 13 | impl TessVSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[1] == 1 16 | } 17 | } 18 | 19 | impl SwapFinder for TessVSwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[1].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[1] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_a_to_b(&ix.data) { 30 | ( 31 | ix.accounts[5].pubkey, 32 | ix.accounts[6].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[6].pubkey, 37 | ix.accounts[5].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[5] as usize], // base 46 | account_keys[inner_ix.accounts[6] as usize], // quote 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[6] as usize], // quote 51 | account_keys[inner_ix.accounts[5] as usize], // base 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[4].pubkey, 60 | ix.accounts[3].pubkey, 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[3].pubkey, 65 | ix.accounts[4].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[4] as usize], // base 74 | account_keys[inner_ix.accounts[3] as usize], // quote 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[3] as usize], // quote 79 | account_keys[inner_ix.accounts[4] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &TESS_V_PUBKEY, &[0x10], 0, 18), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/alpha.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::ALPHA_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for AlphaSwapFinder {} 7 | 8 | pub struct AlphaSwapFinder {} 9 | 10 | /// SolFi a single swap instruction 11 | /// [1] is a_to_b 12 | /// user a/b: 3/2, pool a/b: 5/4 13 | impl AlphaSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[1] == 1 16 | } 17 | } 18 | impl SwapFinder for AlphaSwapFinder { 19 | fn amm_ix(ix: &Instruction) -> Pubkey { 20 | ix.accounts[2].pubkey 21 | } 22 | 23 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 24 | account_keys[inner_ix.accounts[2] as usize] 25 | } 26 | 27 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 28 | if Self::is_a_to_b(&ix.data) { 29 | ( 30 | ix.accounts[3].pubkey, 31 | ix.accounts[4].pubkey, 32 | ) 33 | } else { 34 | ( 35 | ix.accounts[4].pubkey, 36 | ix.accounts[3].pubkey, 37 | ) 38 | } 39 | 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[3] as usize], 46 | account_keys[inner_ix.accounts[4] as usize], 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[4] as usize], 51 | account_keys[inner_ix.accounts[3] as usize], 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[6].pubkey, // quote 60 | ix.accounts[5].pubkey, // base 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[5].pubkey, // quote 65 | ix.accounts[6].pubkey, // base 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[6] as usize], // quote 74 | account_keys[inner_ix.accounts[5] as usize], // base 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[5] as usize], // quote 79 | account_keys[inner_ix.accounts[6] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &ALPHA_PUBKEY, &[0x0c], 0, 18), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/goonfi.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::GOONFI_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for GoonFiSwapFinder {} 7 | 8 | pub struct GoonFiSwapFinder {} 9 | 10 | /// SolFi a single swap instruction 11 | /// [1] is a_to_b 12 | /// user a/b: 3/2, pool a/b: 5/4 13 | impl GoonFiSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[1] == 1 16 | } 17 | } 18 | 19 | impl SwapFinder for GoonFiSwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[1].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[1] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_a_to_b(&ix.data) { 30 | ( 31 | ix.accounts[3].pubkey, 32 | ix.accounts[2].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[2].pubkey, 37 | ix.accounts[3].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[3] as usize], // base 46 | account_keys[inner_ix.accounts[2] as usize], // quote 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[2] as usize], // quote 51 | account_keys[inner_ix.accounts[3] as usize], // base 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[4].pubkey, 60 | ix.accounts[5].pubkey, 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[5].pubkey, 65 | ix.accounts[4].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[4] as usize], // base 74 | account_keys[inner_ix.accounts[5] as usize], // quote 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[5] as usize], // quote 79 | account_keys[inner_ix.accounts[4] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &GOONFI_PUBKEY, &[0x02], 0, 19), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/openbook_v2.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::OPENBOOK_V2_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for OpenbookV2SwapFinder {} 7 | 8 | pub struct OpenbookV2SwapFinder {} 9 | 10 | /// Openbook is actually a CLOB program, but there's placeTakeOrder which works like a swap 11 | /// Parsing pending events is not supported here 12 | /// Market base/quote: 6/7 13 | /// User base/quote: 9/10 14 | impl OpenbookV2SwapFinder { 15 | fn is_ask(data: &[u8]) -> bool { 16 | data[8] == 1 17 | } 18 | } 19 | impl SwapFinder for OpenbookV2SwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[2].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[2] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_ask(&ix.data) { 30 | ( 31 | ix.accounts[9].pubkey, 32 | ix.accounts[10].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[10].pubkey, 37 | ix.accounts[9].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_ask(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[9] as usize], 46 | account_keys[inner_ix.accounts[10] as usize], 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[10] as usize], 51 | account_keys[inner_ix.accounts[9] as usize], 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(_ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_ask(&_ix.data) { 58 | ( 59 | _ix.accounts[7].pubkey, 60 | _ix.accounts[6].pubkey, 61 | ) 62 | } else { 63 | ( 64 | _ix.accounts[6].pubkey, 65 | _ix.accounts[7].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(_inner_ix: &InnerInstruction, _account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_ask(&_inner_ix.data) { 72 | ( 73 | _account_keys[_inner_ix.accounts[7] as usize], 74 | _account_keys[_inner_ix.accounts[6] as usize], 75 | ) 76 | } else { 77 | ( 78 | _account_keys[_inner_ix.accounts[6] as usize], 79 | _account_keys[_inner_ix.accounts[7] as usize], 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | // placeTakeOrder 86 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &OPENBOOK_V2_PUBKEY, &[0x03, 0x2c, 0x47, 0x03, 0x1a, 0xc7, 0xcb, 0x55], 0, 35) 87 | } 88 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/saros_dlmm.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::SAROS_DLMM_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for SarosDLMMSwapFinder {} 7 | 8 | pub struct SarosDLMMSwapFinder {} 9 | 10 | /// Saros DLMM has a single swap instruction 11 | /// [24] is a_to_b 12 | /// user a/b: 7/8, pool a/b: 5/6 13 | impl SarosDLMMSwapFinder { 14 | fn is_a_to_b(data: &[u8]) -> bool { 15 | data[24] == 1 16 | } 17 | } 18 | 19 | impl SwapFinder for SarosDLMMSwapFinder { 20 | fn amm_ix(ix: &Instruction) -> Pubkey { 21 | ix.accounts[0].pubkey 22 | } 23 | 24 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 25 | account_keys[inner_ix.accounts[0] as usize] 26 | } 27 | 28 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 29 | if Self::is_a_to_b(&ix.data) { 30 | ( 31 | ix.accounts[7].pubkey, 32 | ix.accounts[8].pubkey, 33 | ) 34 | } else { 35 | ( 36 | ix.accounts[8].pubkey, 37 | ix.accounts[7].pubkey, 38 | ) 39 | } 40 | } 41 | 42 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 43 | if Self::is_a_to_b(&inner_ix.data) { 44 | ( 45 | account_keys[inner_ix.accounts[7] as usize], // base 46 | account_keys[inner_ix.accounts[8] as usize], // quote 47 | ) 48 | } else { 49 | ( 50 | account_keys[inner_ix.accounts[8] as usize], // quote 51 | account_keys[inner_ix.accounts[7] as usize], // base 52 | ) 53 | } 54 | } 55 | 56 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 57 | if Self::is_a_to_b(&ix.data) { 58 | ( 59 | ix.accounts[6].pubkey, 60 | ix.accounts[5].pubkey, 61 | ) 62 | } else { 63 | ( 64 | ix.accounts[5].pubkey, 65 | ix.accounts[6].pubkey, 66 | ) 67 | } 68 | } 69 | 70 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 71 | if Self::is_a_to_b(&inner_ix.data) { 72 | ( 73 | account_keys[inner_ix.accounts[6] as usize], // base 74 | account_keys[inner_ix.accounts[5] as usize], // quote 75 | ) 76 | } else { 77 | ( 78 | account_keys[inner_ix.accounts[5] as usize], // quote 79 | account_keys[inner_ix.accounts[6] as usize], // base 80 | ) 81 | } 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // swap 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &SAROS_DLMM_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 25), 88 | ].concat() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/apesu.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::APESU_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for ApesuSwapFinder {} 7 | 8 | pub struct ApesuSwapFinder {} 9 | 10 | /// Apesu a single swap instruction 11 | /// [24]==1 is a_to_b, ==3 is b_to_a, unsure what 0/2/4 does, never seen either 12 | /// user a/b: 1/2, pool a/b: 3/4 13 | /// name is identified through crank's source of funds 14 | impl ApesuSwapFinder { 15 | fn is_a_to_b(data: &[u8]) -> bool { 16 | data[24] == 1 17 | } 18 | } 19 | 20 | impl SwapFinder for ApesuSwapFinder { 21 | fn amm_ix(ix: &Instruction) -> Pubkey { 22 | ix.accounts[0].pubkey 23 | } 24 | 25 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 26 | account_keys[inner_ix.accounts[0] as usize] 27 | } 28 | 29 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 30 | if Self::is_a_to_b(&ix.data) { 31 | ( 32 | ix.accounts[1].pubkey, 33 | ix.accounts[2].pubkey, 34 | ) 35 | } else { 36 | ( 37 | ix.accounts[2].pubkey, 38 | ix.accounts[1].pubkey, 39 | ) 40 | } 41 | } 42 | 43 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 44 | if Self::is_a_to_b(&inner_ix.data) { 45 | ( 46 | account_keys[inner_ix.accounts[1] as usize], 47 | account_keys[inner_ix.accounts[2] as usize], 48 | ) 49 | } else { 50 | ( 51 | account_keys[inner_ix.accounts[2] as usize], 52 | account_keys[inner_ix.accounts[1] as usize], 53 | ) 54 | } 55 | } 56 | 57 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 58 | if Self::is_a_to_b(&ix.data) { 59 | ( 60 | ix.accounts[4].pubkey, 61 | ix.accounts[3].pubkey, 62 | ) 63 | } else { 64 | ( 65 | ix.accounts[3].pubkey, 66 | ix.accounts[4].pubkey, 67 | ) 68 | } 69 | } 70 | 71 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 72 | if Self::is_a_to_b(&inner_ix.data) { 73 | ( 74 | account_keys[inner_ix.accounts[4] as usize], // base 75 | account_keys[inner_ix.accounts[3] as usize], // quote 76 | ) 77 | } else { 78 | ( 79 | account_keys[inner_ix.accounts[3] as usize], // quote 80 | account_keys[inner_ix.accounts[4] as usize], // base 81 | ) 82 | } 83 | } 84 | 85 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 86 | [ 87 | // swap 88 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &APESU_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 25), 89 | ].concat() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /sandwich-finder/src/bin/detector-realtime.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env}; 2 | 3 | use futures::{SinkExt as _, StreamExt}; 4 | use sandwich_finder::{detector::{get_events, LEADER_GROUP_SIZE}, events::{common::Inserter, sandwich::detect}, utils::create_db_pool}; 5 | use yellowstone_grpc_client::GeyserGrpcBuilder; 6 | use yellowstone_grpc_proto::{geyser::{subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, SubscribeRequestFilterBlocksMeta, SubscribeRequestPing}, tonic::transport::Endpoint}; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | dotenv::dotenv().ok(); 11 | let pool = create_db_pool(); 12 | let inserter = Inserter::new(pool.clone()); 13 | 14 | let grpc_url = env::var("GRPC_URL").expect("GRPC_URL is not set"); 15 | println!("connecting to grpc server: {}", grpc_url); 16 | let mut grpc_client = GeyserGrpcBuilder{ 17 | endpoint: Endpoint::from_shared(grpc_url.to_string()).unwrap(), 18 | x_token: None, 19 | x_request_snapshot: false, 20 | send_compressed: None, 21 | accept_compressed: None, 22 | max_decoding_message_size: Some(128 * 1024 * 1024), 23 | max_encoding_message_size: None, 24 | }.connect().await.expect("cannon connect to grpc server"); 25 | println!("connected to grpc server!"); 26 | let mut slots = HashMap::new(); 27 | slots.insert("client".to_string(), SubscribeRequestFilterBlocksMeta {}); 28 | let (mut sink, mut stream) = grpc_client.subscribe_with_request(Some(SubscribeRequest { 29 | blocks_meta: slots, 30 | commitment: Some(CommitmentLevel::Confirmed as i32), 31 | ..Default::default() 32 | })).await.expect("unable to subscribe"); 33 | 34 | while let Some(msg) = stream.next().await { 35 | if msg.is_err() { 36 | println!("grpc error: {:?}", msg.err()); 37 | break; 38 | } 39 | let msg = msg.unwrap(); 40 | match msg.update_oneof { 41 | Some(UpdateOneof::BlockMeta(meta)) => { 42 | // println!("{:?}", meta); 43 | let slot = meta.slot; 44 | if meta.slot % 4 == 3 { 45 | let pool = pool.clone(); 46 | let mut inserter = inserter.clone(); 47 | tokio::spawn(async move { 48 | // Intentionally lag behind slightly to ensure all events are inserted 49 | let start_slot = slot - 2 * LEADER_GROUP_SIZE + 1; 50 | let end_slot = slot - LEADER_GROUP_SIZE; 51 | println!("Processing slots {} - {}", start_slot, end_slot); 52 | let (swaps, transfers, txs) = get_events(pool.clone(), start_slot, end_slot).await; 53 | let sandwiches = detect(&swaps, &transfers, &txs); 54 | println!("Found {} sandwiches in slots {} - {}", sandwiches.len(), start_slot, end_slot); 55 | inserter.insert_sandwiches(start_slot, sandwiches).await; 56 | }); 57 | } 58 | }, 59 | Some(UpdateOneof::Ping(_)) => { 60 | let _ = sink.send(SubscribeRequest { 61 | ping: Some(SubscribeRequestPing {id: 1}), 62 | ..Default::default() 63 | }).await; 64 | }, 65 | _ => {}, 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/limo.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::LIMO_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for LimoSwapFinder {} 7 | 8 | pub struct LimoSwapFinder {} 9 | 10 | /// Limo has one variant 11 | /// take order: [discriminant, input: u64, output: u64, expire_at: i64] 12 | /// Orders are created adhoc so there's no amm, we make one up from the traded mints [5, 6] with xor 13 | impl LimoSwapFinder { 14 | fn keys(ix: &Instruction) -> Vec { 15 | vec![ 16 | // maker in/out 17 | ix.accounts[7].pubkey, 18 | if ix.accounts[10].pubkey == LIMO_PUBKEY { 19 | ix.accounts[11].pubkey 20 | } else { 21 | ix.accounts[10].pubkey 22 | }, 23 | // taker in/out 24 | ix.accounts[9].pubkey, 25 | ix.accounts[8].pubkey, 26 | ] 27 | } 28 | 29 | fn keys_inner(inner_ix: &InnerInstruction, account_keys: &Vec) -> Vec { 30 | vec![ 31 | // maker in/out 32 | account_keys[inner_ix.accounts[7] as usize], 33 | if account_keys[inner_ix.accounts[10] as usize] == LIMO_PUBKEY { 34 | account_keys[inner_ix.accounts[11] as usize] 35 | } else { 36 | account_keys[inner_ix.accounts[10] as usize] 37 | }, 38 | // taker in/out 39 | account_keys[inner_ix.accounts[9] as usize], 40 | account_keys[inner_ix.accounts[8] as usize], 41 | ] 42 | } 43 | } 44 | 45 | impl SwapFinder for LimoSwapFinder { 46 | fn amm_ix(ix: &Instruction) -> Pubkey { 47 | let in_mint = ix.accounts[5].pubkey; 48 | let out_mint = ix.accounts[6].pubkey; 49 | in_mint.to_bytes().iter().zip(out_mint.to_bytes().iter()).map(|(a, b)| a ^ b).collect::>()[..].try_into().expect("wrong length") 50 | } 51 | 52 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 53 | let in_mint = account_keys[inner_ix.accounts[5] as usize]; 54 | let out_mint = account_keys[inner_ix.accounts[6] as usize]; 55 | in_mint.to_bytes().iter().zip(out_mint.to_bytes().iter()).map(|(a, b)| a ^ b).collect::>()[..].try_into().expect("wrong length") 56 | } 57 | 58 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 59 | let keys = Self::keys(ix); 60 | (keys[0], keys[1]) 61 | } 62 | 63 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 64 | let keys = Self::keys_inner(inner_ix, account_keys); 65 | (keys[0], keys[1]) 66 | } 67 | 68 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 69 | let keys = Self::keys(ix); 70 | (keys[2], keys[3]) 71 | } 72 | 73 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 74 | let keys = Self::keys_inner(inner_ix, account_keys); 75 | (keys[2], keys[3]) 76 | } 77 | 78 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 79 | [ 80 | // fill 81 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &LIMO_PUBKEY, &[0xa3, 0xd0, 0x14, 0xac, 0xdf, 0x41, 0xff, 0xe4], 0, 32), 82 | ].concat() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/utils.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, TransactionStatusMeta}; 3 | 4 | use crate::events::addresses::{SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WSOL_MINT}; 5 | 6 | pub fn mint_of(pubkey: &Pubkey, account_keys: &Vec, meta: &TransactionStatusMeta) -> Option { 7 | let target_index = account_keys.iter().position(|key| key == pubkey); 8 | if target_index.is_none() { 9 | return None; 10 | } 11 | let pre = meta.pre_token_balances 12 | .iter() 13 | .find(|&balance| balance.account_index == target_index.unwrap() as u32) 14 | .map_or(None, |balance| Some(balance.mint.clone())); 15 | let post = meta.post_token_balances 16 | .iter() 17 | .find(|&balance| balance.account_index == target_index.unwrap() as u32) 18 | .map_or(None, |balance| Some(balance.mint.clone())); 19 | return pre.or(post); 20 | } 21 | 22 | pub fn token_transferred_inner(inner_ix: &InnerInstruction, account_keys: &Vec, meta: &TransactionStatusMeta) -> Option<(Pubkey, Pubkey, Pubkey, String, u64)> { 23 | // (from, to, mint, amount) 24 | if inner_ix.program_id_index >= account_keys.len() as u32 { 25 | return None; 26 | } 27 | let program_id = account_keys[inner_ix.program_id_index as usize]; 28 | match program_id { 29 | TOKEN_PROGRAM_ID | TOKEN_2022_PROGRAM_ID => { 30 | if inner_ix.data.len() < 9 { 31 | return None; 32 | } 33 | let (from_index, to_index, auth_index) = match inner_ix.data[0] { 34 | 3 => (inner_ix.accounts[0], inner_ix.accounts[1], inner_ix.accounts[2]), // Transfer 35 | 12 => (inner_ix.accounts[0], inner_ix.accounts[2], inner_ix.accounts[3]), // TransferChecked 36 | _ => (255, 255, 255), // Not a transfer, will be caught by bounds check 37 | }; 38 | if from_index as usize >= account_keys.len() || to_index as usize >= account_keys.len() { 39 | return None; 40 | } 41 | let checked_mint = if inner_ix.data[0] == 12 { 42 | Some(account_keys[inner_ix.accounts[1] as usize].to_string()) 43 | } else { 44 | None 45 | }; 46 | let from_mint = mint_of(&account_keys[from_index as usize], &account_keys, &meta); 47 | let to_mint = mint_of(&account_keys[to_index as usize], &account_keys, &meta); 48 | if checked_mint.is_none() && from_mint.is_none() && to_mint.is_none() { 49 | return None; 50 | } 51 | return Some(( 52 | account_keys[from_index as usize], 53 | account_keys[to_index as usize], 54 | account_keys[auth_index as usize], 55 | checked_mint.or(from_mint).or(to_mint).unwrap(), 56 | u64::from_le_bytes(inner_ix.data[1..9].try_into().unwrap()), 57 | )); 58 | }, 59 | SYSTEM_PROGRAM_ID => { 60 | if inner_ix.data.len() < 12 { 61 | return None; 62 | } 63 | if inner_ix.data[0] != 2 { 64 | return None; // Not a transfer 65 | } 66 | return Some(( 67 | account_keys[inner_ix.accounts[0] as usize], 68 | account_keys[inner_ix.accounts[1] as usize], 69 | account_keys[inner_ix.accounts[0] as usize], 70 | WSOL_MINT.to_string(), 71 | u64::from_le_bytes(inner_ix.data[4..12].try_into().unwrap()), 72 | )); 73 | }, 74 | _ => None, 75 | } 76 | // ix, amount[, decimals] 77 | 78 | } 79 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/stake.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::{STAKE_PROGRAM_ID, WSOL_MINT}, transfer::{TransferFinder, TransferV2}, transfers::private::Sealed}; 5 | 6 | impl Sealed for StakeProgramTransferfinder {} 7 | /// [0x02, 0x00, 0x00, 0x00, u64] 8 | pub struct StakeProgramTransferfinder{} 9 | 10 | impl StakeProgramTransferfinder { 11 | /// Returns (from, to, auth, amount) 12 | fn amount_and_endpoint_from_data(data: &[u8]) -> Option<(usize, usize, usize, u64)> { 13 | if data.len() < 12 { 14 | return None; 15 | } 16 | match data[0] { 17 | 4 => Some((0, 1, 4, u64::from_le_bytes(data[4..12].try_into().unwrap()))), // Withdraw 18 | _ => None, 19 | } 20 | } 21 | } 22 | 23 | impl TransferFinder for StakeProgramTransferfinder { 24 | fn find_transfers(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, _meta: &TransactionStatusMeta) -> Vec { 25 | if ix.program_id == STAKE_PROGRAM_ID { 26 | if let Some((from, to, auth, amount)) = Self::amount_and_endpoint_from_data(&ix.data) { 27 | if ix.accounts.len() < 2 { 28 | return vec![]; 29 | } 30 | return vec![TransferV2::new( 31 | None, 32 | STAKE_PROGRAM_ID.to_string().into(), 33 | ix.accounts[auth].pubkey.to_string().into(), 34 | WSOL_MINT.to_string().into(), 35 | amount, 36 | ix.accounts[from].pubkey.to_string().into(), 37 | ix.accounts[to].pubkey.to_string().into(), 38 | 0, 39 | 0, 40 | 0, 41 | None, 42 | 0, 43 | )]; 44 | } 45 | return vec![]; 46 | } 47 | let mut transfers = vec![]; 48 | inner_ixs.instructions.iter().enumerate().for_each(|(i, inner_ix)| { 49 | if inner_ix.program_id_index as usize >= account_keys.len() { 50 | return; 51 | } 52 | if account_keys[inner_ix.program_id_index as usize] != STAKE_PROGRAM_ID { 53 | return; 54 | } 55 | if inner_ix.accounts.len() < 2 { 56 | return; 57 | } 58 | if let Some((from, to, auth, amount)) = Self::amount_and_endpoint_from_data(&inner_ix.data) { 59 | let from = inner_ix.accounts[from] as usize; 60 | let to = inner_ix.accounts[to] as usize; 61 | let auth = inner_ix.accounts[auth] as usize; 62 | if from >= account_keys.len() || to >= account_keys.len() || auth >= account_keys.len() { 63 | return; 64 | } 65 | if from == to { 66 | // Don't log self transfers 67 | return; 68 | } 69 | transfers.push(TransferV2::new( 70 | Some(ix.program_id.to_string().into()), 71 | STAKE_PROGRAM_ID.to_string().into(), 72 | account_keys[auth].to_string().into(), 73 | WSOL_MINT.to_string().into(), 74 | amount, 75 | account_keys[from].to_string().into(), 76 | account_keys[to].to_string().into(), 77 | 0, 78 | 0, 79 | 0, 80 | Some(i as u32), 81 | 0, 82 | )); 83 | } else { 84 | return; 85 | } 86 | }); 87 | transfers 88 | } 89 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/pumpamm.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::PDF2_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for PumpAmmSwapFinder {} 7 | 8 | pub struct PumpAmmSwapFinder {} 9 | 10 | /// Pump.fun have two variants: 11 | /// 1. buy [0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea] 12 | /// 2. sell [0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad] 13 | /// 3. buyExactQuoteIn [0xc6, 0x2e, 0x15, 0x52, 0xb4, 0xd9, 0xe8, 0x70] 14 | /// In/out amounts follows the discriminant, with the first one being exact and the other being the worst acceptable value. 15 | /// Swap direction is determined instruction's name. 16 | impl PumpAmmSwapFinder { 17 | fn user_in_out_index(ix_data: &[u8]) -> (usize, usize) { 18 | if ix_data.starts_with(&[0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea]) || 19 | ix_data.starts_with(&[0xc6, 0x2e, 0x15, 0x52, 0xb4, 0xd9, 0xe8, 0x70]) { 20 | // buy 21 | (6, 5) 22 | } else { 23 | // sell 24 | (5, 6) 25 | } 26 | } 27 | 28 | fn pool_in_out_index(ix_data: &[u8]) -> (usize, usize) { 29 | if ix_data.starts_with(&[0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea]) { 30 | // buy 31 | (7, 8) 32 | } else { 33 | // sell 34 | (8, 7) 35 | } 36 | } 37 | } 38 | 39 | impl SwapFinder for PumpAmmSwapFinder { 40 | fn amm_ix(ix: &Instruction) -> Pubkey { 41 | ix.accounts[0].pubkey 42 | } 43 | 44 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 45 | account_keys[inner_ix.accounts[0] as usize] 46 | } 47 | 48 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 49 | let (in_index, out_index) = Self::user_in_out_index(&ix.data); 50 | ( 51 | ix.accounts[in_index].pubkey, 52 | ix.accounts[out_index].pubkey, 53 | ) 54 | } 55 | 56 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 57 | let (in_index, out_index) = Self::user_in_out_index(&inner_ix.data); 58 | ( 59 | account_keys[inner_ix.accounts[in_index] as usize], 60 | account_keys[inner_ix.accounts[out_index] as usize], 61 | ) 62 | } 63 | 64 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 65 | let (in_index, out_index) = Self::pool_in_out_index(&ix.data); 66 | ( 67 | ix.accounts[in_index].pubkey, 68 | ix.accounts[out_index].pubkey, 69 | ) 70 | } 71 | 72 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 73 | let (in_index, out_index) = Self::pool_in_out_index(&inner_ix.data); 74 | ( 75 | account_keys[inner_ix.accounts[in_index] as usize], 76 | account_keys[inner_ix.accounts[out_index] as usize], 77 | ) 78 | } 79 | 80 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 81 | [ 82 | // buy 83 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &PDF2_PUBKEY, &[0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea], 0, 24), 84 | // sell 85 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &PDF2_PUBKEY, &[0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad], 0, 24), 86 | // buyExactQuoteIn 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &PDF2_PUBKEY, &[0xc6, 0x2e, 0x15, 0x52, 0xb4, 0xd9, 0xe8, 0x70], 0, 24), 88 | ].concat() 89 | } 90 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfer.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use derive_getters::Getters; 4 | use serde::Serialize; 5 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 6 | use yellowstone_grpc_proto::{prelude::{InnerInstructions, TransactionStatusMeta}}; 7 | 8 | use crate::events::common::Timestamp; 9 | 10 | #[derive(Clone, Serialize, Getters)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct TransferV2 { 13 | // The wrapper program for this transfer, if any 14 | outer_program: Option>, 15 | // The actual token/system program 16 | program: Arc, 17 | // Wallet that authorised the transfer 18 | authority: Arc, 19 | // Mint of the token transferred 20 | mint: Arc, 21 | // Amounts of the transfer 22 | amount: u64, 23 | // In/out token accounts 24 | input_ata: Arc, 25 | output_ata: Arc, 26 | // These fields are meant to be replaced when inserting to the db 27 | timestamp: Timestamp, 28 | id: u64, 29 | } 30 | 31 | impl Debug for TransferV2 { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | // f.debug_struct("SwapV2").field("outer_program", &self.outer_program).field("program", &self.program).field("amm", &self.amm).field("input_mint", &self.input_mint).field("output_mint", &self.output_mint).field("input_amount", &self.input_amount).field("output_amount", &self.output_amount).field("input_ata", &self.input_ata).field("output_ata", &self.output_ata).field("sig_id", &self.sig_id).field("slot", &self.slot).field("inclusion_order", &self.inclusion_order).field("ix_index", &self.ix_index).field("inner_ix_index", &self.inner_ix_index).finish() 34 | f.write_str("Transfer")?; 35 | f.write_str(&format!(" in slot {} (order {}, ix {}, inner_ix {:?})\n", self.slot(), self.inclusion_order(), self.ix_index(), self.inner_ix_index()))?; 36 | if let Some(outer_program) = &self.outer_program { 37 | f.write_str(&format!(" via {}\n", outer_program))?; 38 | } 39 | f.write_str(&format!(" on {} mint {}\n", self.program, self.mint))?; 40 | f.write_str(&format!(" Amount {}\n", self.amount))?; 41 | f.write_str(&format!(" ATAs {} -> {}", self.input_ata, self.output_ata))?; 42 | Ok(()) 43 | } 44 | } 45 | 46 | impl TransferV2 { 47 | pub fn new( 48 | outer_program: Option>, 49 | program: Arc, 50 | authority: Arc, 51 | mint: Arc, 52 | amount: u64, 53 | input_ata: Arc, 54 | output_ata: Arc, 55 | slot: u64, 56 | inclusion_order: u32, 57 | ix_index: u32, 58 | inner_ix_index: Option, 59 | id: u64, 60 | ) -> Self { 61 | Self { 62 | outer_program, 63 | program, 64 | authority, 65 | mint, 66 | amount, 67 | input_ata, 68 | output_ata, 69 | timestamp: Timestamp::new( 70 | slot, 71 | inclusion_order, 72 | ix_index, 73 | inner_ix_index, 74 | ), 75 | id, 76 | } 77 | } 78 | 79 | pub fn slot(&self) -> &u64 { 80 | self.timestamp.slot() 81 | } 82 | pub fn inclusion_order(&self) -> &u32 { 83 | self.timestamp.inclusion_order() 84 | } 85 | pub fn ix_index(&self) -> &u32 { 86 | self.timestamp.ix_index() 87 | } 88 | pub fn inner_ix_index(&self) -> &Option { 89 | self.timestamp.inner_ix_index() 90 | } 91 | } 92 | 93 | pub trait TransferFinder { 94 | /// Returns the transfers utilising a program found in the given instruction and inner instructions. 95 | fn find_transfers(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec; 96 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/system.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::{SYSTEM_PROGRAM_ID, WSOL_MINT}, transfer::{TransferFinder, TransferV2}, transfers::private::Sealed}; 5 | 6 | impl Sealed for SystemProgramTransferfinder {} 7 | /// [0x02, 0x00, 0x00, 0x00, u64] 8 | pub struct SystemProgramTransferfinder{} 9 | 10 | impl SystemProgramTransferfinder { 11 | fn amount_and_dest_from_data(data: &[u8]) -> Option<(usize, u64)> { 12 | if data.len() < 12 { 13 | return None; 14 | } 15 | match data[0] { 16 | 0 => Some((1, u64::from_le_bytes(data[4..12].try_into().unwrap()))), // CreateAccount 17 | 2 => Some((1, u64::from_le_bytes(data[4..12].try_into().unwrap()))), // Transfer 18 | 3 => { 19 | // 0..4: discriminator, 4..36: base, 36..44: seed len, 44..(44+seed len): seed, (44+seed len)..(52+seed len): lamports 20 | let start = 44 + u64::from_le_bytes(data[36..44].try_into().unwrap()) as usize; 21 | let end = start + 8; 22 | Some((1, u64::from_le_bytes(data[start..end].try_into().unwrap()))) 23 | }, // CreateAccountWithSeed 24 | 13 => Some((2, u64::from_le_bytes(data[4..12].try_into().unwrap()))), // TransferWithSeed 25 | _ => None, 26 | } 27 | } 28 | } 29 | 30 | impl TransferFinder for SystemProgramTransferfinder { 31 | fn find_transfers(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, _meta: &TransactionStatusMeta) -> Vec { 32 | if ix.program_id == SYSTEM_PROGRAM_ID { 33 | if let Some((to, amount)) = Self::amount_and_dest_from_data(&ix.data) { 34 | if ix.accounts.len() < 2 { 35 | return vec![]; 36 | } 37 | return vec![TransferV2::new( 38 | None, 39 | SYSTEM_PROGRAM_ID.to_string().into(), 40 | ix.accounts[0].pubkey.to_string().into(), 41 | WSOL_MINT.to_string().into(), 42 | amount, 43 | ix.accounts[0].pubkey.to_string().into(), 44 | ix.accounts[to].pubkey.to_string().into(), 45 | 0, 46 | 0, 47 | 0, 48 | None, 49 | 0, 50 | )]; 51 | } 52 | return vec![]; 53 | } 54 | let mut transfers = vec![]; 55 | inner_ixs.instructions.iter().enumerate().for_each(|(i, inner_ix)| { 56 | if inner_ix.program_id_index as usize >= account_keys.len() { 57 | return; 58 | } 59 | if account_keys[inner_ix.program_id_index as usize] != SYSTEM_PROGRAM_ID { 60 | return; 61 | } 62 | if inner_ix.accounts.len() < 2 { 63 | return; 64 | } 65 | if let Some((to, amount)) = Self::amount_and_dest_from_data(&inner_ix.data) { 66 | let from = inner_ix.accounts[0] as usize; 67 | let to = inner_ix.accounts[to] as usize; 68 | if from >= account_keys.len() || to >= account_keys.len() { 69 | return; 70 | } 71 | if from == to { 72 | // Don't log self transfers 73 | return; 74 | } 75 | transfers.push(TransferV2::new( 76 | Some(ix.program_id.to_string().into()), 77 | SYSTEM_PROGRAM_ID.to_string().into(), 78 | account_keys[from].to_string().into(), 79 | WSOL_MINT.to_string().into(), 80 | amount, 81 | account_keys[from].to_string().into(), 82 | account_keys[to].to_string().into(), 83 | 0, 84 | 0, 85 | 0, 86 | Some(i as u32), 87 | 0, 88 | )); 89 | } else { 90 | return; 91 | } 92 | }); 93 | transfers 94 | } 95 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/jup_perps.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::JUP_PERPS_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | enum JupPerpsSwapVariant { 7 | Swap2, 8 | SwapWithTokenLedger, 9 | InstantIncreasePositionPreSwap, 10 | } 11 | 12 | impl Sealed for JupPerpsSwapFinder {} 13 | 14 | pub struct JupPerpsSwapFinder {} 15 | 16 | impl JupPerpsSwapFinder { 17 | fn variant_from_data(data: &[u8]) -> Option { 18 | if data.starts_with(&[0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]) { 19 | Some(JupPerpsSwapVariant::Swap2) 20 | } else if data.starts_with(&[0x8b, 0x8d, 0xee, 0xc5, 0x29, 0xd3, 0xac, 0x13]) { 21 | Some(JupPerpsSwapVariant::SwapWithTokenLedger) 22 | } else if data.starts_with(&[0xc5, 0x26, 0x56, 0xa5, 0xc7, 0x17, 0x26, 0xea]) { 23 | Some(JupPerpsSwapVariant::InstantIncreasePositionPreSwap) 24 | } else { 25 | None 26 | } 27 | } 28 | } 29 | 30 | /// Jup perps swaps have two variants: 31 | /// 1. swap2 [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88] 32 | /// 2. swapWithTokenLedger [0x8b, 0x8d, 0xee, 0xc5, 0x29, 0xd3, 0xac, 0x13] 33 | /// 3. instantIncreasePositionPreSwap [0xc5, 0x26, 0x56, 0xa5, 0xc7, 0x17, 0x26, 0xea] 34 | /// In/min out amounts follows the discriminant 35 | impl SwapFinder for JupPerpsSwapFinder { 36 | fn amm_ix(ix: &Instruction) -> Pubkey { 37 | ix.accounts[5].pubkey 38 | } 39 | 40 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 41 | account_keys[inner_ix.accounts[5] as usize] 42 | } 43 | 44 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 45 | ( 46 | ix.accounts[1].pubkey, 47 | ix.accounts[2].pubkey, 48 | ) 49 | } 50 | 51 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 52 | ( 53 | account_keys[inner_ix.accounts[1] as usize], 54 | account_keys[inner_ix.accounts[2] as usize], 55 | ) 56 | } 57 | 58 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 59 | match Self::variant_from_data(&ix.data) { 60 | Some(JupPerpsSwapVariant::Swap2) | Some(JupPerpsSwapVariant::SwapWithTokenLedger) => ( 61 | ix.accounts[13].pubkey, 62 | ix.accounts[9].pubkey, 63 | ), 64 | Some(JupPerpsSwapVariant::InstantIncreasePositionPreSwap) => ( 65 | ix.accounts[11].pubkey, 66 | ix.accounts[8].pubkey, 67 | ), 68 | None => unreachable!(), 69 | } 70 | } 71 | 72 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 73 | match Self::variant_from_data(&inner_ix.data) { 74 | Some(JupPerpsSwapVariant::Swap2) | Some(JupPerpsSwapVariant::SwapWithTokenLedger) => ( 75 | account_keys[inner_ix.accounts[13] as usize], 76 | account_keys[inner_ix.accounts[9] as usize], 77 | ), 78 | Some(JupPerpsSwapVariant::InstantIncreasePositionPreSwap) => ( 79 | account_keys[inner_ix.accounts[11] as usize], 80 | account_keys[inner_ix.accounts[8] as usize], 81 | ), 82 | _ => unreachable!(), 83 | } 84 | } 85 | 86 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 87 | [ 88 | // swap_base_input 89 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &JUP_PERPS_PUBKEY, &[0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88], 0, 24), 90 | // swap_base_output 91 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &JUP_PERPS_PUBKEY, &[0x8b, 0x8d, 0xee, 0xc5, 0x29, 0xd3, 0xac, 0x13], 0, 24), 92 | // instant_increase_position_pre_swap 93 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &JUP_PERPS_PUBKEY, &[0xc5, 0x26, 0x56, 0xa5, 0xc7, 0x17, 0x26, 0xea], 0, 24), 94 | ].concat() 95 | } 96 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/raydium_lp.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::RAYDIUM_LP_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for RaydiumLPSwapFinder {} 7 | 8 | pub struct RaydiumLPSwapFinder {} 9 | 10 | /// Ray Launchpad swaps have two variants: 11 | /// 1. buy_exact_in [0xfa, 0xea, 0x0d, 0x7b, 0xd5, 0x9c, 0x13, 0xec] (4, 5=base, 6=quote) 12 | /// 2. sell_exact_in [0x95, 0x27, 0xde, 0x9b, 0xd3, 0x7c, 0x98, 0x1a] (4, 5=base, 6=quote) 13 | /// 3. buy_exact_out [0x18, 0xd3, 0x74, 0x28, 0x69, 0x03, 0x99, 0x38] (4, 5=base, 6=quote) 14 | /// 4. sell_exact_out [0x5f, 0xc8, 0x47, 0x22, 0x8, 0x9, 0xb, 0xa6] (4, 5=base, 6=quote) 15 | /// In/out amounts follows the discriminant, with one being exact and the other being the worst acceptable value. 16 | /// share_fee_rate follows the above but we don't care. 17 | /// Swap direction is determined by the instruction's name. 18 | /// Buy = quote->base, sell = base->quote. 19 | /// All 4 instructions follow the same structure: 20 | /// [4]=amm, [5]=user base ata, [6]=user quote ata, [7]=pool base ATA, [8]=pool quote ATA 21 | impl RaydiumLPSwapFinder { 22 | fn user_in_out_index(ix_data: &[u8]) -> (usize, usize) { 23 | if ix_data[0] == 0xfa || ix_data[0] == 0x18 { 24 | // buy 25 | (6, 5) // quote, base 26 | } else { 27 | // sell 28 | (5, 6) // base, quote 29 | } 30 | } 31 | 32 | fn pool_in_out_index(ix_data: &[u8]) -> (usize, usize) { 33 | if ix_data[0] == 0xfa || ix_data[0] == 0x18 { 34 | // buy 35 | (7, 8) // base, quote 36 | } else { 37 | // sell 38 | (8, 7) // quote, base 39 | } 40 | } 41 | } 42 | 43 | impl SwapFinder for RaydiumLPSwapFinder { 44 | fn amm_ix(ix: &Instruction) -> Pubkey { 45 | ix.accounts[4].pubkey 46 | } 47 | 48 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 49 | account_keys[inner_ix.accounts[4] as usize] 50 | } 51 | 52 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 53 | let (in_index, out_index) = Self::user_in_out_index(&ix.data); 54 | ( 55 | ix.accounts[in_index].pubkey, 56 | ix.accounts[out_index].pubkey, 57 | ) 58 | } 59 | 60 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 61 | let (in_index, out_index) = Self::user_in_out_index(&inner_ix.data); 62 | ( 63 | account_keys[inner_ix.accounts[in_index] as usize], 64 | account_keys[inner_ix.accounts[out_index] as usize], 65 | ) 66 | } 67 | 68 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 69 | let (in_index, out_index) = Self::pool_in_out_index(&ix.data); 70 | ( 71 | ix.accounts[in_index].pubkey, 72 | ix.accounts[out_index].pubkey, 73 | ) 74 | } 75 | 76 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 77 | let (in_index, out_index) = Self::pool_in_out_index(&inner_ix.data); 78 | ( 79 | account_keys[inner_ix.accounts[in_index] as usize], 80 | account_keys[inner_ix.accounts[out_index] as usize], 81 | ) 82 | } 83 | 84 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 85 | [ 86 | // buy_exact_in 87 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_LP_PUBKEY, &[0xfa, 0xea, 0x0d, 0x7b, 0xd5, 0x9c, 0x13, 0xec], 0, 32), 88 | // sell_exact_in 89 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_LP_PUBKEY, &[0x95, 0x27, 0xde, 0x9b, 0xd3, 0x7c, 0x98, 0x1a], 0, 32), 90 | // buy_exact_out 91 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_LP_PUBKEY, &[0x18, 0xd3, 0x74, 0x28, 0x69, 0x03, 0x99, 0x38], 0, 32), 92 | // sell_exact_out 93 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &RAYDIUM_LP_PUBKEY, &[0x5f, 0xc8, 0x47, 0x22, 0x08, 0x09, 0x0b, 0xa6], 0, 32), 94 | ].concat() 95 | } 96 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/humidifi.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::HUMIDIFI_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for HumidiFiSwapFinder {} 7 | 8 | pub struct HumidiFiSwapFinder {} 9 | 10 | /// HumidiFi doesn't have any published IDL so it's guesswork from solscan/jup txs 11 | /// from multiple samples it appears base->quote swaps have 0x38 at [16] of the calldata, 0x39 for quote->base swaps 12 | /// all swaps seem to have 0xff2dffe0bae9c33d at [17..25] 13 | /// pool base/quote are at [2] and [3], user base/quote are at [4] and [5] 14 | 15 | impl HumidiFiSwapFinder { 16 | fn is_base_to_quote(data: &[u8]) -> bool { 17 | data[16] == 0x38 18 | } 19 | 20 | fn is_quote_to_base(data: &[u8]) -> bool { 21 | data[16] == 0x39 22 | } 23 | } 24 | 25 | impl SwapFinder for HumidiFiSwapFinder { 26 | fn amm_ix(ix: &Instruction) -> Pubkey { 27 | ix.accounts[1].pubkey 28 | } 29 | 30 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 31 | account_keys[inner_ix.accounts[1] as usize] 32 | } 33 | 34 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 35 | if Self::is_base_to_quote(&ix.data) { 36 | // base->quote 37 | ( 38 | ix.accounts[4].pubkey, // base 39 | ix.accounts[5].pubkey, // quote 40 | ) 41 | } else if Self::is_quote_to_base(&ix.data) { 42 | // quote->base 43 | ( 44 | ix.accounts[5].pubkey, // quote 45 | ix.accounts[4].pubkey, // base 46 | ) 47 | } else { 48 | panic!("Unknown HumidiFi swap direction"); 49 | } 50 | } 51 | 52 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 53 | if Self::is_base_to_quote(&inner_ix.data) { 54 | // base->quote 55 | ( 56 | account_keys[inner_ix.accounts[4] as usize], // base 57 | account_keys[inner_ix.accounts[5] as usize], // quote 58 | ) 59 | } else if Self::is_quote_to_base(&inner_ix.data) { 60 | // quote->base 61 | ( 62 | account_keys[inner_ix.accounts[5] as usize], // quote 63 | account_keys[inner_ix.accounts[4] as usize], // base 64 | ) 65 | } else { 66 | panic!("Unknown HumidiFi swap direction"); 67 | } 68 | } 69 | 70 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 71 | if Self::is_base_to_quote(&ix.data) { 72 | // base->quote 73 | ( 74 | ix.accounts[3].pubkey, // quote 75 | ix.accounts[2].pubkey, // base 76 | ) 77 | } else if Self::is_quote_to_base(&ix.data) { 78 | // quote->base 79 | ( 80 | ix.accounts[2].pubkey, // base 81 | ix.accounts[3].pubkey, // quote 82 | ) 83 | } else { 84 | panic!("Unknown HumidiFi swap direction"); 85 | } 86 | } 87 | 88 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 89 | if Self::is_base_to_quote(&inner_ix.data) { 90 | // base->quote 91 | ( 92 | account_keys[inner_ix.accounts[3] as usize], // quote 93 | account_keys[inner_ix.accounts[2] as usize], // base 94 | ) 95 | } else if Self::is_quote_to_base(&inner_ix.data) { 96 | // quote->base 97 | ( 98 | account_keys[inner_ix.accounts[2] as usize], // base 99 | account_keys[inner_ix.accounts[3] as usize], // quote 100 | ) 101 | } else { 102 | panic!("Unknown HumidiFi swap direction"); 103 | } 104 | } 105 | 106 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 107 | [ 108 | // swap 109 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &HUMIDIFI_PUBKEY, &[0xff, 0x2d, 0xff, 0xe0, 0xba, 0xe9, 0xc3, 0x3d], 17, 25), 110 | ].concat() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/jup_order_engine.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::JUP_ORDER_ENGINE_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for JupOrderEngineSwapFinder {} 7 | 8 | pub struct JupOrderEngineSwapFinder {} 9 | 10 | /// Jup order engine has one variant 11 | /// fill: [discriminant, input: u64, output: u64, expire_at: i64] 12 | /// Special care is required for swaps involving SOL since the "token account" is the program id 13 | /// Need to set that to the corresponding party 14 | /// Orders are created adhoc so there's no amm, we make one up from the traded mints [6, 8] with xor 15 | impl JupOrderEngineSwapFinder { 16 | fn keys(ix: &Instruction) -> Vec { 17 | let mut keys = vec![ 18 | // taker in/out 19 | ix.accounts[2].pubkey, 20 | ix.accounts[4].pubkey, 21 | // maker in/out 22 | ix.accounts[5].pubkey, 23 | ix.accounts[3].pubkey, 24 | ]; 25 | let taker = ix.accounts[0].pubkey; 26 | let maker = ix.accounts[1].pubkey; 27 | // if the taker is paying sol: it gets system-transfer'd to the taker's ata (only replace user input) 28 | // if the taker is receiving sol: the maker transfers the sol to a temp ata, then closed to the taker and system transfer'd (replace both) 29 | if keys[0] == JUP_ORDER_ENGINE_PUBKEY { 30 | keys[0] = taker; 31 | } 32 | if keys[1] == JUP_ORDER_ENGINE_PUBKEY { 33 | keys[1] = taker; 34 | keys[2] = maker; 35 | } 36 | keys.iter().enumerate().map(|(i,k)| if *k == JUP_ORDER_ENGINE_PUBKEY { if i < 2 { taker } else { maker } } else { *k }).collect::>() 37 | } 38 | 39 | fn keys_inner(inner_ix: &InnerInstruction, account_keys: &Vec) -> Vec { 40 | let mut keys = vec![ 41 | // taker in/out 42 | account_keys[inner_ix.accounts[2] as usize], 43 | account_keys[inner_ix.accounts[4] as usize], 44 | // maker in/out 45 | account_keys[inner_ix.accounts[5] as usize], 46 | account_keys[inner_ix.accounts[3] as usize], 47 | ]; 48 | let taker = account_keys[inner_ix.accounts[0] as usize]; 49 | let maker = account_keys[inner_ix.accounts[1] as usize]; 50 | if keys[0] == JUP_ORDER_ENGINE_PUBKEY { 51 | keys[0] = taker; 52 | } 53 | if keys[1] == JUP_ORDER_ENGINE_PUBKEY { 54 | keys[1] = taker; 55 | keys[2] = maker; 56 | } 57 | keys 58 | } 59 | } 60 | 61 | impl SwapFinder for JupOrderEngineSwapFinder { 62 | fn amm_ix(ix: &Instruction) -> Pubkey { 63 | let in_mint = ix.accounts[6].pubkey; 64 | let out_mint = ix.accounts[8].pubkey; 65 | in_mint.to_bytes().iter().zip(out_mint.to_bytes().iter()).map(|(a, b)| a ^ b).collect::>()[..].try_into().expect("wrong length") 66 | } 67 | 68 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 69 | let in_mint = account_keys[inner_ix.accounts[6] as usize]; 70 | let out_mint = account_keys[inner_ix.accounts[8] as usize]; 71 | in_mint.to_bytes().iter().zip(out_mint.to_bytes().iter()).map(|(a, b)| a ^ b).collect::>()[..].try_into().expect("wrong length") 72 | } 73 | 74 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 75 | let keys = Self::keys(ix); 76 | (keys[0], keys[1]) 77 | } 78 | 79 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 80 | let keys = Self::keys_inner(inner_ix, account_keys); 81 | (keys[0], keys[1]) 82 | } 83 | 84 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 85 | let keys = Self::keys(ix); 86 | (keys[2], keys[3]) 87 | } 88 | 89 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 90 | let keys = Self::keys_inner(inner_ix, account_keys); 91 | (keys[2], keys[3]) 92 | } 93 | 94 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 95 | [ 96 | // fill 97 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &JUP_ORDER_ENGINE_PUBKEY, &[0xa8, 0x60, 0xb7, 0xa3, 0x5c, 0x0a, 0x28, 0xa0], 0, 32), 98 | ].concat() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /sandwich-finder/src/detector.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::{HashMap, HashSet}, sync::Arc}; 2 | 3 | use mysql::{prelude::Queryable, Pool, Row}; 4 | use crate::events::{common::Timestamp, swap::SwapV2, transaction::TransactionV2, transfer::TransferV2}; 5 | 6 | pub const LEADER_GROUP_SIZE: u64 = 4; // slots per leader group 7 | 8 | pub async fn get_events(conn: Pool, start_slot: u64, end_slot: u64) -> (Vec, Vec, Vec) { 9 | let conn = &mut conn.get_conn().unwrap(); 10 | let res: Vec = conn.exec("select id, event_type, slot, inclusion_order, ix_index, inner_ix_index, authority, outer_program, program, amm, input_mint, output_mint, input_amount, output_amount, input_ata, output_ata, input_inner_ix_index, output_inner_ix_index from event_view where slot between ? and ?", vec![start_slot, end_slot]).unwrap(); 11 | let mut swaps = vec![]; 12 | let mut transfers = vec![]; 13 | let mut txs = vec![]; 14 | for row in res { 15 | let id: u64 = row.get("id").unwrap(); 16 | let event_type: Arc = row.get("event_type").unwrap(); 17 | let slot: u64 = row.get("slot").unwrap(); 18 | let inclusion_order: u32 = row.get("inclusion_order").unwrap(); 19 | let ix_index: u32 = row.get("ix_index").unwrap(); 20 | let inner_ix_index: Option = row.get("inner_ix_index").unwrap(); 21 | let authority: Arc = row.get("authority").unwrap(); 22 | let outer_program: Option> = row.get("outer_program").unwrap(); 23 | let program: Arc = row.get("program").unwrap(); 24 | let amm: Option> = row.get("amm").unwrap(); 25 | let input_mint: Arc = row.get("input_mint").unwrap(); 26 | let output_mint: Arc = row.get("output_mint").unwrap(); 27 | let input_amount: u64 = row.get("input_amount").unwrap(); 28 | let output_amount: u64 = row.get("output_amount").unwrap(); 29 | let input_ata: Arc = row.get("input_ata").unwrap(); 30 | let output_ata: Arc = row.get("output_ata").unwrap(); 31 | let input_inner_ix_index: Option = row.get("input_inner_ix_index").unwrap(); 32 | let output_inner_ix_index: Option = row.get("output_inner_ix_index").unwrap(); 33 | let inner_ix_index = inner_ix_index.filter(|&x| x >= 0).map(|x| x as u32); 34 | let input_inner_ix_index = input_inner_ix_index.filter(|&x| x >= 0).map(|x| x as u32); 35 | let output_inner_ix_index = output_inner_ix_index.filter(|&x| x >= 0).map(|x| x as u32); 36 | match event_type.as_ref() { 37 | "SWAP" => { 38 | swaps.push(SwapV2::new(outer_program, program, authority, amm.unwrap(), input_mint, output_mint, input_amount, output_amount, input_ata, output_ata, input_inner_ix_index, output_inner_ix_index, slot, inclusion_order, ix_index, inner_ix_index, id)); 39 | }, 40 | "TRANSFER" => { 41 | transfers.push(TransferV2::new(outer_program, program, authority, input_mint, input_amount, input_ata, output_ata, slot, inclusion_order, ix_index, inner_ix_index, id)); 42 | }, 43 | _ => {}, 44 | } 45 | } 46 | let res: Vec = conn.exec("select slot, inclusion_order, sig, fee, cu_actual, ifnull(dont_front, 0) as dont_front from transactions where slot between ? and ?", vec![start_slot, end_slot]).unwrap(); 47 | for row in res { 48 | let slot: u64 = row.get("slot").unwrap(); 49 | let inclusion_order: u32 = row.get("inclusion_order").unwrap(); 50 | let sig: String = row.get("sig").unwrap(); 51 | let fee: u64 = row.get("fee").unwrap(); 52 | let cu_actual: u64 = row.get("cu_actual").unwrap(); 53 | let dont_front: bool = row.get("dont_front").unwrap(); 54 | txs.push(TransactionV2::new(slot, inclusion_order, sig.into(), fee, cu_actual, dont_front)); 55 | } 56 | 57 | // Filter out swap leg transfers 58 | let mut transfer_map: HashMap = transfers.into_iter() 59 | .map(|t| (*t.timestamp(), t)) 60 | .collect(); 61 | for ele in swaps.iter() { 62 | if let Some(input_inner_ix) = ele.input_inner_ix_index() { 63 | transfer_map.remove(&Timestamp::new(*ele.slot(), *ele.inclusion_order(), *ele.ix_index(), Some(*input_inner_ix))); 64 | } 65 | if let Some(output_inner_ix) = ele.output_inner_ix_index() { 66 | transfer_map.remove(&Timestamp::new(*ele.slot(), *ele.inclusion_order(), *ele.ix_index(), Some(*output_inner_ix))); 67 | } 68 | } 69 | let transfers: Vec<_> = transfer_map.into_iter().map(|(_k, v)| v).collect(); 70 | 71 | // Filter out transfers from AMMs (gets rid of some noise from fees) 72 | let amms = swaps.iter().map(|s| s.amm()).collect::>(); 73 | let mut transfers: Vec = transfers.into_iter().filter(|t| !amms.contains(t.input_ata()) && !amms.contains(t.output_ata()) && !amms.contains(t.authority())).collect(); 74 | 75 | // Sort events in chronological order 76 | swaps.sort_by_cached_key(|s| *s.timestamp()); 77 | transfers.sort_by_cached_key(|t| *t.timestamp()); 78 | txs.sort_by_cached_key(|t| Timestamp::new(*t.slot(), *t.inclusion_order(), 0, None)); 79 | 80 | (swaps, transfers, txs) 81 | } 82 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/addresses.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | pub const RAYDIUM_V4_PUBKEY: Pubkey = Pubkey::from_str_const("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"); 4 | pub const RAYDIUM_V5_PUBKEY: Pubkey = Pubkey::from_str_const("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"); 5 | pub const RAYDIUM_LP_PUBKEY: Pubkey = Pubkey::from_str_const("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"); 6 | pub const RAYDIUM_CL_PUBKEY: Pubkey = Pubkey::from_str_const("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"); 7 | pub const PDF_PUBKEY: Pubkey = Pubkey::from_str_const("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); 8 | pub const PDF2_PUBKEY: Pubkey = Pubkey::from_str_const("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); 9 | pub const WHIRLPOOL_PUBKEY: Pubkey = Pubkey::from_str_const("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"); 10 | pub const METEORA_DLMM_PUBKEY: Pubkey = Pubkey::from_str_const("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo"); 11 | pub const METEORA_PUBKEY: Pubkey = Pubkey::from_str_const("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB"); 12 | pub const METEORA_DBC_PUBKEY: Pubkey = Pubkey::from_str_const("dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN"); 13 | pub const METEORA_DAMMV2_PUBKEY: Pubkey = Pubkey::from_str_const("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG"); 14 | pub const OPENBOOK_V2_PUBKEY: Pubkey = Pubkey::from_str_const("opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb"); 15 | pub const ZEROFI_PUBKEY: Pubkey = Pubkey::from_str_const("ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY"); 16 | pub const JUP_ORDER_ENGINE_PUBKEY: Pubkey = Pubkey::from_str_const("61DFfeTKM7trxYcPQCM78bJ794ddZprZpAwAnLiwTpYH"); 17 | pub const PANCAKE_SWAP_PUBKEY: Pubkey = Pubkey::from_str_const("HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq"); 18 | pub const FLUXBEAM_PUBKEY: Pubkey = Pubkey::from_str_const("FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X"); 19 | pub const HUMIDIFI_PUBKEY: Pubkey = Pubkey::from_str_const("9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp"); 20 | pub const SAROS_DLMM_PUBKEY: Pubkey = Pubkey::from_str_const("1qbkdrr3z4ryLA7pZykqxvxWPoeifcVKo6ZG9CfkvVE"); 21 | pub const SOLFI_PUBKEY: Pubkey = Pubkey::from_str_const("SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe"); 22 | pub const GOONFI_PUBKEY: Pubkey = Pubkey::from_str_const("goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j"); 23 | pub const SUGAR_PUBKEY: Pubkey = Pubkey::from_str_const("deus4Bvftd5QKcEkE5muQaWGWDoma8GrySvPFrBPjhS"); 24 | pub const TESS_V_PUBKEY: Pubkey = Pubkey::from_str_const("TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"); 25 | pub const SV2E_PUBKEY: Pubkey = Pubkey::from_str_const("SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF"); 26 | pub const LIFINITY_V2_PUBKEY: Pubkey = Pubkey::from_str_const("2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c"); 27 | pub const APESU_PUBKEY: Pubkey = Pubkey::from_str_const("5FyWAoG8V6hxgY6XM9hZStNxSW4D6mkv8HmYrxuPPDhv"); 28 | pub const ONEDEX_PUBKEY: Pubkey = Pubkey::from_str_const("DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm"); 29 | pub const AQUA_PUBKEY: Pubkey = Pubkey::from_str_const("AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45"); 30 | pub const STABBLE_WEIGHTED_PUBKEY: Pubkey = Pubkey::from_str_const("swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW"); 31 | pub const JUP_PERPS_PUBKEY: Pubkey = Pubkey::from_str_const("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); 32 | pub const DOOAR_PUBKEY: Pubkey = Pubkey::from_str_const("Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j"); 33 | pub const PUMPUP_PUBKEY: Pubkey = Pubkey::from_str_const("PdMDrKEMaX8q7CCJb7NvUCxerBCcsFUa4LjBEynTtEd"); 34 | pub const CLEARPOOL_PUBKEY: Pubkey = Pubkey::from_str_const("C1ear1po7kcLBZiiArGMXPhGnjRZ8KxkqQ8EEskzHWmc"); 35 | pub const FUSIONAMM_PUBKEY: Pubkey = Pubkey::from_str_const("fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9"); 36 | pub const ALPHA_PUBKEY: Pubkey = Pubkey::from_str_const("ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA"); 37 | pub const LIMO_PUBKEY: Pubkey = Pubkey::from_str_const("LiMoM9rMhrdYrfzUCxQppvxCSG1FcrUK9G8uLq4A1GF"); 38 | 39 | pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 40 | pub const TOKEN_2022_PROGRAM_ID: Pubkey = Pubkey::from_str_const("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); 41 | pub const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111"); 42 | pub const STAKE_PROGRAM_ID: Pubkey = Pubkey::from_str_const("Stake11111111111111111111111111111111111111"); 43 | pub const WSOL_MINT: Pubkey = Pubkey::from_str_const("So11111111111111111111111111111111111111112"); 44 | 45 | pub const JUP_V6_PROGRAM_ID: Pubkey = Pubkey::from_str_const("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"); 46 | pub const JUP_V4_PROGRAM_ID: Pubkey = Pubkey::from_str_const("JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB"); 47 | pub const DFLOW_PROGRAM_ID: Pubkey = Pubkey::from_str_const("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"); 48 | 49 | pub const DONT_FRONT_START: [u8; 32] = [10,241,195,67,33,136,202,58,99,81,53,161,58,24,149,26,206,189,41,230,172,45,174,103,255,219,6,215,64,0,0,0]; 50 | pub const DONT_FRONT_END: [u8; 32] = [10,241,195,67,33,136,202,58,99,82,11,83,236,186,243,27,60,23,98,46,152,130,58,175,28,197,174,53,128,0,0,0]; 51 | 52 | pub fn is_known_aggregator(program_id: &Pubkey) -> bool { 53 | matches!( 54 | *program_id, 55 | JUP_V6_PROGRAM_ID 56 | | JUP_V4_PROGRAM_ID 57 | | DFLOW_PROGRAM_ID 58 | ) 59 | } -------------------------------------------------------------------------------- /sandwich-finder/src/bin/detector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::{AtomicU64, Ordering}, Arc}; 2 | 3 | use sandwich_finder::{detector::{get_events, LEADER_GROUP_SIZE}, events::{common::Inserter, sandwich::detect}, utils::create_db_pool}; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio::task::JoinSet; 6 | 7 | const MAX_CHUNK_SIZE: u64 = 1000; // max slots to fetch at a time 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | struct GraphNode { 11 | id: String, 12 | label: String, 13 | #[serde(rename = "type")] 14 | node_type: String, // "token_account" or "market" 15 | value: Option, 16 | mint: Option, // For token accounts 17 | } 18 | 19 | #[derive(Debug, Serialize, Deserialize)] 20 | struct GraphEdge { 21 | source: String, 22 | target: String, 23 | label: String, 24 | amount: u64, 25 | timestamp: String, // Serialized timestamp for ordering 26 | order: usize, 27 | edge_type: String, // "swap" or "transfer" 28 | trading_pair: Option, // For swaps 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | struct TransferGraph { 33 | nodes: Vec, 34 | edges: Vec, 35 | slot: u64, 36 | } 37 | 38 | // Swap in slot 371237175 (order 1242, ix 1, inner_ix Some(1)) 39 | // Swap in slot 371237175 (order 1247, ix 5, inner_ix None) 40 | // Swap in slot 371237175 (order 1248, ix 2, inner_ix Some(0)) 41 | 42 | #[tokio::main] 43 | async fn main() { 44 | dotenv::dotenv().ok(); 45 | // let slot = 371237175; 46 | // parse the 1st arg for slot 47 | let args: Vec = std::env::args().collect(); 48 | if args.len() < 2 { 49 | println!("Usage: {} ", args[0]); 50 | return; 51 | } 52 | let start_slot: u64 = args[1].parse().expect("Invalid slot"); 53 | let end_slot: u64 = if args.len() >= 3 { 54 | args[2].parse().expect("Invalid slot") 55 | } else { 56 | start_slot 57 | }; 58 | // alignment 59 | let start_slot = start_slot / LEADER_GROUP_SIZE * LEADER_GROUP_SIZE; 60 | let end_slot = end_slot / LEADER_GROUP_SIZE * LEADER_GROUP_SIZE + LEADER_GROUP_SIZE - 1; 61 | // fetch events for up to 1k slots at a time and process in groups of 4 slots 62 | let pool = create_db_pool(); 63 | let inserter = Inserter::new(pool.clone()); 64 | let chunk_size = ((end_slot - start_slot + 1) / 16).min(MAX_CHUNK_SIZE - LEADER_GROUP_SIZE) / LEADER_GROUP_SIZE * LEADER_GROUP_SIZE + LEADER_GROUP_SIZE; 65 | println!("Processing slots {} to {} ({} leader groups)", start_slot, end_slot, (end_slot - start_slot + 1) / LEADER_GROUP_SIZE); 66 | let progress = Arc::from(AtomicU64::new(0)); 67 | let mut set = JoinSet::new(); 68 | for chunk_start in (start_slot..=end_slot).step_by(chunk_size as usize) { 69 | let chunk_end = (chunk_start + chunk_size - 1).min(end_slot); 70 | let pool = pool.clone(); // docs said this is cloneable 71 | let mut inserter = inserter.clone(); 72 | let progress = progress.clone(); 73 | set.spawn(async move { 74 | println!("Fetching events for slots {} to {}", chunk_start, chunk_end); 75 | let (swaps, transfers, txs) = get_events(pool.clone(), chunk_start, chunk_end).await; 76 | let mut swaps_start = 0; 77 | let mut transfers_start = 0; 78 | let mut txs_start = 0; 79 | for slot in (chunk_start..=chunk_end).step_by(LEADER_GROUP_SIZE as usize) { 80 | let swaps_end = swaps.iter().skip(swaps_start).position(|s| *s.slot() >= slot + LEADER_GROUP_SIZE).map(|n| n + swaps_start).unwrap_or(swaps.len()); 81 | let transfers_end = transfers.iter().skip(transfers_start).position(|t| *t.slot() >= slot + LEADER_GROUP_SIZE).map(|n| n + transfers_start).unwrap_or(transfers.len()); 82 | let txs_end = txs.iter().skip(txs_start).position(|t| *t.slot() >= slot + LEADER_GROUP_SIZE).map(|n| n + txs_start).unwrap_or(txs.len()); 83 | 84 | let slot_swaps = &swaps[swaps_start..swaps_end]; 85 | let slot_transfers = &transfers[transfers_start..transfers_end]; 86 | let slot_txs = &txs[txs_start..txs_end]; 87 | println!("Processing slots {} to {}", slot, slot + LEADER_GROUP_SIZE - 1); 88 | // println!("Swaps: {:#?}", slot_swaps.len()); 89 | // println!("Transfers: {:#?}", slot_transfers.len()); 90 | // println!("Txs: {:#?}", slot_txs.len()); 91 | let sandwiches = detect(slot_swaps, slot_transfers, slot_txs); 92 | // for sandwich in sandwiches.iter() { 93 | // println!("Detected sandwich: {:#?}", sandwich); 94 | // } 95 | inserter.insert_sandwiches(slot, sandwiches).await; 96 | 97 | swaps_start = swaps_end; 98 | transfers_start = transfers_end; 99 | txs_start = txs_end; 100 | let completed = progress.fetch_add(1, Ordering::AcqRel); 101 | // if completed % 100 == 0 { 102 | println!("{}/{}", completed, (end_slot - start_slot + 1) / LEADER_GROUP_SIZE); 103 | // } 104 | } 105 | }); 106 | if set.len() >= 16 { 107 | set.join_next().await; 108 | } 109 | } 110 | set.join_all().await; 111 | } 112 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/discoverer.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, sync::Arc}; 2 | 3 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 4 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 5 | 6 | use crate::events::{swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, utils::token_transferred_inner}}; 7 | 8 | const BLACKLISTED_COMBINATIONS: &[(Pubkey, &[u8], usize)] = &[ // program, discriminant, offset 9 | (Pubkey::from_str_const("DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei"), &[], 0), // appears to be something that does smth with the audio token 10 | (Pubkey::from_str_const("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"), &[], 0), // metaplex 11 | (Pubkey::from_str_const("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB"), &[0xa9, 0x20, 0x4f, 0x89, 0x88, 0xe8, 0x46, 0x89], 0), // meteora claim fees 12 | (Pubkey::from_str_const("dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN"), &[0x9c, 0xa9, 0xe6, 0x67, 0x35, 0xe4, 0x50, 0x40], 0), // dbc migrate 13 | (Pubkey::from_str_const("mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc"), &[], 0), // metaplex mmm, nft trades that we aren't interested in 14 | (Pubkey::from_str_const("M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K"), &[], 0), // magic eden 15 | (Pubkey::from_str_const("APR1MEny25pKupwn72oVqMH4qpDouArsX8zX4VwwfoXD"), &[], 0), // star atlas stuff 16 | (Pubkey::from_str_const("SAGE2HAwep459SNq61LHvjxPk4pLPEJLoMETef7f7EE"), &[], 0), // star atlas stuff 17 | (Pubkey::from_str_const("Cargo2VNTPPTi9c1vq1Jw5d3BWUNr18MjRtSupAghKEk"), &[], 0), // star atlas stuff 18 | ]; 19 | 20 | impl Sealed for Discoverer {} 21 | 22 | pub struct Discoverer {} 23 | 24 | /// Outputs txid and program that triggered >=2 swaps in its inner instructions and emit a special swap event. 25 | impl SwapFinder for Discoverer { 26 | fn amm_ix(_ix: &Instruction) -> Pubkey { 27 | Pubkey::default() 28 | } 29 | 30 | fn amm_inner_ix(_inner_ix: &InnerInstruction, _account_keys: &Vec) -> Pubkey { 31 | Pubkey::default() 32 | } 33 | 34 | fn user_ata_ix(_ix: &Instruction) -> (Pubkey, Pubkey) { 35 | ( 36 | Pubkey::default(), 37 | Pubkey::default(), 38 | ) 39 | } 40 | 41 | fn user_ata_inner_ix(_inner_ix: &InnerInstruction, _account_keys: &Vec) -> (Pubkey, Pubkey) { 42 | ( 43 | Pubkey::default(), 44 | Pubkey::default(), 45 | ) 46 | } 47 | 48 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 49 | // ignore known programs 50 | match ix.program_id { 51 | // RAYDIUM_V4_PUBKEY | RAYDIUM_V5_PUBKEY | RAYDIUM_LP_PUBKEY | RAYDIUM_CL_PUBKEY | PDF_PUBKEY | PDF2_PUBKEY | WHIRLPOOL_PUBKEY | DLMM_PUBKEY | METEORA_PUBKEY => vec![], 52 | _ => { 53 | let mut transfer_count = 0; 54 | let mut authorities = HashSet::new(); 55 | let mut mints = HashSet::new(); 56 | for comb in BLACKLISTED_COMBINATIONS { 57 | if ix.program_id == comb.0 { 58 | if ix.data.len() >= comb.2 + comb.1.len() { 59 | if &ix.data[comb.2..comb.2 + comb.1.len()] == comb.1 { 60 | return vec![]; 61 | } 62 | } 63 | } 64 | } 65 | for inner_ix in &inner_ixs.instructions { 66 | if let Some((_from, _to, _auth, mint, _amount)) = token_transferred_inner(&inner_ix, &account_keys, &meta) { 67 | transfer_count += 1; 68 | match inner_ix.data[0] { 69 | 2 => { // System transfer 70 | if inner_ix.accounts.len() >= 1 { 71 | let authority = account_keys[inner_ix.accounts[0] as usize]; 72 | authorities.insert(authority); 73 | } 74 | }, 75 | 3 => { // Transfer 76 | if inner_ix.accounts.len() >= 3 { 77 | let authority = account_keys[inner_ix.accounts[2] as usize]; 78 | authorities.insert(authority); 79 | } 80 | }, 81 | 12 => { // TransferChecked 82 | if inner_ix.accounts.len() >= 4 { 83 | let authority = account_keys[inner_ix.accounts[3] as usize]; 84 | authorities.insert(authority); 85 | } 86 | }, 87 | _ => {} 88 | } 89 | mints.insert(mint); 90 | } 91 | } 92 | if transfer_count >= 2 && authorities.len() >= 2 && mints.len() >= 2 { 93 | let empty_str: Arc = Arc::from(""); 94 | return vec![ 95 | SwapV2::new(None, ix.program_id.to_string().into(), empty_str.clone(), empty_str.clone(), empty_str.clone(), empty_str.clone(), 0, 0, empty_str.clone(), empty_str, None, None, 0, 0, 0, None, 0), 96 | ]; 97 | } 98 | vec![] 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /sandwich-finder/src/bin/populate-profits.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use mysql::{prelude::Queryable, Pool}; 4 | 5 | const WSOL_MINT: &str = "So11111111111111111111111111111111111111112"; 6 | // const DEBUG_SANDWICH_ID: u64 = 0; 7 | 8 | fn est_val(amt: u128, n: u128, d: u128) -> u64 { 9 | if d == 0 { 10 | return 0; 11 | } 12 | // (amt as u128 * n as u128 / d as u128) as u64 13 | 0 14 | } 15 | 16 | fn calc_est_profit(fr_in: u64, fr_out: u64, br_in: u64, br_out: u64, t1_total: u64, t2_total: u64, min_order: u64, max_order: u64, size: u64, t1_mint: &Option, t2_mint: &Option, debug: bool) -> u64 { 17 | // sol_profit + token_profit * sol_per_token 18 | let t1_diff = br_out - fr_in; 19 | let t2_diff = fr_out - br_in; 20 | if debug { 21 | println!("frontrun {fr_in} -> {fr_out}"); 22 | println!("backrun {br_in} -> {br_out}"); 23 | println!("diff {t1_diff} / {t2_diff}"); 24 | println!("total {t1_total} / {t2_total}"); 25 | println!("direction: {t1_mint:?} -> {t2_mint:?}"); 26 | println!("order in block: {min_order} - {max_order} #{size}"); 27 | } 28 | // if max_order - min_order > 2 * size { // the ingredients are spread throughout the block, maybe false +ve 29 | // return 0; 30 | // } 31 | if let Some(t1_mint) = t1_mint { 32 | if t1_mint == WSOL_MINT { 33 | let est_profit = t1_diff + est_val(t2_diff as u128, t1_total as u128, t2_total as u128); 34 | if debug {println!("t1 est profit {}", est_profit);} 35 | return est_profit; 36 | } 37 | } 38 | if let Some(t2_mint) = t2_mint { 39 | if t2_mint == WSOL_MINT { 40 | let est_profit = t2_diff + est_val(t1_diff as u128, t2_total as u128, t1_total as u128); 41 | if debug {println!("t2 est profit {}", est_profit);} 42 | return est_profit; 43 | } 44 | } 45 | return 0; 46 | } 47 | 48 | fn main() { 49 | dotenv::dotenv().ok(); 50 | let args: Vec = env::args().collect(); 51 | let debug_sandwich_id: u64 = if args.len() > 1 { 52 | args[1].parse().unwrap_or(0) 53 | } else { 54 | 0 55 | }; 56 | let mysql_url = env::var("MYSQL").unwrap(); 57 | let pool = Pool::new(mysql_url.as_str()).unwrap(); 58 | let mut conn: mysql::PooledConn = pool.get_conn().unwrap(); 59 | let stmt = conn.prep("SELECT ifnull(max(id), 0) FROM `sandwich` where est_profit_lamports>0").unwrap(); 60 | let max_id: u64 = conn.exec_first(&stmt, ()).unwrap().unwrap_or(0); 61 | let op = if debug_sandwich_id > 0 { "=" } else { ">=" }; 62 | let stmt = conn.prep(format!("SELECT sandwich_id, order_in_block, input_mint, input_amount, output_mint, output_amount, swap_type from sandwich_view where sandwich_id {} ? order by sandwich_id asc", op)).unwrap(); 63 | 64 | let mut update_conn = pool.get_conn().unwrap(); 65 | let update_stmt = update_conn.prep("UPDATE sandwich SET est_profit_lamports=? WHERE id=?").unwrap(); 66 | 67 | let mut t1_total: u64 = 0; 68 | let mut t2_total: u64 = 0; 69 | let mut t1_mint: Option = None; 70 | let mut t2_mint: Option = None; 71 | 72 | let mut fr_in: u64 = 0; 73 | let mut fr_out: u64 = 0; 74 | let mut br_in: u64 = 0; 75 | let mut br_out: u64 = 0; 76 | 77 | let mut max_order: u64 = 0; 78 | let mut min_order: u64 = 99999999; 79 | let mut size: u64 = 0; 80 | 81 | let mut cur_id = if debug_sandwich_id > 0 { debug_sandwich_id } else { max_id + 1 }; 82 | conn.exec_map(&stmt, (cur_id,), |(sandwich_id, order_in_block, input_mint, input_amount, output_mint, output_amount, swap_type): (u64, u64, String, u64, String, u64, String)| { 83 | if sandwich_id != cur_id { 84 | let est_profit = calc_est_profit(fr_in, fr_out, br_in, br_out, t1_total, t2_total, min_order, max_order, size, &t1_mint, &t2_mint, debug_sandwich_id > 0); 85 | println!("sandwich_id: {cur_id} est_profit: {est_profit}"); 86 | if est_profit > 0 && est_profit < 1000_000_000_000 && debug_sandwich_id == 0 { 87 | update_conn.exec_drop(&update_stmt, (est_profit, cur_id)).unwrap(); 88 | } 89 | // reset vars 90 | t1_total = 0; 91 | t2_total = 0; 92 | t1_mint = None; 93 | t2_mint = None; 94 | fr_in = 0; 95 | fr_out = 0; 96 | br_in = 0; 97 | br_out = 0; 98 | max_order = 0; 99 | min_order = 99999999; 100 | size = 0; 101 | cur_id = sandwich_id; 102 | } 103 | if t1_mint.is_none() { 104 | if swap_type == "BACKRUN" { 105 | t2_mint = Some(input_mint.clone()); 106 | t1_mint = Some(output_mint.clone()); 107 | } else { 108 | t1_mint = Some(input_mint.clone()); 109 | t2_mint = Some(output_mint.clone()); 110 | } 111 | } 112 | match swap_type.as_str() { 113 | "FRONTRUN" => { 114 | fr_in += input_amount; 115 | fr_out += output_amount; 116 | t1_total += input_amount; 117 | t2_total += output_amount; 118 | } 119 | "BACKRUN" => { 120 | br_in += input_amount; 121 | br_out += output_amount; 122 | // t1_total -= output_amount; 123 | // t2_total -= input_amount; 124 | } 125 | "VICTIM" => { 126 | // t1_total += input_amount; 127 | // t2_total += output_amount; 128 | } 129 | _ => { 130 | panic!("Unknown swap type: {}", swap_type); 131 | } 132 | } 133 | max_order = max_order.max(order_in_block); 134 | min_order = min_order.min(order_in_block); 135 | size += 1; 136 | }).unwrap(); 137 | let est_profit = calc_est_profit(fr_in, fr_out, br_in, br_out, t1_total, t2_total, min_order, max_order, size, &t1_mint, &t2_mint, debug_sandwich_id > 0); 138 | println!("sandwich_id: {cur_id} est_profit: {est_profit}"); 139 | if est_profit > 0 && est_profit < 1000_000_000_000 && debug_sandwich_id == 0 { 140 | update_conn.exec_drop(&update_stmt, (est_profit, cur_id)).unwrap(); 141 | } 142 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/pumpup.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 4 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 5 | 6 | use crate::{events::{addresses::PUMPUP_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::private::Sealed}, utils::pubkey_from_slice}; 7 | 8 | impl Sealed for PumpupSwapFinder {} 9 | 10 | pub struct PumpupSwapFinder {} 11 | 12 | // Includes both the ix and event discrimant 13 | const LOG_DISCRIMINANT: &[u8] = &[ 14 | 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 15 | 0xa3, 0x26, 0x5b, 0x65, 0x78, 0x94, 0x97, 0x5a, 16 | ]; 17 | const SWAP: &[u8] = &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; 18 | 19 | /// pdmd appears to have 1 variant 20 | impl PumpupSwapFinder { 21 | fn swap_from_pdf_trade_event(outer_program: Option>, amm: Pubkey, input_ata: Pubkey, output_ata: Pubkey, data: &[u8], inner_ix_index: Option) -> SwapV2 { 22 | let input_mint = pubkey_from_slice(&data[89..121]); 23 | let input_amount = u64::from_le_bytes(data[121..129].try_into().unwrap()); 24 | let output_mint = pubkey_from_slice(&data[49..81]); 25 | let output_amount = u64::from_le_bytes(data[81..89].try_into().unwrap()); 26 | SwapV2::new( 27 | outer_program, 28 | PUMPUP_PUBKEY.to_string().into(), 29 | pubkey_from_slice(&data[137..169]).to_string().into(), 30 | amm.to_string().into(), 31 | input_mint.to_string().into(), 32 | output_mint.to_string().into(), 33 | input_amount, 34 | output_amount, 35 | input_ata.to_string().into(), 36 | output_ata.to_string().into(), 37 | // todo: should try to locate the actual ix 38 | None, 39 | None, 40 | 0, 41 | 0, 42 | 0, 43 | inner_ix_index, 44 | 0, 45 | ) 46 | } 47 | } 48 | 49 | impl SwapFinder for PumpupSwapFinder { 50 | fn amm_ix(ix: &Instruction) -> Pubkey { 51 | ix.accounts[0].pubkey 52 | } 53 | 54 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 55 | account_keys[inner_ix.accounts[0] as usize] 56 | } 57 | 58 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 59 | ( 60 | ix.accounts[3].pubkey, 61 | ix.accounts[4].pubkey, 62 | ) 63 | } 64 | 65 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 66 | ( 67 | account_keys[inner_ix.accounts[3] as usize], 68 | account_keys[inner_ix.accounts[4] as usize], 69 | ) 70 | } 71 | 72 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 73 | ( 74 | ix.accounts[2].pubkey, 75 | ix.accounts[1].pubkey, 76 | ) 77 | } 78 | 79 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 80 | ( 81 | account_keys[inner_ix.accounts[2] as usize], 82 | account_keys[inner_ix.accounts[1] as usize], 83 | ) 84 | } 85 | 86 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, _meta: &TransactionStatusMeta) -> Vec { 87 | if ix.program_id == PUMPUP_PUBKEY { 88 | for inner_ix in inner_ixs.instructions.iter() { 89 | if inner_ix.data.len() >= 193 && inner_ix.data[0..16] == LOG_DISCRIMINANT[..] { 90 | return vec![ 91 | Self::swap_from_pdf_trade_event( 92 | None, 93 | ix.accounts[0].pubkey, 94 | ix.accounts[3].pubkey, 95 | ix.accounts[4].pubkey, 96 | &inner_ix.data, 97 | None, 98 | ) 99 | ]; 100 | } 101 | } 102 | } 103 | let mut swaps = vec![]; 104 | let mut next_logical_ix = 0; 105 | for (i, inner_ix) in inner_ixs.instructions.iter().enumerate() { 106 | if inner_ix.program_id_index >= account_keys.len() as u32 || i < next_logical_ix { 107 | continue; // Skip already processed instructions or invalid program ID 108 | } 109 | if account_keys[inner_ix.program_id_index as usize] != PUMPUP_PUBKEY { 110 | continue; // Not a sugar instruction 111 | } 112 | if inner_ix.data.len() < 24 { 113 | continue; // Not a swap 114 | } 115 | match &inner_ix.data[..8] { 116 | SWAP => { 117 | // Valid swap instruction 118 | let (input_ata, output_ata) = Self::user_ata_inner_ix(inner_ix, account_keys); 119 | for j in i + 1..inner_ixs.instructions.len() { 120 | let next_inner_ix = &inner_ixs.instructions[j]; 121 | if next_inner_ix.program_id_index >= account_keys.len() as u32 { 122 | continue; // Skip invalid program ID 123 | } 124 | if account_keys[next_inner_ix.program_id_index as usize] != PUMPUP_PUBKEY { 125 | continue; // Not a Pump.fun instruction 126 | } 127 | if next_inner_ix.data.len() < 193 || next_inner_ix.data[0..16] != LOG_DISCRIMINANT[..] { 128 | continue; // Not an event 129 | } 130 | swaps.push(Self::swap_from_pdf_trade_event( 131 | Some(ix.program_id.to_string().into()), 132 | Self::amm_inner_ix(inner_ix, account_keys), 133 | input_ata, 134 | output_ata, 135 | &next_inner_ix.data, 136 | Some(i as u32), 137 | )); 138 | next_logical_ix = j + 1; 139 | } 140 | }, 141 | _ => continue, // Not a swap 142 | 143 | } 144 | } 145 | swaps 146 | } 147 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/transfers/token.rs: -------------------------------------------------------------------------------- 1 | use std::u64; 2 | 3 | use solana_sdk::{instruction::Instruction, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey}; 4 | use yellowstone_grpc_proto::prelude::{InnerInstructions, TransactionStatusMeta}; 5 | 6 | use crate::events::{addresses::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID}, swaps::utils::mint_of, transfer::{TransferFinder, TransferV2}, transfers::private::Sealed}; 7 | 8 | impl Sealed for TokenProgramTransferFinder {} 9 | pub struct TokenProgramTransferFinder {} 10 | 11 | impl TokenProgramTransferFinder { 12 | fn is_token_program(program_id: Pubkey) -> bool { 13 | program_id == TOKEN_PROGRAM_ID || program_id == TOKEN_2022_PROGRAM_ID 14 | } 15 | 16 | fn amount_from_data(data: &[u8]) -> Option { 17 | match data[0] { 18 | 3 => Some(u64::from_le_bytes(data[1..9].try_into().unwrap())), // Transfer 19 | 7 => Some(u64::from_le_bytes(data[1..9].try_into().unwrap())), // MintTo 20 | 9 => Some(1_000_000_000 * LAMPORTS_PER_SOL), // CloseAccount, amount is not specified unless we replay the entire tx 21 | 12 => Some(u64::from_le_bytes(data[1..9].try_into().unwrap())), // TransferChecked 22 | 14 => Some(u64::from_le_bytes(data[1..9].try_into().unwrap())), // MintToChecked 23 | _ => return None, // Not something that resembles a transfer 24 | } 25 | } 26 | 27 | /// Returns (from_index, to_index, auth_index) 28 | fn from_to_indexs(data: &[u8]) -> Option<(usize, usize, usize)> { 29 | match data[0] { 30 | 3 => Some((0, 1, 2)), // Transfer 31 | 7 => Some((0, 1, 2)), // MintTo, tokens are minted so we specify the mint as the "from" 32 | 9 => Some((0, 1, 2)), // CloseAccount 33 | 12 => Some((0, 2, 3)), // TransferChecked 34 | 14 => Some((0, 1, 2)), // MintToChecked 35 | _ => None, // Not a transfer 36 | } 37 | } 38 | } 39 | 40 | impl TransferFinder for TokenProgramTransferFinder { 41 | fn find_transfers(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 42 | if Self::is_token_program(ix.program_id) { 43 | if let Some(amount) = Self::amount_from_data(&ix.data) { 44 | if let Some((from_index, to_index, auth_index)) = Self::from_to_indexs(&ix.data) { 45 | if from_index < ix.accounts.len() && to_index < ix.accounts.len() { 46 | let from_ata = ix.accounts[from_index].pubkey; 47 | let to_ata = ix.accounts[to_index].pubkey; 48 | if from_ata == to_ata { 49 | // Don't log self transfers 50 | return vec![]; 51 | } 52 | let auth = ix.accounts[auth_index].pubkey; 53 | let mint = mint_of(&from_ata, account_keys, meta) 54 | .or_else(|| mint_of(&to_ata, account_keys, meta)); 55 | if let Some(mint) = mint { 56 | return vec![TransferV2::new( 57 | None, 58 | ix.program_id.to_string().into(), 59 | auth.to_string().into(), 60 | mint.into(), 61 | amount, 62 | from_ata.to_string().into(), 63 | to_ata.to_string().into(), 64 | 0, // slot to be filled later 65 | 0, // inclusion_order to be filled later 66 | 0, // ix_index to be filled later 67 | None, 68 | 0, 69 | )]; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | let mut transfers = vec![]; 76 | inner_ixs.instructions.iter().enumerate().for_each(|(i, inner_ix)| { 77 | if inner_ix.program_id_index as usize >= account_keys.len() { 78 | return; 79 | } 80 | if !Self::is_token_program(account_keys[inner_ix.program_id_index as usize]) { 81 | return; 82 | } 83 | if let Some(amount) = Self::amount_from_data(&inner_ix.data) { 84 | if let Some((from_index, to_index, auth_index)) = Self::from_to_indexs(&inner_ix.data) { 85 | if from_index < inner_ix.accounts.len() && to_index < inner_ix.accounts.len() { 86 | let from_ata = inner_ix.accounts[from_index] as usize; 87 | let to_ata = inner_ix.accounts[to_index] as usize; 88 | if from_ata >= account_keys.len() || to_ata >= account_keys.len() { 89 | return; 90 | } 91 | if from_ata == to_ata { 92 | // Don't log self transfers 93 | return; 94 | } 95 | let auth = inner_ix.accounts[auth_index] as usize; 96 | if auth >= account_keys.len() { 97 | return; 98 | } 99 | let from_ata_pubkey = account_keys[from_ata]; 100 | let to_ata_pubkey = account_keys[to_ata]; 101 | let auth_pubkey = account_keys[auth]; 102 | let mint = mint_of(&from_ata_pubkey, account_keys, meta) 103 | .or_else(|| mint_of(&to_ata_pubkey, account_keys, meta)); 104 | if let Some(mint) = mint { 105 | transfers.push(TransferV2::new( 106 | Some(ix.program_id.to_string().into()), 107 | account_keys[inner_ix.program_id_index as usize].to_string().into(), 108 | auth_pubkey.to_string().into(), 109 | mint.into(), 110 | amount, 111 | from_ata_pubkey.to_string().into(), 112 | to_ata_pubkey.to_string().into(), 113 | 0, // slot to be filled later 114 | 0, // inclusion_order to be filled later 115 | 0, // ix_index to be filled later 116 | Some(i as u32), 117 | 0, 118 | )); 119 | } 120 | } 121 | } 122 | } 123 | }); 124 | transfers 125 | } 126 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swap.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use derive_getters::Getters; 4 | use serde::Serialize; 5 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 6 | use yellowstone_grpc_proto::{prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}}; 7 | 8 | use crate::events::common::Timestamp; 9 | 10 | #[derive(Clone, Serialize, Getters)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct SwapV2 { 13 | // The wrapper program for this swap, if any 14 | outer_program: Option>, 15 | // The actual AMM program 16 | program: Arc, 17 | // Wallet that authorised the swap 18 | authority: Arc, 19 | // The AMM used for this trade 20 | amm: Arc, 21 | // In/out mints of the swap 22 | input_mint: Arc, 23 | output_mint: Arc, 24 | // In/out amounts of the swap 25 | input_amount: u64, 26 | output_amount: u64, 27 | // In/out token accounts 28 | input_ata: Arc, 29 | output_ata: Arc, 30 | // In/out inner ix indexes 31 | input_inner_ix_index: Option, 32 | output_inner_ix_index: Option, 33 | // These fields are meant to be replaced when inserting to the db 34 | timestamp: Timestamp, 35 | id: u64, 36 | } 37 | 38 | impl SwapV2 { 39 | pub fn new( 40 | outer_program: Option>, 41 | program: Arc, 42 | authority: Arc, 43 | amm: Arc, 44 | input_mint: Arc, 45 | output_mint: Arc, 46 | input_amount: u64, 47 | output_amount: u64, 48 | input_ata: Arc, 49 | output_ata: Arc, 50 | input_inner_ix_index: Option, 51 | output_inner_ix_index: Option, 52 | slot: u64, 53 | inclusion_order: u32, 54 | ix_index: u32, 55 | inner_ix_index: Option, 56 | id: u64, 57 | ) -> Self { 58 | Self { 59 | outer_program, 60 | program, 61 | authority, 62 | amm, 63 | input_mint, 64 | output_mint, 65 | input_amount, 66 | output_amount, 67 | input_ata, 68 | output_ata, 69 | input_inner_ix_index, 70 | output_inner_ix_index, 71 | timestamp: Timestamp::new( 72 | slot, 73 | inclusion_order, 74 | ix_index, 75 | inner_ix_index, 76 | ), 77 | id, 78 | } 79 | } 80 | 81 | pub fn slot(&self) -> &u64 { 82 | self.timestamp.slot() 83 | } 84 | pub fn inclusion_order(&self) -> &u32 { 85 | self.timestamp.inclusion_order() 86 | } 87 | pub fn ix_index(&self) -> &u32 { 88 | self.timestamp.ix_index() 89 | } 90 | pub fn inner_ix_index(&self) -> &Option { 91 | self.timestamp.inner_ix_index() 92 | } 93 | } 94 | 95 | impl Debug for SwapV2 { 96 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 97 | // f.debug_struct("SwapV2").field("outer_program", &self.outer_program).field("program", &self.program).field("amm", &self.amm).field("input_mint", &self.input_mint).field("output_mint", &self.output_mint).field("input_amount", &self.input_amount).field("output_amount", &self.output_amount).field("input_ata", &self.input_ata).field("output_ata", &self.output_ata).field("sig_id", &self.sig_id).field("slot", &self.slot).field("inclusion_order", &self.inclusion_order).field("ix_index", &self.ix_index).field("inner_ix_index", &self.inner_ix_index).finish() 98 | f.write_str("Swap")?; 99 | if self.id != 0 { 100 | f.write_str(&format!(" #{}", self.id))?; 101 | } 102 | f.write_str(&format!(" in slot {} (order {}, ix {}, inner_ix {:?})\n", self.slot(), self.inclusion_order(), self.ix_index(), self.inner_ix_index()))?; 103 | if let Some(outer_program) = &self.outer_program { 104 | f.write_str(&format!(" via {}\n", outer_program))?; 105 | } 106 | f.write_str(&format!(" on {} market {}\n", self.program, self.amm))?; 107 | f.write_str(&format!(" Route {} -> {}", self.input_mint, self.output_mint))?; 108 | f.write_str(&format!(" Amounts {} -> {}\n", self.input_amount, self.output_amount))?; 109 | f.write_str(&format!(" ATAs {} -> {}", self.input_ata, self.output_ata))?; 110 | Ok(()) 111 | } 112 | } 113 | 114 | pub trait SwapFinder { 115 | /// Returns the swaps utilising a program found in the given instruction and inner instructions. 116 | /// A swap involves an inner instruction that the user's out ATA sends tokens to the pool's in ATA, 117 | /// and one that the pool's out ATA sends tokens to the user's in ATA. 118 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec; 119 | 120 | /// Returns the AMM address for the swap instruction. The instruction will have matching program ID, discriminant and enough instruction data. 121 | fn amm_ix(ix: &Instruction) -> Pubkey; 122 | /// Like [`SwapFinder::amm_ix`], but takes an inner instruction and the account keys vector for key resolution. 123 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey; 124 | 125 | /// Returns the user's in/out ATAs involved in the swap, in that order. The instruction follows the same constraints as above. 126 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey); 127 | /// Like [`SwapFinder::user_ata_ix`], but takes an inner instruction and the account keys vector for key resolution. 128 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey); 129 | 130 | /// Returns the pool's in/out ATAs involved in the swap, in that order. The instruction follows the same constraints as above. 131 | /// Can return [`Pubkey::default()`] to bypass this check. 132 | fn pool_ata_ix(_ix: &Instruction) -> (Pubkey, Pubkey) { 133 | return ( 134 | Pubkey::default(), 135 | Pubkey::default(), 136 | ); 137 | } 138 | /// Like [`SwapFinder::pool_ata_ix`], but takes an inner instruction and the account keys vector for key resolution. 139 | fn pool_ata_inner_ix(_inner_ix: &InnerInstruction, _account_keys: &Vec) -> (Pubkey, Pubkey) { 140 | return ( 141 | Pubkey::default(), 142 | Pubkey::default(), 143 | ); 144 | } 145 | 146 | /// Number of inner instructions to skip before the actual relevant transfers. 147 | fn ixs_to_skip() -> usize { 148 | 0 149 | } 150 | 151 | /// The indexes of the accounts that definitely won't be involved in the swap, such as referral/fee accounts. 152 | fn blacklist_ata_indexs() -> Vec { 153 | vec![] 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /sandwich.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 5.2.1deb3 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: localhost:3306 6 | -- Generation Time: Feb 05, 2025 at 04:55 AM 7 | -- Server version: 10.11.8-MariaDB-0ubuntu0.24.04.1 8 | -- PHP Version: 8.3.6 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | START TRANSACTION; 12 | SET time_zone = "+00:00"; 13 | 14 | 15 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 16 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 17 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 18 | /*!40101 SET NAMES utf8mb4 */; 19 | 20 | -- 21 | -- Database: `sandwich` 22 | -- 23 | 24 | -- -------------------------------------------------------- 25 | 26 | -- 27 | -- Table structure for table `block` 28 | -- 29 | 30 | CREATE TABLE `block` ( 31 | `slot` bigint(20) NOT NULL, 32 | `timestamp` bigint(20) NOT NULL, 33 | `tx_count` int(11) NOT NULL 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 35 | 36 | -- -------------------------------------------------------- 37 | 38 | -- 39 | -- Table structure for table `sandwich` 40 | -- 41 | 42 | CREATE TABLE `sandwich` ( 43 | `id` int(11) NOT NULL 44 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 45 | 46 | -- -------------------------------------------------------- 47 | 48 | -- 49 | -- Stand-in structure for view `sandwich_view` 50 | -- (See below for the actual view) 51 | -- 52 | CREATE TABLE `sandwich_view` ( 53 | `tx_hash` varchar(89) 54 | ,`signer` varchar(45) 55 | ,`slot` bigint(20) 56 | ,`order_in_block` int(11) 57 | ,`sandwich_id` int(11) 58 | ,`outer_program` varchar(45) 59 | ,`inner_program` varchar(45) 60 | ,`amm` varchar(45) 61 | ,`subject` varchar(45) 62 | ,`input_amount` varchar(45) 63 | ,`input_mint` varchar(45) 64 | ,`output_amount` varchar(45) 65 | ,`output_mint` varchar(45) 66 | ,`swap_type` enum('FRONTRUN','VICTIM','BACKRUN') 67 | ); 68 | 69 | -- -------------------------------------------------------- 70 | 71 | -- 72 | -- Table structure for table `swap` 73 | -- 74 | 75 | CREATE TABLE `swap` ( 76 | `id` int(11) NOT NULL, 77 | `sandwich_id` int(11) NOT NULL, 78 | `outer_program` varchar(45) DEFAULT NULL COMMENT 'wrapper program of the swap', 79 | `inner_program` varchar(45) NOT NULL COMMENT 'facilitator program of the swap', 80 | `amm` varchar(45) NOT NULL COMMENT 'market pubkey', 81 | `subject` varchar(45) NOT NULL COMMENT 'beneficial owner of the tokens swapped', 82 | `input_mint` varchar(45) NOT NULL, 83 | `output_mint` varchar(45) NOT NULL, 84 | `input_amount` varchar(45) NOT NULL, 85 | `output_amount` varchar(45) NOT NULL, 86 | `tx_id` int(11) NOT NULL, 87 | `swap_type` enum('FRONTRUN','VICTIM','BACKRUN') NOT NULL 88 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 89 | 90 | -- -------------------------------------------------------- 91 | 92 | -- 93 | -- Stand-in structure for view `swaps_by_wrapper` 94 | -- (See below for the actual view) 95 | -- 96 | CREATE TABLE `swaps_by_wrapper` ( 97 | `outer_program` varchar(45) 98 | ,`swap_type` enum('FRONTRUN','VICTIM','BACKRUN') 99 | ,`count(*)` bigint(21) 100 | ); 101 | 102 | -- -------------------------------------------------------- 103 | 104 | -- 105 | -- Table structure for table `transaction` 106 | -- 107 | 108 | CREATE TABLE `transaction` ( 109 | `id` int(11) NOT NULL, 110 | `tx_hash` varchar(89) NOT NULL, 111 | `signer` varchar(45) NOT NULL, 112 | `slot` bigint(20) NOT NULL, 113 | `order_in_block` int(11) NOT NULL 114 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; 115 | 116 | -- -------------------------------------------------------- 117 | 118 | -- 119 | -- Structure for view `sandwich_view` 120 | -- 121 | DROP TABLE IF EXISTS `sandwich_view`; 122 | 123 | CREATE ALGORITHM=UNDEFINED DEFINER=`sandwich_admin`@`%` SQL SECURITY DEFINER VIEW `sandwich_view` AS SELECT `t`.`tx_hash` AS `tx_hash`, `t`.`signer` AS `signer`, `t`.`slot` AS `slot`, `t`.`order_in_block` AS `order_in_block`, `s`.`sandwich_id` AS `sandwich_id`, `s`.`outer_program` AS `outer_program`, `s`.`inner_program` AS `inner_program`, `s`.`amm` AS `amm`, `s`.`subject` AS `subject`, `s`.`input_amount` AS `input_amount`, `s`.`input_mint` AS `input_mint`, `s`.`output_amount` AS `output_amount`, `s`.`output_mint` AS `output_mint`, `s`.`swap_type` AS `swap_type` FROM ((`swap` `s` join `transaction` `t`) join `block` `b`) WHERE `s`.`tx_id` = `t`.`id` AND `t`.`slot` = `b`.`slot` ORDER BY `s`.`sandwich_id` ASC, `s`.`tx_id` ASC ; 124 | 125 | -- -------------------------------------------------------- 126 | 127 | -- 128 | -- Structure for view `swaps_by_wrapper` 129 | -- 130 | DROP TABLE IF EXISTS `swaps_by_wrapper`; 131 | 132 | CREATE ALGORITHM=UNDEFINED DEFINER=`sandwich_admin`@`%` SQL SECURITY DEFINER VIEW `swaps_by_wrapper` AS SELECT `sandwich_view`.`outer_program` AS `outer_program`, `sandwich_view`.`swap_type` AS `swap_type`, count(0) AS `count(*)` FROM `sandwich_view` GROUP BY `sandwich_view`.`outer_program`, `sandwich_view`.`swap_type` ORDER BY `sandwich_view`.`swap_type` ASC, count(0) ASC ; 133 | 134 | -- 135 | -- Indexes for dumped tables 136 | -- 137 | 138 | -- 139 | -- Indexes for table `block` 140 | -- 141 | ALTER TABLE `block` 142 | ADD PRIMARY KEY (`slot`); 143 | 144 | -- 145 | -- Indexes for table `sandwich` 146 | -- 147 | ALTER TABLE `sandwich` 148 | ADD PRIMARY KEY (`id`); 149 | 150 | -- 151 | -- Indexes for table `swap` 152 | -- 153 | ALTER TABLE `swap` 154 | ADD PRIMARY KEY (`id`), 155 | ADD KEY `outer_program` (`outer_program`), 156 | ADD KEY `inner_program` (`inner_program`), 157 | ADD KEY `amm` (`amm`), 158 | ADD KEY `subject` (`subject`), 159 | ADD KEY `input_mint` (`input_mint`), 160 | ADD KEY `output_mint` (`output_mint`), 161 | ADD KEY `input_amount` (`input_amount`), 162 | ADD KEY `output_amount` (`output_amount`), 163 | ADD KEY `tx_id` (`tx_id`), 164 | ADD KEY `sandwich_id` (`sandwich_id`); 165 | 166 | -- 167 | -- Indexes for table `transaction` 168 | -- 169 | ALTER TABLE `transaction` 170 | ADD PRIMARY KEY (`id`), 171 | ADD KEY `slot` (`slot`); 172 | 173 | -- 174 | -- AUTO_INCREMENT for dumped tables 175 | -- 176 | 177 | -- 178 | -- AUTO_INCREMENT for table `sandwich` 179 | -- 180 | ALTER TABLE `sandwich` 181 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 182 | 183 | -- 184 | -- AUTO_INCREMENT for table `swap` 185 | -- 186 | ALTER TABLE `swap` 187 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 188 | 189 | -- 190 | -- AUTO_INCREMENT for table `transaction` 191 | -- 192 | ALTER TABLE `transaction` 193 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; 194 | 195 | -- 196 | -- Constraints for dumped tables 197 | -- 198 | 199 | -- 200 | -- Constraints for table `swap` 201 | -- 202 | ALTER TABLE `swap` 203 | ADD CONSTRAINT `swap_ibfk_1` FOREIGN KEY (`tx_id`) REFERENCES `transaction` (`id`), 204 | ADD CONSTRAINT `swap_ibfk_2` FOREIGN KEY (`sandwich_id`) REFERENCES `sandwich` (`id`); 205 | 206 | -- 207 | -- Constraints for table `transaction` 208 | -- 209 | ALTER TABLE `transaction` 210 | ADD CONSTRAINT `transaction_ibfk_1` FOREIGN KEY (`slot`) REFERENCES `block` (`slot`); 211 | COMMIT; 212 | 213 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 214 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 215 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 216 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/pumpfun.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::{events::{addresses::{PDF_PUBKEY, WSOL_MINT}, swap::{SwapFinder, SwapV2}, swaps::private::Sealed}, utils::pubkey_from_slice}; 5 | 6 | impl Sealed for PumpFunSwapFinder {} 7 | 8 | pub struct PumpFunSwapFinder {} 9 | 10 | // Includes both the ix and event discrimant 11 | const LOG_DISCRIMINANT: &[u8] = &[ 12 | 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 13 | 0xbd, 0xdb, 0x7f, 0xd3, 0x4e, 0xe6, 0x61, 0xee, 14 | ]; 15 | 16 | /// Pump.fun have two variants: 17 | /// 1. buy [0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea] (3, 6=in sol, 5=out token) 18 | /// 2. sell [0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad] (3, 6=out sol, 5=in token) 19 | /// In/out amounts follows the discriminant, with the first one being exact and the other being the worst acceptable value. 20 | /// SOL transfers use the system program instead of token program. 21 | /// Swap direction is determined instruction's name. 22 | /// This one requires custom logic for event parsing since it issues so many transfer for all sorts of fees (all in SOL). 23 | /// mint[16..48], sol amount [48..56], token amount [56..64], is buy [64], user [65..97], fee [177..185], creator fee [225..233] 24 | impl PumpFunSwapFinder { 25 | fn user_in_out_index(ix_data: &[u8]) -> (usize, usize) { 26 | if ix_data[0] == 0x66 { 27 | // buy 28 | (6, 5) 29 | } else { 30 | // sell 31 | (5, 6) 32 | } 33 | } 34 | 35 | fn swap_from_pdf_trade_event(outer_program: Option, amm: Pubkey, input_ata: Pubkey, output_ata: Pubkey, data: &[u8], inner_ix_index: Option) -> SwapV2 { 36 | let mint = pubkey_from_slice(&data[16..48]); 37 | let sol_amount = u64::from_le_bytes(data[48..56].try_into().unwrap()); 38 | let token_amount = u64::from_le_bytes(data[56..64].try_into().unwrap()); 39 | let is_buy = data[64] != 0; 40 | let fee = u64::from_le_bytes(data[177..185].try_into().unwrap()); 41 | let creator_fee = u64::from_le_bytes(data[225..233].try_into().unwrap()); 42 | let (input_mint, output_mint) = if is_buy { 43 | (WSOL_MINT, mint) 44 | } else { 45 | (mint, WSOL_MINT) 46 | }; 47 | let (input_amount, output_amount) = if is_buy { 48 | (sol_amount + fee + creator_fee, token_amount) 49 | } else { 50 | (token_amount, sol_amount - fee - creator_fee) 51 | }; 52 | SwapV2::new( 53 | outer_program.map(|s| s.into()), 54 | PDF_PUBKEY.to_string().into(), 55 | pubkey_from_slice(&data[65..97]).to_string().into(), 56 | amm.to_string().into(), 57 | input_mint.to_string().into(), 58 | output_mint.to_string().into(), 59 | input_amount, 60 | output_amount, 61 | input_ata.to_string().into(), 62 | output_ata.to_string().into(), 63 | // todo: should try to locate the actual ix 64 | None, 65 | None, 66 | 0, 67 | 0, 68 | 0, 69 | inner_ix_index, 70 | 0, 71 | ) 72 | } 73 | } 74 | 75 | impl SwapFinder for PumpFunSwapFinder { 76 | fn amm_ix(ix: &Instruction) -> Pubkey { 77 | ix.accounts[3].pubkey 78 | } 79 | 80 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 81 | account_keys[inner_ix.accounts[3] as usize] 82 | } 83 | 84 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 85 | let (in_index, out_index) = Self::user_in_out_index(&ix.data); 86 | ( 87 | ix.accounts[in_index].pubkey, 88 | ix.accounts[out_index].pubkey, 89 | ) 90 | } 91 | 92 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 93 | let (in_index, out_index) = Self::user_in_out_index(&inner_ix.data); 94 | ( 95 | account_keys[inner_ix.accounts[in_index] as usize], 96 | account_keys[inner_ix.accounts[out_index] as usize], 97 | ) 98 | } 99 | 100 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, _meta: &TransactionStatusMeta) -> Vec { 101 | if ix.program_id == PDF_PUBKEY { 102 | for inner_ix in inner_ixs.instructions.iter() { 103 | if inner_ix.data.len() >= 266 && inner_ix.data[0..16] == LOG_DISCRIMINANT[..] { 104 | let is_buy = inner_ix.data[64] != 0; 105 | let (in_index, out_index) = if is_buy { 106 | (6, 5) // in sol, out token 107 | } else { 108 | (5, 6) // in token, out sol 109 | }; 110 | return vec![ 111 | Self::swap_from_pdf_trade_event( 112 | None, 113 | ix.accounts[3].pubkey, 114 | ix.accounts[in_index].pubkey, 115 | ix.accounts[out_index].pubkey, 116 | &inner_ix.data, 117 | None, 118 | ) 119 | ]; 120 | } 121 | } 122 | } 123 | let mut swaps = vec![]; 124 | let mut next_logical_ix = 0; 125 | for (i, inner_ix) in inner_ixs.instructions.iter().enumerate() { 126 | if inner_ix.program_id_index >= account_keys.len() as u32 || i < next_logical_ix { 127 | continue; // Skip already processed instructions or invalid program ID 128 | } 129 | if account_keys[inner_ix.program_id_index as usize] != PDF_PUBKEY { 130 | continue; // Not a Pump.fun instruction 131 | } 132 | if inner_ix.data.len() < 24 { 133 | continue; // Not a swap 134 | } 135 | if inner_ix.data.starts_with(&[0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea]) || 136 | inner_ix.data.starts_with(&[0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad]) { 137 | // Valid swap instruction 138 | let (input_ata, output_ata) = Self::user_ata_inner_ix(inner_ix, account_keys); 139 | for j in i + 1..inner_ixs.instructions.len() { 140 | let next_inner_ix = &inner_ixs.instructions[j]; 141 | if next_inner_ix.program_id_index >= account_keys.len() as u32 { 142 | continue; // Skip invalid program ID 143 | } 144 | if account_keys[next_inner_ix.program_id_index as usize] != PDF_PUBKEY { 145 | continue; // Not a Pump.fun instruction 146 | } 147 | if next_inner_ix.data.len() < 266 || next_inner_ix.data[0..16] != LOG_DISCRIMINANT[..] { 148 | continue; // Not an event 149 | } 150 | swaps.push(Self::swap_from_pdf_trade_event( 151 | Some(ix.program_id.to_string()), 152 | Self::amm_inner_ix(inner_ix, account_keys), 153 | input_ata, 154 | output_ata, 155 | &next_inner_ix.data, 156 | Some(i as u32), 157 | )); 158 | next_logical_ix = j + 1; 159 | } 160 | } 161 | } 162 | swaps 163 | } 164 | } -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/sugar.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 4 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 5 | 6 | use crate::{events::{addresses::{SUGAR_PUBKEY, WSOL_MINT}, swap::{SwapFinder, SwapV2}, swaps::private::Sealed}, utils::pubkey_from_slice}; 7 | 8 | impl Sealed for SugarSwapFinder {} 9 | 10 | pub struct SugarSwapFinder {} 11 | 12 | // Includes both the ix and event discrimant 13 | const LOG_DISCRIMINANT: &[u8] = &[ 14 | 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 15 | 0xbd, 0xdb, 0x7f, 0xd3, 0x4e, 0xe6, 0x61, 0xee, 16 | ]; 17 | const BUY_EXACT_IN: &[u8] = &[0xfa, 0xea, 0x0d, 0x7b, 0xd5, 0x9c, 0x13, 0xec]; 18 | const BUY_EXACT_OUT: &[u8] = &[0x18, 0xd3, 0x74, 0x28, 0x69, 0x03, 0x99, 0x38]; 19 | const BUY_MAX_OUT: &[u8] = &[0x60, 0xb1, 0xcb, 0x75, 0xb7, 0x41, 0xc4, 0xb1]; 20 | const SELL_EXACT_IN: &[u8] = &[0x95, 0x27, 0xde, 0x9b, 0xd3, 0x7c, 0x98, 0x1a]; 21 | const SELL_EXACT_OUT: &[u8] = &[0x5f, 0xc8, 0x47, 0x22, 0x08, 0x09, 0x0b, 0xa6]; 22 | 23 | /// ~~Pump.fun~~ Sugar have a few variants but it doesn't matter since we rely on the logging instruction here 24 | /// buyExactIn, buyExactOut, buyMaxOut, sellExactIn, sellExactOut 25 | /// This one requires custom logic for event parsing since it issues so many transfer for all sorts of fees (all in SOL). 26 | /// mint[16..48], sol amount [48..56], token amount [56..64], is buy [64], user [65..97] 27 | /// suspiciously sumilar to pump.fun 28 | impl SugarSwapFinder { 29 | fn user_in_out_index(ix_data: &[u8]) -> (usize, usize) { 30 | match &ix_data[..8] { 31 | BUY_EXACT_IN | BUY_EXACT_OUT | BUY_MAX_OUT => (6, 5), // in sol, out token 32 | SELL_EXACT_IN | SELL_EXACT_OUT => (5, 7), // in token, out sol 33 | _ => (0, 0), // Unknown instruction 34 | } 35 | } 36 | 37 | fn pool_in_out_index(ix_data: &[u8]) -> (usize, usize) { 38 | match &ix_data[..8] { 39 | BUY_EXACT_IN | BUY_EXACT_OUT | BUY_MAX_OUT => (4, 3), // in token, out sol 40 | SELL_EXACT_IN | SELL_EXACT_OUT => (3, 4), // in sol, out token 41 | _ => (0, 0), // Unknown instruction 42 | } 43 | } 44 | 45 | fn swap_from_pdf_trade_event(outer_program: Option>, amm: Pubkey, input_ata: Pubkey, output_ata: Pubkey, data: &[u8], inner_ix_index: Option) -> SwapV2 { 46 | let mint = pubkey_from_slice(&data[16..48]); 47 | let sol_amount = u64::from_le_bytes(data[48..56].try_into().unwrap()); 48 | let token_amount = u64::from_le_bytes(data[56..64].try_into().unwrap()); 49 | let is_buy = data[64] != 0; 50 | // let fee = u64::from_le_bytes(data[177..185].try_into().unwrap()); 51 | // let creator_fee = u64::from_le_bytes(data[225..233].try_into().unwrap()); 52 | let fee = if is_buy { 53 | sol_amount * 9 / 991 // 0.9% fee according to their docs 54 | } else { 55 | 0 56 | }; 57 | let (input_mint, output_mint) = if is_buy { 58 | (WSOL_MINT, mint) 59 | } else { 60 | (mint, WSOL_MINT) 61 | }; 62 | let (input_amount, output_amount) = if is_buy { 63 | (sol_amount + fee, token_amount) 64 | } else { 65 | (token_amount, sol_amount - fee) 66 | }; 67 | SwapV2::new( 68 | outer_program, 69 | SUGAR_PUBKEY.to_string().into(), 70 | pubkey_from_slice(&data[65..97]).to_string().into(), 71 | amm.to_string().into(), 72 | input_mint.to_string().into(), 73 | output_mint.to_string().into(), 74 | input_amount, 75 | output_amount, 76 | input_ata.to_string().into(), 77 | output_ata.to_string().into(), 78 | // todo: should try to locate the actual ix 79 | None, 80 | None, 81 | 0, 82 | 0, 83 | 0, 84 | inner_ix_index, 85 | 0, 86 | ) 87 | } 88 | } 89 | 90 | impl SwapFinder for SugarSwapFinder { 91 | fn amm_ix(ix: &Instruction) -> Pubkey { 92 | ix.accounts[2].pubkey 93 | } 94 | 95 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 96 | account_keys[inner_ix.accounts[2] as usize] 97 | } 98 | 99 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 100 | let (in_index, out_index) = Self::user_in_out_index(&ix.data); 101 | ( 102 | ix.accounts[in_index].pubkey, 103 | ix.accounts[out_index].pubkey, 104 | ) 105 | } 106 | 107 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 108 | let (in_index, out_index) = Self::user_in_out_index(&inner_ix.data); 109 | ( 110 | account_keys[inner_ix.accounts[in_index] as usize], 111 | account_keys[inner_ix.accounts[out_index] as usize], 112 | ) 113 | } 114 | 115 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 116 | let (in_index, out_index) = Self::pool_in_out_index(&ix.data); 117 | ( 118 | ix.accounts[in_index].pubkey, 119 | ix.accounts[out_index].pubkey, 120 | ) 121 | } 122 | 123 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 124 | let (in_index, out_index) = Self::pool_in_out_index(&inner_ix.data); 125 | ( 126 | account_keys[inner_ix.accounts[in_index] as usize], 127 | account_keys[inner_ix.accounts[out_index] as usize], 128 | ) 129 | } 130 | 131 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, _meta: &TransactionStatusMeta) -> Vec { 132 | if ix.program_id == SUGAR_PUBKEY { 133 | for inner_ix in inner_ixs.instructions.iter() { 134 | if inner_ix.data.len() == 137 && inner_ix.data[0..16] == LOG_DISCRIMINANT[..] { 135 | let (in_index, out_index) = Self::user_in_out_index(&ix.data); 136 | return vec![ 137 | Self::swap_from_pdf_trade_event( 138 | None, 139 | ix.accounts[2].pubkey, 140 | ix.accounts[in_index].pubkey, 141 | ix.accounts[out_index].pubkey, 142 | &inner_ix.data, 143 | None, 144 | ) 145 | ]; 146 | } 147 | } 148 | } 149 | let mut swaps = vec![]; 150 | let mut next_logical_ix = 0; 151 | for (i, inner_ix) in inner_ixs.instructions.iter().enumerate() { 152 | if inner_ix.program_id_index >= account_keys.len() as u32 || i < next_logical_ix { 153 | continue; // Skip already processed instructions or invalid program ID 154 | } 155 | if account_keys[inner_ix.program_id_index as usize] != SUGAR_PUBKEY { 156 | continue; // Not a sugar instruction 157 | } 158 | if inner_ix.data.len() < 24 { 159 | continue; // Not a swap 160 | } 161 | match &inner_ix.data[..8] { 162 | BUY_EXACT_IN | BUY_EXACT_OUT | BUY_MAX_OUT | 163 | SELL_EXACT_IN | SELL_EXACT_OUT => { 164 | // Valid swap instruction 165 | let (input_ata, output_ata) = Self::user_ata_inner_ix(inner_ix, account_keys); 166 | for j in i + 1..inner_ixs.instructions.len() { 167 | let next_inner_ix = &inner_ixs.instructions[j]; 168 | if next_inner_ix.program_id_index >= account_keys.len() as u32 { 169 | continue; // Skip invalid program ID 170 | } 171 | if account_keys[next_inner_ix.program_id_index as usize] != SUGAR_PUBKEY { 172 | continue; // Not a Pump.fun instruction 173 | } 174 | if next_inner_ix.data.len() != 137 || next_inner_ix.data[0..16] != LOG_DISCRIMINANT[..] { 175 | continue; // Not an event 176 | } 177 | swaps.push(Self::swap_from_pdf_trade_event( 178 | Some(ix.program_id.to_string().into()), 179 | Self::amm_inner_ix(inner_ix, account_keys), 180 | input_ata, 181 | output_ata, 182 | &next_inner_ix.data, 183 | Some(i as u32), 184 | )); 185 | next_logical_ix = j + 1; 186 | } 187 | }, 188 | _ => continue, // Not a swap 189 | 190 | } 191 | } 192 | swaps 193 | } 194 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Sandwich Finder 2 | ## Overview 3 | This section will come back soon as v2 is getting ready! 4 | 5 | ## Preface 6 | Sandwiching refers to the action of forcing the earlier inclusion of a transaction (frontrun) before a transaction published earlier (victim), with another transaction after the victim transaction to realise a profit (backrun), while abusing the victim's slippage settings. We define a sandwich as "a set of transactions that include exactly one frontrun and exactly one backrun transaction, as well as at least one victim transaction", a sandwicher as "a party that sandwiches", and a colluder as "a validator that forwards transactions they receive to a sandwicher". 7 | 8 | Some have [mentioned that](https://discord.com/channels/938287290806042626/938287767446753400/1325923301205344297) users should issue transactions with lower slippage instead but it's not entirely possible when trading token pairs with extremely high volatility. Being forced to issue transactions with low slippage may lead to higher transaction failure rates and missed opportunities, which is also suboptimal. 9 | 10 | The reasons why sandwiching is harmful to the ecosystem had been detailed by [another researcher](https://github.com/a-guard/malicious-validators/blob/main/README.md#why-are-sandwich-attacks-harmful) and shall not be repeated in detail here, but it mainly boils down to breaking trust, transparency and fairness. 11 | 12 | We believe that colluder identification should be a continuous effort since [generating new keys](https://docs.anza.xyz/cli/wallets/file-system) to run a new validator is essentially free, and with a certain stake pool willing to sell stake to any validator regardless of operating history, one-off removals will prove ineffective. This repository aims to serve as a tool to continuously identify sandwiches and colluders such that relevant parties can remove stake from sandwichers as soon as possible. 13 | 14 | ## Methodology 15 | ### Why we believe this works 16 | Law of large numbers - the average of the results obtained from a large number of independent random samples converges to the true value, if it exists [[source]](https://en.wikipedia.org/wiki/Law_of_large_numbers). In other words, an average validator running the average software should produce average numbers in the long run, the longer the run, the closer the validator's average is to the global average. 17 | 18 | In this application, we consider an observation of "how many sandwiches are in the block" and "is there a sandwich in the block" a sample. Forus to apply LLM here we need to be reasonably sure that: 19 | 1. The samples are independent; 20 | 2. The average exists. 21 | 22 | It's clear that the average clearly exists - it should be very close to the observered cluster average given the large number of slots we're aggregating over. 23 | 24 | According to [Anza's docs](https://docs.anza.xyz/consensus/leader-rotation#leader-schedule-generation-algorithm), the distribution of leader slots is random but stake-weighted. While it's possible to influence the distribution (e.g. maximise the chances that a certain set of validators' slots follows another set's) by strategically creating stake accounts, and technically it would be beneficial to avoid having leader slots after validators known to be less performant to avoid skipped slots (therefore missing out of block rewards), this has nothing to do with sandwiching as validators are economically incentivised to leave the transactions that pay the most to themselves. This also applies to sandwichable transactions, if a validator knows that a transaction is sandwichable and is willing to exploit it, its only option would be to exploit the transaction itself, or forward it to a sandwicher. In other words, sandwicher colluders (RPCs validators alike) normally won't forward sandwich-able transactions to the next leader "just to mess with their numbers". As such, the leader slot distribution depends entirely on the cluster's actions and is considered random. 25 | 26 | Another important factor to consider is the difference between transaction delivery across nodes. Some transaction senders may decide to not have their transactions sent directly from RPC nodes to certain validators due to different concerns, such as being sandwiched, but it's unlikely that any given transaction sender will blacklist the majority of the validators to supress their sandwiching numbers. If and when such facilities are used, it'll most likely decrease the number of transactions reaching known sandwacher colluders, supressing their numbers instead. There is little data on the usage of such facilities but we expect their usage to not affect the independence of the sampling. 27 | 28 | From our analysis above, we're confident that LLM can be applied to sandwicher colluder identification as the average we're looking for exists, and the samples (or at least groups of 4 samples, corresponding a leader group) are independent. Which means, if your sandwiching numbers deviate from the cluster average significantly, we're pretty sure (but not 100% as with any statistics-based hypothesis) you're engaged with something related to sandwiching. 29 | 30 | ### Sandwich identification 31 | A sandwich is defined by a set of transactions that satisfies all of the following: 32 | 33 | 1. Has at least 1 transaction for frontrunning and backrunning, with at least 1 victim transaction between the frontrun and backrun. 34 | 2. The inputs of the backrun must match the output of the frontrun, or be connected by transfers. 35 | 3. The frontrun and the victim transactions trades in the same direction, the backrun's one is in reverse; 36 | 4. Output of backrun >= Input of frontrun and Output of frontrun >= Input of backrun (profitability constraint); 37 | 5. All transactions use the same AMM; 38 | 6. Each victim transaction's signer differs from the frontrun's and the backrun's; 39 | 7. The wrapper program in the frontrun and backrun are the same; 40 | 41 | For each sandwich identified in newly emitted blocks by the cluster, we insert that to a database for report generation. 42 | 43 | Note that we don't require the frontrun and the backrun to have the same signer as it's a valid strategy to use multiple wallets to evade detection by moving tokens across wallets. 44 | The "victim signer differs from attackers" and "wrapper program present" constraints were removed in v2 since pvp is fun, and we've identified sandwiches that invoke certain AMM programs directly, but the frontrun's output matches the backrun's input exactly, suggesting sandwiching intention. 45 | 46 | ### Report generation 47 | With the sandwich dataset, we're able to calculate the cluster wide and per validator proportion of sandwich-inclusive blocks and sandwich per block. Our hypothesis is that colluders will exhibit above cluster average values on both metrics. Due to transaction landing delays, the report generation tool also "credits" sandwiches to earlier slots. 48 | 49 | The hypothesises are as follows:
50 | Null hypothesis: At least one metric is in line with the cluster average
51 | Alternative hypothesis: Both metrics exceeds cluster average
52 | 53 | For the proportion of sandwich-inclusive blocks metric, each block is treated as a Bernoulli trial, where success means a block is sandwich-inclusive and failure means the otherwise. For each validator, the number of blocks emitted (N) and the number of sandwich-inclusive blocks (k) is used to calculate a 99.99% confidence interval of their true proportion of sandwich-inclusion blocks. A validator will be deemed to be above cluster average if the lower bound of the confidence interval is above the cluster average. 54 | 55 | For the sandwiches per block metric, the mean and standard deviation of the cluster wide number of sandwiches per block is taken, and a 99.99% confidence interval of the expected number of sandwiches per block should the validator is in line with the cluster wide average is calculated. A validator will be deemed to be above cluster average if the validator's metric is above the confidence interval's upper bound. 56 | 57 | Validators satisfying the alternative hypothesis, signaling collusion for an extended period, will be flagged. 58 | 59 | For flagging on [Hanabi Staking's dashboard](https://hanabi.so/marinade-stake-selling), flagged validators with fewer than 50 blocks as well as those only exceeding the thresholds marginally but reputable are excluded. 60 | 61 | ## Report Interpretation 62 | There are two CSV files, `report.csv` and `filtered_report.csv`. The first file shows all validators' metrics while the second one shows the ones with abnormally high values. It's normal for your validator to show up in `report.csv`. 63 | 64 | The CSV files contain 14 columns each and their meanings are as follows: 65 | |Column(s)|Meaning| 66 | |---|---| 67 | |leader/vote|The validator's identity and vote account pubkeys| 68 | |name|The validator's name according to onchain data| 69 | |Sc|"Score", normalised weighted number of sandwiches| 70 | |Sc_p|"Presence score", normalised number of blocks with sandwiches, which roughly means proportion of sandwich inclusive blocks| 71 | |R-Sc/R-Sc_p|Unnormalised Sc and Sc_p| 72 | |slots|Number of leader slots observed for the validator| 73 | |Sc_p_{lb\|ub}|Bounds of the confidence interval of the validator's true proportion of sandwich inclusive blocks. Flagged if the lower bound is above the cluster mean| 74 | |Sc_{lb\|ub}|Bounds of the confidence interval of which the validator is considered to have an "average" number of sandwiches per block. Flagged if `Sc` is above the upper bound| 75 | {Sc_p\|Sc}_flag|True if the validator is being flagged due to the respective metric, false otherwise| 76 | 77 | ## Dataset Access 78 | For dataset access, [join the Hanabi Staking Discord](https://discord.gg/VpJuWFRJfb) and open a ticket. 79 | -------------------------------------------------------------------------------- /sandwich-finder/src/bin/report.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env, time}; 2 | 3 | use mysql::{prelude::Queryable, Pool}; 4 | use serde::Deserialize; 5 | use tokio::task::JoinHandle; 6 | use std::fs::File; 7 | use std::io::Write; 8 | 9 | const Z: f64 = 3.89059188641; // p-value 0.0001 10 | const FILTERED_SLOT_THRESHOLD: i32 = 200; 11 | 12 | fn p_conf_interval(n: f64, k: f64) -> (f64, f64) { 13 | let p = k / n; 14 | let a = (p + Z * Z / (2.0 * n)) / (1.0 + Z * Z / n); 15 | let b = Z / (1.0 + Z * Z / n) * (p * (1.0 - p) / n + Z * Z / (4.0 * n * n)).sqrt(); 16 | (a - b, a + b) 17 | } 18 | 19 | fn count_conf_interval(mu: f64, stdev: f64, n: f64) -> (f64, f64) { 20 | let a = mu - Z * stdev / n.sqrt(); 21 | let b = mu + Z * stdev / n.sqrt(); 22 | (a, b) 23 | } 24 | 25 | #[derive(Deserialize)] 26 | #[serde(rename_all = "camelCase")] 27 | struct ValidatorInfo { 28 | pub identity: String, 29 | pub vote_pubkey: Option, 30 | pub name: Option, 31 | } 32 | 33 | /// Sandwicher-colluder report 34 | /// The main metrics we're looking for here are sandwiches per slot (Sc) and proportion of slots with sandwiches (Sc_p), 35 | /// and our hypothesis is that colluders will have a higher value in both values, compared to the cluster average. 36 | /// Solana validators typically only receive transactions when it's close to their leader slot, 37 | /// and colluders relays these transactions to the sandwichers, who will sandwich the transactions where feasible and submit ASAP, 38 | /// or the tx may land on its own (without its slippage being artifically inflated!). 39 | /// Therefore, colluders are expected to have higher Sc and Sc_p values compared to non-colluders. 40 | /// Since txs may take a couple slots to land (sent to a colluder but landed after the colluder's leader slots), leaders 41 | /// of prior slots (`offset_range`) will also be credited for any given sandwich. Ideally, slots farther away should receive 42 | /// less credits, and the exact distribution should resemble that of the actual latency of sandwichable txs, but that's unimplemented for now. 43 | #[tokio::main] 44 | async fn main() { 45 | dotenv::dotenv().ok(); 46 | let mut args = env::args(); 47 | args.next(); // argv[0] 48 | let slot_range: (i64, i64) = (args.next().unwrap().parse().unwrap(), args.next().unwrap().parse().unwrap()); 49 | let validator_info_fut: JoinHandle> = tokio::spawn(async move { 50 | let resp = reqwest::get("https://hanabi.so/api/validators/info").await.unwrap(); 51 | let text = resp.text().await.unwrap(); 52 | serde_json::from_str(&text).unwrap() 53 | }); 54 | let now = time::Instant::now(); 55 | let mysql_url = env::var("MYSQL").unwrap(); 56 | let pool = Pool::new(mysql_url.as_str()).unwrap(); 57 | let mut conn = pool.get_conn().unwrap(); 58 | let mut report = File::create("report.csv").unwrap(); 59 | let mut filtered_report = File::create("filtered_report.csv").unwrap(); 60 | eprintln!("[+{:7}ms] Connected to MySQL", now.elapsed().as_millis()); 61 | let offset_range = [0.2, 1.0, 0.6, 0.4, 0.2]; 62 | // fetch leaders within the concerned slot range to serve as the basis of normalisation 63 | let leader_count = conn.exec_fold("select leader, count(*) from leader_schedule where slot between ? and ? group by leader", slot_range, HashMap::new(), |mut acc, row: (String, u64)| { 64 | let count = acc.entry(row.0).or_insert(0); 65 | *count += row.1; 66 | acc 67 | }).unwrap(); 68 | eprintln!("[+{:7}ms] Consolidated leader schedule", now.elapsed().as_millis()); 69 | conn.exec_drop("drop table if exists sandwich_slot", ()).unwrap(); 70 | conn.exec_drop("create table sandwich_slot (select s.sandwich_id, min(t.slot) as slot from swap s, `transaction` t where s.tx_id=t.id group by s.sandwich_id);", ()).unwrap(); 71 | conn.exec_drop("ALTER TABLE `sandwich_slot` CHANGE `slot` `slot` BIGINT(20) NOT NULL; ", ()).unwrap(); 72 | conn.exec_drop("ALTER TABLE `sandwich_slot` ADD INDEX(`slot`); ", ()).unwrap(); 73 | eprintln!("[+{:7}ms] Created temp tables", now.elapsed().as_millis()); 74 | // mean and sd of sandwiches per slot 75 | let n = slot_range.1 - slot_range.0; 76 | let mut sx = 0.0; 77 | let mut sxx = 0.0; 78 | conn.exec_iter("SELECT count(*) FROM `sandwich_slot` where slot between ? and ? group by slot;", slot_range).unwrap().for_each(|row| { 79 | let count: i32 = mysql::from_row(row.unwrap()); 80 | let x = count as f64; 81 | sx += x; 82 | sxx += x * x; 83 | }); 84 | let mean = sx / n as f64; 85 | let stdev = (sxx / n as f64 - mean * mean).sqrt(); 86 | eprintln!("[+{:7}ms] Consolidated frequencies", now.elapsed().as_millis()); 87 | // raw score calculations (sandwiches in leader slot with offset to account for tx delay) 88 | let offset_stmt = conn.prep("select l.leader, count(*) from (SELECT slot-? as slot FROM `sandwich_slot`) t1, leader_schedule l where t1.slot=l.slot and t1.slot between ? and ? group by l.leader;").unwrap(); 89 | let presence_offset_stmt = conn.prep("select l.leader, count(*) from (SELECT distinct slot-? as slot FROM `sandwich_slot`) t1, leader_schedule l where t1.slot=l.slot and t1.slot between ? and ? group by l.leader;").unwrap(); 90 | let dontfront_stmt = conn.prep("SELECT l.leader, sum(dont_front) FROM `swap` s, transaction t, leader_schedule l where s.tx_id=t.id and t.slot=l.slot and s.swap_type='VICTIM' and t.slot between ? and ? group by l.leader").unwrap(); 91 | let mut scores: HashMap = HashMap::new(); 92 | let mut presence_scores: HashMap = HashMap::new(); 93 | let mut dont_front: HashMap = HashMap::new(); 94 | let mut total_score = 0.0; 95 | let mut total_presence_score = 0.0; 96 | conn.exec_iter(&dontfront_stmt, slot_range).unwrap().for_each(|row| { 97 | let (leader, count): (String, i32) = mysql::from_row(row.unwrap()); 98 | dont_front.insert(leader, count as u64); 99 | }); 100 | eprintln!("[+{:7}ms] Consolidated dont_front", now.elapsed().as_millis()); 101 | for (i, _) in offset_range.iter().enumerate() { 102 | conn.exec_iter(&offset_stmt, (i, slot_range.0, slot_range.1)).unwrap().for_each(|row| { 103 | let (leader, count): (String, i32) = mysql::from_row(row.unwrap()); 104 | let count = count as f64 * offset_range[i]; 105 | let score = scores.entry(leader).or_insert(0.0); 106 | *score += count; 107 | total_score += count; 108 | }); 109 | conn.exec_iter(&presence_offset_stmt, (i, slot_range.0, slot_range.1)).unwrap().for_each(|row| { 110 | let (leader, count): (String, i32) = mysql::from_row(row.unwrap()); 111 | let count = count as f64 * offset_range[i]; 112 | let score = presence_scores.entry(leader).or_insert(0.0); 113 | *score += count; 114 | total_presence_score += count; 115 | }); 116 | eprintln!("[+{:7}ms] Completed iteration {i}", now.elapsed().as_millis()); 117 | } 118 | // normalise scores into an approximate measure of sandwiches per slot 119 | let norm_factor = offset_range.iter().sum::(); 120 | let normalised_scores = scores.iter().map(|(k, v)| { 121 | let count = leader_count.get(k).unwrap_or(&0); 122 | (k.clone(), *v / *count as f64 / norm_factor) 123 | }).collect::>(); 124 | let presence_normalised_scores = presence_scores.iter().map(|(k, v)| { 125 | let count = leader_count.get(k).unwrap_or(&0); 126 | (k.clone(), *v / *count as f64 / norm_factor) 127 | }).collect::>(); 128 | let mut entries = normalised_scores.iter().map(|(k, v)| { 129 | let slots = leader_count[k] as f64; 130 | (k, v, presence_normalised_scores[k], v * slots, presence_normalised_scores[k] * slots, slots as i32) 131 | }).collect::>(); 132 | // and sort by presence, then frequency 133 | entries.sort_by(|a, b| { 134 | let a = (a.2, a.1); 135 | let b = (b.2, b.1); 136 | b.partial_cmp(&a).unwrap() 137 | }); 138 | // wait for validator info 139 | let validator_info = validator_info_fut.await.unwrap(); 140 | let validator_info = validator_info.into_iter().map(|v| (v.identity.clone(), v)).collect::>(); 141 | // print report 142 | writeln!(report, "leader,vote,name,Sc,Sc_p,R-Sc,R-Sc_p,slots,Sc_p_lb,Sc_p_ub,Sc_p_flag,Sc_lb,Sc_ub,Sc_flag,dontfront_violations").unwrap(); 143 | writeln!(filtered_report, "leader,vote,name,Sc,Sc_p,R-Sc,R-Sc_p,slots,Sc_p_lb,Sc_p_ub,Sc_p_flag,Sc_lb,Sc_ub,Sc_flag,dontfront_violations").unwrap(); 144 | let w_sc_p = total_presence_score / (slot_range.1 - slot_range.0) as f64 / norm_factor; 145 | let w_sc = total_score / (slot_range.1 - slot_range.0) as f64 / norm_factor; 146 | for (leader, sc, sc_p, rsc, rsc_p, slots) in entries.iter() { 147 | let (lb, ub) = p_conf_interval(*slots as f64, *rsc_p); 148 | let (n_lb, n_ub) = count_conf_interval(mean, stdev as f64, *slots as f64); 149 | let entry = validator_info.get(*leader); 150 | let (vote, name) = match entry { 151 | Some(v) => (v.vote_pubkey.clone().unwrap_or("".to_string()), v.name.clone().unwrap_or("".to_string())), 152 | None => ("".to_string(), "".to_string()) 153 | }; 154 | writeln!(report, "{},{},\"{}\",{},{},{},{},{},{},{},{},{},{},{},{}", leader, vote, name.replace("\"", "\"\""), sc, sc_p, rsc, rsc_p, slots, lb, ub, lb > w_sc_p, n_lb, n_ub, n_ub < **sc, dont_front.get(*leader).unwrap_or(&0)).unwrap(); 155 | if lb > w_sc_p && n_ub < **sc && *slots >= FILTERED_SLOT_THRESHOLD { 156 | writeln!(filtered_report, "{},{},\"{}\",{},{},{},{},{},{},{},{},{},{},{},{}", leader, vote, name.replace("\"", "\"\""), sc, sc_p, rsc, rsc_p, slots, lb, ub, lb > w_sc_p, n_lb, n_ub, n_ub < **sc, dont_front.get(*leader).unwrap_or(&0)).unwrap(); 157 | } 158 | } 159 | writeln!(report, "Weighted avg Sc_p,{:.5},,,,,,,,,,,,,", w_sc_p).unwrap(); 160 | writeln!(report, "Weighted avg Sc,{:.5},,,,,,,,,,,,,", w_sc).unwrap(); 161 | writeln!(report, "Global stdev,{:.5},,,,,,,,,,,,,", stdev).unwrap(); 162 | writeln!(report, "Slot count,{},,,,,,,,,,,,,", slot_range.1 - slot_range.0 + 1).unwrap(); 163 | writeln!(filtered_report, "Weighted avg Sc_p,{:.5},,,,,,,,,,,,,", w_sc_p).unwrap(); 164 | writeln!(filtered_report, "Weighted avg Sc,{:.5},,,,,,,,,,,,,", w_sc).unwrap(); 165 | writeln!(filtered_report, "Global stdev,{:.5},,,,,,,,,,,,,", stdev).unwrap(); 166 | writeln!(filtered_report, "Slot count,{},,,,,,,,,,,,,", slot_range.1 - slot_range.0 + 1).unwrap(); 167 | } 168 | -------------------------------------------------------------------------------- /sandwich-finder/src/events/swaps/whirlpool.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; 2 | use yellowstone_grpc_proto::prelude::{InnerInstruction, InnerInstructions, TransactionStatusMeta}; 3 | 4 | use crate::events::{addresses::WHIRLPOOL_PUBKEY, swap::{SwapFinder, SwapV2}, swaps::{private::Sealed, swap_finder_ext::SwapFinderExt}}; 5 | 6 | impl Sealed for WhirlpoolSwapFinder {} 7 | 8 | pub struct WhirlpoolSwapFinder {} 9 | 10 | /// Whirlpool 1-hop swaps have two variants: 11 | /// 1. swap [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8] 12 | /// 2. swapV2 [0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62] 13 | /// For swap, [amm, userA, poolA, userB, poolB] = [2, 3, 4, 5, 6] 14 | /// For swapV2, [amm, userA, poolA, userB, poolB] = [4, 7, 8, 9, 10] 15 | /// As far as swap amounts are concerned, both instructions has the same data layout 16 | /// in amount, min out, sqrt price limit, amount is in, aToB 17 | /// aToB determines trade direction. 18 | impl WhirlpoolSwapFinder { 19 | fn is_swap_v2(ix_data: &[u8]) -> bool { 20 | ix_data.starts_with(&[0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62]) 21 | } 22 | 23 | fn is_from_a_to_b(ix_data: &[u8]) -> bool { 24 | ix_data[41] != 0 25 | } 26 | } 27 | 28 | impl SwapFinder for WhirlpoolSwapFinder { 29 | fn amm_ix(ix: &Instruction) -> Pubkey { 30 | if Self::is_swap_v2(&ix.data) { 31 | ix.accounts[4].pubkey // swapV2 32 | } else { 33 | ix.accounts[2].pubkey // swap 34 | } 35 | } 36 | 37 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 38 | if Self::is_swap_v2(&inner_ix.data) { 39 | account_keys[inner_ix.accounts[4] as usize] // swapV2 40 | } else { 41 | account_keys[inner_ix.accounts[2] as usize] // swap 42 | } 43 | } 44 | 45 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 46 | match (Self::is_swap_v2(&ix.data), Self::is_from_a_to_b(&ix.data)) { 47 | (true, true) => (ix.accounts[7].pubkey, ix.accounts[9].pubkey), // swapV2, aToB 48 | (true, false) => (ix.accounts[9].pubkey, ix.accounts[7].pubkey), // swapV2, bToA 49 | (false, true) => (ix.accounts[3].pubkey, ix.accounts[5].pubkey), // swap, aToB 50 | (false, false) => (ix.accounts[5].pubkey, ix.accounts[3].pubkey), // swap, bToA 51 | } 52 | } 53 | 54 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 55 | match (Self::is_swap_v2(&inner_ix.data), Self::is_from_a_to_b(&inner_ix.data)) { 56 | (true, true) => ( 57 | account_keys[inner_ix.accounts[7] as usize], 58 | account_keys[inner_ix.accounts[9] as usize], 59 | ), // swapV2, aToB 60 | (true, false) => ( 61 | account_keys[inner_ix.accounts[9] as usize], 62 | account_keys[inner_ix.accounts[7] as usize], 63 | ), // swapV2, bToA 64 | (false, true) => ( 65 | account_keys[inner_ix.accounts[3] as usize], 66 | account_keys[inner_ix.accounts[5] as usize], 67 | ), // swap, aToB 68 | (false, false) => ( 69 | account_keys[inner_ix.accounts[5] as usize], 70 | account_keys[inner_ix.accounts[3] as usize], 71 | ), // swap, bToA 72 | } 73 | } 74 | 75 | fn pool_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 76 | match (Self::is_swap_v2(&ix.data), Self::is_from_a_to_b(&ix.data)) { 77 | (true, true) => (ix.accounts[10].pubkey, ix.accounts[8].pubkey), // swapV2, aToB 78 | (true, false) => (ix.accounts[8].pubkey, ix.accounts[10].pubkey), // swapV2, bToA 79 | (false, true) => (ix.accounts[6].pubkey, ix.accounts[4].pubkey), // swap, aToB 80 | (false, false) => (ix.accounts[4].pubkey, ix.accounts[6].pubkey), // swap, bToA 81 | } 82 | } 83 | 84 | fn pool_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 85 | match (Self::is_swap_v2(&inner_ix.data), Self::is_from_a_to_b(&inner_ix.data)) { 86 | (true, true) => ( 87 | account_keys[inner_ix.accounts[10] as usize], 88 | account_keys[inner_ix.accounts[8] as usize], 89 | ), // swapV2, aToB 90 | (true, false) => ( 91 | account_keys[inner_ix.accounts[8] as usize], 92 | account_keys[inner_ix.accounts[10] as usize], 93 | ), // swapV2, bToA 94 | (false, true) => ( 95 | account_keys[inner_ix.accounts[6] as usize], 96 | account_keys[inner_ix.accounts[4] as usize], 97 | ), // swap, aToB 98 | (false, false) => ( 99 | account_keys[inner_ix.accounts[4] as usize], 100 | account_keys[inner_ix.accounts[6] as usize], 101 | ), // swap, bToA 102 | } 103 | } 104 | 105 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 106 | [ 107 | // swap 108 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &WHIRLPOOL_PUBKEY, &[0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8], 0, 24), 109 | // swap_v2 110 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &WHIRLPOOL_PUBKEY, &[0x2b, 0x04, 0xed, 0x0b, 0x1a, 0xc9, 0x1e, 0x62], 0, 24), 111 | ].concat() 112 | } 113 | } 114 | 115 | pub struct WhirlpoolTwoHopSwapFinder< 116 | const A2B: usize, 117 | const AMM_INDEX: usize, 118 | const USER_A_INDEX: usize, 119 | const USER_B_INDEX: usize, 120 | const POOL_A_INDEX: usize, 121 | const POOL_B_INDEX: usize, 122 | const DATA_SIZE: usize, 123 | const D0: u8, 124 | const D1: u8, 125 | const D2: u8, 126 | const D3: u8, 127 | const D4: u8, 128 | const D5: u8, 129 | const D6: u8, 130 | const D7: u8, 131 | >; 132 | 133 | impl< 134 | const A2B: usize, 135 | const AMM: usize, 136 | const UA: usize, 137 | const UB: usize, 138 | const PA: usize, 139 | const PB: usize, 140 | const DS: usize, 141 | const D0: u8, 142 | const D1: u8, 143 | const D2: u8, 144 | const D3: u8, 145 | const D4: u8, 146 | const D5: u8, 147 | const D6: u8, 148 | const D7: u8, 149 | > Sealed for WhirlpoolTwoHopSwapFinder {} 150 | 151 | impl< 152 | const A2B: usize, 153 | const AMM: usize, 154 | const UA: usize, 155 | const UB: usize, 156 | const PA: usize, 157 | const PB: usize, 158 | const DS: usize, 159 | const D0: u8, 160 | const D1: u8, 161 | const D2: u8, 162 | const D3: u8, 163 | const D4: u8, 164 | const D5: u8, 165 | const D6: u8, 166 | const D7: u8, 167 | > WhirlpoolTwoHopSwapFinder { 168 | pub fn is_from_a_to_b(ix_data: &[u8]) -> bool { 169 | ix_data[A2B] != 0 170 | } 171 | } 172 | 173 | impl< 174 | const A2B: usize, 175 | const AMM: usize, 176 | const UA: usize, 177 | const UB: usize, 178 | const PA: usize, 179 | const PB: usize, 180 | const DS: usize, 181 | const D0: u8, 182 | const D1: u8, 183 | const D2: u8, 184 | const D3: u8, 185 | const D4: u8, 186 | const D5: u8, 187 | const D6: u8, 188 | const D7: u8, 189 | > SwapFinder for WhirlpoolTwoHopSwapFinder { 190 | fn amm_ix(ix: &Instruction) -> Pubkey { 191 | ix.accounts[AMM].pubkey 192 | } 193 | 194 | fn amm_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> Pubkey { 195 | account_keys[inner_ix.accounts[AMM] as usize] 196 | } 197 | 198 | fn user_ata_ix(ix: &Instruction) -> (Pubkey, Pubkey) { 199 | if Self::is_from_a_to_b(&ix.data) { 200 | (ix.accounts[UA].pubkey, ix.accounts[UB].pubkey) // aToB 201 | } else { 202 | (ix.accounts[UB].pubkey, ix.accounts[UA].pubkey) // bToA 203 | } 204 | } 205 | 206 | fn user_ata_inner_ix(inner_ix: &InnerInstruction, account_keys: &Vec) -> (Pubkey, Pubkey) { 207 | if Self::is_from_a_to_b(&inner_ix.data) { 208 | ( 209 | account_keys[inner_ix.accounts[UA] as usize], 210 | account_keys[inner_ix.accounts[UB] as usize], 211 | ) // aToB 212 | } else { 213 | ( 214 | account_keys[inner_ix.accounts[UB] as usize], 215 | account_keys[inner_ix.accounts[UA] as usize], 216 | ) // bToA 217 | } 218 | } 219 | 220 | fn pool_ata_ix(_ix: &Instruction) -> (Pubkey, Pubkey) { 221 | if Self::is_from_a_to_b(&_ix.data) { 222 | (_ix.accounts[PB].pubkey, _ix.accounts[PA].pubkey) // aToB 223 | } else { 224 | (_ix.accounts[PA].pubkey, _ix.accounts[PB].pubkey) // bToA 225 | } 226 | } 227 | 228 | fn pool_ata_inner_ix(_inner_ix: &InnerInstruction, _account_keys: &Vec) -> (Pubkey, Pubkey) { 229 | if Self::is_from_a_to_b(&_inner_ix.data) { 230 | ( 231 | _account_keys[_inner_ix.accounts[PB] as usize], 232 | _account_keys[_inner_ix.accounts[PA] as usize], 233 | ) // aToB 234 | } else { 235 | ( 236 | _account_keys[_inner_ix.accounts[PA] as usize], 237 | _account_keys[_inner_ix.accounts[PB] as usize], 238 | ) // bToA 239 | } 240 | } 241 | 242 | fn find_swaps(ix: &Instruction, inner_ixs: &InnerInstructions, account_keys: &Vec, meta: &TransactionStatusMeta) -> Vec { 243 | Self::find_swaps_generic(ix, inner_ixs, account_keys, meta, &WHIRLPOOL_PUBKEY, &[D0, D1, D2, D3, D4, D5, D6, D7], 0, DS) 244 | } 245 | } 246 | 247 | /// Whirlpool also has 2-hop swaps with two variants 248 | /// It's much easier to run 2 passes for the 2 hops 249 | /// Hop 1: [amm, userA, poolA, userB, poolB] = [2, 4, 5, 6, 7] 250 | /// Hop 2: [amm, userA, poolA, userB, poolB] = [3, 8, 9, 10, 11] 251 | /// 252 | pub type WhirlpoolTwoHopSwapFinder1 = WhirlpoolTwoHopSwapFinder<25, 2, 4, 6, 5, 7, 59, 0xc3, 0x60, 0xed, 0x6c, 0x44, 0xa2, 0xdb, 0xe6>; 253 | pub type WhirlpoolTwoHopSwapFinder2 = WhirlpoolTwoHopSwapFinder<26, 3, 8, 10, 9, 11, 59, 0xc3, 0x60, 0xed, 0x6c, 0x44, 0xa2, 0xdb, 0xe6>; 254 | 255 | /// For TwoHopSwapV2 there's only 3 transfers, but the second one is reused (both the output of the 1st hop and the input of the 2nd hop) 256 | /// The structure looks something like this 257 | /// A->B B->C 258 | /// pump->sol->usdt 259 | /// [8] UA /UA1 [pump] 260 | /// [9] P1A/PA1 [pump] 261 | /// [10] P1B/PB1/UA2 [sol] 262 | /// [11] P2B/UB1/PA2 [sol] 263 | /// [12] P2C /PB2 [usdt] 264 | /// [13] UC /UB2 [usdt] 265 | /// swap 1: UA->P1A, P1B->P2B 266 | /// swap 2: P1B->P2B, P2C->UC 267 | /// We set A2B to 0 since it's one of the discriminant bytes and is guaranteed to be non zero 268 | pub type WhirlpoolTwoHopSwapV2Finder1 = WhirlpoolTwoHopSwapFinder<0, 0, 8, 11, 9, 10, 59, 0xba, 0x8f, 0xd1, 0x1d, 0xfe, 0x02, 0xc2, 0x75>; 269 | pub type WhirlpoolTwoHopSwapV2Finder2 = WhirlpoolTwoHopSwapFinder<0, 1, 10, 13, 11, 12, 59, 0xba, 0x8f, 0xd1, 0x1d, 0xfe, 0x02, 0xc2, 0x75>; 270 | --------------------------------------------------------------------------------