forge-guard 0.3.6

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! Gas analysis — analyzes gas usage patterns and suggests optimizations.

use crate::core::{Finding, ForgeGuardError, Severity};

/// Analyze gas usage across all source files.
pub fn analyze_gas(
    files: &[std::path::PathBuf],
    _config: &crate::core::ProjectConfig,
) -> Result<Vec<Finding>, ForgeGuardError> {
    let mut findings = Vec::new();

    for file in files {
        let content = std::fs::read_to_string(file)?;
        let file_name = file.to_string_lossy().to_string();
        let lines: Vec<&str> = content.lines().collect();

        // Check for gas-inefficient patterns
        findings.extend(check_unnecessary_storage(&file_name, &lines));
        findings.extend(check_loop_gas(&file_name, &lines));
        findings.extend(check_inefficient_data_types(&file_name, &lines));
    }

    Ok(findings)
}

fn check_unnecessary_storage(file: &str, lines: &[&str]) -> Vec<Finding> {
    let mut findings = Vec::new();
    let mut storage_accesses = 0;

    for line in lines.iter() {
        let trimmed = line.trim();
        if trimmed.starts_with("function ") {
            storage_accesses = 0;
        } else if trimmed.starts_with('}') {
            // end of function, nothing to reset
        } else if trimmed.contains("storage.")
            || (trimmed.contains('.')
                && !trimmed.contains("memory")
                && !trimmed.contains("calldata"))
        {
            storage_accesses += 1;
        }
    }

    if storage_accesses > 5 {
        findings.push(
            Finding::builder()
                .id("FA-GAS-001")
                .title("Excessive Storage Reads")
                .description(
                    "Function reads from storage multiple times — cache in local variables",
                )
                .severity(Severity::Medium)
                .file(file)
                .code("Multiple storage reads detected — consider caching in memory")
                .recommendation("Cache storage variables in memory at the start of the function")
                .category("Gas")
                .build(),
        );
    }

    findings
}

fn check_loop_gas(file: &str, lines: &[&str]) -> Vec<Finding> {
    let mut findings = Vec::new();
    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();
        if trimmed.contains("for (") && trimmed.contains(".length") {
            findings.push(
                Finding::builder()
                    .id(&format!("FA-GAS-{}", i + 1))
                    .title("Loop Cache Array Length")
                    .description("Array length fetched on each iteration — cache before loop")
                    .severity(Severity::Low)
                    .file(file)
                    .location(i + 1, 0)
                    .code(trimmed)
                    .recommendation(
                        "Cache the array length before the loop: uint256 len = array.length",
                    )
                    .category("Gas")
                    .build(),
            );
            if findings.len() >= 3 {
                break;
            }
        }
    }
    findings
}

fn check_inefficient_data_types(_file: &str, _lines: &[&str]) -> Vec<Finding> {
    // Placeholder for future data type optimization checks
    Vec::new()
}

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

    #[test]
    fn test_analyze_gas_empty_files() {
        let files: Vec<std::path::PathBuf> = vec![];
        let config = crate::core::ProjectConfig::default();
        let result = analyze_gas(&files, &config).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_check_unnecessary_storage() {
        let lines = vec![
            "function doSomething() public {",
            "    storage.balance = 100;",
            "    storage.owner = msg.sender;",
            "    storage.count = 5;",
            "    storage.locked = true;",
            "    storage.data = 'test';",
            "    storage.meta = 'xyz';",
            "}",
        ];
        let findings = check_unnecessary_storage("test.sol", &lines);
        assert!(!findings.is_empty());
    }

    #[test]
    fn test_check_loop_gas() {
        let lines = vec![
            "function process() public {",
            "    for (uint i = 0; i < users.length; i++) {",
            "        // process each user",
            "    }",
            "}",
        ];
        let findings = check_loop_gas("test.sol", &lines);
        assert!(!findings.is_empty());
        assert!(findings.iter().any(|f| f.title.contains("Loop")));
    }
}