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
7 changes: 4 additions & 3 deletions packages/rules/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ pub use unused_state_variables::UnusedStateVariablesRule;

// Export Soroban types specifically
pub use soroban::{
InefficientBytesAllocationRule, SorobanAnalyzer, SorobanContract, SorobanField,
SorobanFunction, SorobanImpl, SorobanParam, SorobanParser, SorobanResult, SorobanRuleEngine,
SorobanStruct,
EventEmissionCostRule, InefficientBytesAllocationRule, InefficientErrorConstructionRule,
MemoryAllocationRule, SorobanAnalyzer, SorobanContract, SorobanField, SorobanFunction,
SorobanImpl, SorobanParam, SorobanParser, SorobanResult, SorobanRuleEngine, SorobanStruct,
UnnecessaryCloningRule,
};

// Export Vyper types (keeping glob here is fine if Vyper module is clean, but let's be safe)
Expand Down
187 changes: 187 additions & 0 deletions packages/rules/src/soroban/event_emission.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! Rule: Soroban Event Emission Cost Analyzer (Issue #778)
//!
//! Every event emitted by a Soroban contract is recorded in the ledger's
//! transaction meta and contributes to the transaction's resource usage
//! (both CPU and footprint). Excessive or oversized event payloads therefore
//! directly inflate transaction costs for end-users.
//!
//! ## What this rule detects
//!
//! * Functions that emit events frequently (multiple `env.events().publish`
//! calls), where some could be merged or removed.
//! * Event payloads that include large types (`Vec`, `Map`, `Bytes`,
//! `BytesN`, `String`) which increase serialization overhead.
//! * Redundant events emitted with identical topic patterns, suggesting
//! duplicate or no-op emissions.
//! * Events emitted inside loops, which multiplies their resource cost.
//!
//! ## Suggested fix
//!
//! * Merge related events into a single emission with a richer data payload.
//! * Use lightweight `Symbol` topics and small integer/boolean data fields.
//! * Move event emission outside loops where possible, emitting a summary
//! event once rather than one event per iteration.
//! * Remove redundant events that carry no unique information.

use crate::soroban::rule_engine::SorobanRule;
use crate::soroban::SorobanContract;
use crate::{RuleViolation, ViolationSeverity};

/// Soroban event publish call patterns.
const EVENT_EMIT_PATTERNS: &[&str] = &[
"env.events().publish(",
"events().publish(",
".publish(",
];

/// Large payload types that make events expensive to serialize.
const LARGE_PAYLOAD_TYPES: &[&str] = &[
"Vec<",
"Map<",
"Bytes",
"BytesN",
"soroban_sdk::String",
"String",
];

/// Loop keywords for detecting in-loop event emission.
const LOOP_KEYWORDS: &[&str] = &["for ", "while ", "loop {"];

/// Rule for detecting expensive event emission patterns in Soroban contracts.
pub struct EventEmissionCostRule {
enabled: bool,
}

impl Default for EventEmissionCostRule {
fn default() -> Self {
Self { enabled: true }
}
}

impl SorobanRule for EventEmissionCostRule {
fn id(&self) -> &str {
"soroban-event-emission-cost"
}

fn name(&self) -> &str {
"Soroban Event Emission Cost"
}

fn description(&self) -> &str {
"Detects expensive event emission patterns including frequent emissions, large \
payloads, redundant events, and in-loop emissions that inflate Soroban \
transaction resource costs."
}

fn severity(&self) -> ViolationSeverity {
ViolationSeverity::Medium
}

fn is_enabled(&self) -> bool {
self.enabled
}

fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}

fn apply(&self, contract: &SorobanContract) -> Vec<RuleViolation> {
let mut violations = Vec::new();

for implementation in &contract.implementations {
for function in &implementation.functions {
let src = &function.raw_definition;

// Count total event emissions in this function
let emit_count: usize = EVENT_EMIT_PATTERNS
.iter()
.map(|p| src.matches(p).count())
.sum();

if emit_count == 0 {
continue;
}

// Flag frequent event emission (multiple per function)
if emit_count >= 3 {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: format!(
"Function '{}' emits {} events; frequent emission inflates \
transaction resource usage",
function.name, emit_count
),
suggestion: "Merge related events into a single emission. Emit only \
state-changing events and remove informational duplicates."
.to_string(),
line_number: function.line_number,
column_number: 0,
variable_name: function.name.clone(),
severity: self.severity(),
});
}

// Flag large payload types in event-emitting functions
for payload_type in LARGE_PAYLOAD_TYPES {
if src.contains(payload_type) {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: format!(
"Function '{}' emits an event with a large '{}' payload, \
increasing serialization cost",
function.name, payload_type
),
suggestion: format!(
"Avoid including '{}' in event data. Use lightweight types \
(Symbol, u64, bool) as event data to reduce ledger footprint.",
payload_type
),
line_number: function.line_number,
column_number: 0,
variable_name: function.name.clone(),
severity: ViolationSeverity::Low,
});
break; // one per function is sufficient
}
}

