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)); (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 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 .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, ð_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)); (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, ð_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(¶ms.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 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; 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]; 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]); 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}