diff --git a/packages/rules/src/optimization/storage/multiple_storage_reads.rs b/packages/rules/src/optimization/storage/multiple_storage_reads.rs new file mode 100644 index 0000000..d1e04df --- /dev/null +++ b/packages/rules/src/optimization/storage/multiple_storage_reads.rs @@ -0,0 +1,71 @@ +//! Detect Multiple Storage Reads +//! +//! Flags repeated reads from the same storage variable within a function. +//! Suggests caching the value in a local variable to reduce gas costs. + +use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity}; +use syn::{Item, Stmt, Expr}; +use std::collections::HashMap; + +pub struct MultipleStorageReadsRule; + +impl Rule for MultipleStorageReadsRule { + fn name(&self) -> &str { + "multiple-storage-reads" + } + + fn description(&self) -> &str { + "Detects repeated reads from the same storage slot. \ + Cache the value in a local variable to save gas." + } + + fn check(&self, ast: &[Item]) -> Vec { + let mut violations = Vec::new(); + for item in ast { + if let Item::Fn(func) = item { + self.check_stmts(&func.block.stmts, &mut violations); + } + } + violations + } +} + +impl MultipleStorageReadsRule { + fn check_stmts(&self, stmts: &[Stmt], violations: &mut Vec) { + let mut read_counts: HashMap = HashMap::new(); + for stmt in stmts { + if let Stmt::Expr(Expr::MethodCall(call), _) = stmt { + let method = call.method.to_string(); + if method == "get" || method == "read" { + let key = format!("{:?}", call.receiver); + *read_counts.entry(key.clone()).or_insert(0) += 1; + if read_counts[&key] == 2 { + violations.push(RuleViolation { + rule_name: self.name().to_string(), + description: "Storage slot read more than once in the same function." + .to_string(), + severity: ViolationSeverity::Medium, + line_number: 0, + column_number: 0, + variable_name: key, + suggestion: "Cache the storage value in a local `let` binding." + .to_string(), + }); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_file; + + #[test] + fn no_violation_single_read() { + let ast = parse_file("fn f() { let x = s.get(&k); }").expect("parse"); + assert!(MultipleStorageReadsRule.check(&ast.items).is_empty()); + } +} \ No newline at end of file diff --git a/packages/rules/src/security/randomness/mod.rs b/packages/rules/src/security/randomness/mod.rs new file mode 100644 index 0000000..a005e83 --- /dev/null +++ b/packages/rules/src/security/randomness/mod.rs @@ -0,0 +1,2 @@ +pub mod unsafe_randomness; +pub use unsafe_randomness::UnsafeRandomnessRule; \ No newline at end of file diff --git a/packages/rules/src/security/randomness/unsafe_randomness.rs b/packages/rules/src/security/randomness/unsafe_randomness.rs new file mode 100644 index 0000000..b4d7532 --- /dev/null +++ b/packages/rules/src/security/randomness/unsafe_randomness.rs @@ -0,0 +1,71 @@ +//! Detect Unsafe Randomness Patterns +//! +//! Flags use of blockhash or block.timestamp as a source of randomness, +//! which are predictable and manipulable by miners/validators. + +use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity}; +use syn::Item; + +pub struct UnsafeRandomnessRule; + +impl Rule for UnsafeRandomnessRule { + fn name(&self) -> &str { + "unsafe-randomness" + } + + fn description(&self) -> &str { + "Detects use of blockhash or block.timestamp as randomness sources. \ + These values are predictable and can be manipulated." + } + + fn check(&self, ast: &[Item]) -> Vec { + let mut violations = Vec::new(); + for item in ast { + if let Item::Fn(func) = item { + let src = quote::quote!(#func).to_string(); + for pattern in &["blockhash", "block.timestamp", "block_timestamp"] { + if src.contains(pattern) { + violations.push(RuleViolation { + rule_name: self.name().to_string(), + description: format!( + "Use of `{}` as randomness source is unsafe.", + pattern + ), + severity: ViolationSeverity::High, + line_number: 0, + column_number: 0, + variable_name: pattern.to_string(), + suggestion: "Use a verifiable random function (VRF) or \ + commit-reveal scheme instead." + .to_string(), + }); + } + } + } + } + violations + } +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_file; + + fn check(code: &str) -> Vec { + let ast = parse_file(code).expect("parse failed"); + UnsafeRandomnessRule.check(&ast.items) + } + + #[test] + fn flags_block_timestamp() { + let code = r#"fn rand() -> u64 { let r = block_timestamp(); r }"#; + assert!(!check(code).is_empty()); + } + + #[test] + fn no_violation_for_safe_code() { + let code = r#"fn safe() -> u64 { 42 }"#; + assert!(check(code).is_empty()); + } +} \ No newline at end of file