// Flag event emission inside loops
let has_loop = LOOP_KEYWORDS.iter().any(|kw| src.contains(kw));
if has_loop {
let loop_start = LOOP_KEYWORDS
.iter()
.filter_map(|kw| src.find(kw))
.min();

if let Some(loop_pos) = loop_start {
let in_loop_section = &src[loop_pos..];
let in_loop_emit = EVENT_EMIT_PATTERNS
.iter()
.any(|p| in_loop_section.contains(p));

if in_loop_emit {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: format!(
"Function '{}' emits events inside a loop, multiplying \
resource costs per iteration",
function.name
),
suggestion: "Move event emission outside the loop. Accumulate \
relevant data and emit a single summary event after the loop \
completes."
.to_string(),
line_number: function.line_number,
column_number: 0,
variable_name: function.name.clone(),
severity: ViolationSeverity::High,
});
}
}
}
}
}

violations
}
}
147 changes: 147 additions & 0 deletions packages/rules/src/soroban/inefficient_error_construction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! Rule: Detect Inefficient Soroban Error Construction (Issue #777)
//!
//! Complex or repeated error construction in Soroban contracts adds avoidable
//! execution and serialization overhead. On error paths, allocating strings,
//! formatting messages, or constructing compound error types wastes metered
//! CPU and memory budget — especially because the error path is exercised on
//! every failing invocation.
//!
//! ## What this rule detects
//!
//! * Repeated construction of the same error type within a single function
//! (suggests the error value could be cached or inlined as a constant).
//! * Use of `format!()` or string allocation on error paths, which triggers
//! expensive host-string operations.
//! * Allocations (e.g. `Vec::new`, `String::new`) that only appear on error
//! branches — avoidable by using simple enum variants instead.
//!
//! ## Suggested fix
//!
//! * Use simple Soroban `contracterror` enum variants with numeric codes
//! rather than string payloads.
//! * Avoid `format!()` in error returns; prefer pre-defined error constants.
//! * Deduplicate repeated `Err(SomeError::Variant)` constructions by
//! extracting a helper or returning early once.

use crate::soroban::rule_engine::SorobanRule;
use crate::soroban::SorobanContract;
use crate::{RuleViolation, ViolationSeverity};

/// Patterns that indicate expensive error payload construction.
const EXPENSIVE_ERROR_PATTERNS: &[&str] = &[
"format!(",
"String::from(",
"String::new(",
".to_string()",
"Vec::new(",
"vec![",
];

/// Patterns that indicate an error is being returned.
const ERROR_RETURN_PATTERNS: &[&str] = &[
"return Err(",
"Err(",
"panic!(",
".unwrap_or_else(",
".map_err(",
];

/// Rule for detecting inefficient error construction in Soroban contracts.
pub struct InefficientErrorConstructionRule {
enabled: bool,
}

impl Default for InefficientErrorConstructionRule {
fn default() -> Self {
Self { enabled: true }
}
}

impl SorobanRule for InefficientErrorConstructionRule {
fn id(&self) -> &str {
"soroban-inefficient-error-construction"
}

fn name(&self) -> &str {
"Inefficient Soroban Error Construction"
}

fn description(&self) -> &str {
"Detects costly error construction patterns — string formatting, heap allocations, \
or repeated identical error instantiations — that increase execution overhead \
on Soroban error paths."
}

fn severity(&self) -> ViolationSeverity {
ViolationSeverity::Medium
}

fn is_enabled(&self) -> bool {
self.enabled
}

fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}

fn apply(&self, contract: &SorobanContract) -> Vec<RuleViolation> {
let mut violations = Vec::new();

for implementation in &contract.implementations {
for function in &implementation.functions {
let src = &function.raw_definition;

let has_error_return = ERROR_RETURN_PATTERNS.iter().any(|p| src.contains(p));
if !has_error_return {
continue;
}

// Detect expensive allocations on error paths
for pattern in EXPENSIVE_ERROR_PATTERNS {
if src.contains(pattern) {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: format!(
"Function '{}' uses '{}' on an error path, adding unnecessary \
allocation overhead",
function.name, pattern
),
suggestion: "Replace string/allocation-based error payloads with \
simple #[contracterror] enum variants that carry only integer \
codes. This eliminates heap allocations on the error path."
.to_string(),
line_number: function.line_number,
column_number: 0,
variable_name: function.name.clone(),
severity: self.severity(),
});
break; // one violation per function is enough
}
}

// Detect repeated error construction (same Err( pattern appears multiple times)
let err_count = src.matches("Err(").count();
if err_count >= 3 {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: format!(
"Function '{}' constructs errors {} times; consider consolidating \
repeated error returns",
function.name, err_count
),
suggestion: "Extract repeated error-return logic into a single early-exit \
or use the `?` operator to propagate errors without re-constructing \
them at every site."
.to_string(),
line_number: function.line_number,
column_number: 0,
variable_name: function.name.clone(),
severity: ViolationSeverity::Low,
});
}
}
}

violations
}
}
Loading
Loading