diff --git a/packages/rules/src/lib.rs b/packages/rules/src/lib.rs index 087ea11..333d14d 100644 --- a/packages/rules/src/lib.rs +++ b/packages/rules/src/lib.rs @@ -18,7 +18,7 @@ pub use rule_engine::{ extract_struct_fields, find_variable_usage, Rule, RuleEngine, RuleViolation, ViolationSeverity, }; pub use security::{HardcodedAddressesRule, MissingDomainSeparationRule, defi::MissingSlippageValidationRule}; -pub use solidity::{StateVariablePackingRule, MappingIterationRule}; +pub use solidity::{StateVariablePackingRule, MappingIterationRule, AbiEncodingRule}; pub use optimization::storage::detect_mapping_iteration; pub use unused_state_variables::UnusedStateVariablesRule; diff --git a/packages/rules/src/optimization/encoding/abi_encode.rs b/packages/rules/src/optimization/encoding/abi_encode.rs new file mode 100644 index 0000000..c50a62a --- /dev/null +++ b/packages/rules/src/optimization/encoding/abi_encode.rs @@ -0,0 +1,112 @@ +use gasguard_ast::{Language, UnifiedAST}; +use crate::rule_engine::{RuleViolation, ViolationSeverity}; + +/// Detects inefficient use of `abi.encode` that could be replaced with `abi.encodePacked`. +pub fn detect_abi_encoding_inefficiencies(ast: &UnifiedAST) -> Vec { + let mut violations = Vec::new(); + + if ast.language != Language::Solidity { + return violations; + } + + for contract in &ast.contracts { + for func in &contract.functions { + let body = &func.body_raw; + + // Simple check for abi.encode usage while trying to avoid false positives + // like abi.encodePacked, abi.encodeWithSignature, etc. + + // Basic search strategy: + // Find occurrences of "abi.encode" and ensure the next character is "(" or whitespace then "(" + let mut search_start = 0; + while let Some(index) = body[search_start..].find("abi.encode") { + let absolute_index = search_start + index; + let remainder = &body[absolute_index + "abi.encode".len()..]; + + // Check if the next non-whitespace char is '(' + if let Some(next_char) = remainder.chars().find(|c| !c.is_whitespace()) { + if next_char == '(' { + violations.push(RuleViolation { + rule_name: "abi-encoding-inefficiency".to_string(), + description: "Detected potentially inefficient use of abi.encode".to_string(), + severity: ViolationSeverity::Medium, + line_number: func.line_number, // We use the function's start line as a proxy + column_number: 1, + variable_name: "abi.encode".to_string(), + suggestion: "Consider using abi.encodePacked to save gas, unless you require 32-byte padding (e.g., to prevent hash collisions with multiple dynamic types or interfacing with standard ABI requirements).".to_string(), + }); + } + } + + search_start = absolute_index + "abi.encode".len(); + } + } + } + + violations +} + +#[cfg(test)] +mod tests { + use super::*; + use gasguard_ast::{ContractNode, FunctionNode, Visibility}; + + #[test] + fn test_detect_abi_encoding() { + let ast = UnifiedAST { + language: Language::Solidity, + source: "".to_string(), + file_path: "".to_string(), + structs: vec![], + enums: vec![], + contracts: vec![ContractNode { + name: "Test".to_string(), + line_number: 1, + state_variables: vec![], + functions: vec![ + FunctionNode { + name: "testInefficient".to_string(), + params: vec![], + return_type: None, + visibility: Visibility::Public, + decorators: vec![], + is_constructor: false, + is_external: false, + is_payable: false, + line_number: 5, + body_raw: "bytes memory data = abi.encode(1, 2);".to_string(), + }, + FunctionNode { + name: "testEfficient".to_string(), + params: vec![], + return_type: None, + visibility: Visibility::Public, + decorators: vec![], + is_constructor: false, + is_external: false, + is_payable: false, + line_number: 10, + body_raw: "bytes memory data = abi.encodePacked(1, 2);".to_string(), + }, + FunctionNode { + name: "testEncodeWithSelector".to_string(), + params: vec![], + return_type: None, + visibility: Visibility::Public, + decorators: vec![], + is_constructor: false, + is_external: false, + is_payable: false, + line_number: 15, + body_raw: "bytes memory data = abi.encodeWithSelector(bytes4(keccak256(\"foo()\")));".to_string(), + } + ], + }], + }; + + let violations = detect_abi_encoding_inefficiencies(&ast); + assert_eq!(violations.len(), 1); + assert_eq!(violations[0].line_number, 5); + assert_eq!(violations[0].variable_name, "abi.encode"); + } +} diff --git a/packages/rules/src/optimization/encoding/mod.rs b/packages/rules/src/optimization/encoding/mod.rs new file mode 100644 index 0000000..4f7d9b1 --- /dev/null +++ b/packages/rules/src/optimization/encoding/mod.rs @@ -0,0 +1,3 @@ +pub mod abi_encode; + +pub use abi_encode::detect_abi_encoding_inefficiencies; diff --git a/packages/rules/src/optimization/mod.rs b/packages/rules/src/optimization/mod.rs index 767a185..c84203d 100644 --- a/packages/rules/src/optimization/mod.rs +++ b/packages/rules/src/optimization/mod.rs @@ -1,4 +1,5 @@ pub mod deployment; +pub mod encoding; pub mod storage; pub use storage::{ @@ -7,3 +8,4 @@ pub use storage::{ }; pub use deployment::{estimate_bytecode_size, ExcessiveContractSizeRule}; +pub use encoding::detect_abi_encoding_inefficiencies; diff --git a/packages/rules/src/solidity/abi_encode.rs b/packages/rules/src/solidity/abi_encode.rs new file mode 100644 index 0000000..a8f503c --- /dev/null +++ b/packages/rules/src/solidity/abi_encode.rs @@ -0,0 +1,43 @@ +use crate::optimization::encoding::detect_abi_encoding_inefficiencies; +use crate::rule_engine::{Rule, RuleViolation}; +use gasguard_ast::{Language, UnifiedAST}; +use syn::Item; + +pub struct AbiEncodingRule; + +impl Rule for AbiEncodingRule { + fn name(&self) -> &str { + "abi-encoding-inefficiency" + } + + fn description(&self) -> &str { + "Detects inefficient use of abi.encode that could be replaced with abi.encodePacked" + } + + fn check(&self, _ast: &[Item]) -> Vec { + // Handled via analyze + Vec::new() + } +} + +impl AbiEncodingRule { + pub fn analyze(&self, ast: &UnifiedAST) -> Vec { + if ast.language != Language::Solidity { + return Vec::new(); + } + + detect_abi_encoding_inefficiencies(ast) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_abi_encoding_rule_info() { + let rule = AbiEncodingRule; + assert_eq!(rule.name(), "abi-encoding-inefficiency"); + assert!(rule.description().contains("abi.encode")); + } +} diff --git a/packages/rules/src/solidity/mod.rs b/packages/rules/src/solidity/mod.rs index 0152fc6..91a31a6 100644 --- a/packages/rules/src/solidity/mod.rs +++ b/packages/rules/src/solidity/mod.rs @@ -1,6 +1,8 @@ pub mod state_variable_packing; pub mod uint8_vs_uint256; pub mod mapping_iteration; +pub mod abi_encode; pub use state_variable_packing::StateVariablePackingRule; pub use mapping_iteration::MappingIterationRule; +pub use abi_encode::AbiEncodingRule; diff --git a/pr_body.txt b/pr_body.txt new file mode 100644 index 0000000..bc736dd --- /dev/null +++ b/pr_body.txt @@ -0,0 +1,4 @@ +Closes #342 + +### Summary +This PR adds the MappingIterationRule to detect gas-heavy mapping iteration workarounds where developers use an unbounded helper array to simulate mapping iterability on-chain. This rule will alert developers and suggest off-chain solutions or pagination.