diff --git a/packages/rules/src/lib.rs b/packages/rules/src/lib.rs index dbed9f5..f9558bd 100644 --- a/packages/rules/src/lib.rs +++ b/packages/rules/src/lib.rs @@ -11,6 +11,7 @@ pub mod vyper; // Explicitly export core types to avoid ambiguity pub use optimization::arrays::detect_dynamic_array_deletions; pub use optimization::deployment::{estimate_bytecode_size, ExcessiveContractSizeRule}; +pub use optimization::gas::YulCandidatesRule; pub use optimization::storage::detect_mapping_iteration; pub use optimization::storage::{ detect_packing_opportunities, find_consecutive_packable_groups, get_type_size, diff --git a/packages/rules/src/optimization/gas/g010_yul_candidates.rs b/packages/rules/src/optimization/gas/g010_yul_candidates.rs new file mode 100644 index 0000000..c2924b0 --- /dev/null +++ b/packages/rules/src/optimization/gas/g010_yul_candidates.rs @@ -0,0 +1,193 @@ +use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity}; +use syn::Item; + +pub struct YulCandidatesRule; + +impl Rule for YulCandidatesRule { + fn name(&self) -> &str { + "yul-candidates" + } + + fn description(&self) -> &str { + "Identifies complex, repetitive Solidity math/storage routines that would benefit from inline Yul optimization, typically yielding >15% gas savings." + } + + fn check(&self, _ast: &[Item]) -> Vec { + Vec::new() + } +} + +impl YulCandidatesRule { + pub fn detect_yul_candidates(source: &str) -> Vec { + let mut violations = Vec::new(); + + let function_re = regex::Regex::new(r"function\s+(\w+)\s*\([^)]*\)[^{]*\{").unwrap(); + let assembly_re = regex::Regex::new(r"assembly\s*\{").unwrap(); + + for cap in function_re.captures_iter(source) { + let func_name = cap.get(1).unwrap().as_str(); + let func_start = cap.get(0).unwrap().end(); + + let func_body = Self::extract_function_body(&source[func_start..]); + if func_body.is_empty() { + continue; + } + + if assembly_re.is_match(&func_body) { + continue; + } + + let score = Self::compute_yul_score(&func_body); + if score >= 6 { + let line_number = source[..func_start].lines().count(); + violations.push(RuleViolation { + rule_name: self::YulCandidatesRule.name().to_string(), + description: format!( + "Function `{}` contains repetitive math/storage patterns (score: {}) that likely benefit from inline Yul. Converting to Yul can yield >15% gas savings by eliminating Solidity overhead.", + func_name, score + ), + severity: ViolationSeverity::Medium, + line_number, + column_number: 1, + variable_name: func_name.to_string(), + suggestion: format!( + "Consider rewriting the expensive math/storage routines in `{}` using inline Yul assembly to reduce gas costs.", + func_name + ), + }); + } + } + + violations + } + + fn extract_function_body(rest: &str) -> String { + let mut depth = 0i32; + let mut body = String::new(); + let mut started = false; + + for ch in rest.chars() { + if ch == '{' { + depth += 1; + started = true; + } else if ch == '}' { + depth -= 1; + if started && depth == 0 { + body.push(ch); + break; + } + } + if started { + body.push(ch); + } + } + + body + } + + fn compute_yul_score(body: &str) -> i32 { + let mut score = 0i32; + + let keccak_count = body.matches("keccak256(").count(); + score += (keccak_count as i32) * 3; + + let sload_count = body.matches("sload(").count(); + score += (sload_count as i32) * 2; + + let sstore_count = body.matches("sstore(").count(); + score += (sstore_count as i32) * 3; + + let bitwise_ops = body.matches('&').count() + + body.matches('|').count() + + body.matches('^').count() + + body.matches("<<").count() + + body.matches(">>").count(); + score += (bitwise_ops as i32); + + let addmod_count = body.matches("addmod(").count(); + score += (addmod_count as i32) * 2; + + let mulmod_count = body.matches("mulmod(").count(); + score += (mulmod_count as i32) * 2; + + score + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flags_repeated_keccak256_as_yul_candidate() { + let src = r#" + contract Merkle { + function verify(bytes32[] memory proof, bytes32 root) public view returns (bool) { + bytes32 computed = proof[0]; + for (uint i = 0; i < proof.length; i++) { + computed = keccak256(abi.encodePacked(computed, proof[i])); + } + return computed == root; + } + } + "#; + let violations = YulCandidatesRule::detect_yul_candidates(src); + assert!( + !violations.is_empty(), + "should flag repeated keccak256 loop as Yul candidate" + ); + assert_eq!(violations[0].variable_name, "verify"); + } + + #[test] + fn flags_repeated_storage_access_as_yul_candidate() { + let src = r#" + contract Storage { + function batchRead() public view returns (uint) { + uint a = slotA; + uint b = slotB; + uint c = slotC; + uint d = slotD; + uint e = slotE; + return a + b + c + d + e; + } + } + "#; + let violations = YulCandidatesRule::detect_yul_candidates(src); + assert!( + !violations.is_empty(), + "should flag repeated storage reads as Yul candidate" + ); + } + + #[test] + fn does_not_flag_simple_function() { + let src = r#" + contract Simple { + function add(uint a, uint b) public pure returns (uint) { + return a + b; + } + } + "#; + let violations = YulCandidatesRule::detect_yul_candidates(src); + assert!(violations.is_empty(), "simple functions should not be flagged"); + } + + #[test] + fn does_not_flag_function_already_using_assembly() { + let src = r#" + contract Optimized { + function hash(uint a) public pure returns (bytes32) { + assembly { + let result := keccak256(add(a, 1), 0) + } + } + } + "#; + let violations = YulCandidatesRule::detect_yul_candidates(src); + assert!( + violations.is_empty(), + "functions already using assembly should not be flagged" + ); + } +} diff --git a/packages/rules/src/optimization/gas/mod.rs b/packages/rules/src/optimization/gas/mod.rs new file mode 100644 index 0000000..13717ae --- /dev/null +++ b/packages/rules/src/optimization/gas/mod.rs @@ -0,0 +1,5 @@ +pub mod duplicate_require_statements; +pub mod g010_yul_candidates; + +pub use duplicate_require_statements::detect_duplicate_require_statements; +pub use g010_yul_candidates::YulCandidatesRule; diff --git a/packages/rules/src/optimization/mod.rs b/packages/rules/src/optimization/mod.rs index 8a5177d..7d9942d 100644 --- a/packages/rules/src/optimization/mod.rs +++ b/packages/rules/src/optimization/mod.rs @@ -5,6 +5,7 @@ pub mod storage; pub mod visibility; pub use arrays::detect_dynamic_array_deletions; +pub use gas::g010_yul_candidates::YulCandidatesRule; pub use storage::{ detect_packing_opportunities, find_consecutive_packable_groups, get_type_size, is_packable_type, PackingOpportunity, VariableInfo,