use super::SimulateArgs;
use anyhow::{Context, Result};
use colored::*;
use std::time::Instant;
const SANDWICH_SENSITIVE_PATTERNS: &[&str] = &[
"amm",
"swap",
"pool",
"liquidity",
"reserve",
"price",
"twap",
"oracle",
"slippage",
"minAmount",
"amountOutMin",
"sqrtPrice",
];
pub fn run(args: &SimulateArgs) -> Result<()> {
let start = Instant::now();
eprintln!("{}\n", "๐งช Forge Guard โ Deployment Simulation".bold());
println!(" Blocks: {}", args.blocks);
println!(
" MEV: {}",
if args.mev {
"enabled".green()
} else {
"disabled".dimmed()
}
);
println!(" Contract: {}", args.contract.as_deref().unwrap_or("all"));
println!(
" Deployer: {}",
args.deployer.as_deref().unwrap_or("default")
);
println!();
eprintln!("๐ Phase 1: Analyzing contract bytecode for MEV vectors...\n");
let mev_signals = analyze_mev_patterns(args);
if !mev_signals.is_empty() {
println!(" โ ๏ธ Potential MEV patterns found:\n");
for signal in &mev_signals {
println!(
" โข {} โ {}",
signal.pattern.yellow(),
signal.description
);
}
println!();
} else {
println!(" โ
No obvious MEV patterns detected.\n");
}
eprintln!("๐ Phase 2: Running deployment simulation...\n");
let mut cmd = std::process::Command::new("forge");
cmd.arg("script");
if let Some(contract) = &args.contract {
cmd.arg(contract);
}
if let Some(deployer) = &args.deployer {
cmd.arg("--sender").arg(deployer);
}
cmd.arg("--slow");
eprintln!(
" Running: forge script {}\n",
args.contract.as_deref().unwrap_or("<default>")
);
let output = match cmd.output().context("Failed to run forge script") {
Ok(o) => o,
Err(e) => {
eprintln!(" โ ๏ธ Simulation failed: {e}");
eprintln!(" This is non-fatal โ forge may not be installed or configured.");
eprintln!(" Security audit findings are still available.");
return Ok(());
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!(" โ ๏ธ Simulation stderr:\n{}", stderr);
eprintln!(" Simulation had warnings โ these are non-fatal.");
} else {
extract_simulation_results(&String::from_utf8_lossy(&output.stdout));
}
if args.mev {
eprintln!("\n๐ Phase 3: MEV analysis...\n");
run_mev_analysis(args.blocks);
}
let elapsed = start.elapsed();
println!(
"\n{}",
"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ".bright_blue()
);
println!(
" {} SIMULATION COMPLETE in {:.2}s",
"โ
".green(),
elapsed.as_secs_f64()
);
println!(
"{}",
"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ".bright_blue()
);
Ok(())
}
struct MevSignal {
pattern: &'static str,
description: String,
}
fn analyze_mev_patterns(args: &SimulateArgs) -> Vec<MevSignal> {
let mut signals = Vec::new();
let src_dir = args.shared.project.join("src");
if !src_dir.exists() {
return signals;
}
let mut source_files = Vec::new();
for entry in walkdir::WalkDir::new(&src_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "sol") {
source_files.push(path.to_path_buf());
}
}
for file in &source_files {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => continue,
};
let lower = content.to_lowercase();
for pattern in SANDWICH_SENSITIVE_PATTERNS {
if lower.contains(pattern) {
signals.push(MevSignal {
pattern: "Sandwich / Slippage",
description: format!(
"Contract uses '{}' patterns that may be vulnerable to sandwich attacks. Ensure proper slippage protection.",
pattern
),
});
break;
}
}
if lower.contains("flashloan")
|| lower.contains("flash_loan")
|| lower.contains("onflashloan")
{
signals.push(MevSignal {
pattern: "Flash Loan",
description:
"Contract supports flash loans โ verify price oracle manipulation resistance."
.into(),
});
}
if lower.contains("chainlink") || lower.contains("oracle") || lower.contains("twap") {
signals.push(MevSignal {
pattern: "Oracle Dependency",
description:
"Contract depends on price oracles โ ensure freshness and manipulation resistance."
.into(),
});
}
if lower.contains(".call{value")
|| lower.contains(".delegatecall(")
|| lower.contains("selfdestruct")
{
signals.push(MevSignal {
pattern: "Value Extraction",
description:
"Contract has value-moving operations that could be MEV extraction targets."
.into(),
});
}
}
signals.dedup_by_key(|s| s.pattern);
signals
}
fn extract_simulation_results(stdout: &str) {
let mut gas_used: Vec<u64> = Vec::new();
let mut txn_count = 0u32;
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.contains("gas used") || trimmed.contains("Gas used") {
if let Some(gas_str) = trimmed
.split_whitespace()
.find(|w| w.chars().all(|c| c.is_ascii_digit()))
{
if let Ok(gas) = gas_str.parse::<u64>() {
gas_used.push(gas);
}
}
}
if trimmed.contains("Transaction") || trimmed.starts_with("==LOG==") {
txn_count += 1;
}
}
if let Some(&total) = gas_used.last() {
let estimated_eth_cost = total as f64 * 20_000_000_000.0 / 1_000_000_000_000_000_000.0; println!(" โฝ Estimated gas: {}", total.to_string().bold());
println!(
" ๐ฐ Estimated cost: {:.6} ETH at 20 gwei",
estimated_eth_cost
);
}
if txn_count > 0 {
println!(" ๐ Transactions: {}", txn_count);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_mev_patterns_no_source_dir() {
let args = SimulateArgs {
shared: super::super::SharedFlags {
chain: "ethereum".into(),
project: std::path::PathBuf::from("/nonexistent"),
json: false,
markdown: false,
html: false,
strict: false,
offline: true,
production: false,
report: false,
parallelism: 4,
},
blocks: 100,
deployer: None,
mev: false,
contract: None,
};
let signals = analyze_mev_patterns(&args);
assert!(signals.is_empty(), "No src dir should yield no signals");
}
#[test]
fn test_analyze_mev_patterns_with_src_dir_no_sol_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src").join("readme.md"), "# nothing").unwrap();
let args = SimulateArgs {
shared: super::super::SharedFlags {
chain: "ethereum".into(),
project: dir.path().to_path_buf(),
json: false,
markdown: false,
html: false,
strict: false,
offline: true,
production: false,
report: false,
parallelism: 4,
},
blocks: 100,
deployer: None,
mev: false,
contract: None,
};
let signals = analyze_mev_patterns(&args);
assert!(signals.is_empty());
}
#[test]
fn test_extract_simulation_results_empty() {
extract_simulation_results("");
}
#[test]
fn test_extract_simulation_results_with_gas() {
let stdout = "some output\nGas used: 50000\nmore output\nTransaction #1\n";
extract_simulation_results(stdout);
}
#[test]
fn test_run_mev_analysis_basic() {
run_mev_analysis(100);
}
#[test]
fn test_run_mev_analysis_zero_blocks() {
run_mev_analysis(0);
}
#[test]
fn test_mev_signal_dedup() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(
src.join("Pool.sol"),
"contract Pool { function swap() { uint price = oracle.getPrice(); } }",
)
.unwrap();
let args = SimulateArgs {
shared: super::super::SharedFlags {
chain: "ethereum".into(),
project: dir.path().to_path_buf(),
json: false,
markdown: false,
html: false,
strict: false,
offline: true,
production: false,
report: false,
parallelism: 4,
},
blocks: 100,
deployer: None,
mev: false,
contract: None,
};
let signals = analyze_mev_patterns(&args);
assert!(
!signals.is_empty(),
"Should find MEV patterns in Pool contract"
);
assert_eq!(
signals.len(),
signals
.iter()
.map(|s| s.pattern)
.collect::<std::collections::HashSet<_>>()
.len(),
"Patterns should be deduplicated"
);
}
}
fn run_mev_analysis(blocks: u32) {
println!(" Analyzing {blocks} blocks for MEV opportunities...\n");
let analyses = [
(
"Frontrunning",
"Check if transactions can be displaced by priority gas auctions",
),
(
"Sandwiching",
"Detect AMM swap patterns vulnerable to sandwich attacks",
),
(
"Backrunning",
"Identify post-transaction value extraction opportunities",
),
(
"Liquidation",
"Scan for liquidation opportunities and competition",
),
("Arbitrage", "Detect cross-pool/CEX-DEX arbitrage patterns"),
];
for (category, description) in &analyses {
println!(" โข {}: {}", category.bold(), description.dimmed());
}
println!(
"\n {} MEV analysis requires a full archival node and historical data for accuracy.",
"๐ก".dimmed()
);
println!(
" For production use, consider dedicated MEV tools: searcher, libmev, or Flashbots.",
);
}