Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/rules/src/auditability/docs/missing_natspec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Detect Missing NatSpec Documentation
//!
//! Flags public/external functions that lack NatSpec (`///` or `/** */`) comments.
//! Poor documentation reduces auditability and maintainability.

use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity};
use syn::{Item, Visibility};

pub struct MissingNatspecRule;

impl Rule for MissingNatspecRule {
fn name(&self) -> &str {
"missing-natspec"
}

fn description(&self) -> &str {
"Detects public functions missing NatSpec documentation comments."
}

fn check(&self, ast: &[Item]) -> Vec<RuleViolation> {
let mut violations = Vec::new();
for item in ast {
if let Item::Fn(func) = item {
let is_public = matches!(func.vis, Visibility::Public(_));
let has_doc = func.attrs.iter().any(|a| a.path().is_ident("doc"));
if is_public && !has_doc {
violations.push(RuleViolation {
rule_name: self.name().to_string(),
description: format!(
"Public function `{}` is missing NatSpec documentation.",
func.sig.ident
),
severity: ViolationSeverity::Low,
line_number: 0,
column_number: 0,
variable_name: func.sig.ident.to_string(),
suggestion: "Add `/// @notice`, `/// @param`, and `/// @return` \
NatSpec comments above the function."
.to_string(),
});
}
}
}
violations
}
}

#[cfg(test)]
mod tests {
use super::*;
use syn::parse_file;

fn check(code: &str) -> Vec<RuleViolation> {
let ast = parse_file(code).expect("parse failed");
MissingNatspecRule.check(&ast.items)
}

#[test]
fn flags_undocumented_public_fn() {
assert!(!check("pub fn transfer() {}").is_empty());
}

#[test]
fn no_violation_for_documented_fn() {
assert!(check("/// @notice transfers tokens\npub fn transfer() {}").is_empty());
}

#[test]
fn no_violation_for_private_fn() {
assert!(check("fn internal_helper() {}").is_empty());
}
}
2 changes: 2 additions & 0 deletions packages/rules/src/auditability/docs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod missing_natspec;
pub use missing_natspec::MissingNatspecRule;
2 changes: 2 additions & 0 deletions packages/rules/src/auditability/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod docs;
pub use docs::MissingNatspecRule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Detect External Calls Inside Loops
//!
//! Flags external function calls that appear inside loop bodies.
//! This pattern increases gas costs and expands the reentrancy attack surface.

use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity};
use syn::{Expr, Item, Stmt};

pub struct ExternalCallsInLoopsRule;

impl Rule for ExternalCallsInLoopsRule {
fn name(&self) -> &str {
"external-calls-in-loops"
}

fn description(&self) -> &str {
"Detects external calls inside loop bodies. \
This increases gas risk and expands the reentrancy attack surface."
}

fn check(&self, ast: &[Item]) -> Vec<RuleViolation> {
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 ExternalCallsInLoopsRule {
fn check_stmts(&self, stmts: &[Stmt], violations: &mut Vec<RuleViolation>) {
for stmt in stmts {
if let Stmt::Expr(Expr::ForLoop(loop_expr), _) = stmt {
self.check_loop_body(&loop_expr.body.stmts, violations);
}
if let Stmt::Expr(Expr::While(loop_expr), _) = stmt {
self.check_loop_body(&loop_expr.body.stmts, violations);
}
}
}

fn check_loop_body(&self, stmts: &[Stmt], violations: &mut Vec<RuleViolation>) {
for stmt in stmts {
if let Stmt::Expr(Expr::MethodCall(call), _) = stmt {
let method = call.method.to_string();
if matches!(method.as_str(), "transfer" | "call" | "send" | "invoke") {
violations.push(RuleViolation {
rule_name: self.name().to_string(),
description: format!(
"External call `{}` inside a loop increases gas and reentrancy risk.",
method
),
severity: ViolationSeverity::High,
line_number: 0,
column_number: 0,
variable_name: method,
suggestion: "Collect results in the loop and perform external calls after."
.to_string(),
});
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use syn::parse_file;

#[test]
fn no_violation_no_loop() {
let ast = parse_file("fn f() { x.transfer(1); }").expect("parse");
assert!(ExternalCallsInLoopsRule.check(&ast.items).is_empty());
}
}
2 changes: 2 additions & 0 deletions packages/rules/src/security/external_calls/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod external_calls_in_loops;
pub use external_calls_in_loops::ExternalCallsInLoopsRule;
Loading