oxidized_builder/core/
strategy.rs

1use crate::common::constants::MIN_PROFIT_THRESHOLD_WEI;
2use crate::common::error::AppError;
3use crate::core::executor::{BundleItem, SharedBundleSender};
4use crate::core::nonce::NonceManager;
5use crate::core::portfolio::PortfolioManager;
6use crate::core::safety::SafetyGuard;
7use crate::core::simulation::Simulator;
8use crate::data::db::Database;
9use crate::network::gas::{GasFees, GasOracle};
10use crate::network::price_feed::PriceFeed;
11use crate::network::provider::HttpProvider;
12use crate::network::mev_share::MevShareHint;
13use alloy::consensus::{SignableTransaction, Transaction as ConsensusTxTrait, TxEip1559};
14use alloy::eips::eip2718::Encodable2718;
15use alloy::network::{TransactionResponse, TxSignerSync};
16use alloy::primitives::{Address, TxKind, B256, U256};
17use alloy::providers::Provider;
18use alloy::rpc::types::eth::state::StateOverridesBuilder;
19use alloy::rpc::types::eth::Transaction;
20use alloy::rpc::types::eth::TransactionInput;
21use alloy::rpc::types::eth::TransactionRequest;
22use alloy::rpc::types::Header;
23use alloy::signers::local::PrivateKeySigner;
24use alloy::sol;
25use alloy::sol_types::SolCall;
26use alloy_consensus::TxEnvelope;
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::sync::Arc;
29use std::collections::HashSet;
30use tokio::sync::{broadcast::Receiver, mpsc::UnboundedReceiver};
31use crate::common::retry::retry_async;
32use std::time::Duration;
33
34#[derive(Debug)]
35pub enum StrategyWork {
36    Mempool(Transaction),
37    MevShareHint(MevShareHint),
38}
39
40sol! {
41    #[derive(Debug, PartialEq, Eq)]
42    #[sol(rpc)]
43    contract UniV2Router {
44        function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable returns (uint256[] memory amounts);
45        function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) returns (uint256[] memory amounts);
46        function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) returns (uint256[] memory amounts);
47        function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts);
48    }
49
50    #[derive(Debug, PartialEq, Eq)]
51    #[sol(rpc)]
52    contract UniV3Router {
53        struct ExactInputSingleParams {
54            address tokenIn;
55            address tokenOut;
56            uint24 fee;
57            address recipient;
58            uint256 deadline;
59            uint256 amountIn;
60            uint256 amountOutMinimum;
61            uint160 sqrtPriceLimitX96;
62        }
63        struct ExactInputParams {
64            bytes path;
65            address recipient;
66            uint256 deadline;
67            uint256 amountIn;
68            uint256 amountOutMinimum;
69        }
70        function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
71        function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
72    }
73}
74
75use UniV2Router::{swapExactETHForTokensCall, swapExactTokensForETHCall, swapExactTokensForTokensCall};
76
77#[derive(Default)]
78pub struct StrategyStats {
79    pub processed: AtomicU64,
80    pub submitted: AtomicU64,
81    pub skipped: AtomicU64,
82    pub failed: AtomicU64,
83    pub skip_unknown_router: AtomicU64,
84    pub skip_decode_failed: AtomicU64,
85    pub skip_missing_wrapped: AtomicU64,
86    pub skip_gas_cap: AtomicU64,
87    pub skip_sim_failed: AtomicU64,
88    pub skip_profit_guard: AtomicU64,
89    pub skip_unsupported_router: AtomicU64,
90    pub skip_token_call: AtomicU64,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94enum RouterKind {
95    V2Like,
96    V3Like,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100enum SwapDirection {
101    BuyWithEth,
102    SellForEth,
103    Other,
104}
105
106pub struct StrategyExecutor {
107    tx_rx: UnboundedReceiver<StrategyWork>,
108    mut_block_rx: Receiver<Header>,
109    safety_guard: Arc<SafetyGuard>,
110    bundle_sender: SharedBundleSender,
111    db: Database,
112    portfolio: Arc<PortfolioManager>,
113    gas_oracle: GasOracle,
114    price_feed: PriceFeed,
115    chain_id: u64,
116    stats: Arc<StrategyStats>,
117    max_gas_price_gwei: u64,
118    simulator: Simulator,
119    signer: PrivateKeySigner,
120    nonce_manager: NonceManager,
121    slippage_bps: u64,
122    http_provider: HttpProvider,
123    dry_run: bool,
124    router_allowlist: HashSet<Address>,
125    wrapped_native: Address,
126}
127
128impl StrategyExecutor {
129    fn log_skip(&self, reason: &str, detail: &str) {
130        if self.dry_run {
131            tracing::info!(target: "strategy_skip", %reason, %detail, "Dry-run skip");
132        } else {
133            tracing::debug!(target: "strategy_skip", %reason, %detail);
134        }
135
136        match reason {
137            "unknown_router" => {
138                self.stats
139                    .skip_unknown_router
140                    .fetch_add(1, Ordering::Relaxed);
141            }
142            "decode_failed" => {
143                self.stats
144                    .skip_decode_failed
145                    .fetch_add(1, Ordering::Relaxed);
146            }
147            "zero_amount_or_no_wrapped_native" => {
148                self.stats
149                    .skip_missing_wrapped
150                    .fetch_add(1, Ordering::Relaxed);
151            }
152            "gas_price_cap" => {
153                self.stats.skip_gas_cap.fetch_add(1, Ordering::Relaxed);
154            }
155            "simulation_failed" => {
156                self.stats
157                    .skip_sim_failed
158                    .fetch_add(1, Ordering::Relaxed);
159            }
160            "profit_or_gas_guard" => {
161                self.stats
162                    .skip_profit_guard
163                    .fetch_add(1, Ordering::Relaxed);
164            }
165            "unsupported_router_type" => {
166                self.stats
167                    .skip_unsupported_router
168                    .fetch_add(1, Ordering::Relaxed);
169            }
170            "token_call" => {
171                self.stats
172                    .skip_token_call
173                    .fetch_add(1, Ordering::Relaxed);
174            }
175            _ => {}
176        }
177    }
178}
179
180#[derive(Clone, Debug)]
181struct ObservedSwap {
182    router: Address,
183    path: Vec<Address>,
184    amount_in: U256,
185    min_out: U256,
186    recipient: Address,
187    router_kind: RouterKind,
188}
189
190struct BackrunTx {
191    raw: Vec<u8>,
192    hash: B256,
193    to: Address,
194    value: U256,
195    request: TransactionRequest,
196    expected_out: U256,
197}
198
199struct FrontRunTx {
200    raw: Vec<u8>,
201    hash: B256,
202    to: Address,
203    value: U256,
204    request: TransactionRequest,
205    expected_tokens: U256,
206}
207impl StrategyExecutor {
208    pub fn new(
209        tx_rx: UnboundedReceiver<StrategyWork>,
210        block_rx: Receiver<Header>,
211        safety_guard: Arc<SafetyGuard>,
212        bundle_sender: SharedBundleSender,
213        db: Database,
214        portfolio: Arc<PortfolioManager>,
215        gas_oracle: GasOracle,
216        price_feed: PriceFeed,
217        chain_id: u64,
218        max_gas_price_gwei: u64,
219        simulator: Simulator,
220        stats: Arc<StrategyStats>,
221        signer: PrivateKeySigner,
222        nonce_manager: NonceManager,
223        slippage_bps: u64,
224        http_provider: HttpProvider,
225        dry_run: bool,
226        router_allowlist: HashSet<Address>,
227        wrapped_native: Address,
228    ) -> Self {
229        Self {
230            tx_rx,
231            mut_block_rx: block_rx,
232            safety_guard,
233            bundle_sender,
234            db,
235            portfolio,
236            gas_oracle,
237            price_feed,
238            chain_id,
239            stats,
240            max_gas_price_gwei,
241            simulator,
242            signer,
243            nonce_manager,
244            slippage_bps,
245            http_provider,
246            dry_run,
247            router_allowlist,
248            wrapped_native,
249        }
250    }
251
252    pub async fn run(mut self) -> Result<(), AppError> {
253        tracing::info!("StrategyExecutor: waiting for pending transactions");
254        while let Some(work) = self.tx_rx.recv().await {
255            while let Ok(header) = self.mut_block_rx.try_recv() {
256                tracing::debug!("StrategyExecutor: observed new block {:?}", header.hash);
257            }
258
259            self.safety_guard.check()?;
260
261            let outcome = match work {
262                StrategyWork::Mempool(tx) => {
263                    let from = tx.from();
264                    let res = self.evaluate_mempool_tx(&tx).await;
265                    (res, Some(from), Some(tx.tx_hash()))
266                }
267                StrategyWork::MevShareHint(hint) => {
268                    let res = self.evaluate_mev_share_hint(&hint).await;
269                    (res, hint.from, Some(hint.tx_hash))
270                }
271            };
272
273            match outcome {
274                (Ok(Some(tx_hash)), from, _) => {
275                    tracing::info!(
276                        target: "strategy",
277                        from = ?from,
278                        tx_hash = %tx_hash,
279                        "Bundle submitted"
280                    );
281                    self.safety_guard.report_success();
282                    self.stats.submitted.fetch_add(1, Ordering::Relaxed);
283                }
284                (Ok(None), from, tx_hash) => {
285                    tracing::debug!(
286                        target: "strategy",
287                        from=?from,
288                        tx_hash=?tx_hash,
289                        "Skipped item"
290                    );
291                    self.stats.skipped.fetch_add(1, Ordering::Relaxed);
292                }
293                (Err(e), _, _) => {
294                    self.safety_guard.report_failure();
295                    self.stats.failed.fetch_add(1, Ordering::Relaxed);
296                    tracing::error!(target: "strategy", error=%e, "Strategy failed");
297                }
298            };
299
300            let processed = self.stats.processed.fetch_add(1, Ordering::Relaxed) + 1;
301            if processed % 50 == 0 {
302                tracing::info!(
303                    target: "strategy_summary",
304                    processed,
305                    submitted = self.stats.submitted.load(Ordering::Relaxed),
306                    skipped = self.stats.skipped.load(Ordering::Relaxed),
307                    failed = self.stats.failed.load(Ordering::Relaxed),
308                    skip_unknown_router = self.stats.skip_unknown_router.load(Ordering::Relaxed),
309                    skip_decode = self.stats.skip_decode_failed.load(Ordering::Relaxed),
310                    skip_missing_wrapped = self.stats.skip_missing_wrapped.load(Ordering::Relaxed),
311                    skip_gas_cap = self.stats.skip_gas_cap.load(Ordering::Relaxed),
312                    skip_sim_failed = self.stats.skip_sim_failed.load(Ordering::Relaxed),
313                    skip_profit_guard = self.stats.skip_profit_guard.load(Ordering::Relaxed),
314                    skip_unsupported_router = self.stats.skip_unsupported_router.load(Ordering::Relaxed),
315                    skip_token_call = self.stats.skip_token_call.load(Ordering::Relaxed),
316                    "Strategy loop summary"
317                );
318            }
319        }
320
321        Ok(())
322    }
323
324    async fn evaluate_mempool_tx(&self, tx: &Transaction) -> Result<Option<String>, AppError> {
325        let to_addr = match tx.kind() {
326            TxKind::Call(addr) => addr,
327            TxKind::Create => return Ok(None),
328        };
329
330        if !self.router_allowlist.contains(&to_addr) {
331            if Self::is_common_token_call(tx.input()) {
332                self.log_skip("token_call", "erc20 transfer/approve");
333                return Ok(None);
334            }
335            self.log_skip("unknown_router", &format!("to={to_addr:#x}"));
336            return Ok(None);
337        }
338
339        let Some(observed_swap) = Self::decode_swap(tx) else {
340            self.log_skip("decode_failed", "unable to decode swap input");
341            return Ok(None);
342        };
343        if observed_swap.router_kind == RouterKind::V3Like {
344            self.log_skip("unsupported_router_type", "uniswap_v3 not yet implemented for backrun");
345            return Ok(None);
346        }
347        if observed_swap.amount_in.is_zero() || !observed_swap.path.contains(&self.wrapped_native) {
348            self.log_skip(
349                "zero_amount_or_no_wrapped_native",
350                "path missing wrapped native or zero amount",
351            );
352            return Ok(None);
353        }
354        let direction = Self::direction(&observed_swap, self.wrapped_native);
355        let tx_value = tx.value();
356
357        let gas_fees: GasFees = self.gas_oracle.estimate_eip1559_fees().await?;
358        let gas_cap_wei = U256::from(self.max_gas_price_gwei) * U256::from(1_000_000_000u64);
359        if U256::from(gas_fees.max_fee_per_gas) > gas_cap_wei {
360            self.log_skip(
361                "gas_price_cap",
362                &format!(
363                    "max_fee_per_gas={} cap_gwei={}",
364                    gas_fees.max_fee_per_gas, self.max_gas_price_gwei
365                ),
366            );
367            return Ok(None);
368        }
369
370        let real_balance = self
371            .portfolio
372            .update_eth_balance(self.chain_id)
373            .await?;
374        let (wallet_chain_balance, used_mock_balance) = if self.dry_run {
375            let gas_headroom =
376                U256::from(tx.gas_limit()) * U256::from(gas_fees.max_fee_per_gas);
377            let value_headroom = tx.value().saturating_mul(U256::from(2u64));
378            let mock = gas_headroom
379                .saturating_add(value_headroom)
380                .max(U256::from(500_000_000_000_000_000u128)); // floor 0.5 ETH
381            (mock, true)
382        } else {
383            (real_balance, false)
384        };
385        let base_gas_budget = U256::from(tx.gas_limit()) * U256::from(gas_fees.max_fee_per_gas);
386        if !self.dry_run {
387            self.portfolio
388                .ensure_funding(self.chain_id, base_gas_budget)?;
389        }
390
391        // Optional front-run for buy-side victims. We assume approvals for selling tokens are pre-set by the operator.
392        let mut attack_value_eth = U256::ZERO;
393        let mut bundle_requests: Vec<TransactionRequest> = Vec::new();
394        let mut raw_bundle: Vec<Vec<u8>> = Vec::new();
395
396        let mut front_run: Option<FrontRunTx> = None;
397        if direction == SwapDirection::BuyWithEth {
398            let nonce_front = self.nonce_manager.get_next_nonce().await?;
399            match self
400                .build_front_run_tx(
401                    &observed_swap,
402                    gas_fees.max_fee_per_gas,
403                    gas_fees.max_priority_fee_per_gas,
404                    wallet_chain_balance,
405                    tx.gas_limit(),
406                    nonce_front,
407                )
408                .await
409            {
410                Ok(Some(f)) => {
411                    attack_value_eth = f.value;
412                    bundle_requests.push(f.request.clone());
413                    raw_bundle.push(f.raw.clone());
414                    front_run = Some(f);
415                }
416                Ok(None) => {}
417                Err(e) => {
418                    self.log_skip("front_run_build_failed", &e.to_string());
419                    return Ok(None);
420                }
421            }
422        }
423
424        let nonce_backrun = self.nonce_manager.get_next_nonce().await?;
425
426        let backrun = match self
427            .build_backrun_tx(
428                &observed_swap,
429                gas_fees.max_fee_per_gas,
430                gas_fees.max_priority_fee_per_gas,
431                wallet_chain_balance,
432                tx.gas_limit(),
433                front_run.as_ref().map(|f| f.expected_tokens),
434                nonce_backrun,
435            )
436            .await
437        {
438            Ok(b) => b,
439            Err(e) => {
440                self.log_skip("backrun_build_failed", &e.to_string());
441                return Ok(None);
442            }
443        };
444        let backrun_raw = backrun.raw.clone();
445        let sim_nonce = front_run
446            .as_ref()
447            .and_then(|f| f.request.nonce)
448            .or_else(|| backrun.request.nonce)
449            .unwrap_or_default();
450
451        let overrides = StateOverridesBuilder::default()
452            .with_balance(self.signer.address(), wallet_chain_balance)
453            // Simulate with the exact nonce we will broadcast to avoid stale RPC reads.
454            .with_nonce(self.signer.address(), sim_nonce)
455            .build();
456
457        bundle_requests.push(tx.clone().into_request());
458        bundle_requests.push(backrun.request.clone());
459        raw_bundle.push(tx.inner.encoded_2718());
460        raw_bundle.push(backrun_raw.clone());
461        let bundle_sims = retry_async(
462            move |_| {
463                let simulator = self.simulator.clone();
464                let reqs = bundle_requests.clone();
465                let overrides = overrides.clone();
466                async move { simulator.simulate_bundle_requests(&reqs, Some(overrides)).await }
467            },
468            2,
469            Duration::from_millis(100),
470        )
471        .await?;
472        if bundle_sims.iter().any(|o| !o.success) {
473            self.log_skip("simulation_failed", "bundle sim returned failure");
474            return Ok(None);
475        }
476
477        let mut gas_used_total = 0u64;
478        for sim in &bundle_sims {
479            gas_used_total = gas_used_total.saturating_add(sim.gas_used);
480        }
481        let bundle_gas_limit = gas_used_total.max(tx.gas_limit());
482        let gas_cost_wei =
483            U256::from(bundle_gas_limit) * U256::from(gas_fees.max_fee_per_gas);
484
485        if !self.dry_run {
486            let spend = backrun.value.saturating_add(attack_value_eth);
487            self.portfolio
488                .ensure_funding(self.chain_id, spend + gas_cost_wei)?;
489        }
490
491        let total_eth_in = backrun.value.saturating_add(attack_value_eth);
492        let gross_profit_wei = backrun.expected_out.saturating_sub(total_eth_in);
493        let net_profit_wei = gross_profit_wei.saturating_sub(gas_cost_wei);
494        let eth_quote = self.price_feed.get_price("ETHUSD").await?;
495        let profit_eth = wei_to_eth_f64(gross_profit_wei);
496        let gas_cost_eth = wei_to_eth_f64(gas_cost_wei);
497        let net_profit_eth = profit_eth - gas_cost_eth;
498        let profit_floor = StrategyExecutor::dynamic_profit_floor(wallet_chain_balance);
499        let gas_ratio_ok = if gross_profit_wei.is_zero() {
500            false
501        } else {
502            gas_cost_wei
503                .saturating_mul(U256::from(100u64))
504                <= gross_profit_wei.saturating_mul(U256::from(50u64))
505        };
506
507        tracing::info!(
508            target: "strategy",
509            gas_limit = bundle_gas_limit,
510            max_fee_per_gas = gas_fees.max_fee_per_gas,
511            gas_cost_eth = gas_cost_eth,
512            backrun_value_eth = wei_to_eth_f64(backrun.value),
513            expected_out_eth = wei_to_eth_f64(backrun.expected_out),
514            front_run_value_eth = wei_to_eth_f64(attack_value_eth),
515            net_profit_eth = net_profit_eth,
516            wallet_eth = wei_to_eth_f64(wallet_chain_balance),
517            price_source = %eth_quote.source,
518            price = eth_quote.price,
519            victim_min_out = ?observed_swap.min_out,
520            victim_recipient = ?observed_swap.recipient,
521            path_len = observed_swap.path.len(),
522            path = ?observed_swap.path,
523            router = ?observed_swap.router,
524            used_mock_balance,
525            profit_floor_wei = %profit_floor,
526            gas_ratio_ok,
527            sandwich = front_run.is_some(),
528            "Strategy evaluation"
529        );
530
531        if net_profit_wei < profit_floor || net_profit_eth <= 0.0 || !gas_ratio_ok {
532            self.log_skip(
533                "profit_or_gas_guard",
534                &format!(
535                    "net_profit_wei={} floor={} gas_ratio_ok={} net_profit_eth={}",
536                    net_profit_wei, profit_floor, gas_ratio_ok, net_profit_eth
537                ),
538            );
539            return Ok(None);
540        }
541
542        let tx_hash = tx.tx_hash();
543
544        if self.dry_run {
545            tracing::info!(
546                target: "strategy_dry_run",
547                tx_hash = %format!("{:#x}", tx_hash),
548                net_profit_eth = net_profit_eth,
549                gross_profit_eth = profit_eth,
550                gas_cost_eth = gas_cost_eth,
551                front_run_value_eth = wei_to_eth_f64(attack_value_eth),
552                wallet_eth = wei_to_eth_f64(wallet_chain_balance),
553                path_len = observed_swap.path.len(),
554                router = ?observed_swap.router,
555                used_mock_balance,
556                sandwich = front_run.is_some(),
557                "Dry-run only: simulated profitable bundle (not sent)"
558            );
559            return Ok(Some(format!("{tx_hash:#x}")));
560        }
561
562        let _ = self
563            .db
564            .update_status(&format!("{:#x}", backrun.hash), None, Some(false))
565            .await;
566
567        self.bundle_sender
568            .send_bundle(&raw_bundle, self.chain_id)
569            .await?;
570
571        let to_addr = match tx.kind() {
572            TxKind::Call(addr) => Some(addr),
573            TxKind::Create => None,
574        };
575
576        self.db
577            .save_transaction(
578                &format!("{tx_hash:#x}"),
579                self.chain_id,
580                &format!("{:#x}", tx.from()),
581                to_addr.as_ref().map(|a| format!("{:#x}", a)).as_deref(),
582                tx_value.to_string().as_str(),
583                Some("strategy_v1"),
584            )
585            .await?;
586
587        self.db
588            .save_transaction(
589                &format!("{:#x}", backrun.hash),
590                self.chain_id,
591                &format!("{:#x}", self.signer.address()),
592                Some(format!("{:#x}", backrun.to)).as_deref(),
593                backrun.value.to_string().as_str(),
594                Some("strategy_backrun"),
595            )
596            .await?;
597        if let Some(f) = &front_run {
598            self.db
599                .save_transaction(
600                    &format!("{:#x}", f.hash),
601                    self.chain_id,
602                    &format!("{:#x}", self.signer.address()),
603                    Some(format!("{:#x}", f.to)).as_deref(),
604                    f.value.to_string().as_str(),
605                    Some("strategy_front_run"),
606                )
607                .await?;
608        }
609
610        self.db
611            .save_profit_record(
612                &format!("{tx_hash:#x}"),
613                self.chain_id,
614                "strategy_v1",
615                profit_eth,
616                gas_cost_eth,
617                net_profit_eth,
618            )
619            .await?;
620
621        self.portfolio
622            .record_profit(self.chain_id, profit_eth, gas_cost_eth);
623
624        let _ = self
625            .db
626            .save_market_price(self.chain_id, "ETHUSD", eth_quote.price, &eth_quote.source)
627            .await;
628
629        let _ = self.await_receipt(&backrun.hash).await;
630
631        Ok(Some(format!("{tx_hash:#x}")))
632    }
633
634    async fn evaluate_mev_share_hint(
635        &self,
636        hint: &MevShareHint,
637    ) -> Result<Option<String>, AppError> {
638        if !self.router_allowlist.contains(&hint.router) {
639            self.log_skip("unknown_router", &format!("to={:#x}", hint.router));
640            return Ok(None);
641        }
642
643        let Some(observed_swap) =
644            Self::decode_swap_input(hint.router, &hint.call_data, hint.value)
645        else {
646            self.log_skip("decode_failed", "unable to decode swap input");
647            return Ok(None);
648        };
649
650        if observed_swap.router_kind == RouterKind::V3Like {
651            self.log_skip(
652                "unsupported_router_type",
653                "uniswap_v3 not yet implemented for backrun",
654            );
655            return Ok(None);
656        }
657
658        if observed_swap.amount_in.is_zero() || !observed_swap.path.contains(&self.wrapped_native) {
659            self.log_skip(
660                "zero_amount_or_no_wrapped_native",
661                "path missing wrapped native or zero amount",
662            );
663            return Ok(None);
664        }
665
666        let direction = Self::direction(&observed_swap, self.wrapped_native);
667        let gas_limit_hint = hint.gas_limit.unwrap_or(220_000);
668
669        let gas_fees: GasFees = self.gas_oracle.estimate_eip1559_fees().await?;
670        let gas_cap_wei = U256::from(self.max_gas_price_gwei) * U256::from(1_000_000_000u64);
671        if U256::from(gas_fees.max_fee_per_gas) > gas_cap_wei {
672            self.log_skip(
673                "gas_price_cap",
674                &format!(
675                    "max_fee_per_gas={} cap_gwei={}",
676                    gas_fees.max_fee_per_gas, self.max_gas_price_gwei
677                ),
678            );
679            return Ok(None);
680        }
681
682        let real_balance = self.portfolio.update_eth_balance(self.chain_id).await?;
683        let (wallet_chain_balance, used_mock_balance) = if self.dry_run {
684            let gas_headroom =
685                U256::from(gas_limit_hint) * U256::from(gas_fees.max_fee_per_gas);
686            let value_headroom = hint.value.saturating_mul(U256::from(2u64));
687            let mock = gas_headroom
688                .saturating_add(value_headroom)
689                .max(U256::from(500_000_000_000_000_000u128)); // floor 0.5 ETH
690            (mock, true)
691        } else {
692            (real_balance, false)
693        };
694        let base_gas_budget = U256::from(gas_limit_hint) * U256::from(gas_fees.max_fee_per_gas);
695        if !self.dry_run {
696            self.portfolio
697                .ensure_funding(self.chain_id, base_gas_budget)?;
698        }
699
700        let mut attack_value_eth = U256::ZERO;
701        let mut bundle_requests: Vec<TransactionRequest> = Vec::new();
702        let mut bundle_body: Vec<BundleItem> = Vec::new();
703
704        let mut front_run: Option<FrontRunTx> = None;
705        if direction == SwapDirection::BuyWithEth {
706            let nonce_front = self.nonce_manager.get_next_nonce().await?;
707            match self
708                .build_front_run_tx(
709                    &observed_swap,
710                    gas_fees.max_fee_per_gas,
711                    gas_fees.max_priority_fee_per_gas,
712                    wallet_chain_balance,
713                    gas_limit_hint,
714                    nonce_front,
715                )
716                .await
717            {
718                Ok(Some(f)) => {
719                    attack_value_eth = f.value;
720                    bundle_requests.push(f.request.clone());
721                    front_run = Some(f);
722                }
723                Ok(None) => {}
724                Err(e) => {
725                    self.log_skip("front_run_build_failed", &e.to_string());
726                    return Ok(None);
727                }
728            }
729        }
730
731        let nonce_backrun = self.nonce_manager.get_next_nonce().await?;
732        let backrun = match self
733            .build_backrun_tx(
734                &observed_swap,
735                gas_fees.max_fee_per_gas,
736                gas_fees.max_priority_fee_per_gas,
737                wallet_chain_balance,
738                gas_limit_hint,
739                front_run.as_ref().map(|f| f.expected_tokens),
740                nonce_backrun,
741            )
742            .await
743        {
744            Ok(b) => b,
745            Err(e) => {
746                self.log_skip("backrun_build_failed", &e.to_string());
747                return Ok(None);
748            }
749        };
750        let backrun_raw = backrun.raw.clone();
751        let sim_nonce = front_run
752            .as_ref()
753            .and_then(|f| f.request.nonce)
754            .or_else(|| backrun.request.nonce)
755            .unwrap_or_default();
756
757        let overrides = StateOverridesBuilder::default()
758            .with_balance(self.signer.address(), wallet_chain_balance)
759            .with_nonce(self.signer.address(), sim_nonce)
760            .build();
761
762        let max_fee_hint = hint
763            .max_fee_per_gas
764            .unwrap_or(gas_fees.max_fee_per_gas);
765        let max_prio_hint = hint
766            .max_priority_fee_per_gas
767            .unwrap_or(gas_fees.max_priority_fee_per_gas);
768        let victim_request = TransactionRequest {
769            from: hint.from,
770            to: Some(TxKind::Call(hint.router)),
771            max_fee_per_gas: Some(max_fee_hint),
772            max_priority_fee_per_gas: Some(max_prio_hint),
773            gas: Some(gas_limit_hint),
774            value: Some(hint.value),
775            input: TransactionInput::new(hint.call_data.clone().into()),
776            nonce: None,
777            chain_id: Some(self.chain_id),
778            ..Default::default()
779        };
780        bundle_requests.push(victim_request);
781        bundle_requests.push(backrun.request.clone());
782        if let Some(f) = &front_run {
783            bundle_body.push(BundleItem::Tx {
784                tx: format!("0x{}", hex::encode(&f.raw)),
785                can_revert: false,
786            });
787        }
788        bundle_body.push(BundleItem::Hash {
789            hash: format!("{:#x}", hint.tx_hash),
790        });
791        bundle_body.push(BundleItem::Tx {
792            tx: format!("0x{}", hex::encode(&backrun_raw)),
793            can_revert: false,
794        });
795
796        let bundle_sims = retry_async(
797            move |_| {
798                let simulator = self.simulator.clone();
799                let reqs = bundle_requests.clone();
800                let overrides = overrides.clone();
801                async move { simulator.simulate_bundle_requests(&reqs, Some(overrides)).await }
802            },
803            2,
804            Duration::from_millis(100),
805        )
806        .await?;
807        if bundle_sims.iter().any(|o| !o.success) {
808            self.log_skip("simulation_failed", "bundle sim returned failure");
809            return Ok(None);
810        }
811
812        let mut gas_used_total = 0u64;
813        for sim in &bundle_sims {
814            gas_used_total = gas_used_total.saturating_add(sim.gas_used);
815        }
816        let bundle_gas_limit = gas_used_total.max(gas_limit_hint);
817        let gas_cost_wei =
818            U256::from(bundle_gas_limit) * U256::from(gas_fees.max_fee_per_gas);
819
820        if !self.dry_run {
821            let spend = backrun.value.saturating_add(attack_value_eth);
822            self.portfolio
823                .ensure_funding(self.chain_id, spend + gas_cost_wei)?;
824        }
825
826        let total_eth_in = backrun.value.saturating_add(attack_value_eth);
827        let gross_profit_wei = backrun.expected_out.saturating_sub(total_eth_in);
828        let net_profit_wei = gross_profit_wei.saturating_sub(gas_cost_wei);
829        let eth_quote = self.price_feed.get_price("ETHUSD").await?;
830        let profit_eth = wei_to_eth_f64(gross_profit_wei);
831        let gas_cost_eth = wei_to_eth_f64(gas_cost_wei);
832        let net_profit_eth = profit_eth - gas_cost_eth;
833        let profit_floor = StrategyExecutor::dynamic_profit_floor(wallet_chain_balance);
834        let gas_ratio_ok = if gross_profit_wei.is_zero() {
835            false
836        } else {
837            gas_cost_wei
838                .saturating_mul(U256::from(100u64))
839                <= gross_profit_wei.saturating_mul(U256::from(50u64))
840        };
841
842        tracing::info!(
843            target: "strategy",
844            gas_limit = bundle_gas_limit,
845            max_fee_per_gas = gas_fees.max_fee_per_gas,
846            gas_cost_eth = gas_cost_eth,
847            backrun_value_eth = wei_to_eth_f64(backrun.value),
848            expected_out_eth = wei_to_eth_f64(backrun.expected_out),
849            front_run_value_eth = wei_to_eth_f64(attack_value_eth),
850            net_profit_eth = net_profit_eth,
851            wallet_eth = wei_to_eth_f64(wallet_chain_balance),
852            price_source = %eth_quote.source,
853            price = eth_quote.price,
854            victim_min_out = ?observed_swap.min_out,
855            victim_recipient = ?observed_swap.recipient,
856            path_len = observed_swap.path.len(),
857            path = ?observed_swap.path,
858            router = ?observed_swap.router,
859            used_mock_balance,
860            profit_floor_wei = %profit_floor,
861            gas_ratio_ok,
862            sandwich = front_run.is_some(),
863            "MEV-Share strategy evaluation"
864        );
865
866        if net_profit_wei < profit_floor || net_profit_eth <= 0.0 || !gas_ratio_ok {
867            self.log_skip(
868                "profit_or_gas_guard",
869                &format!(
870                    "net_profit_wei={} floor={} gas_ratio_ok={} net_profit_eth={}",
871                    net_profit_wei, profit_floor, gas_ratio_ok, net_profit_eth
872                ),
873            );
874            return Ok(None);
875        }
876
877        let tx_hash = format!("{:#x}", hint.tx_hash);
878
879        if self.dry_run {
880            tracing::info!(
881                target: "strategy_dry_run",
882                tx_hash = %tx_hash,
883                net_profit_eth = net_profit_eth,
884                gross_profit_eth = profit_eth,
885                gas_cost_eth = gas_cost_eth,
886                front_run_value_eth = wei_to_eth_f64(attack_value_eth),
887                wallet_eth = wei_to_eth_f64(wallet_chain_balance),
888                path_len = observed_swap.path.len(),
889                router = ?observed_swap.router,
890                used_mock_balance,
891                sandwich = front_run.is_some(),
892                "Dry-run only: simulated profitable MEV-Share bundle (not sent)"
893            );
894            return Ok(Some(tx_hash));
895        }
896
897        let _ = self
898            .db
899            .update_status(&tx_hash, None, Some(false))
900            .await;
901
902        self.bundle_sender
903            .send_mev_share_bundle(&bundle_body)
904            .await?;
905
906        let from_addr = hint.from.unwrap_or(Address::ZERO);
907        self.db
908            .save_transaction(
909                &tx_hash,
910                self.chain_id,
911                &format!("{:#x}", from_addr),
912                Some(format!("{:#x}", hint.router)).as_deref(),
913                hint.value.to_string().as_str(),
914                Some("strategy_mev_share"),
915            )
916            .await?;
917
918        self.db
919            .save_transaction(
920                &format!("{:#x}", backrun.hash),
921                self.chain_id,
922                &format!("{:#x}", self.signer.address()),
923                Some(format!("{:#x}", backrun.to)).as_deref(),
924                backrun.value.to_string().as_str(),
925                Some("strategy_backrun"),
926            )
927            .await?;
928        if let Some(f) = &front_run {
929            self.db
930                .save_transaction(
931                    &format!("{:#x}", f.hash),
932                    self.chain_id,
933                    &format!("{:#x}", self.signer.address()),
934                    Some(format!("{:#x}", f.to)).as_deref(),
935                    f.value.to_string().as_str(),
936                    Some("strategy_front_run"),
937                )
938                .await?;
939        }
940
941        self.db
942            .save_profit_record(
943                &tx_hash,
944                self.chain_id,
945                "strategy_mev_share",
946                profit_eth,
947                gas_cost_eth,
948                net_profit_eth,
949            )
950            .await?;
951
952        self.portfolio
953            .record_profit(self.chain_id, profit_eth, gas_cost_eth);
954
955        let _ = self
956            .db
957            .save_market_price(self.chain_id, "ETHUSD", eth_quote.price, &eth_quote.source)
958            .await;
959
960        let _ = self.await_receipt(&backrun.hash).await;
961
962        Ok(Some(tx_hash))
963    }
964}
965
966impl StrategyExecutor {
967    fn dynamic_profit_floor(wallet_balance: U256) -> U256 {
968        let abs_floor = *MIN_PROFIT_THRESHOLD_WEI;
969        let scaled = wallet_balance
970            .checked_div(U256::from(100_000u64))
971            .unwrap_or(U256::ZERO);
972        if scaled > abs_floor {
973            scaled
974        } else {
975            abs_floor
976        }
977    }
978
979    fn dynamic_backrun_value(
980        observed_in: U256,
981        wallet_balance: U256,
982        slippage_bps: u64,
983        gas_limit_hint: u64,
984        max_fee_per_gas: u128,
985    ) -> Result<U256, AppError> {
986        let mut value =
987            observed_in.saturating_mul(U256::from(slippage_bps)) / U256::from(10_000u64);
988
989        let min_backrun = U256::from(100_000_000_000_000u64);
990        if value < min_backrun {
991            value = min_backrun;
992        }
993
994        let mut max_value = wallet_balance
995            .checked_div(U256::from(4u64))
996            .unwrap_or(wallet_balance);
997        let gas_buffer =
998            U256::from(max_fee_per_gas).saturating_mul(U256::from(gas_limit_hint.max(210_000)));
999        if gas_buffer > wallet_balance / U256::from(8u64) {
1000            max_value = wallet_balance
1001                .checked_div(U256::from(8u64))
1002                .unwrap_or(wallet_balance);
1003        }
1004        if value > max_value {
1005            value = max_value;
1006        }
1007        if value.is_zero() {
1008            return Err(AppError::Strategy(
1009                "Backrun value is zero after caps".into(),
1010            ));
1011        }
1012        Ok(value)
1013    }
1014
1015    fn decode_swap(tx: &Transaction) -> Option<ObservedSwap> {
1016        let router = match tx.kind() {
1017            TxKind::Call(addr) => addr,
1018            TxKind::Create => return None,
1019        };
1020        Self::decode_swap_input(router, tx.input(), tx.value())
1021    }
1022
1023    fn decode_swap_input(router: Address, input: &[u8], eth_value: U256) -> Option<ObservedSwap> {
1024        if input.len() < 4 {
1025            return None;
1026        }
1027
1028        if let Ok(decoded) = swapExactETHForTokensCall::abi_decode(input) {
1029            return Some(ObservedSwap {
1030                router,
1031                path: decoded.path,
1032                amount_in: eth_value,
1033                min_out: decoded.amountOutMin,
1034                recipient: decoded.to,
1035                router_kind: RouterKind::V2Like,
1036            });
1037        }
1038
1039        if let Ok(decoded) = swapExactTokensForETHCall::abi_decode(input) {
1040            return Some(ObservedSwap {
1041                router,
1042                path: decoded.path,
1043                amount_in: decoded.amountIn,
1044                min_out: decoded.amountOutMin,
1045                recipient: decoded.to,
1046                router_kind: RouterKind::V2Like,
1047            });
1048        }
1049
1050        if let Ok(decoded) = swapExactTokensForTokensCall::abi_decode(input) {
1051            return Some(ObservedSwap {
1052                router,
1053                path: decoded.path,
1054                amount_in: decoded.amountIn,
1055                min_out: decoded.amountOutMin,
1056                recipient: decoded.to,
1057                router_kind: RouterKind::V2Like,
1058            });
1059        }
1060
1061        if let Ok(decoded) = UniV3Router::exactInputSingleCall::abi_decode(input) {
1062            let params = decoded.params;
1063            return Some(ObservedSwap {
1064                router,
1065                path: vec![params.tokenIn, params.tokenOut],
1066                amount_in: params.amountIn,
1067                min_out: params.amountOutMinimum,
1068                recipient: params.recipient,
1069                router_kind: RouterKind::V3Like,
1070            });
1071        }
1072
1073        if let Ok(decoded) = UniV3Router::exactInputCall::abi_decode(input) {
1074            let params = decoded.params;
1075            if let Some(path) = Self::parse_v3_path(&params.path) {
1076                return Some(ObservedSwap {
1077                    router,
1078                    path,
1079                    amount_in: params.amountIn,
1080                    min_out: params.amountOutMinimum,
1081                    recipient: params.recipient,
1082                    router_kind: RouterKind::V3Like,
1083                });
1084            }
1085        }
1086
1087        None
1088    }
1089
1090    fn target_token(path: &[Address], wrapped_native: Address) -> Option<Address> {
1091        path.iter().copied().find(|addr| addr != &wrapped_native)
1092    }
1093
1094    fn direction(observed: &ObservedSwap, wrapped_native: Address) -> SwapDirection {
1095        let starts_with_native = observed.path.first().copied() == Some(wrapped_native);
1096        let ends_with_native = observed.path.last().copied() == Some(wrapped_native);
1097        if starts_with_native {
1098            SwapDirection::BuyWithEth
1099        } else if ends_with_native {
1100            SwapDirection::SellForEth
1101        } else {
1102            SwapDirection::Other
1103        }
1104    }
1105
1106    fn parse_v3_path(path: &[u8]) -> Option<Vec<Address>> {
1107        // Expected layout: token (20) + [fee (3) + token (20)]*
1108        if path.len() < 43 {
1109            return None;
1110        }
1111        let mut tokens = Vec::new();
1112        let mut offset = 0;
1113        tokens.push(Address::from_slice(&path[offset..offset + 20]));
1114        offset += 20;
1115        while offset < path.len() {
1116            if path.len() < offset + 3 + 20 {
1117                return None;
1118            }
1119            offset += 3; // skip fee
1120            tokens.push(Address::from_slice(&path[offset..offset + 20]));
1121            offset += 20;
1122        }
1123        Some(tokens)
1124    }
1125
1126    fn is_common_token_call(input: &[u8]) -> bool {
1127        if input.len() < 4 {
1128            return false;
1129        }
1130        let selector = &input[..4];
1131        const TRANSFER: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
1132        const TRANSFER_FROM: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd];
1133        const APPROVE: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
1134        const PERMIT: [u8; 4] = [0xd5, 0x05, 0xac, 0xcf]; // EIP-2612
1135        selector == TRANSFER
1136            || selector == TRANSFER_FROM
1137            || selector == APPROVE
1138            || selector == PERMIT
1139    }
1140
1141    async fn build_front_run_tx(
1142        &self,
1143        observed: &ObservedSwap,
1144        max_fee_per_gas: u128,
1145        max_priority_fee_per_gas: u128,
1146        wallet_balance: U256,
1147        gas_limit_hint: u64,
1148        nonce: u64,
1149    ) -> Result<Option<FrontRunTx>, AppError> {
1150        if wallet_balance.is_zero() {
1151            return Ok(None);
1152        }
1153        let target_token = Self::target_token(&observed.path, self.wrapped_native)
1154            .ok_or_else(|| AppError::Strategy("Unable to derive target token".into()))?;
1155
1156        let value = StrategyExecutor::dynamic_backrun_value(
1157            observed.amount_in,
1158            wallet_balance,
1159            self.slippage_bps,
1160            gas_limit_hint,
1161            max_fee_per_gas,
1162        )?;
1163
1164        let router_contract = UniV2Router::new(observed.router, self.http_provider.clone());
1165        let path = vec![self.wrapped_native, target_token];
1166        let quote_path = path.clone();
1167        let quote_contract = router_contract.clone();
1168        let quote_value = value;
1169        let quote: Vec<U256> = retry_async(
1170            move |_| {
1171                let c = quote_contract.clone();
1172                let p = quote_path.clone();
1173                async move { c.getAmountsOut(quote_value, p.clone()).call().await }
1174            },
1175            3,
1176            Duration::from_millis(100),
1177        )
1178        .await
1179        .map_err(|e| AppError::Strategy(format!("Front-run quote failed: {}", e)))?;
1180        let expected_tokens = *quote
1181            .last()
1182            .ok_or_else(|| AppError::Strategy("Front-run quote missing amounts".into()))?;
1183
1184        let min_out = expected_tokens
1185            .saturating_mul(U256::from(10_000u64 - self.slippage_bps))
1186            / U256::from(10_000u64);
1187        let deadline = U256::from((chrono::Utc::now().timestamp() as u64) + 300);
1188        let calldata = router_contract
1189            .swapExactETHForTokens(min_out, path, self.signer.address(), deadline)
1190            .calldata()
1191            .to_vec();
1192
1193        let mut gas_limit = gas_limit_hint
1194            .saturating_mul(11)
1195            .checked_div(10)
1196            .unwrap_or(300_000);
1197        if gas_limit < 160_000 {
1198            gas_limit = 160_000;
1199        }
1200
1201        let mut tx = TxEip1559 {
1202            chain_id: self.chain_id,
1203            nonce,
1204            max_priority_fee_per_gas,
1205            max_fee_per_gas,
1206            gas_limit,
1207            to: TxKind::Call(observed.router),
1208            value,
1209            access_list: Default::default(),
1210            input: calldata.clone().into(),
1211        };
1212
1213        let sig = TxSignerSync::sign_transaction_sync(&self.signer, &mut tx)
1214            .map_err(|e| AppError::Strategy(format!("Sign front-run failed: {}", e)))?;
1215        let signed: TxEnvelope = tx.into_signed(sig).into();
1216        let raw = signed.encoded_2718();
1217
1218        let request = TransactionRequest {
1219            from: Some(self.signer.address()),
1220            to: Some(TxKind::Call(observed.router)),
1221            max_fee_per_gas: Some(max_fee_per_gas),
1222            max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
1223            gas: Some(gas_limit),
1224            value: Some(value),
1225            input: TransactionInput::new(calldata.into()),
1226            nonce: Some(nonce),
1227            chain_id: Some(self.chain_id),
1228            ..Default::default()
1229        };
1230
1231        Ok(Some(FrontRunTx {
1232            raw,
1233            hash: *signed.tx_hash(),
1234            to: observed.router,
1235            value,
1236            request,
1237            expected_tokens,
1238        }))
1239    }
1240
1241    async fn build_backrun_tx(
1242        &self,
1243        observed: &ObservedSwap,
1244        max_fee_per_gas: u128,
1245        max_priority_fee_per_gas: u128,
1246        wallet_balance: U256,
1247        gas_limit_hint: u64,
1248        token_in_override: Option<U256>,
1249        nonce: u64,
1250    ) -> Result<BackrunTx, AppError> {
1251        let target_token = Self::target_token(&observed.path, self.wrapped_native)
1252            .ok_or_else(|| AppError::Strategy("Unable to derive target token".into()))?;
1253
1254        if wallet_balance.is_zero() {
1255            return Err(AppError::Strategy(
1256                "No balance available for backrun".into(),
1257            ));
1258        }
1259
1260        let router_contract = UniV2Router::new(observed.router, self.http_provider.clone());
1261        let (value, expected_out, calldata) = if let Some(tokens_in) = token_in_override {
1262            let sell_path = vec![target_token, self.wrapped_native];
1263            let quote_path = sell_path.clone();
1264            let quote_contract = router_contract.clone();
1265            let sell_amount = tokens_in;
1266            let quote: Vec<U256> = retry_async(
1267                move |_| {
1268                    let c = quote_contract.clone();
1269                    let p = quote_path.clone();
1270                    async move { c.getAmountsOut(sell_amount, p.clone()).call().await }
1271                },
1272                3,
1273                Duration::from_millis(100),
1274            )
1275            .await
1276            .map_err(|e| AppError::Strategy(format!("Sell quote failed: {}", e)))?;
1277            let expected_out = *quote
1278                .last()
1279                .ok_or_else(|| AppError::Strategy("Sell quote missing amounts".into()))?;
1280            let min_out = expected_out
1281                .saturating_mul(U256::from(10_000u64 - self.slippage_bps))
1282                / U256::from(10_000u64);
1283            let deadline = U256::from((chrono::Utc::now().timestamp() as u64) + 300);
1284            let calldata = router_contract
1285                .swapExactTokensForETH(sell_amount, min_out, sell_path, self.signer.address(), deadline)
1286                .calldata()
1287                .to_vec();
1288            (U256::ZERO, expected_out, calldata)
1289        } else {
1290            let value = StrategyExecutor::dynamic_backrun_value(
1291                observed.amount_in,
1292                wallet_balance,
1293                self.slippage_bps,
1294                gas_limit_hint,
1295                max_fee_per_gas,
1296            )?;
1297
1298            let buy_path = vec![self.wrapped_native, target_token, self.wrapped_native];
1299            let quote_path = buy_path.clone();
1300            let quote_contract = router_contract.clone();
1301            let quote_value = value;
1302            let quote: Vec<U256> = retry_async(
1303                move |_| {
1304                    let c = quote_contract.clone();
1305                    let p = quote_path.clone();
1306                    async move { c.getAmountsOut(quote_value, p.clone()).call().await }
1307                },
1308                3,
1309                Duration::from_millis(100),
1310            )
1311            .await
1312            .map_err(|e| AppError::Strategy(format!("Quote failed: {}", e)))?;
1313            let expected_out = *quote
1314                .last()
1315                .ok_or_else(|| AppError::Strategy("Quote missing amounts".into()))?;
1316
1317            let re_quote_contract = router_contract.clone();
1318            let re_quote_path = buy_path.clone();
1319            let second_quote: Vec<U256> = retry_async(
1320                move |_| {
1321                    let c = re_quote_contract.clone();
1322                    let p = re_quote_path.clone();
1323                    async move { c.getAmountsOut(value, p.clone()).call().await }
1324                },
1325                3,
1326                Duration::from_millis(100),
1327            )
1328            .await
1329            .map_err(|e| AppError::Strategy(format!("Re-quote failed: {}", e)))?;
1330            let second_out = *second_quote
1331                .last()
1332                .ok_or_else(|| AppError::Strategy("Re-quote missing amounts".into()))?;
1333            let drift = if expected_out > second_out {
1334                expected_out - second_out
1335            } else {
1336                second_out - expected_out
1337            };
1338            let tolerance = expected_out
1339                .saturating_mul(U256::from(self.slippage_bps * 2))
1340                / U256::from(10_000u64);
1341            if drift > tolerance {
1342                return Err(AppError::Strategy("Quote drift too high, skip".into()));
1343            }
1344
1345            let min_out = expected_out.saturating_mul(U256::from(10_000u64 - self.slippage_bps))
1346                / U256::from(10_000u64);
1347            let deadline = U256::from((chrono::Utc::now().timestamp() as u64) + 300);
1348            let calldata = router_contract
1349                .swapExactETHForTokens(min_out, buy_path, self.signer.address(), deadline)
1350                .calldata()
1351                .to_vec();
1352            (value, expected_out, calldata)
1353        };
1354
1355        let mut gas_limit = gas_limit_hint
1356            .saturating_mul(12)
1357            .checked_div(10)
1358            .unwrap_or(350_000);
1359        if gas_limit < 200_000 {
1360            gas_limit = 200_000;
1361        }
1362        let mut tx = TxEip1559 {
1363            chain_id: self.chain_id,
1364            nonce,
1365            max_priority_fee_per_gas,
1366            max_fee_per_gas,
1367            gas_limit,
1368            to: TxKind::Call(observed.router),
1369            value,
1370            access_list: Default::default(),
1371            input: calldata.clone().into(),
1372        };
1373
1374        let sig = TxSignerSync::sign_transaction_sync(&self.signer, &mut tx)
1375            .map_err(|e| AppError::Strategy(format!("Sign backrun failed: {}", e)))?;
1376        let signed: TxEnvelope = tx.into_signed(sig).into();
1377        let raw = signed.encoded_2718();
1378
1379        let request = TransactionRequest {
1380            from: Some(self.signer.address()),
1381            to: Some(TxKind::Call(observed.router)),
1382            max_fee_per_gas: Some(max_fee_per_gas),
1383            max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
1384            gas: Some(gas_limit),
1385            value: Some(value),
1386            input: TransactionInput::new(calldata.into()),
1387            nonce: Some(nonce),
1388            chain_id: Some(self.chain_id),
1389            ..Default::default()
1390        };
1391
1392        Ok(BackrunTx {
1393            raw,
1394            hash: *signed.tx_hash(),
1395            to: observed.router,
1396            value,
1397            request,
1398            expected_out,
1399        })
1400    }
1401
1402    async fn await_receipt(&self, hash: &B256) -> Result<(), AppError> {
1403        for _ in 0..3 {
1404            if let Ok(Some(rcpt)) = self.http_provider.get_transaction_receipt(*hash).await {
1405                let block_num = rcpt.block_number;
1406                let status = rcpt.status();
1407                let _ = self.db.update_status(
1408                    &format!("{:#x}", hash),
1409                    block_num.map(|b| b as i64),
1410                    Some(status),
1411                );
1412                break;
1413            }
1414            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1415        }
1416        Ok(())
1417    }
1418}
1419
1420fn wei_to_eth_f64(value: U256) -> f64 {
1421    let wei_in_eth = 1_000_000_000_000_000_000u128;
1422    let num: u128 = value.try_into().unwrap_or(u128::MAX);
1423    (num as f64) / (wei_in_eth as f64)
1424}
1425
1426#[cfg(test)]
1427mod tests {
1428    use super::*;
1429    use crate::common::constants::WETH_MAINNET;
1430
1431    #[test]
1432    fn decodes_eth_swap() {
1433        let router = WETH_MAINNET;
1434        let call = swapExactETHForTokensCall {
1435            amountOutMin: U256::from(5u64),
1436            path: vec![WETH_MAINNET, Address::from([2u8; 20])],
1437            to: Address::from([3u8; 20]),
1438            deadline: U256::from(100u64),
1439        };
1440        let data = call.abi_encode();
1441        let decoded = StrategyExecutor::decode_swap_input(
1442            router,
1443            &data,
1444            U256::from(1_000_000_000_000_000_000u128),
1445        )
1446        .expect("decode");
1447        assert_eq!(decoded.path.len(), 2);
1448        assert_eq!(decoded.min_out, U256::from(5u64));
1449    }
1450
1451    #[test]
1452    fn wei_to_eth_conversion() {
1453        let two_eth = U256::from(2_000_000_000_000_000_000u128);
1454        let eth = wei_to_eth_f64(two_eth);
1455        assert!((eth - 2.0).abs() < 1e-9);
1456    }
1457
1458    #[test]
1459    fn decodes_uniswap_v3_exact_input_single() {
1460        use alloy::primitives::{U160, aliases::U24};
1461        let params = UniV3Router::ExactInputSingleParams {
1462            tokenIn: WETH_MAINNET,
1463            tokenOut: Address::from([2u8; 20]),
1464            fee: U24::from(500u32),
1465            recipient: Address::from([3u8; 20]),
1466            deadline: U256::from(100u64),
1467            amountIn: U256::from(1_000_000_000_000_000_000u128),
1468            amountOutMinimum: U256::from(5u64),
1469            sqrtPriceLimitX96: U160::ZERO,
1470        };
1471        let call = UniV3Router::exactInputSingleCall { params };
1472        let data = call.abi_encode();
1473        let decoded = StrategyExecutor::decode_swap_input(
1474            WETH_MAINNET,
1475            &data,
1476            U256::from(0u64),
1477        )
1478        .expect("decode v3 single");
1479        assert_eq!(decoded.router_kind, RouterKind::V3Like);
1480        assert_eq!(decoded.path.len(), 2);
1481    }
1482
1483    #[test]
1484    fn parses_uniswap_v3_path() {
1485        let mut path: Vec<u8> = Vec::new();
1486        path.extend_from_slice(WETH_MAINNET.as_slice());
1487        path.extend_from_slice(&[0u8, 1u8, 244u8]); // fee 500
1488        let out = Address::from([9u8; 20]);
1489        path.extend_from_slice(out.as_slice());
1490        let parsed = StrategyExecutor::parse_v3_path(&path).expect("parse path");
1491        assert_eq!(parsed.len(), 2);
1492        assert_eq!(parsed[1], out);
1493    }
1494
1495    #[test]
1496    fn detects_token_calls() {
1497        let transfer_selector = [0xa9, 0x05, 0x9c, 0xbb, 0u8];
1498        assert!(StrategyExecutor::is_common_token_call(&transfer_selector));
1499        let random = [0x12, 0x34, 0x56, 0x78];
1500        assert!(!StrategyExecutor::is_common_token_call(&random));
1501    }
1502
1503    #[test]
1504    fn classifies_swap_direction() {
1505        let buy = ObservedSwap {
1506            router: Address::ZERO,
1507            path: vec![WETH_MAINNET, Address::from([2u8; 20])],
1508            amount_in: U256::from(1u64),
1509            min_out: U256::ZERO,
1510            recipient: Address::ZERO,
1511            router_kind: RouterKind::V2Like,
1512        };
1513        assert_eq!(
1514            StrategyExecutor::direction(&buy, WETH_MAINNET),
1515            SwapDirection::BuyWithEth
1516        );
1517        let sell = ObservedSwap {
1518            path: vec![Address::from([2u8; 20]), WETH_MAINNET],
1519            ..buy
1520        };
1521        assert_eq!(
1522            StrategyExecutor::direction(&sell, WETH_MAINNET),
1523            SwapDirection::SellForEth
1524        );
1525    }
1526}