Skip to main content

forge_lint/sol/gas/
costly_loop.rs

1use super::CostlyLoop;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::write_target},
5};
6use solar::{
7    ast::DataLocation,
8    interface::data_structures::Never,
9    sema::{
10        Gcx, Hir,
11        builtins::Builtin,
12        hir::{self, Expr, ExprKind, Function, Stmt, StmtKind, Visit as _},
13    },
14};
15use std::ops::ControlFlow;
16
17declare_forge_lint!(COSTLY_LOOP, Severity::Gas, "costly-loop", "storage write inside a loop");
18
19impl<'gcx> LateLintPass<'gcx> for CostlyLoop {
20    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
21        let mut finder = LoopWriteFinder { ctx, gcx, loop_depth: 0 };
22        let _ = finder.visit_function(func);
23    }
24}
25
26struct LoopWriteFinder<'a, 'gcx> {
27    ctx: &'a LintContext<'a, 'a>,
28    gcx: Gcx<'gcx>,
29    loop_depth: u32,
30}
31
32impl<'gcx> hir::Visit<'gcx> for LoopWriteFinder<'_, 'gcx> {
33    type BreakValue = Never;
34
35    fn hir(&self) -> &'gcx Hir<'gcx> {
36        &self.gcx.hir
37    }
38
39    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
40        let is_loop = matches!(stmt.kind, StmtKind::Loop(..));
41        self.loop_depth += is_loop as u32;
42        let flow = self.walk_stmt(stmt);
43        self.loop_depth -= is_loop as u32;
44        flow
45    }
46
47    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
48        if self.loop_depth > 0
49            && write_target(expr).is_some_and(|lvalue| lvalue_is_state_var(self.gcx, lvalue))
50        {
51            self.ctx.emit(&COSTLY_LOOP, expr.span);
52        }
53        self.walk_expr(expr)
54    }
55}
56
57/// Returns `true` if the lvalue expression ultimately writes to a storage variable.
58///
59/// Peels through index accesses, member accesses, and slices to find a state variable or an
60/// expression that returns a storage reference.
61fn lvalue_is_state_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
62    let expr = expr.peel_parens();
63    match &expr.kind {
64        ExprKind::Ident(_) => {
65            gcx.resolved_variable(expr).is_some_and(|id| gcx.hir.variable(id).is_state_variable())
66        }
67        ExprKind::Call(callee, ..) => {
68            gcx.resolved_builtin(callee) == Some(Builtin::ArrayPush0)
69                || gcx
70                    .type_of_expr(expr.id)
71                    .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage))
72        }
73        ExprKind::Index(base, _)
74        | ExprKind::Slice(base, _, _)
75        | ExprKind::Member(base, _)
76        | ExprKind::Payable(base) => lvalue_is_state_var(gcx, base),
77        ExprKind::Tuple(exprs) => exprs.iter().flatten().any(|e| lvalue_is_state_var(gcx, e)),
78        _ => false,
79    }
80}