forge-guard 0.3.6

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! Multi-chain support — chain-specific analyzers and configuration.

use crate::core::{ChainId, ForgeGuardError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Information about a supported blockchain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainInfo {
    pub name: String,
    pub chain_id: u64,
    pub currency: String,
    pub explorer_url: String,
    pub rpc_urls: Vec<String>,
    pub is_evm: bool,
    pub supported: bool,
}

/// A chain-specific analyzer for EVM and non-EVM chains.
pub trait ChainAnalyzer: Send + Sync {
    /// The chain this analyzer supports.
    fn chain_id(&self) -> ChainId;

    /// Get chain information.
    fn info(&self) -> ChainInfo;

    /// Analyze deployment for this chain's specifics.
    fn analyze_deployment(&self, _contract_address: &str) -> Result<Vec<String>, ForgeGuardError> {
        Ok(Vec::new())
    }

    /// Estimate gas for this chain.
    fn estimate_gas(&self, _gas_used: u64) -> Result<f64, ForgeGuardError> {
        Ok(0.0)
    }
}

/// Registry of all supported chains and their analyzers.
#[derive(Clone)]
pub struct ChainRegistry {
    chains: HashMap<ChainId, ChainInfo>,
}

impl Default for ChainRegistry {
    fn default() -> Self {
        let mut registry = Self {
            chains: HashMap::new(),
        };
        registry.register_defaults();
        registry
    }
}

impl ChainRegistry {
    /// Register all default supported chains.
    fn register_defaults(&mut self) {
        let chains = vec![
            ChainInfo {
                name: "Ethereum".into(),
                chain_id: 1,
                currency: "ETH".into(),
                explorer_url: "https://etherscan.io".into(),
                rpc_urls: vec!["https://eth.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Base".into(),
                chain_id: 8453,
                currency: "ETH".into(),
                explorer_url: "https://basescan.org".into(),
                rpc_urls: vec!["https://base.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Arbitrum".into(),
                chain_id: 42161,
                currency: "ETH".into(),
                explorer_url: "https://arbiscan.io".into(),
                rpc_urls: vec!["https://arbitrum.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Optimism".into(),
                chain_id: 10,
                currency: "ETH".into(),
                explorer_url: "https://optimistic.etherscan.io".into(),
                rpc_urls: vec!["https://optimism.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Polygon".into(),
                chain_id: 137,
                currency: "MATIC".into(),
                explorer_url: "https://polygonscan.com".into(),
                rpc_urls: vec!["https://polygon.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "BNB Chain".into(),
                chain_id: 56,
                currency: "BNB".into(),
                explorer_url: "https://bscscan.com".into(),
                rpc_urls: vec!["https://bsc.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Avalanche".into(),
                chain_id: 43114,
                currency: "AVAX".into(),
                explorer_url: "https://snowtrace.io".into(),
                rpc_urls: vec!["https://avalanche.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Scroll".into(),
                chain_id: 534352,
                currency: "ETH".into(),
                explorer_url: "https://scrollscan.com".into(),
                rpc_urls: vec!["https://scroll.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Linea".into(),
                chain_id: 59144,
                currency: "ETH".into(),
                explorer_url: "https://lineascan.build".into(),
                rpc_urls: vec!["https://linea.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Unichain".into(),
                chain_id: 130,
                currency: "ETH".into(),
                explorer_url: "https://uniscan.xyz".into(),
                rpc_urls: vec!["https://unichain.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "ZKSync".into(),
                chain_id: 324,
                currency: "ETH".into(),
                explorer_url: "https://explorer.zksync.io".into(),
                rpc_urls: vec!["https://zksync.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "HyperEVM".into(),
                chain_id: 999,
                currency: "HYPE".into(),
                explorer_url: "https://explorer.hyperliquid.xyz".into(),
                rpc_urls: vec!["https://hyperliquid.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Monad".into(),
                chain_id: 10143,
                currency: "MON".into(),
                explorer_url: "https://explorer.monad.xyz".into(),
                rpc_urls: vec!["https://monad.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Sonic".into(),
                chain_id: 146,
                currency: "S".into(),
                explorer_url: "https://sonicscan.org".into(),
                rpc_urls: vec!["https://sonic.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Blast".into(),
                chain_id: 81457,
                currency: "ETH".into(),
                explorer_url: "https://blastscan.io".into(),
                rpc_urls: vec!["https://blast.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Mantle".into(),
                chain_id: 5000,
                currency: "MNT".into(),
                explorer_url: "https://mantlescan.xyz".into(),
                rpc_urls: vec!["https://mantle.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
            ChainInfo {
                name: "Robinhood".into(),
                chain_id: 31753,
                currency: "ETH".into(),
                explorer_url: "https://explorer.robinhood.com".into(),
                rpc_urls: vec!["https://robinhood.llamarpc.com".into()],
                is_evm: true,
                supported: true,
            },
        ];

        for chain in chains {
            let id = ChainId::from_name(&chain.name);
            self.chains.insert(id, chain);
        }
    }

    /// List all registered chains.
    pub fn list_chains(&self) -> Result<Vec<ChainInfo>, ForgeGuardError> {
        let mut chains: Vec<ChainInfo> = self.chains.values().cloned().collect();
        chains.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(chains)
    }

    /// Get chain info by name.
    pub fn get_chain(&self, name: &str) -> Option<&ChainInfo> {
        let id = ChainId::from_name(name);
        self.chains.get(&id)
    }

    /// Register a custom chain.
    pub fn register_chain(&mut self, info: ChainInfo) {
        let id = ChainId::from_name(&info.name);
        self.chains.insert(id, info);
    }

    /// Get all EVM-compatible chains.
    pub fn evm_chains(&self) -> Vec<&ChainInfo> {
        self.chains.values().filter(|c| c.is_evm).collect()
    }

    /// Get a chain by its numeric chain ID.
    pub fn get_by_chain_id(&self, chain_id: u64) -> Option<&ChainInfo> {
        self.chains.values().find(|c| c.chain_id == chain_id)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_registry_default() {
        let registry = ChainRegistry::default();
        let chains = registry.list_chains().unwrap();
        assert_eq!(chains.len(), 17, "Should have 17 supported chains");
    }

    #[test]
    fn test_get_chain_by_name() {
        let registry = ChainRegistry::default();
        let eth = registry.get_chain("ethereum").unwrap();
        assert_eq!(eth.chain_id, 1);
        assert_eq!(eth.currency, "ETH");
        assert!(eth.is_evm);
        assert!(eth.supported);

        let base = registry.get_chain("base").unwrap();
        assert_eq!(base.chain_id, 8453);
    }

    #[test]
    fn test_get_chain_not_found() {
        let registry = ChainRegistry::default();
        assert!(registry.get_chain("nonexistent-chain").is_none());
    }

    #[test]
    fn test_get_by_chain_id() {
        let registry = ChainRegistry::default();
        let chain = registry.get_by_chain_id(1).unwrap();
        assert_eq!(chain.name, "Ethereum");

        let chain = registry.get_by_chain_id(8453).unwrap();
        assert_eq!(chain.name, "Base");
    }

    #[test]
    fn test_evm_chains() {
        let registry = ChainRegistry::default();
        let evm = registry.evm_chains();
        assert_eq!(evm.len(), 17, "All 17 chains should be EVM");
        assert!(evm.iter().all(|c| c.is_evm));
    }

    #[test]
    fn test_register_custom_chain() {
        let mut registry = ChainRegistry::default();
        let custom = ChainInfo {
            name: "CustomChain".into(),
            chain_id: 99999,
            currency: "CST".into(),
            explorer_url: "https://example.com".into(),
            rpc_urls: vec!["https://rpc.example.com".into()],
            is_evm: true,
            supported: true,
        };
        registry.register_chain(custom.clone());

        let retrieved = registry.get_chain("CustomChain").unwrap();
        assert_eq!(retrieved.chain_id, 99999);
    }

    #[test]
    fn test_chain_name_display() {
        let registry = ChainRegistry::default();
        let eth = registry.get_chain("ethereum").unwrap();
        assert_eq!(eth.name, "Ethereum");
    }
}