forge-guard 0.1.0

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge deploy` — deploy with security checks.

use crate::chains::ChainRegistry;
use crate::core::{OutputFormat, ProjectConfig};
use crate::deployment::DeploymentGuard;
use crate::plugins::PluginRegistry;
use crate::security::SecurityEngine;
use crate::utils::Cache;

use super::DeployArgs;
use anyhow::{Context, Result};
use colored::*;

/// Deploy contracts with automatic security checks.
pub fn run(args: &DeployArgs) -> Result<()> {
    let mut config = ProjectConfig::from_default_location();
    config.chain = args.shared.chain.clone();
    config.project_root = args.shared.project.clone();
    config.strict = args.shared.strict;
    config.offline = args.shared.offline;
    config.production = args.shared.production;
    config.output = if args.shared.json {
        OutputFormat::Json
    } else if args.shared.markdown {
        OutputFormat::Markdown
    } else {
        OutputFormat::Terminal
    };

    let _cache = Cache::new(&config)?;
    let chain_registry = ChainRegistry::default();
    let plugin_registry = PluginRegistry::new(&config)?;
    let security_engine = SecurityEngine::new(&config, &plugin_registry)?;
    let deploy_guard = DeploymentGuard::new(&config, &security_engine)?;

    eprintln!("{}", "🛡️  Forge Guard — Deployment Guard".bold());
    eprintln!("{}", "───────────────────────────────────".dimmed());

    // Phase 1: Pre-deployment security audit
    eprintln!("\n📋 Phase 1: Running pre-deployment security audit...");
    let findings = deploy_guard.run_pre_deployment_checks(&config, &chain_registry)?;

    // Phase 2: Scoring & risk analysis
    eprintln!("\n📊 Phase 2: Analyzing risk...");
    let scores = security_engine.calculate_scores(&findings);
    let overall = deploy_guard.calculate_overall_score(&scores);
    let risk = deploy_guard.determine_risk_level(overall, &findings);

    // Phase 3: Deployment decision
    eprintln!("\n⚖️  Phase 3: Deployment decision...");
    let can_deploy = if args.force {
        eprintln!(
            "⚠️  {} {}",
            "WARNING:".yellow().bold(),
            "Deployment forced with --force. Warnings still apply.".yellow()
        );
        true
    } else {
        deploy_guard.can_deploy(&findings, overall, risk)
    };

    // Print decision summary
    println!(
        "\n{}",
        "═══════════════════════════════════════".bright_blue()
    );
    println!(
        "{}",
        "         DEPLOYMENT GUARD REPORT        "
            .bright_blue()
            .bold()
    );
    println!(
        "{}",
        "═══════════════════════════════════════".bright_blue()
    );

    println!(
        "\n  Security Score:  {}/100",
        overall
            .to_string()
            .color(if overall >= 85 {
                "green"
            } else if overall >= 70 {
                "yellow"
            } else {
                "red"
            })
            .bold()
    );
    println!(
        "  Risk Level:      {}",
        format!("{}", risk)
            .color(match risk {
                crate::core::RiskLevel::Critical => "red",
                crate::core::RiskLevel::High => "red",
                crate::core::RiskLevel::Medium => "yellow",
                _ => "green",
            })
            .bold()
    );
    println!(
        "  Findings:        {} total, {} critical, {} high",
        findings.len(),
        findings
            .iter()
            .filter(|f| f.severity == crate::core::Severity::Critical)
            .count(),
        findings
            .iter()
            .filter(|f| f.severity == crate::core::Severity::High)
            .count(),
    );

    if can_deploy {
        println!(
            "\n  {}  {}",
            "✅ DEPLOYMENT: APPROVED".green().bold(),
            if args.force {
                "(forced)".yellow()
            } else {
                "".normal()
            }
        );

        if let Some(contract) = &args.contract {
            println!("  📄 Contract:    {}", contract);
        }
        if let Some(salt) = &args.salt {
            println!("  🧂 Create2 Salt: {}", salt);
        }

        eprintln!("\n🚀 Proceeding with deployment...");

        // Call forge deploy
        let deploy_output = execute_forge_deploy(args)?;

        // Extract deployed contract address from forge output
        let deployed_address = extract_deployed_address(&deploy_output);
        if let Some(ref addr) = deployed_address {
            println!("\n  📍 Deployed at: {}", addr);

            // Auto-verify if requested
            if args.verify || config.deployment.auto_verify {
                eprintln!("\n🔍 Running post-deployment verification...");
                let contract_name = args.contract.as_deref().unwrap_or("Contract");
                let result =
                    deploy_guard.verify_contract(addr, contract_name, args.args.as_deref());

                match result {
                    Ok(vr) if vr.verified => {
                        println!("{}", vr.details.green());
                    }
                    Ok(vr) => {
                        eprintln!("  ⚠️  {}", vr.details.yellow());
                        // Attempt bytecode match as fallback
                        eprintln!("  🔄 Trying bytecode match...");
                        let rpc =
                            crate::deployment::verifier::rpc_url_for_chain(&args.shared.chain);
                        let bm = deploy_guard.verify_bytecode(addr, contract_name, &rpc);
                        if bm.verified {
                            println!("{}", bm.details.green());
                        } else {
                            eprintln!("  ⚠️  {}", bm.details.yellow());
                        }
                    }
                    Err(e) => {
                        eprintln!("  ⚠️  Verification error: {}", e);
                    }
                }
            }
        }
    } else {
        println!(
            "\n  {}  {}",
            "❌ DEPLOYMENT: BLOCKED".red().bold(),
            "(use --force to bypass)".yellow()
        );
        println!(
            "\n  Blocked by {} issues:",
            findings.iter().filter(|f| f.blocks_deployment).count()
        );

        for f in findings.iter().filter(|f| f.blocks_deployment).take(5) {
            println!(
                "    • [{}] {}{}",
                f.severity.to_string().red().bold(),
                f.title,
                f.file.as_deref().unwrap_or("?")
            );
            println!("      💡 {}", f.recommendation);
        }

        if !args.force {
            println!(
                "\n  {} Use {} to bypass: forge deploy --force",
                "⚠️".yellow(),
                "WARNING".yellow().bold()
            );
        }

        anyhow::bail!("Deployment blocked by security guard");
    }

    Ok(())
}

fn execute_forge_deploy(args: &DeployArgs) -> Result<String> {
    let mut cmd = std::process::Command::new("forge");
    cmd.arg("create");

    if let Some(contract) = &args.contract {
        cmd.arg("--contract").arg(contract);
    }

    if let Some(salt) = &args.salt {
        cmd.arg("--salt").arg(salt);
    }

    if let Some(args_str) = &args.args {
        for arg in args_str.split(',') {
            cmd.arg("--constructor-args").arg(arg.trim());
        }
    }

    cmd.arg("--rpc-url");
    // Use configured RPC or foundry default
    cmd.arg(crate::deployment::verifier::rpc_url_for_chain(
        &args.shared.chain,
    ));

    let output = cmd.output().context("Failed to execute forge create")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("{}", stderr);
        anyhow::bail!("Forge deployment failed");
    }

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    println!("{}", stdout);
    Ok(stdout)
}

/// Extract the deployed contract address from forge create output.
pub fn extract_deployed_address(output: &str) -> Option<String> {
    // forge create outputs: "Deployed to: 0x..."
    for line in output.lines() {
        let trimmed = line.trim();
        if let Some(addr) = trimmed.strip_prefix("Deployed to: ") {
            let addr = addr.trim();
            if addr.starts_with("0x") || addr.starts_with("0X") {
                return Some(addr.to_owned());
            }
        }
    }
    // Also look for address in JSON output
    if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(output) {
        if let Some(addr) = parsed["deployedTo"].as_str() {
            return Some(addr.to_owned());
        }
        if let Some(addr) = parsed["address"].as_str() {
            return Some(addr.to_owned());
        }
    }
    None
}