,
-}
-
-impl BundleExecutor
-where
- P: Provider,
- N: Network,
-{
- /// Create a new bundle executor with custom providers
- pub fn new(bundle_provider: P, provider: P, signers: Vec) -> Self {
- let signers: HashMap<_, _> = signers
- .into_iter()
- .map(|s| (s.address(), EthereumWallet::new(s)))
- .collect();
-
- for signer in signers.keys() {
- info!("bundle executor: setting up signer {:#x}", signer);
- }
-
- Self {
- bundle_provider,
- provider,
- signers,
- _phantom: PhantomData,
- }
- }
-
- /// Sign an unsigned transaction
- async fn sign_transaction(&self, tx: TransactionRequest) -> Result {
- let mut tx = tx;
-
- let account = tx.from.ok_or_else(|| eyre::eyre!("missing sender address"))?;
-
- let signer = self
- .signers
- .get(&account)
- .ok_or_else(|| eyre::eyre!("missing signer for {:#x}", account))?;
-
- // Get nonce if not set
- if tx.nonce.is_none() {
- let nonce = self.provider.get_transaction_count(account).await?;
- tx.set_nonce(nonce);
- }
-
- // Build and sign transaction
- let signed = tx.build(signer).await?;
- Ok(signed.encoded_2718().into())
- }
-
- /// Process bundle and convert all transactions to raw signed format
- async fn process_bundle(&self, bundle: Bundle) -> Result> {
- let mut raw_txs = Vec::with_capacity(bundle.txs.len());
-
- for (idx, tx) in bundle.txs.into_iter().enumerate() {
- let raw_tx = match tx {
- SignedOrUnsignedTx::Signed(bytes) => bytes,
- SignedOrUnsignedTx::Unsigned(tx_request) => match self.sign_transaction(tx_request).await {
- Ok(signed) => signed,
- Err(err) => {
- error!(index = idx, "failed to sign transaction: {err:#}");
- return Err(err);
- }
- },
- };
-
- // Convert to hex string with 0x prefix
- raw_txs.push(format!("0x{}", hex::encode(&raw_tx)));
- }
-
- Ok(raw_txs)
- }
-
- /// Submit bundle to block builder
- async fn submit_bundle(&self, bundle: Bundle) -> Result<()> {
- let block_number = bundle.block;
- let raw_txs = self.process_bundle(bundle).await?;
-
- info!(block = block_number, tx_count = raw_txs.len(), "submitting bundle");
-
- // Remove 0x prefix from transaction hex strings as required by bloXroute API
- let txs_without_prefix: Vec = raw_txs
- .iter()
- .map(|tx| tx.strip_prefix("0x").unwrap_or(tx).to_string())
- .collect();
-
- // Set bundle expiration time: 3 minutes from now
- let current_timestamp = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap()
- .as_secs();
- let max_timestamp = current_timestamp + 180; // 3 minutes = 180 seconds
-
- info!(
- current_ts = current_timestamp,
- max_ts = max_timestamp,
- "bundle expires in 3 minutes"
- );
-
- let params = json!({
- "transaction": txs_without_prefix,
- "blockchain_network": "BSC-Mainnet",
- "block_number": format!("0x{:x}", block_number),
- "min_timestamp": 0,
- "max_timestamp": max_timestamp,
- "mev_builders": {
- "all": ""
- }
- });
-
- let result: serde_json::Value = self
- .bundle_provider
- .root()
- .raw_request("blxr_submit_bundle".into(), params)
- .await?;
-
- if let Some(bundle_hash) = result.get("bundleHash") {
- info!(bundle_hash = %bundle_hash, block = block_number, "bundle submitted successfully");
- } else {
- info!(result = %result, "bundle submitted");
- }
-
- Ok(())
- }
-
- /// Test function to submit a dummy bundle with 2 simple transactions
- ///
- /// # Arguments
- /// * `to_address` - Recipient address for the test transactions
- /// * `amount_wei` - Amount to send in wei (e.g. 1000000000000000 for 0.001 BNB)
- pub async fn submit_test_bundle(&self, to_address: Address, amount_wei: u128) -> Result<()> {
- info!("creating test bundle with 2 simple transfer transactions");
-
- // Get current block number
- let current_block = self.provider.get_block_number().await?;
- let target_block = current_block + 5;
-
- info!(
- current = current_block,
- target = target_block,
- "target block for test bundle"
- );
-
- // Get the first signer address
- let from_address = self
- .signers
- .keys()
- .next()
- .ok_or_else(|| eyre::eyre!("no signers available"))?;
-
- // Get current gas price
- let gas_price = self.provider.get_gas_price().await?;
- info!(gas_price = %gas_price, "current gas price");
-
- // Create two simple transfer transactions
- let tx1 = TransactionRequest::default()
- .from(*from_address)
- .to(to_address)
- .value(alloy::primitives::U256::from(amount_wei))
- .gas_limit(21000)
- .gas_price(gas_price);
-
- let tx2 = TransactionRequest::default()
- .from(*from_address)
- .to(to_address)
- .value(alloy::primitives::U256::from(amount_wei))
- .gas_limit(21000)
- .gas_price(gas_price);
-
- info!(
- from = ?from_address,
- to = ?to_address,
- amount_wei = amount_wei,
- "created 2 test transactions"
- );
-
- // Create bundle
- let bundle = Bundle {
- block: target_block,
- txs: vec![SignedOrUnsignedTx::Unsigned(tx1), SignedOrUnsignedTx::Unsigned(tx2)],
- };
-
- // Submit the bundle
- self.submit_bundle(bundle).await
- }
-}
-
-#[async_trait]
-impl harpoon::IExecutor for BundleExecutor
-where
- P: Provider + Clone + Send + Sync + 'static,
- N: Network + Send + Sync + 'static,
-{
- fn name(&self) -> &str {
- "Bundle Executor (bloXroute BSC)"
- }
-
- async fn execute(&self, action: Action) -> Result<()> {
- match action {
- Action::SendBundle(bundle) => {
- if let Err(err) = self.submit_bundle(bundle).await {
- error!("failed to submit bundle: {err:#}");
- }
- }
- }
- Ok(())
- }
-}
diff --git a/src/sandwich/logger.rs b/src/sandwich/logger.rs
deleted file mode 100644
index 0065f3e..0000000
--- a/src/sandwich/logger.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
-
-const LOG_LEVEL: &str = "info";
-
-/// Initialize the logger
-pub fn init() {
- let default_filter = format!("{}", LOG_LEVEL);
-
- let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| default_filter.into());
-
- use time::UtcOffset;
- use tracing_subscriber::fmt::time::OffsetTime;
-
- let timer = OffsetTime::new(
- UtcOffset::from_hms(8, 0, 0).unwrap(),
- time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
- );
-
- tracing_subscriber::registry()
- .with(filter)
- .with(
- tracing_subscriber::fmt::layer()
- .with_timer(timer)
- .with_target(false)
- .with_thread_ids(false)
- .with_level(true),
- )
- .init();
-}
diff --git a/src/sandwich/meme.rs b/src/sandwich/meme.rs
deleted file mode 100644
index a3fbb2e..0000000
--- a/src/sandwich/meme.rs
+++ /dev/null
@@ -1,280 +0,0 @@
-use std::collections::HashSet;
-
-use crate::sandwich::abi::Fourmeme;
-use crate::sandwich::abi::{fourmeme, ierc20};
-use crate::sandwich::constant::{ETHER, FOUR_MEME, GWEI};
-use alloy::{
- consensus::Transaction,
- eips::BlockId,
- network::Network,
- primitives::{Address, U256},
- providers::Provider,
- rpc::types::Block,
- sol_types::SolCall,
- uint,
-};
-use eyre::eyre;
-
-#[derive(Clone, Debug, Hash, PartialEq, Eq)]
-pub struct Buy {
- pub token: Address,
- pub tx_value: U256,
- pub amount: U256,
- pub min_received: U256,
-}
-
-impl TryFrom<&dyn Transaction> for Buy {
- type Error = ();
-
- fn try_from(tx: &dyn Transaction) -> Result {
- if !tx.to().map(|to| to == FOUR_MEME).unwrap_or(false) {
- return Err(());
- }
-
- let target_selector1 = Fourmeme::buyTokenAMAP_0Call::SELECTOR;
- let target_selector2 = Fourmeme::buyTokenAMAP_1Call::SELECTOR;
-
- let condition1 = tx.input().starts_with(&target_selector1) && tx.input().len() == 4 + 32 * 3;
- let condition2 = tx.input().starts_with(&target_selector2) && tx.input().len() == 4 + 32 * 4;
- // let condition3 = tx.input().starts_with(&target_selector3) && tx.input().len() == 4 + 32 * 3;
-
- if condition1 {
- let decoded_input = Fourmeme::buyTokenAMAP_0Call::abi_decode(tx.input()).unwrap();
- return Ok(Buy {
- tx_value: tx.value(),
- token: decoded_input.token,
- amount: decoded_input.funds,
- min_received: decoded_input.minAmount,
- });
- }
-
- if condition2 {
- let decoded_input = Fourmeme::buyTokenAMAP_1Call::abi_decode(tx.input()).unwrap();
- return Ok(Buy {
- tx_value: tx.value(),
- token: decoded_input.token,
- amount: decoded_input.funds,
- min_received: decoded_input.minAmount,
- });
- }
-
- // TODO: how to calculate the min receive?
- // if condition3 {
- // return Ok(Buy {
- // tx_value: tx.value(),
- // token: Address::from_slice(&tx.input()[16..36]),
- // amount: U256::from_be_slice(&tx.input()[36..68]),
- // min_received: U256::from_be_slice(&tx.input()[68..100]),
- // });
- // }
-
- return Err(());
- }
-}
-
-#[derive(Debug, Hash, PartialEq, Eq)]
-pub struct Sell {
- pub token: Address,
- pub amount: U256,
-}
-
-impl TryFrom<&dyn Transaction> for Sell {
- type Error = ();
-
- fn try_from(tx: &dyn Transaction) -> Result {
- if !tx.to().map(|to| to == FOUR_MEME).unwrap_or(false) {
- return Err(());
- }
-
- let target_selector1 = Fourmeme::sellToken_0Call::SELECTOR;
- let target_selector2 = Fourmeme::sellToken_1Call::SELECTOR;
-
- let condition1 = tx.input().starts_with(&target_selector1) && tx.input().len() == 4 + 32 * 3;
- let condition2 = tx.input().starts_with(&target_selector2) && tx.input().len() == 4 + 32 * 2;
-
- // TODO: should we add the min receive check?
- if condition1 {
- let decoded_input = Fourmeme::sellToken_0Call::abi_decode(tx.input()).unwrap();
- return Ok(Self {
- token: decoded_input.token,
- amount: decoded_input.amount,
- });
- }
-
- if condition2 {
- let decoded_input = Fourmeme::sellToken_1Call::abi_decode(tx.input()).unwrap();
- return Ok(Self {
- token: decoded_input.token,
- amount: decoded_input.amount,
- });
- }
-
- return Err(());
- }
-}
-
-#[derive(Debug, Copy, Clone)]
-pub struct TokenInfo {
- pub k: U256,
- pub t: U256,
- pub offer: U256,
- pub ether: U256,
-}
-
-pub async fn get_fee_rate(provider: P, block_id: BlockId) -> eyre::Result
-where
- P: Provider,
- N: Network,
-{
- provider
- .get_storage_at(FOUR_MEME, uint!(0x164_U256))
- .block_id(block_id)
- .await
- .map_err(|err| eyre!("{err:#}"))
-}
-
-pub async fn get_min_fee(provider: P, block_id: BlockId) -> eyre::Result
-where
- P: Provider,
- N: Network,
-{
- provider
- .get_storage_at(FOUR_MEME, uint!(0x163_U256))
- .block_id(block_id)
- .await
- .map_err(|err| eyre!("{err:#}"))
-}
-
-pub async fn get_balance(provider: P, token: Address, account: Address, block: BlockId) -> eyre::Result
-where
- P: Provider,
- N: Network,
-{
- let c = ierc20::IERC20::new(token, provider);
-
- c.balanceOf(account)
- .block(block)
- .call()
- .await
- .map(|r| r.into())
- .map_err(|err| eyre!("fail to call balanceOf(): {err:#}"))
-}
-
-pub async fn get_allowance(
- provider: P,
- token: Address,
- owner: Address,
- spender: Address,
- block: BlockId,
-) -> eyre::Result
-where
- P: Provider,
- N: Network,
-{
- let c = ierc20::IERC20::new(token, provider);
-
- c.allowance(owner, spender)
- .block(block)
- .call()
- .await
- .map(|r| r.into())
- .map_err(|err| eyre!("fail to call allowance(): {err:#}"))
-}
-
-pub async fn get_token_info(provider: P, token: Address, block_id: BlockId) -> eyre::Result
-where
- P: Provider,
- N: Network,
-{
- let contract = fourmeme::Fourmeme::new(FOUR_MEME, provider);
- let infos = contract._tokenInfos(token).block(block_id).call().await?;
-
- Ok(TokenInfo {
- k: infos.K,
- t: infos.T,
- offer: infos.offers,
- ether: infos.funds,
- })
-}
-
-// 原理:计算买入所有可用代币(offer)时价格的增幅。这个增幅代表了从当前价格到最高价格的差值。
-// 在bonding curve中,当供应减少时价格上升,这个函数计算理论上的最大价格变化。
-pub fn calc_max_buy(token: &TokenInfo) -> Option {
- (token.k * ETHER)
- .checked_div(token.t.checked_sub(token.offer)?)?
- .checked_sub((token.k * ETHER).checked_div(token.t)?)
-}
-
-// 原理:这是一个数值方法,用于计算在支付expect_buy(预期成本)的情况下,实际能买入多少代币。
-// 这是通过迭代逼近来求解bonding curve方程。
-pub fn calc_actual_buy(expect_buy: U256, token: &TokenInfo) -> Option {
- let v0 = (token.k * ETHER).checked_div(token.t)?;
- let v1 = (token.k * ETHER).checked_div(expect_buy.checked_add(v0)?)?;
- Some(token.t.checked_sub(v1)? / GWEI * GWEI)
-}
-
-// 原理:计算买入actual_buy数量代币所需的BNB成本。在bonding curve中,买入代币会使价格上升,成本等于价格变化量。
-// 这是简化的近似计算(实际应该用积分,但这里用差值近似)。
-pub fn calc_buy(actual_buy: U256, token: &TokenInfo) -> Option {
- let v0 = (token.k * ETHER).checked_div(token.t.checked_sub(actual_buy)?)?;
- let v1 = (token.k * ETHER).checked_div(token.t)?;
- Some(v0 - v1)
-}
-
-// 原理:计算卖出token_amount代币能获得的BNB收益。卖出代币会使价格下降,收益等于价格变化量。
-pub fn calc_sell(token_amount: U256, token: &TokenInfo) -> Option {
- let v0 = (token.k * ETHER).checked_div(token.t)?;
- let v1 = (token.k * ETHER).checked_div(token_amount + token.t)?;
- Some(v0 - v1)
-}
-
-/// MinFee on slot 0x163
-/// FeeRate on slot 0x164
-pub fn calc_fee(ether: U256, fee_rate: U256, min_fee: U256) -> U256 {
- let fee = fee_rate * ether / uint!(10_000_U256);
- fee.max(min_fee)
-}
-
-/*
-作用:更新买入交易后的池子状态
-原理:遵循AMM(自动做市商)模型的基本规则
-状态变化:
-代币储备减少:t -= actual_buy,offer -= actual_buy
-t: 池子中的代币总量减少
-offer: 可用代币数量减少
-BNB储备增加:ether += buy_cost
-买家支付的BNB进入池子
-*/
-pub fn post_buy_update_status(token_info: &mut TokenInfo, actual_buy: &U256, buy_cost: &U256) -> Option<()> {
- token_info.t = token_info.t.checked_sub(*actual_buy)?;
- token_info.offer = token_info.offer.checked_sub(*actual_buy)?;
- token_info.ether += buy_cost; // unlikely to overflow because ether is BNB received
- Some(())
-}
-
-pub fn find_sandwich_bot(block: &Block) -> Vec {
- let txs = match block.transactions.as_transactions() {
- None => return vec![],
- Some(tx) => tx,
- };
-
- let all_token_buyers = txs
- .iter()
- .filter_map(|tx| {
- let inner: &dyn Transaction = &*tx.inner;
- Buy::try_from(inner).ok().map(|buy| (tx.inner.signer(), buy.token))
- })
- .collect::>();
-
- let all_token_sellers = txs
- .iter()
- .filter_map(|tx| {
- let inner: &dyn Transaction = &*tx.inner;
- Sell::try_from(inner).ok().map(|sell| (tx.inner.signer(), sell.token))
- })
- .collect::>();
-
- let common = all_token_buyers.intersection(&all_token_sellers);
-
- common.map(|(user, _)| *user).collect()
-}
diff --git a/src/sandwich/mod.rs b/src/sandwich/mod.rs
deleted file mode 100644
index e512c78..0000000
--- a/src/sandwich/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-pub mod abi;
-pub mod constant;
-pub mod executor;
-pub mod logger;
-pub mod meme;
-pub mod search;
-pub mod simulate;
-pub mod start;
-pub mod strategy;
diff --git a/src/sandwich/search.rs b/src/sandwich/search.rs
deleted file mode 100644
index f84fa14..0000000
--- a/src/sandwich/search.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-use alloy::primitives::U256;
-use tracing::info;
-
-use crate::sandwich::{
- constant::{ETHER, GWEI},
- meme,
-};
-
-#[derive(Debug, Clone, Default)]
-pub struct Solution {
- pub profit: U256,
- pub ether_spent: U256,
- pub token_bought: U256,
- pub token_sold: U256,
-}
-
-#[derive(Debug, Clone)]
-pub struct Context {
- pub token_info: meme::TokenInfo,
- pub fee_rate: U256,
- pub min_fee: U256,
- pub buy: meme::Buy,
- pub token_balance: U256,
-}
-
-pub fn go(mut context: Context) -> Option {
- let mut upper_bound = meme::calc_max_buy(&context.token_info)?;
- info!("initial upper_bound: {:?}", upper_bound);
- let mut lower_bound = GWEI; // 1 gwei
- let mut solution = Solution::default();
- let new_context = context.clone();
-
- for _ in 0..100 {
- context = new_context.clone();
- let m = (upper_bound - lower_bound) / U256::from(3);
-
- let s1 = trial_ultimate_without_fee(&mut context, lower_bound + m).unwrap_or_default();
- let s2 = trial_ultimate_without_fee(&mut context, lower_bound + m + m).unwrap_or_default();
-
- if s1.profit >= s2.profit {
- upper_bound = lower_bound + m + m;
- } else {
- lower_bound += m;
- }
-
- if s1.profit > solution.profit {
- solution = s1;
- }
-
- if s2.profit > solution.profit {
- solution = s2
- }
- }
- info!("final upper_bound: {:?}", upper_bound);
- info!("final lower_bound: {:?}", lower_bound);
- info!("solution: {:?}", solution);
-
- Some(solution)
-}
-
-/// v1: we don't considerate the fee
-fn trial_ultimate_without_fee(context: &mut Context, to_buy: U256) -> Option {
- let my_token_received = meme::calc_actual_buy(to_buy, &context.token_info)?;
- let my_buy_cost = meme::calc_buy(my_token_received, &context.token_info)?;
-
- meme::post_buy_update_status(&mut context.token_info, &my_token_received, &my_buy_cost)?;
-
- // Victim buy
- let victim_actual_buy = meme::calc_actual_buy(context.buy.amount, &context.token_info)?;
- let cover_min_received = victim_actual_buy > context.buy.min_received;
- let dont_over_buy = victim_actual_buy + ETHER <= context.token_info.offer;
-
- if !(cover_min_received && dont_over_buy) {
- return None;
- }
-
- let victim_buy_cost = meme::calc_buy(victim_actual_buy, &context.token_info)?;
- meme::post_buy_update_status(&mut context.token_info, &victim_actual_buy, &victim_buy_cost)?;
-
- // We sell
- let amount_sold = my_token_received + context.token_balance - GWEI;
- let ether_received = meme::calc_sell(amount_sold, &context.token_info)?;
-
- Some(Solution {
- profit: ether_received.checked_sub(my_buy_cost).unwrap_or(U256::ZERO),
- ether_spent: my_buy_cost,
- token_sold: amount_sold,
- token_bought: to_buy,
- })
-}
-
-// fn trial_ultimate_with_fee(context: &mut Context, to_buy: U256) -> Option {
-// todo!()
-// }
diff --git a/src/sandwich/simulate.rs b/src/sandwich/simulate.rs
deleted file mode 100644
index 78d3e7b..0000000
--- a/src/sandwich/simulate.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-extern crate core;
-
-use alloy::{
- primitives::{B256, U256},
- providers::{Provider, ProviderBuilder},
-};
-use clap::Parser;
-use std::{sync::Arc, time::Instant};
-use tracing::info;
-
-use crate::sandwich::{meme, search};
-
-#[derive(Debug, Parser)]
-pub struct Args {
- tx: B256,
-
- #[arg(long, env = "RPC_URL")]
- rpc_url: String,
-}
-
-// tx1: cargo run -- sandwich simulate 0xf2b34277bedded976ae9293f190a84b8c15223fdaa52b7d7e9697caee295689f --rpc-url
-// tx2: cargo run -- sandwich simulate 0xdf06039109525245665d9c54cae1552d7da4b5d8cc14237a510212926db9c1bb --rpc-url
-pub async fn simulate(args: Args) {
- let provider = ProviderBuilder::new().connect_http(args.rpc_url.parse().unwrap());
-
- let provider = Arc::new(provider);
-
- let tx = provider
- .get_transaction_by_hash(args.tx)
- .await
- .expect("failed to get transaction")
- .expect("transaction not found");
-
- let inner: &dyn alloy::consensus::Transaction = &*tx.inner;
- let buy = meme::Buy::try_from(inner).expect("not a buy tx");
-
- let block = provider
- .root()
- .raw_request::<_, Option>(
- "eth_getBlockByNumber".into(),
- (format!("0x{:x}", tx.block_number.unwrap()), false),
- )
- .await
- .expect("failed to get block")
- .expect("block not found");
-
- let block_number = block.header.number;
-
- let token_info = meme::get_token_info(Arc::clone(&provider), buy.token, block_number.into())
- .await
- .expect("fail to get token info");
-
- let fee_rate = meme::get_fee_rate(provider.clone(), block_number.into())
- .await
- .expect("fail to get fee rate");
-
- let min_fee = meme::get_min_fee(provider.clone(), block_number.into())
- .await
- .expect("fail to get min fee");
- info!("fee rate: {}, min fee: {}", fee_rate, min_fee);
-
- let context = search::Context {
- token_info,
- fee_rate,
- min_fee,
- buy,
- token_balance: U256::ZERO,
- };
-
- let start = Instant::now();
- let solution = search::go(context).expect("cannot find solution");
- let optimal_elapsed = start.elapsed();
-
- println!("profit: {}", solution.profit);
- println!("time: {:?}", optimal_elapsed);
-}
diff --git a/src/sandwich/start.rs b/src/sandwich/start.rs
deleted file mode 100644
index 4c6c908..0000000
--- a/src/sandwich/start.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-use std::sync::Arc;
-
-use alloy::{
- primitives::B256,
- providers::{IpcConnect, Provider, ProviderBuilder},
- signers::{Signer, local::PrivateKeySigner},
-};
-use clap::Parser;
-use harpoon::{
- Engine,
- collector::{FullBlockCollector, MempoolCollector},
- map_collector,
-};
-
-use crate::sandwich::{
- executor::BundleExecutor,
- strategy::{Config, Event, Strategy},
-};
-
-#[derive(Debug, Parser)]
-pub struct Args {
- #[arg(long, env = "RPC_URL")]
- pub ipc_url: String,
-
- #[arg(long)]
- pub private_key: B256,
-
- #[command(flatten)]
- pub config: Config,
-}
-
-pub async fn run(args: Args) {
- tracing_subscriber::fmt::init();
-
- let ipc_path = "/tmp/reth.ipc";
- let ipc = IpcConnect::new(ipc_path.to_string());
-
- let provider = ProviderBuilder::new().connect_ipc(ipc).await.unwrap();
-
- let chain_id = provider.get_chain_id().await.expect("fail to get chain id");
-
- let signer = PrivateKeySigner::from_bytes(&args.private_key)
- .expect("fail to parse private key")
- .with_chain_id(Some(chain_id));
-
- let attacker = signer.address();
-
- let mut engine = Engine::default();
-
- engine.add_collector(map_collector!(
- MempoolCollector::new(Arc::new(provider.clone())),
- Event::PendingTx
- ));
- engine.add_collector(map_collector!(
- FullBlockCollector::new(Arc::new(provider.clone())),
- Event::FullBlock
- ));
-
- let strategy = Strategy::new(args.config.clone().into(), attacker, provider.clone());
- engine.add_strategy(Box::new(strategy));
-
- let bundle_provider = ProviderBuilder::new().connect_http("https://bsc.rpc.blxrbdn.com".parse().unwrap());
- let bundle_executor = BundleExecutor::new(bundle_provider.clone(), provider.clone(), vec![signer.clone()]);
- engine.add_executor(Box::new(bundle_executor));
-
- engine.run_and_join().await.unwrap();
-}
diff --git a/src/sandwich/strategy.rs b/src/sandwich/strategy.rs
deleted file mode 100644
index 63c478e..0000000
--- a/src/sandwich/strategy.rs
+++ /dev/null
@@ -1,389 +0,0 @@
-use std::{collections::HashSet, marker::PhantomData, num::NonZeroUsize, sync::Arc, time::Instant};
-
-use alloy::{
- network::{Network, TransactionBuilder},
- primitives::{Address, B256, Bytes, U256},
- providers::Provider,
- rpc::types::{Block, Transaction, TransactionRequest},
- sol_types::SolInterface,
-};
-use clap::Parser;
-use eyre::{ContextCompat, WrapErr};
-use harpoon::{IStrategy, interface::action_submitter::IActionSubmitter};
-use lru::LruCache;
-use tracing::{debug, error, info, instrument};
-
-use crate::sandwich::{
- abi::{
- Fourmeme::{FourmemeCalls, buyTokenAMAP_1Call, sellToken_1Call},
- IERC20::{IERC20Calls, approveCall},
- },
- constant::FOUR_MEME,
- meme::{self, find_sandwich_bot},
- search,
-};
-
-#[derive(Debug, Clone, Parser)]
-pub struct Config {
- #[arg(long, default_value = "1000000000", help = "Gas price in wei")]
- gas_price: u128,
-}
-
-pub struct Strategy {
- sender: Address,
- provider: P,
- config: Arc,
-
- /// Current block number
- block: Block,
- bots: HashSet,
- visited_tx: LruCache,
- _phantom: PhantomData,
-}
-
-#[derive(Clone, Debug)]
-pub enum Event {
- FullBlock(Block),
- PendingTx(Transaction),
-}
-
-#[derive(Clone, Debug)]
-pub enum SignedOrUnsignedTx {
- Unsigned(TransactionRequest),
- Signed(Bytes),
-}
-
-#[derive(Clone, Debug)]
-pub struct Bundle {
- pub txs: Vec,
- pub block: u64,
-}
-
-#[derive(Debug, Clone)]
-pub enum Action {
- SendBundle(Bundle),
-}
-
-impl Strategy
-where
- P: Provider + Clone,
- N: Network,
-{
- pub fn new(config: Arc, sender: Address, provider: P) -> Self {
- Self {
- sender,
- provider,
- config,
- block: Block::default(),
- bots: Default::default(),
- visited_tx: LruCache::new(NonZeroUsize::new(1000).unwrap()),
- _phantom: PhantomData,
- }
- }
-
- pub async fn on_pending_tx(&mut self, tx: Transaction, submitter: Arc>) {
- debug!(tx = %tx.inner.hash(), block = self.block.header.number, "received new transaction");
-
- let raw_tx = match self
- .provider
- .root()
- .raw_request("eth_getRawTransactionByHash".into(), tx.inner.hash())
- .await
- {
- Ok(raw_tx) => raw_tx,
- Err(err) => {
- error!(tx = %tx.inner.hash(), %err, "failed to get raw transaction");
- return;
- }
- };
-
- self.handle_tx(tx, raw_tx, submitter).await;
- }
-
- pub async fn handle_tx(&mut self, tx: Transaction, raw_tx: Bytes, submitter: Arc>) {
- if self.should_skip_tx(&tx) {
- return;
- }
-
- let inner: &dyn alloy::consensus::Transaction = &*tx.inner;
- let buy = match meme::Buy::try_from(inner) {
- Ok(buy) => buy,
- Err(_) => return,
- };
-
- info!(
- tx = %tx.inner.hash(),
- token = ?buy.token,
- amount = %buy.amount,
- min_received = %buy.min_received,
- "found new buy");
-
- self.handle_buy_optimal(tx, raw_tx, buy, submitter).await;
- }
-
- pub fn tx_visited(&mut self, tx: &Transaction) -> bool {
- if self.visited_tx.contains(tx.inner.hash()) {
- return true;
- }
-
- self.visited_tx.push(*tx.inner.hash(), ());
-
- false
- }
-
- pub fn should_skip_tx(&mut self, tx: &Transaction) -> bool {
- if tx.block_number.is_some() || tx.inner.signer() == self.sender {
- return true;
- }
-
- if self.bots.contains(&tx.inner.signer()) {
- info!(tx = %tx.inner.hash(), "skip bot tx");
- return true;
- }
-
- false
- }
-
- pub async fn get_search_context(&self, buy: meme::Buy, user: Address) -> eyre::Result {
- let block = self.block.header.number;
-
- let token_info = meme::get_token_info(self.provider.clone(), buy.token, block.into())
- .await
- .context("fail to get token info")?;
-
- let token_balance = meme::get_balance(self.provider.clone(), buy.token, user, block.into())
- .await
- .context("fail to get token balance")?;
-
- let fee_rate = meme::get_fee_rate(self.provider.clone(), block.into())
- .await
- .context("fail to get fee rate")?;
-
- let min_fee = meme::get_min_fee(self.provider.clone(), block.into())
- .await
- .context("fail to get min fee")?;
-
- Ok(search::Context {
- token_info,
- fee_rate,
- min_fee,
- buy,
- token_balance,
- })
- }
-
- #[instrument(skip_all, fields(tx = %tx.inner.hash()))]
- pub async fn handle_buy_optimal(
- &mut self,
- tx: Transaction,
- raw_tx: Bytes,
- buy: meme::Buy,
- submitter: Arc>,
- ) {
- let block = self.block.header.number;
- let context = match self.get_search_context(buy.clone(), tx.inner.signer()).await {
- Ok(c) => c,
- Err(err) => {
- error!("fail to build search context: {err:#}");
- return;
- }
- };
-
- info!(?context);
-
- let start = Instant::now();
- let solution = match search::go(context) {
- Some(s) if !s.profit.is_zero() => s,
- Some(_) => {
- info!(elapsed = ?start.elapsed(), "no profit");
- return;
- }
- None => {
- error!(elapsed = ?start.elapsed(), "failed to find optimal solution");
- return;
- }
- };
-
- info!(
- tx = %tx.inner.hash(),
- profit = %solution.profit,
- to_buy = %solution.token_bought,
- spent = %solution.ether_spent,
- token_sold = %solution.token_sold,
- elapsed = ?start.elapsed(), "found solution");
-
- let allowance =
- match meme::get_allowance(self.provider.clone(), buy.token, self.sender, FOUR_MEME, block.into()).await {
- Ok(a) => a,
- Err(err) => {
- error!("fail to get allowance: {err:#}");
- return;
- }
- };
-
- info!(%allowance);
-
- let buy_tx = TransactionRequest::default()
- .with_from(self.sender)
- .with_to(FOUR_MEME)
- .with_input(
- FourmemeCalls::buyTokenAMAP_1 {
- 0: buyTokenAMAP_1Call {
- token: buy.token,
- to: self.sender,
- funds: solution.token_bought,
- minAmount: Default::default(),
- },
- }
- .abi_encode(),
- )
- .with_value(solution.ether_spent)
- .with_gas_limit(150000)
- .with_gas_price(self.config.gas_price);
-
- let sell_tx = TransactionRequest::default()
- .with_from(self.sender)
- .with_to(FOUR_MEME)
- .with_input(
- FourmemeCalls::sellToken_1 {
- 0: sellToken_1Call {
- token: buy.token,
- amount: solution.token_sold,
- },
- }
- .abi_encode(),
- )
- .with_gas_limit(130000)
- .with_gas_price(self.config.gas_price);
-
- let approve_tx = if allowance < solution.token_sold {
- TransactionRequest::default()
- .with_from(self.sender)
- .with_to(buy.token)
- .with_input(
- IERC20Calls::approve {
- 0: approveCall {
- _spender: FOUR_MEME,
- _value: U256::MAX,
- },
- }
- .abi_encode(),
- )
- .with_gas_limit(50000)
- .with_gas_price(self.config.gas_price)
- .into()
- } else {
- None
- };
-
- let cost = self.calculate_cost(&buy_tx, &sell_tx, &approve_tx);
-
- let mut profit = U256::from(solution.profit);
- if profit < cost {
- info!("profit {profit} is less than cost {cost}");
- return;
- }
-
- profit -= cost;
-
- // Bribe?
-
- let bundle = self.build_bundle(raw_tx, buy_tx, sell_tx, approve_tx);
-
- submitter.submit(Action::SendBundle(bundle));
- }
-
- fn build_bundle(
- &mut self,
- raw_tx: Bytes,
- buy_tx: TransactionRequest,
- sell_tx: TransactionRequest,
- approve_tx: Option,
- ) -> Bundle {
- let mut bundle = Bundle {
- txs: vec![
- SignedOrUnsignedTx::Unsigned(buy_tx.with_gas_price(self.config.gas_price)),
- SignedOrUnsignedTx::Signed(raw_tx),
- ],
- block: self.block.header.number,
- };
-
- if let Some(tx) = approve_tx {
- let tx = SignedOrUnsignedTx::Unsigned(tx.with_gas_price(self.config.gas_price));
- bundle.txs.push(tx);
- }
-
- bundle.txs.push(SignedOrUnsignedTx::Unsigned(
- sell_tx.with_gas_price(self.config.gas_price),
- ));
-
- for (i, tx) in bundle.txs.iter().enumerate() {
- debug!(index = i, "tx: {tx:?}");
- }
-
- bundle
- }
-
- fn calculate_cost(
- &mut self,
- buy_tx: &TransactionRequest,
- sell_tx: &TransactionRequest,
- approve_tx: &Option,
- ) -> U256 {
- let total_gas =
- buy_tx.gas.unwrap() + approve_tx.as_ref().and_then(|t| t.gas).unwrap_or_default() + sell_tx.gas.unwrap();
-
- U256::from(total_gas) * U256::from(self.config.gas_price)
- }
-
- pub async fn on_new_block(&mut self, block: Block) {
- let bots = find_sandwich_bot(&block);
- info!(block = block.header.number, "new block, bots: {:?}", bots);
-
- for bot in bots {
- if bot == self.sender {
- continue;
- }
-
- if self.bots.contains(&bot) {
- continue;
- }
-
- self.bots.insert(bot);
- info!(%bot, "found new bot");
- }
-
- self.block = block;
- }
-}
-
-#[harpoon::async_trait]
-impl IStrategy for Strategy
-where
- P: Provider + Clone,
- N: Network,
-{
- fn name(&self) -> &str {
- "arbitrage"
- }
-
- async fn sync_state(&mut self, _submitter: Arc>) -> eyre::Result<()> {
- self.block = self
- .provider
- .root()
- .raw_request::<_, Option>("eth_getBlockByNumber".into(), ("latest", false))
- .await
- .context("fail to get latest block")?
- .context("latest block not found")?;
-
- Ok(())
- }
-
- async fn process_event(&mut self, event: Event, submitter: Arc>) {
- match event {
- Event::FullBlock(block) => self.on_new_block(block).await,
- Event::PendingTx(tx) if !self.tx_visited(&tx) => self.on_pending_tx(tx, submitter).await,
- _ => {}
- }
- }
-}
diff --git a/src/pvp/service/auto_sell.rs b/src/service/auto_sell.rs
similarity index 99%
rename from src/pvp/service/auto_sell.rs
rename to src/service/auto_sell.rs
index 2f93516..d737059 100644
--- a/src/pvp/service/auto_sell.rs
+++ b/src/service/auto_sell.rs
@@ -20,7 +20,7 @@ use chrono::{Duration as ChronoDuration, Utc};
use harpoon::bloXroute_private_tx::BloXrouteService;
use tokio::{sync::Mutex, time};
-use crate::pvp::{
+use crate::{
abi::IERC20::IERC20Instance,
config::EnvConfig,
db::db::TradeInfo,
diff --git a/src/pvp/service/db_cleaner.rs b/src/service/db_cleaner.rs
similarity index 95%
rename from src/pvp/service/db_cleaner.rs
rename to src/service/db_cleaner.rs
index 8b25e42..b5d7467 100644
--- a/src/pvp/service/db_cleaner.rs
+++ b/src/service/db_cleaner.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::Duration;
-use crate::pvp::{config::EnvConfig, interface::db::IDatabase, misc::logger::Logger};
+use crate::{config::EnvConfig, interface::db::IDatabase, misc::logger::Logger};
use tokio::time;
/// Database cleaner service that periodically removes old records
diff --git a/src/pvp/service/mod.rs b/src/service/mod.rs
similarity index 100%
rename from src/pvp/service/mod.rs
rename to src/service/mod.rs
diff --git a/src/pvp/start.rs b/src/start.rs
similarity index 99%
rename from src/pvp/start.rs
rename to src/start.rs
index 4302101..e8ec046 100644
--- a/src/pvp/start.rs
+++ b/src/start.rs
@@ -17,7 +17,7 @@ use tokio::{
time::{interval, sleep},
};
-use crate::pvp::{
+use crate::{
abi::{
Fourmeme::FourmemeInstance, PancakeSwapPair, PancakeSwapRouter::PancakeSwapRouterInstance, fourmeme::Fourmeme,
},
diff --git a/src/pvp/strategy/helper.rs b/src/strategy/helper.rs
similarity index 99%
rename from src/pvp/strategy/helper.rs
rename to src/strategy/helper.rs
index 6afeb21..5b170d2 100644
--- a/src/pvp/strategy/helper.rs
+++ b/src/strategy/helper.rs
@@ -12,7 +12,7 @@ use chrono::{DateTime, Duration, Utc};
use harpoon::calculate_pair_address;
use std::str::FromStr;
-use crate::pvp::{
+use crate::{
abi::{Fourmeme::FourmemeInstance, IERC20::IERC20Instance},
db::db::TradeInfo,
interface::{IOracle, db::IDatabase},
diff --git a/src/pvp/strategy/mod.rs b/src/strategy/mod.rs
similarity index 100%
rename from src/pvp/strategy/mod.rs
rename to src/strategy/mod.rs
diff --git a/src/pvp/strategy/strategy.rs b/src/strategy/strategy.rs
similarity index 96%
rename from src/pvp/strategy/strategy.rs
rename to src/strategy/strategy.rs
index 3be1efb..f358b45 100644
--- a/src/pvp/strategy/strategy.rs
+++ b/src/strategy/strategy.rs
@@ -10,15 +10,15 @@ use harpoon::{IStrategy, interface::action_submitter::IActionSubmitter};
use std::{marker::PhantomData, sync::Arc};
use tokio::sync::Mutex;
-use crate::pvp::abi::PancakeSwapPair;
-use crate::pvp::config::EnvConfig;
-use crate::pvp::db::db::PoolType;
-use crate::pvp::interface::db::IDatabase;
-use crate::pvp::misc::constant::ZERO_ADDRESS;
-use crate::pvp::misc::logger::Logger;
-use crate::pvp::strategy::helper::{check_is_migrated, parse_token_from_migrated_info};
-use crate::pvp::strategy::{strategy_fourmeme, strategy_pancake};
-use crate::pvp::{abi::fourmeme::Fourmeme, misc::error::CustomError};
+use crate::abi::PancakeSwapPair;
+use crate::config::EnvConfig;
+use crate::db::db::PoolType;
+use crate::interface::db::IDatabase;
+use crate::misc::constant::ZERO_ADDRESS;
+use crate::misc::logger::Logger;
+use crate::strategy::helper::{check_is_migrated, parse_token_from_migrated_info};
+use crate::strategy::{strategy_fourmeme, strategy_pancake};
+use crate::{abi::fourmeme::Fourmeme, misc::error::CustomError};
#[derive(Clone, Debug)]
pub enum Event {
diff --git a/src/pvp/strategy/strategy_fourmeme.rs b/src/strategy/strategy_fourmeme.rs
similarity index 99%
rename from src/pvp/strategy/strategy_fourmeme.rs
rename to src/strategy/strategy_fourmeme.rs
index ad4cef4..ebdd47e 100644
--- a/src/pvp/strategy/strategy_fourmeme.rs
+++ b/src/strategy/strategy_fourmeme.rs
@@ -1,4 +1,4 @@
-use crate::pvp::{
+use crate::{
config::EnvConfig,
db::db::{MarketTradeInfo, MigratedTokenInfo, PoolType},
interface::db::IDatabase,
diff --git a/src/pvp/strategy/strategy_pancake.rs b/src/strategy/strategy_pancake.rs
similarity index 99%
rename from src/pvp/strategy/strategy_pancake.rs
rename to src/strategy/strategy_pancake.rs
index 6c23729..0a74c0d 100644
--- a/src/pvp/strategy/strategy_pancake.rs
+++ b/src/strategy/strategy_pancake.rs
@@ -1,4 +1,4 @@
-use crate::pvp::{
+use crate::{
config::EnvConfig,
db::db::{MarketTradeInfo, PoolType},
interface::db::IDatabase,
diff --git a/src/pvp/trade/README.md b/src/trade/README.md
similarity index 100%
rename from src/pvp/trade/README.md
rename to src/trade/README.md
diff --git a/src/pvp/trade/inner_pool_trader.rs b/src/trade/inner_pool_trader.rs
similarity index 99%
rename from src/pvp/trade/inner_pool_trader.rs
rename to src/trade/inner_pool_trader.rs
index 5154905..9d8ec7f 100644
--- a/src/pvp/trade/inner_pool_trader.rs
+++ b/src/trade/inner_pool_trader.rs
@@ -16,7 +16,7 @@ use harpoon::{
};
use std::{collections::HashMap, sync::Arc};
-use crate::pvp::{
+use crate::{
abi::{
Fourmeme::{self, FourmemeInstance},
IERC20::IERC20Instance,
@@ -193,7 +193,7 @@ where
let decimals = decimals as i64;
self.db
- .insert_token_info(&crate::pvp::db::db::TokenInfo {
+ .insert_token_info(&crate::db::db::TokenInfo {
chain_id: BSC_CHAIN_ID,
token_address: token_address.clone(),
decimals,
@@ -514,7 +514,7 @@ where
let decimals = decimals as i64;
self.db
- .insert_token_info(&crate::pvp::db::db::TokenInfo {
+ .insert_token_info(&crate::db::db::TokenInfo {
chain_id: BSC_CHAIN_ID,
token_address: token_address.clone(),
decimals,
diff --git a/src/pvp/trade/mod.rs b/src/trade/mod.rs
similarity index 100%
rename from src/pvp/trade/mod.rs
rename to src/trade/mod.rs
diff --git a/src/pvp/trade/oracle.rs b/src/trade/oracle.rs
similarity index 99%
rename from src/pvp/trade/oracle.rs
rename to src/trade/oracle.rs
index 6d55534..c3d63bd 100644
--- a/src/pvp/trade/oracle.rs
+++ b/src/trade/oracle.rs
@@ -7,7 +7,7 @@ use anyhow::Result;
use async_trait::async_trait;
use std::{marker::PhantomData, sync::Arc};
-use crate::pvp::{
+use crate::{
abi::Fourmeme::FourmemeInstance,
interface::IOracle,
misc::constant::FOURMEME_CONTRACT,
diff --git a/src/pvp/trade/outer_pool_trader.rs b/src/trade/outer_pool_trader.rs
similarity index 99%
rename from src/pvp/trade/outer_pool_trader.rs
rename to src/trade/outer_pool_trader.rs
index 2c5d7c4..d31c1b6 100644
--- a/src/pvp/trade/outer_pool_trader.rs
+++ b/src/trade/outer_pool_trader.rs
@@ -17,7 +17,7 @@ use harpoon::{
use std::sync::Arc;
-use crate::pvp::{
+use crate::{
abi::{IERC20::IERC20Instance, PancakeSwapPair, PancakeSwapRouter::PancakeSwapRouterInstance},
config::EnvConfig,
db::db::{PoolType, TradeInfo},
@@ -197,7 +197,7 @@ where
let decimals = decimals as i64;
self.db
- .insert_token_info(&crate::pvp::db::db::TokenInfo {
+ .insert_token_info(&crate::db::db::TokenInfo {
chain_id: BSC_CHAIN_ID,
token_address,
decimals,
diff --git a/src/pvp/trade/types.rs b/src/trade/types.rs
similarity index 100%
rename from src/pvp/trade/types.rs
rename to src/trade/types.rs
diff --git a/tests/db.rs b/tests/db.rs
index 19fc551..ccc757a 100644
--- a/tests/db.rs
+++ b/tests/db.rs
@@ -1,5 +1,5 @@
use chrono::Utc;
-use grouper::pvp::{
+use grouper::{
db::db::{Database, MarketTradeInfo, MigratedTokenInfo, PoolType, TokenInfo, TradeInfo},
interface::db::IDatabase,
};
From 09d1b0b8803cd28267899072eeeadc83fa309724 Mon Sep 17 00:00:00 2001
From: chenqin <490358423@qq.com>
Date: Sun, 4 Jan 2026 21:54:37 +0800
Subject: [PATCH 2/2] doc: cleanup
---
LICENSE | 21 ++++
README.md | 64 ++++++++++++-
doc/paper/README.md | 6 ++
doc/paper/README_CN.md | 7 --
...a43fc31a73013089df59194872ecae4cae14444.md | 12 ---
...547010f8bdc8de796e2b80144f92d9815ef4444.md | 13 ---
.../assets/image-20251116222302758.png | Bin 159225 -> 0 bytes
.../assets/image-20251116224242836.png | Bin 713407 -> 0 bytes
.../assets/image-20251116224817762.png | Bin 1232193 -> 0 bytes
.../assets/image-20251116225350354.png | Bin 826988 -> 0 bytes
.../assets/image-20251116225513937.png | Bin 39213 -> 0 bytes
.../assets/image-20251116225759670.png | Bin 712823 -> 0 bytes
.../assets/image-20251116230207477.png | Bin 221401 -> 0 bytes
.../assets/image-20251116230353647.png | Bin 798267 -> 0 bytes
.../assets/image-20251116230852566.png | Bin 155382 -> 0 bytes
.../assets/image-20251116231017344.png | Bin 904866 -> 0 bytes
.../assets/image-20251116231532040.png | Bin 50189 -> 0 bytes
.../assets/image-20251116231722655.png | Bin 576898 -> 0 bytes
.../assets/image-20251116231905950.png | Bin 284980 -> 0 bytes
.../assets/image-20251116232024470.png | Bin 697565 -> 0 bytes
.../assets/image-20251116232235576.png | Bin 1183111 -> 0 bytes
.../assets/image-20251116232248774.png | Bin 42303 -> 0 bytes
.../assets/image-20251116232554314.png | Bin 216671 -> 0 bytes
.../assets/image-20251116232833688.png | Bin 797395 -> 0 bytes
.../assets/image-20251117144235604.png | Bin 352967 -> 0 bytes
.../assets/image-20251117144856079.png | Bin 304010 -> 0 bytes
.../assets/image-20251117145015337.png | Bin 380364 -> 0 bytes
.../assets/image-20251117145221276.png | Bin 307734 -> 0 bytes
.../assets/image-20251117145548996.png | Bin 252946 -> 0 bytes
.../assets/image-20251117145715383.png | Bin 503400 -> 0 bytes
.../assets/image-20251117150010730.png | Bin 419192 -> 0 bytes
.../assets/image-20251117150234603.png | Bin 353970 -> 0 bytes
.../assets/image-20251117150420835.png | Bin 414611 -> 0 bytes
.../assets/image-20251117150556619.png | Bin 395329 -> 0 bytes
.../assets/image-20251117150717278.png | Bin 835149 -> 0 bytes
.../assets/image-20251117150813009.png | Bin 576464 -> 0 bytes
.../assets/image-20251117151144922.png | Bin 590278 -> 0 bytes
.../assets/image-20251117151502392.png | Bin 642433 -> 0 bytes
.../assets/image-20251117151527661.png | Bin 406786 -> 0 bytes
.../assets/image-20251117151759370.png | Bin 764980 -> 0 bytes
.../assets/image-20251117151936577.png | Bin 253899 -> 0 bytes
.../assets/image-20251117175517917.png | Bin 483168 -> 0 bytes
.../assets/image-20251129181336123.png | Bin 175803 -> 0 bytes
.../assets/image-20251129181501263.png | Bin 242078 -> 0 bytes
.../assets/image-20251129182334475.png | Bin 419199 -> 0 bytes
.../assets/image-20251129182408596.png | Bin 218743 -> 0 bytes
.../assets/image-20251129182713755.png | Bin 192774 -> 0 bytes
.../assets/image-20251129182806839.png | Bin 210724 -> 0 bytes
.../assets/image-20251129183053365.png | Bin 203307 -> 0 bytes
...8352620afa34ad8c82492661481d7ab62634444.md | 11 ---
research/dataset/data.csv | 1 -
research/dataset/research.md | 28 ------
...21\345\260\204\344\271\213\345\220\216.md" | 88 -----------------
...ec31d69b3c289e541b50e30681fd1acad24444.md" | 13 ---
...c8e2c113d5d7824ac6ebe82a3c60b1b9d64444.md" | 11 ---
...4fa68a0fc644485b8df8abfa0a41c2e7744444.md" | 14 ---
...cf1c8e95ba11bf727c0428c4b3bbd6dc064444.md" | 11 ---
...0e9b7091258cdf578136ec8394daea2db84444.md" | 10 --
research/models.md | 10 ++
research/models_CN.md | 15 ---
research/strategy_CN.md | 90 ------------------
61 files changed, 96 insertions(+), 329 deletions(-)
create mode 100644 LICENSE
create mode 100644 doc/paper/README.md
delete mode 100644 doc/paper/README_CN.md
delete mode 100644 research/dataset/4_0x0a43fc31a73013089df59194872ecae4cae14444.md
delete mode 100644 research/dataset/MOME_0x0547010f8bdc8de796e2b80144f92d9815ef4444.md
delete mode 100644 research/dataset/assets/image-20251116222302758.png
delete mode 100644 research/dataset/assets/image-20251116224242836.png
delete mode 100644 research/dataset/assets/image-20251116224817762.png
delete mode 100644 research/dataset/assets/image-20251116225350354.png
delete mode 100644 research/dataset/assets/image-20251116225513937.png
delete mode 100644 research/dataset/assets/image-20251116225759670.png
delete mode 100644 research/dataset/assets/image-20251116230207477.png
delete mode 100644 research/dataset/assets/image-20251116230353647.png
delete mode 100644 research/dataset/assets/image-20251116230852566.png
delete mode 100644 research/dataset/assets/image-20251116231017344.png
delete mode 100644 research/dataset/assets/image-20251116231532040.png
delete mode 100644 research/dataset/assets/image-20251116231722655.png
delete mode 100644 research/dataset/assets/image-20251116231905950.png
delete mode 100644 research/dataset/assets/image-20251116232024470.png
delete mode 100644 research/dataset/assets/image-20251116232235576.png
delete mode 100644 research/dataset/assets/image-20251116232248774.png
delete mode 100644 research/dataset/assets/image-20251116232554314.png
delete mode 100644 research/dataset/assets/image-20251116232833688.png
delete mode 100644 research/dataset/assets/image-20251117144235604.png
delete mode 100644 research/dataset/assets/image-20251117144856079.png
delete mode 100644 research/dataset/assets/image-20251117145015337.png
delete mode 100644 research/dataset/assets/image-20251117145221276.png
delete mode 100644 research/dataset/assets/image-20251117145548996.png
delete mode 100644 research/dataset/assets/image-20251117145715383.png
delete mode 100644 research/dataset/assets/image-20251117150010730.png
delete mode 100644 research/dataset/assets/image-20251117150234603.png
delete mode 100644 research/dataset/assets/image-20251117150420835.png
delete mode 100644 research/dataset/assets/image-20251117150556619.png
delete mode 100644 research/dataset/assets/image-20251117150717278.png
delete mode 100644 research/dataset/assets/image-20251117150813009.png
delete mode 100644 research/dataset/assets/image-20251117151144922.png
delete mode 100644 research/dataset/assets/image-20251117151502392.png
delete mode 100644 research/dataset/assets/image-20251117151527661.png
delete mode 100644 research/dataset/assets/image-20251117151759370.png
delete mode 100644 research/dataset/assets/image-20251117151936577.png
delete mode 100644 research/dataset/assets/image-20251117175517917.png
delete mode 100644 research/dataset/assets/image-20251129181336123.png
delete mode 100644 research/dataset/assets/image-20251129181501263.png
delete mode 100644 research/dataset/assets/image-20251129182334475.png
delete mode 100644 research/dataset/assets/image-20251129182408596.png
delete mode 100644 research/dataset/assets/image-20251129182713755.png
delete mode 100644 research/dataset/assets/image-20251129182806839.png
delete mode 100644 research/dataset/assets/image-20251129183053365.png
delete mode 100644 research/dataset/crypto-guy_0x08352620afa34ad8c82492661481d7ab62634444.md
delete mode 100644 research/dataset/data.csv
delete mode 100644 research/dataset/research.md
delete mode 100644 "research/dataset/\345\217\221\345\260\204\344\271\213\345\220\216.md"
delete mode 100644 "research/dataset/\345\223\210\345\237\272\347\261\263_0x82ec31d69b3c289e541b50e30681fd1acad24444.md"
delete mode 100644 "research/dataset/\345\256\242\346\234\215\345\260\217\344\275\225_0x3ac8e2c113d5d7824ac6ebe82a3c60b1b9d64444.md"
delete mode 100644 "research/dataset/\345\270\201\345\256\211\344\272\272\347\224\237_0x924fa68a0fc644485b8df8abfa0a41c2e7744444.md"
delete mode 100644 "research/dataset/\345\270\201\345\256\211\346\226\207\345\214\226_0x3ecf1c8e95ba11bf727c0428c4b3bbd6dc064444.md"
delete mode 100644 "research/dataset/\351\251\254\345\210\260\346\210\220\345\212\237_0x730e9b7091258cdf578136ec8394daea2db84444.md"
create mode 100644 research/models.md
delete mode 100644 research/models_CN.md
delete mode 100644 research/strategy_CN.md
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e9a4dc7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-2025 draganrakita
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e64b81..e09e883 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,63 @@
# Grouper
-## PVP
+## What is grouper
+
+Grouper is a fourmeme sniper bot built on [Alloy](https://github.com/alloy-rs/alloy) that is triggered by on-chain trading activity to drive its buy and sell decisions.
+
+Features:
+- Sends private transactions—no worries about slippage or sandwich attacks
+- Uses SQLite for data storage: works out of the box with zero extra config
+- Recognizes bundle transactions
+- Trades on both Fourmeme and PancakeSwap platforms
+- Calculates migration progress
+- Integrated with Telegram for real-time alerts on bot activity
+- Market-driven execution with highly customizable strategies
+
+## Quick start
+
+1. In the root directory, create a `.env` file based on `.env.example`.
+
+ ```bash
+ # account
+ BOT_PRIVATE_KEY=
+
+ # rpc
+ BLOXROUTE_AUTH_HEADER=
+ IPC_URL=
+
+ # tg bot
+ TELEGRAM_BOT_TOKEN=
+ TELEGRAM_BOT_CHAT_ID=
+ ```
+
+2. Update the config base on your strategy, or just use the default strategy for quick start.
+
+3. Start the bot:
-1. Add `.env` file
-2. Start the bot:
```bash
-make pvp
-```
\ No newline at end of file
+cargo run .
+```
+
+## Acknowledgements
+
+This forumeme sniper bot only provides a framework and a default trading strategy; it does not guarantee profit. Developers need to conduct their own research and formulate their own trading strategies. Any property loss is not related to this repository.
+
+None of this would have been possible without them, so big shoutout to the materials below:
+
+- [alloy](https://github.com/alloy-rs/alloy): Provided comprehensive and convenient blockchain interaction tools;
+- [burberry](https://github.com/tonyke-bot/burberry): Provided a framework for transaction execution, which itself was modified from [artemis](https://github.com/paradigmxyz/artemis/)
+- Some very outstanding papers, which provide theoretical assistance:
+ - [A Midsummer Meme's Dream: Investigating Market Manipulations in the Meme Coin Ecosystem](https://arxiv.org/abs/2507.01963)
+ - [Ready, Aim, Snipe! Analysis of Sniper Bots and their Impact on the DeFi Ecosystem](https://dl.acm.org/doi/10.1145/3543873.3587612)
+ - [Token Spammers, Rug Pulls, and SniperBots: An Analysis of the Ecosystem of Tokens in Ethereum and in the Binance Smart Chain (BNB)](https://arxiv.org/abs/2206.08202)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/paper/README.md b/doc/paper/README.md
new file mode 100644
index 0000000..c1044b8
--- /dev/null
+++ b/doc/paper/README.md
@@ -0,0 +1,6 @@
+## Paper
+- 2507.01963v1: Systematically introduces the market situation, price manipulation, and various methods of money laundering and running away with the money of Meme tokens. A large amount of data is used as evidence. Its core conclusion is that a significant increase in the price of a Meme token is highly likely to be due to manipulation.
+
+- 3543873.3587612: Discusses the situation of sniping robots on ETH and BSC. How to snipe, the risks of sniping, the returns from sniping, security issues, and how to detect sniping robots.
+
+- 2206.08202v3: Focuses on the connection between sniping robots and RugPull.
\ No newline at end of file
diff --git a/doc/paper/README_CN.md b/doc/paper/README_CN.md
deleted file mode 100644
index d89a99b..0000000
--- a/doc/paper/README_CN.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# 论文总结
-
-## 论文概述
-
-- 2507.01963v1: 系统性地介绍了Meme代币的市场情况、价格等操纵、卷钱跑路的各种手段。使用了大量的数据作为佐证。其核心验证了:一个Meme代币的大涨,绝大概率是认为操纵的。
-- 3543873.3587612: 讲述了ETH和BSC上的狙击机器人情况。如何狙击、狙击的风险、狙击的收益、安全性问题、如何检测狙击机器人
-- 2206.08202v3: 重点讲述了狙击机器人和RugPull之间的联系
\ No newline at end of file
diff --git a/research/dataset/4_0x0a43fc31a73013089df59194872ecae4cae14444.md b/research/dataset/4_0x0a43fc31a73013089df59194872ecae4cae14444.md
deleted file mode 100644
index 8746353..0000000
--- a/research/dataset/4_0x0a43fc31a73013089df59194872ecae4cae14444.md
+++ /dev/null
@@ -1,12 +0,0 @@
-- 添加池子前后:
-
-
-
-
-
-- Fourmeme_model: mode 0l
-- pancake_model: model 2
- - 开始:10-01 12:08
- - 顶峰:10-08 20:00
-
-
\ No newline at end of file
diff --git a/research/dataset/MOME_0x0547010f8bdc8de796e2b80144f92d9815ef4444.md b/research/dataset/MOME_0x0547010f8bdc8de796e2b80144f92d9815ef4444.md
deleted file mode 100644
index 9c3a4ee..0000000
--- a/research/dataset/MOME_0x0547010f8bdc8de796e2b80144f92d9815ef4444.md
+++ /dev/null
@@ -1,13 +0,0 @@
-- 添加池子的前后:
-
-
-
-
-- fourmeme_model: model 2
-
-
-- pancake_model: model A
- - 开始:05-20 15:01
- - 顶峰:05-25 08:00
-
-
diff --git a/research/dataset/assets/image-20251116222302758.png b/research/dataset/assets/image-20251116222302758.png
deleted file mode 100644
index 70f5623a78dfc0d81b918bfb9d95e9382925b37a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 159225
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%0Dwq38VL|YcFWGmkEjCztxDNQ34eI*dr>RHuR
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