Skip to content

Commit 44b8d39

Browse files
authored
Merge pull request #31 from man-croft/feat/vyper-redundant-external-detector
feat(vyper): Add detector for redundant @external decorators
2 parents a8242e7 + 1effc84 commit 44b8d39

13 files changed

Lines changed: 907 additions & 140 deletions

File tree

apps/api/src/main.rs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
use anyhow::Result;
12
use clap::{Parser, Subcommand};
3+
use colored::Colorize;
24
use gasguard_engine::{ContractScanner, ScanAnalyzer};
35
use std::path::PathBuf;
4-
use anyhow::Result;
56

67
#[derive(Parser)]
78
#[command(name = "gasguard")]
@@ -45,17 +46,17 @@ async fn main() -> Result<()> {
4546
match cli.command {
4647
Commands::Scan { file, format } => {
4748
println!("🔍 Scanning file: {:?}", file);
48-
49+
4950
let result = scanner.scan_file(&file)?;
50-
51+
5152
match format.as_str() {
5253
"json" => {
5354
println!("{}", result.to_json()?);
5455
}
5556
_ => {
5657
println!("{}", ScanAnalyzer::format_violations(&result.violations));
5758
println!("{}", ScanAnalyzer::generate_summary(&result.violations));
58-
59+
5960
if !result.violations.is_empty() {
6061
let savings = ScanAnalyzer::calculate_storage_savings(&result.violations);
6162
println!("\n{}", savings);
@@ -65,16 +66,16 @@ async fn main() -> Result<()> {
6566
}
6667
Commands::ScanDir { directory, format } => {
6768
println!("🔍 Scanning directory: {:?}", directory);
68-
69+
6970
let results = scanner.scan_directory(&directory)?;
70-
71+
7172
if results.is_empty() {
7273
println!("✅ No violations found in any files!");
7374
return Ok(());
7475
}
75-
76+
7677
let total_violations: usize = results.iter().map(|r| r.violations.len()).sum();
77-
78+
7879
match format.as_str() {
7980
"json" => {
8081
println!("{}", serde_json::to_string_pretty(&results)?);
@@ -84,49 +85,62 @@ async fn main() -> Result<()> {
8485
println!("\n📁 File: {}", result.source);
8586
println!("{}", ScanAnalyzer::format_violations(&result.violations));
8687
}
87-
88-
println!("\n{}", format!("📊 Total violations across {} files: {}", results.len(), total_violations).bold());
89-
90-
let all_violations: Vec<_> = results.iter().flat_map(|r| r.violations.clone()).collect();
88+
89+
println!(
90+
"\n{}",
91+
format!(
92+
"📊 Total violations across {} files: {}",
93+
results.len(),
94+
total_violations
95+
)
96+
.bold()
97+
);
98+
99+
let all_violations: Vec<_> =
100+
results.iter().flat_map(|r| r.violations.clone()).collect();
91101
let savings = ScanAnalyzer::calculate_storage_savings(&all_violations);
92102
println!("\n{}", savings);
93103
}
94104
}
95105
}
96106
Commands::Analyze { path } => {
97107
println!("📊 Analyzing storage optimization potential: {:?}", path);
98-
108+
99109
let results = if path.is_file() {
100110
vec![scanner.scan_file(&path)?]
101111
} else {
102112
scanner.scan_directory(&path)?
103113
};
104-
114+
105115
if results.is_empty() {
106116
println!("✅ No optimization opportunities found!");
107117
return Ok(());
108118
}
109-
110-
let all_violations: Vec<_> = results.iter().flat_map(|r| r.violations.clone()).collect();
119+
120+
let all_violations: Vec<_> =
121+
results.iter().flat_map(|r| r.violations.clone()).collect();
111122
let savings = ScanAnalyzer::calculate_storage_savings(&all_violations);
112-
123+
113124
println!("\n🎯 Storage Analysis Report");
114125
println!("========================");
115126
println!("Files analyzed: {}", results.len());
116127
println!("Total violations: {}", all_violations.len());
117128
println!("\n{}", savings);
118-
129+
119130
// Group violations by type
120131
let mut unused_vars = 0;
121132
for violation in &all_violations {
122133
if violation.rule_name == "unused-state-variables" {
123134
unused_vars += 1;
124135
}
125136
}
126-
137+
127138
if unused_vars > 0 {
128139
println!("\n🔧 Recommendations:");
129-
println!(" • Remove {} unused state variables to reduce storage costs", unused_vars);
140+
println!(
141+
" • Remove {} unused state variables to reduce storage costs",
142+
unused_vars
143+
);
130144
println!(" • Consider using more efficient data types where possible");
131145
println!(" • Implement lazy loading patterns for rarely accessed data");
132146
}

examples/redundant_external.vy

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# @version ^0.3.0
2+
3+
# Example Vyper contract demonstrating redundant @external decorators
4+
# This file contains violations that GasGuard should detect
5+
6+
# Storage variables
7+
owner: public(address)
8+
balance: public(uint256)
9+
fee_rate: uint256
10+
11+
@external
12+
def __init__():
13+
"""Initialize the contract - legitimate @external"""
14+
self.owner = msg.sender
15+
self.balance = 0
16+
self.fee_rate = 30 # 0.3%
17+
18+
# VIOLATION 1: Internal naming convention with @external
19+
# Functions starting with _ should be @internal
20+
@external
21+
def _calculate_fee(amount: uint256) -> uint256:
22+
"""This function uses internal naming convention but is marked external"""
23+
return amount * self.fee_rate / 10000
24+
25+
# VIOLATION 2: Another internal-named function with @external
26+
@external
27+
def _validate_amount(amount: uint256) -> bool:
28+
"""Validation helper that should be internal"""
29+
return amount > 0 and amount <= self.balance
30+
31+
# CORRECT: Properly marked internal function
32+
@internal
33+
def _update_balance(new_balance: uint256):
34+
"""Internal helper - correctly marked"""
35+
self.balance = new_balance
36+
37+
# CORRECT: Legitimate external function
38+
@external
39+
def deposit():
40+
"""Public deposit function - legitimate @external"""
41+
self.balance += msg.value
42+
43+
# CORRECT: Legitimate external function
44+
@external
45+
def withdraw(amount: uint256):
46+
"""Public withdraw function - legitimate @external"""
47+
assert self._validate_amount(amount), "Invalid amount"
48+
fee: uint256 = self._calculate_fee(amount)
49+
self._update_balance(self.balance - amount)
50+
send(msg.sender, amount - fee)
51+
52+
# VIOLATION 3: Helper function only called internally
53+
@external
54+
def calculate_withdrawal_fee(amount: uint256) -> uint256:
55+
"""This is only called internally but marked external"""
56+
return self._calculate_fee(amount)
57+
58+
# CORRECT: View function for external queries
59+
@external
60+
@view
61+
def get_balance() -> uint256:
62+
"""External view function - legitimate @external"""
63+
return self.balance
64+
65+
# CORRECT: External function with proper visibility
66+
@external
67+
def transfer(to: address, amount: uint256):
68+
"""Transfer function - legitimate @external"""
69+
assert self._validate_amount(amount), "Invalid amount"
70+
self._update_balance(self.balance - amount)
71+
send(to, amount)

libs/engine/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ serde_json = "1.0"
1111
tokio = { version = "1.0", features = ["full"] }
1212
anyhow = "1.0"
1313
colored = "2.0"
14+
chrono = { version = "0.4", features = ["serde"] }
15+
walkdir = "2.0"

libs/engine/src/analyzer.rs

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,67 @@
1-
use gasguard_rules::{RuleViolation, ViolationSeverity};
21
use colored::*;
2+
use gasguard_rules::{RuleViolation, ViolationSeverity};
33
use std::fmt;
44

55
pub struct ScanAnalyzer;
66

77
impl ScanAnalyzer {
88
pub fn format_violations(violations: &[RuleViolation]) -> String {
99
if violations.is_empty() {
10-
return "✅ No violations found! Your contract is optimized.".green().to_string();
10+
return "✅ No violations found! Your contract is optimized."
11+
.green()
12+
.to_string();
1113
}
12-
14+
1315
let mut output = String::new();
1416
let (errors, warnings, info) = Self::categorize_violations(violations);
15-
17+
1618
if !errors.is_empty() {
1719
output.push_str(&format!("🚨 {} Errors:\n", errors.len()).red().bold());
1820
for violation in errors {
1921
output.push_str(&Self::format_single_violation(violation, "ERROR"));
2022
}
2123
output.push('\n');
2224
}
23-
25+
2426
if !warnings.is_empty() {
25-
output.push_str(&format!("⚠️ {} Warnings:\n", warnings.len()).yellow().bold());
27+
output.push_str(
28+
&format!("⚠️ {} Warnings:\n", warnings.len())
29+
.yellow()
30+
.bold(),
31+
);
2632
for violation in warnings {
2733
output.push_str(&Self::format_single_violation(violation, "WARNING"));
2834
}
2935
output.push('\n');
3036
}
31-
37+
3238
if !info.is_empty() {
3339
output.push_str(&format!("ℹ️ {} Info:\n", info.len()).blue().bold());
3440
for violation in info {
3541
output.push_str(&Self::format_single_violation(violation, "INFO"));
3642
}
3743
}
38-
44+
3945
output
4046
}
41-
47+
4248
pub fn generate_summary(violations: &[RuleViolation]) -> String {
4349
let total = violations.len();
4450
let (errors, warnings, info) = Self::categorize_violations(violations);
45-
51+
4652
format!(
4753
"Scan Summary: {} total violations ({} errors, {} warnings, {} info)",
48-
total, errors.len(), warnings.len(), info.len()
54+
total,
55+
errors.len(),
56+
warnings.len(),
57+
info.len()
4958
)
5059
}
51-
60+
5261
pub fn calculate_storage_savings(violations: &[RuleViolation]) -> StorageSavings {
5362
let mut unused_vars = 0;
5463
let mut estimated_savings_kb = 0.0;
55-
64+
5665
for violation in violations {
5766
if violation.rule_name == "unused-state-variables" {
5867
unused_vars += 1;
@@ -61,38 +70,44 @@ impl ScanAnalyzer {
6170
estimated_savings_kb += 2.5; // Average variable size in KB
6271
}
6372
}
64-
73+
6574
StorageSavings {
6675
unused_variables: unused_vars,
6776
estimated_savings_kb,
6877
monthly_ledger_rent_savings: estimated_savings_kb * 0.001, // Rough estimate
6978
}
7079
}
71-
72-
fn categorize_violations(violations: &[RuleViolation]) -> (Vec<&RuleViolation>, Vec<&RuleViolation>, Vec<&RuleViolation>) {
80+
81+
fn categorize_violations(
82+
violations: &[RuleViolation],
83+
) -> (
84+
Vec<&RuleViolation>,
85+
Vec<&RuleViolation>,
86+
Vec<&RuleViolation>,
87+
) {
7388
let mut errors = Vec::new();
7489
let mut warnings = Vec::new();
7590
let mut info = Vec::new();
76-
91+
7792
for violation in violations {
7893
match violation.severity {
7994
ViolationSeverity::Error => errors.push(violation),
8095
ViolationSeverity::Warning => warnings.push(violation),
8196
ViolationSeverity::Info => info.push(violation),
8297
}
8398
}
84-
99+
85100
(errors, warnings, info)
86101
}
87-
102+
88103
fn format_single_violation(violation: &RuleViolation, severity: &str) -> String {
89104
let severity_color = match severity {
90105
"ERROR" => colored::Color::Red,
91106
"WARNING" => colored::Color::Yellow,
92107
"INFO" => colored::Color::Blue,
93108
_ => colored::Color::White,
94109
};
95-
110+
96111
format!(
97112
"{}\n 📍 Line {}: {}\n 📝 {}\n 💡 {}\n\n",
98113
format!(" [{}]", severity).color(severity_color).bold(),

libs/engine/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
pub mod scanner;
21
pub mod analyzer;
2+
pub mod scanner;
33

4-
pub use scanner::*;
54
pub use analyzer::*;
5+
pub use scanner::*;

0 commit comments

Comments
 (0)