forge-guard 0.1.1

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge verify` — verify contract deployments on block explorers.

use super::VerifyArgs;
use crate::deployment::verifier::{
    explorer_api_key_env, explorer_api_url, explorer_base_url, forge_available, rpc_url_for_chain,
    ContractVerifier,
};
use anyhow::Result;
use colored::*;

/// Verify contract deployments on block explorers.
pub fn run(args: &VerifyArgs) -> Result<()> {
    eprintln!("{}\n", "🔍 Forge Guard — Contract Verification".bold());

    if !forge_available() {
        anyhow::bail!(
            "{} `forge` CLI not found. Install Foundry: https://book.getfoundry.sh/getting-started/installation",
            "".red()
        );
    }

    if args.all {
        return verify_all_deployments(args);
    }

    let address = args
        .address
        .as_deref()
        .ok_or_else(|| anyhow::anyhow!("Provide --address or use --all"))?;
    let name = args
        .name
        .as_deref()
        .ok_or_else(|| anyhow::anyhow!("Provide --name or use --all"))?;

    let verifier = ContractVerifier::new(&args.shared.chain, args.api_key.clone());

    // Show what we're doing
    let api_url = explorer_api_url(&args.shared.chain);
    let base_url = explorer_base_url(&args.shared.chain);

    println!("  📄 Contract:  {}", name.bold());
    println!("  📍 Address:   {}", address);
    println!("  ⛓️  Chain:     {}", args.shared.chain);
    if let Some(url) = api_url {
        println!("  🔗 Explorer:  {}", url.dimmed());
    }
    if let Some(url) = base_url {
        println!("  🔍 Address:   {}/address/{}", url, address);
    }
    println!();

    // Attempt 1: Explorer verification via forge verify-contract
    eprintln!("  🔄 Verifying via block explorer...");
    let result = verifier.forge_verify(
        address,
        name,
        &args.shared.chain,
        args.constructor_args.as_deref(),
    );

    if result.verified {
        println!("  {}  {}", "✅ VERIFIED".green().bold(), result.details);
        if let Some(url) = base_url {
            println!("     🔍 {}/address/{}#code", url, address);
        }
        return Ok(());
    }

    // Attempt 2: Bytecode match via RPC
    eprintln!("  ⚠️  Explorer verification: {}", result.details);
    eprintln!("  🔄 Checking bytecode match via RPC...");

    let rpc = rpc_url_for_chain(&args.shared.chain);
    let bytecode_result = verifier.verify_bytecode_match(address, name, &rpc);

    if bytecode_result.verified {
        println!(
            "  {}  {}",
            "✅ BYTECODE MATCH".green().bold(),
            bytecode_result.details
        );
        println!(
            "     {} Try: forge verify-contract {} {} --chain {}",
            "💡".dimmed(),
            address,
            name,
            args.shared.chain
        );
        // Suggest setting up the API key
        let env = explorer_api_key_env(&args.shared.chain);
        println!(
            "     {} Set {} to enable explorer verification",
            "💡".dimmed(),
            env
        );
        return Ok(());
    }

    // Both failed
    eprintln!("{}", bytecode_result.details);
    eprintln!();

    if let Some(env) = Some(explorer_api_key_env(&args.shared.chain)) {
        eprintln!(
            "  {} Set the {} environment variable or pass --api-key",
            "💡".dimmed(),
            env
        );
    }

    anyhow::bail!("Verification failed for {} on {}", name, args.shared.chain);
}

/// Verify all contracts by reading the deployment artifact.
fn verify_all_deployments(args: &VerifyArgs) -> Result<()> {
    eprintln!("  🔍 Checking all deployments...\n");

    // Look for broadcast/ directory with deployment receipts
    let broadcast_dir = args.shared.project.join("broadcast");
    if !broadcast_dir.exists() {
        anyhow::bail!(
            "No broadcast directory found at {}. Run `forge script` first.",
            broadcast_dir.display()
        );
    }

    let verifier = ContractVerifier::new(&args.shared.chain, args.api_key.clone());
    let mut verified = 0u32;
    let mut failed = 0u32;

    // Walk broadcast directory for deployment JSON files
    for entry in walkdir::WalkDir::new(&broadcast_dir)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| {
            e.file_name().to_string_lossy().ends_with(".json")
                && !e.file_name().to_string_lossy().contains("run-latest")
        })
    {
        let content = match std::fs::read_to_string(entry.path()) {
            Ok(c) => c,
            Err(_) => continue,
        };

        let parsed: serde_json::Value = match serde_json::from_str(&content) {
            Ok(v) => v,
            Err(_) => continue,
        };

        // Extract transactions from the deployment artifact
        let transactions = parsed["transactions"].as_array();
        let receipts = parsed["receipts"].as_array();

        // Process deployments from transactions
        if let Some(txs) = transactions {
            for tx in txs {
                let contract_address = tx["contractAddress"].as_str();
                let contract_name = tx["contractName"].as_str();

                if let (Some(addr), Some(name)) = (contract_address, contract_name) {
                    if addr.is_empty() || addr == "0x0000000000000000000000000000000000000000" {
                        continue;
                    }

                    print!("  📄 {} at {} ... ", name, addr);
                    std::io::Write::flush(&mut std::io::stdout()).ok();

                    let result = verifier.forge_verify(addr, name, &args.shared.chain, None);

                    if result.verified {
                        println!("{}", "".green());
                        verified += 1;
                    } else {
                        println!("{} {}", "".red(), result.details);
                        failed += 1;
                    }
                }
            }
        }

        // Also check receipts
        if let Some(recs) = receipts {
            for rec in recs {
                let contract_address = rec["contractAddress"].as_str();
                if let Some(addr) = contract_address {
                    if addr.is_empty() || addr == "0x0000000000000000000000000000000000000000" {
                        continue;
                    }

                    // Check if we already verified this address from transactions
                    let already_verified = transactions
                        .map(|txs| {
                            txs.iter()
                                .any(|tx| tx["contractAddress"].as_str() == Some(addr))
                        })
                        .unwrap_or(false);

                    if !already_verified {
                        print!("  📄 Contract at {} ... ", addr);
                        std::io::Write::flush(&mut std::io::stdout()).ok();

                        let result =
                            verifier.forge_verify(addr, "unknown", &args.shared.chain, None);

                        if result.verified {
                            println!("{}", "".green());
                            verified += 1;
                        } else {
                            println!("{} {}", "".red(), result.details);
                            failed += 1;
                        }
                    }
                }
            }
        }
    }

    if verified == 0 && failed == 0 {
        eprintln!("  ⚠️  No deployments found in broadcast directory.");
    } else {
        println!(
            "\n  {} {} verified, {} failed",
            if failed == 0 {
                "".green()
            } else {
                "⚠️".yellow()
            },
            verified,
            failed
        );
    }

    if failed > 0 {
        anyhow::bail!("{failed} contract(s) failed verification");
    }

    Ok(())
}