use sentri_core::model::{FunctionModel, ProgramModel};
use sentri_core::traits::ChainAnalyzer;
use sentri_core::Finding;
use sentri_core::{AnalysisContext, Result};
use std::collections::BTreeSet;
use std::path::Path;
use tracing::{debug, info, warn};
use crate::ast::{AstContract, SolidityParser, Visibility};
use crate::bytecode::{IssueType, Severity};
use crate::cfg::ControlFlowGraph;
use crate::errors::AnalysisError;
use sentri_utils::SolcManager;
pub struct EvmAnalyzer;
impl EvmAnalyzer {
pub fn new() -> Self {
Self
}
fn parse_contract(&self, path: &Path) -> std::result::Result<AstContract, AnalysisError> {
SolidityParser::parse(path).map_err(|e| {
warn!("Failed to parse contract at {:?}: {}", path, e);
AnalysisError::ast_parsing(format!("Failed to parse {}: {}", path.display(), e))
})
}
fn build_cfg_from_contract(&self, contract: &AstContract) -> ControlFlowGraph {
let mut cfg = ControlFlowGraph::new();
for func in &contract.functions {
debug!("Building CFG for function: {}", func.name);
let _entry_id = cfg.new_block();
let exit_id = cfg.new_block();
let _ = cfg.mark_exit(exit_id);
let _ = cfg.add_edge(_entry_id, exit_id, "normal");
}
cfg
}
fn analyze_bytecode(&self, _contract: &AstContract) -> Vec<(IssueType, Severity, String)> {
Vec::new()
}
fn detect_vulnerabilities(
&self,
contract: &AstContract,
_cfg: &ControlFlowGraph,
_bytecode_issues: &[(IssueType, Severity, String)],
) -> Vec<(String, String)> {
let mut vulnerabilities = Vec::new();
for func in &contract.functions {
let body_str = func.body.join("\n");
if body_str.contains(".call") || body_str.contains("transfer") {
if func.is_mutable {
vulnerabilities.push((
"REENTRANCY".to_string(),
format!("Function '{}' may have reentrancy vulnerability", func.name),
));
}
}
if body_str.contains(".call(") && !body_str.contains("require(") {
vulnerabilities.push((
"UNCHECKED_CALL".to_string(),
format!("Function '{}' has unchecked external call", func.name),
));
}
if matches!(func.visibility, Visibility::Public | Visibility::External)
&& !func.modifiers.iter().any(|m| m.contains("onlyOwner"))
&& !body_str.contains("require(msg.sender")
{
vulnerabilities.push((
"ACCESS_CONTROL".to_string(),
format!("Function '{}' may lack access control", func.name),
));
}
}
vulnerabilities
}
}
impl Default for EvmAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl ChainAnalyzer for EvmAnalyzer {
fn analyze(&self, path: &Path) -> Result<ProgramModel> {
info!("Analyzing EVM contract at {:?}", path);
let ast_contract = self.parse_contract(path).map_err(|e| {
sentri_core::InvarError::Custom(format!("Failed to parse contract with solc: {}", e))
})?;
let cfg = self.build_cfg_from_contract(&ast_contract);
debug!("Built CFG with {} blocks", cfg.block_count());
let bytecode_issues = self.analyze_bytecode(&ast_contract);
debug!("Found {} bytecode issues", bytecode_issues.len());
let _vulnerabilities = self.detect_vulnerabilities(&ast_contract, &cfg, &bytecode_issues);
let mut program = ProgramModel::new(
ast_contract.name.clone(),
"evm".to_string(),
path.to_string_lossy().to_string(),
);
for var in &ast_contract.state_vars {
use sentri_core::model::StateVar;
program.add_state_var(StateVar {
name: var.name.clone(),
type_name: var.type_name.clone(),
is_mutable: var.is_mutable,
visibility: None,
});
}
for func in &ast_contract.functions {
let func_model = FunctionModel {
name: func.name.clone(),
parameters: func.parameters.iter().map(|p| p.name.clone()).collect(),
return_type: if func.returns.is_empty() {
None
} else {
Some(func.returns[0].type_name.clone())
},
mutates: BTreeSet::new(),
reads: BTreeSet::new(),
is_entry_point: matches!(
func.visibility,
Visibility::Public | Visibility::External
),
is_pure: func.is_pure && !func.is_mutable,
};
program.add_function(func_model);
}
Ok(program)
}
fn chain(&self) -> &str {
"evm"
}
}
impl EvmAnalyzer {
pub fn analyze_with_context(&self, path: &Path) -> Result<AnalysisContext> {
let program = self.analyze(path)?;
let mut context = AnalysisContext::new(program);
match self.parse_contract(path) {
Ok(ast_contract) => {
let cfg = self.build_cfg_from_contract(&ast_contract);
let bytecode_issues = self.analyze_bytecode(&ast_contract);
let vulnerabilities =
self.detect_vulnerabilities(&ast_contract, &cfg, &bytecode_issues);
for (vuln_type, description) in &vulnerabilities {
let line_num = 1; context.add_warning(
format!("[{}] {}", vuln_type, description),
path.to_string_lossy().to_string(),
line_num,
None,
None,
);
}
if vulnerabilities.iter().any(|(t, _)| {
t.contains("REENTRANCY") || t.contains("UNCHECKED") || t.contains("Unsafe")
}) {
context.mark_invalid();
}
}
Err(e) => {
warn!("Failed to perform advanced analysis: {}", e);
self.basic_vulnerability_check(path, &mut context)?;
}
}
Ok(context)
}
pub fn analyze_with_ast(&self, path: &Path) -> anyhow::Result<Vec<Finding>> {
let solc = match SolcManager::new() {
Ok(s) => s,
Err(e) => {
warn!(
"solc not available ({}), falling back to pattern analysis",
e
);
return self.analyze_with_patterns(path);
}
};
let source = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", path.display(), e))?;
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
match solc.get_ast_for_source(&source, &file_name) {
Ok(solc_output) => {
debug!("Successfully parsed AST for {}", file_name);
self.run_ast_detectors(&solc_output, &source, &file_name, path)
}
Err(e) => {
warn!("AST parse failed for {} ({}), using patterns", file_name, e);
self.analyze_with_patterns(path)
}
}
}
fn run_ast_detectors(
&self,
output: &sentri_utils::SolcOutput,
_source: &str,
_file_name: &str,
_path: &Path,
) -> anyhow::Result<Vec<Finding>> {
let violations = Vec::new();
for _ in output.sources.values() {
}
Ok(violations)
}
fn analyze_with_patterns(&self, path: &Path) -> anyhow::Result<Vec<Finding>> {
let _source = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", path.display(), e))?;
Ok(vec![])
}
fn basic_vulnerability_check(&self, path: &Path, context: &mut AnalysisContext) -> Result<()> {
let source = std::fs::read_to_string(path).map_err(sentri_core::InvarError::IoError)?;
let lines: Vec<&str> = source.lines().collect();
for (line_idx, line) in lines.iter().enumerate() {
let line_num = line_idx + 1;
if line.contains(".call") && !line.contains("require") && !line.contains("assert") {
context.add_warning(
"Unchecked call return value detected".to_string(),
path.to_string_lossy().to_string(),
line_num,
None,
Some(line.to_string()),
);
}
if (line.contains("+ ") || line.contains("+="))
&& (line.contains("balance") || line.contains("amount"))
{
context.add_warning(
"Potential unchecked arithmetic".to_string(),
path.to_string_lossy().to_string(),
line_num,
None,
Some(line.to_string()),
);
}
if line.contains(".transfer") || line.contains(".call") {
context.add_warning(
"Potential reentrancy vulnerability".to_string(),
path.to_string_lossy().to_string(),
line_num,
None,
Some(line.to_string()),
);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyzer_creation() {
let analyzer = EvmAnalyzer::new();
assert_eq!("evm", analyzer.chain());
}
}