Skip to main content

forge_lint/sol/gas/
costly_loop.rs

1use super::CostlyLoop;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::DataLocation,
8    sema::{
9        Gcx, Hir,
10        builtins::Builtin,
11        hir::{Block, Expr, ExprKind, Function, ItemId, Res, Stmt, StmtKind},
12    },
13};
14
15declare_forge_lint!(COSTLY_LOOP, Severity::Gas, "costly-loop", "storage write inside a loop");
16
17impl<'hir> LateLintPass<'hir> for CostlyLoop {
18    fn check_function(
19        &mut self,
20        ctx: &LintContext,
21        gcx: Gcx<'hir>,
22        hir: &'hir Hir<'hir>,
23        func: &'hir Function<'hir>,
24    ) {
25        if let Some(body) = func.body {
26            check_block(ctx, gcx, hir, body, 0);
27        }
28    }
29}
30
31fn check_block<'hir>(
32    ctx: &LintContext,
33    gcx: Gcx<'hir>,
34    hir: &'hir Hir<'hir>,
35    block: Block<'hir>,
36    loop_depth: u32,
37) {
38    for stmt in block.stmts {
39        check_stmt(ctx, gcx, hir, stmt, loop_depth);
40    }
41}
42
43fn check_stmt<'hir>(
44    ctx: &LintContext,
45    gcx: Gcx<'hir>,
46    hir: &'hir Hir<'hir>,
47    stmt: &'hir Stmt<'hir>,
48    loop_depth: u32,
49) {
50    match &stmt.kind {
51        StmtKind::Loop(block, _) => check_block(ctx, gcx, hir, *block, loop_depth + 1),
52        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
53            check_block(ctx, gcx, hir, *block, loop_depth);
54        }
55        StmtKind::If(_, then_stmt, else_stmt) => {
56            check_stmt(ctx, gcx, hir, then_stmt, loop_depth);
57            if let Some(else_stmt) = else_stmt {
58                check_stmt(ctx, gcx, hir, else_stmt, loop_depth);
59            }
60        }
61        StmtKind::Try(stmt_try) => {
62            for clause in stmt_try.clauses {
63                check_block(ctx, gcx, hir, clause.block, loop_depth);
64            }
65        }
66        StmtKind::Expr(expr) if loop_depth > 0 => {
67            check_expr_for_writes(ctx, gcx, hir, expr);
68        }
69        StmtKind::DeclSingle(var_id) if loop_depth > 0 => {
70            if let Some(init) = hir.variable(*var_id).initializer {
71                check_expr_for_writes(ctx, gcx, hir, init);
72            }
73        }
74        StmtKind::DeclMulti(_, expr) if loop_depth > 0 => {
75            check_expr_for_writes(ctx, gcx, hir, expr);
76        }
77        StmtKind::Return(Some(expr)) if loop_depth > 0 => {
78            check_expr_for_writes(ctx, gcx, hir, expr);
79        }
80        StmtKind::Emit(expr) | StmtKind::Revert(expr) if loop_depth > 0 => {
81            check_expr_for_writes(ctx, gcx, hir, expr);
82        }
83        _ => {}
84    }
85}
86
87fn check_expr_for_writes<'hir>(
88    ctx: &LintContext,
89    gcx: Gcx<'hir>,
90    hir: &'hir Hir<'hir>,
91    expr: &'hir Expr<'hir>,
92) {
93    match &expr.kind {
94        ExprKind::Assign(lhs, _, rhs) => {
95            if lvalue_is_state_var(gcx, hir, lhs) {
96                ctx.emit(&COSTLY_LOOP, expr.span);
97            }
98            check_expr_for_writes(ctx, gcx, hir, lhs);
99            check_expr_for_writes(ctx, gcx, hir, rhs);
100        }
101        ExprKind::Unary(op, inner) => {
102            if op.kind.has_side_effects() && lvalue_is_state_var(gcx, hir, inner) {
103                ctx.emit(&COSTLY_LOOP, expr.span);
104            }
105            check_expr_for_writes(ctx, gcx, hir, inner);
106        }
107        ExprKind::Delete(inner) => {
108            if lvalue_is_state_var(gcx, hir, inner) {
109                ctx.emit(&COSTLY_LOOP, expr.span);
110            }
111            check_expr_for_writes(ctx, gcx, hir, inner);
112        }
113        ExprKind::Binary(lhs, _, rhs) => {
114            check_expr_for_writes(ctx, gcx, hir, lhs);
115            check_expr_for_writes(ctx, gcx, hir, rhs);
116        }
117        ExprKind::Ternary(cond, then_expr, else_expr) => {
118            check_expr_for_writes(ctx, gcx, hir, cond);
119            check_expr_for_writes(ctx, gcx, hir, then_expr);
120            check_expr_for_writes(ctx, gcx, hir, else_expr);
121        }
122        ExprKind::Call(callee, args, named_args) => {
123            check_expr_for_writes(ctx, gcx, hir, callee);
124            for arg in args.exprs() {
125                check_expr_for_writes(ctx, gcx, hir, arg);
126            }
127            if let Some(named_args) = named_args {
128                for arg in named_args.args {
129                    check_expr_for_writes(ctx, gcx, hir, &arg.value);
130                }
131            }
132        }
133        ExprKind::Index(base, index) => {
134            check_expr_for_writes(ctx, gcx, hir, base);
135            if let Some(index) = index {
136                check_expr_for_writes(ctx, gcx, hir, index);
137            }
138        }
139        ExprKind::Slice(base, start, end) => {
140            check_expr_for_writes(ctx, gcx, hir, base);
141            if let Some(start) = start {
142                check_expr_for_writes(ctx, gcx, hir, start);
143            }
144            if let Some(end) = end {
145                check_expr_for_writes(ctx, gcx, hir, end);
146            }
147        }
148        ExprKind::Member(base, _) | ExprKind::Payable(base) => {
149            check_expr_for_writes(ctx, gcx, hir, base);
150        }
151        ExprKind::Tuple(exprs) => {
152            for e in exprs.iter().flatten() {
153                check_expr_for_writes(ctx, gcx, hir, e);
154            }
155        }
156        ExprKind::Array(exprs) => {
157            for e in *exprs {
158                check_expr_for_writes(ctx, gcx, hir, e);
159            }
160        }
161        ExprKind::Ident(_)
162        | ExprKind::Lit(_)
163        | ExprKind::New(_)
164        | ExprKind::TypeCall(_)
165        | ExprKind::Type(_)
166        | ExprKind::YulMember(..)
167        | ExprKind::Err(_) => {}
168    }
169}
170
171/// Returns `true` if the lvalue expression ultimately writes to a storage variable.
172///
173/// Peels through index accesses, member accesses, and slices to find a state variable or an
174/// expression that returns a storage reference.
175fn lvalue_is_state_var(gcx: Gcx<'_>, hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
176    match &expr.peel_parens().kind {
177        ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) => {
178            hir.variable(*id).is_state_variable()
179        }
180        ExprKind::Call(callee, ..) => {
181            gcx.resolved_builtin(callee) == Some(Builtin::ArrayPush0)
182                || gcx
183                    .type_of_expr(expr.peel_parens().id)
184                    .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage))
185        }
186        ExprKind::Index(base, _)
187        | ExprKind::Slice(base, _, _)
188        | ExprKind::Member(base, _)
189        | ExprKind::Payable(base) => lvalue_is_state_var(gcx, hir, base),
190        ExprKind::Tuple(exprs) => exprs.iter().flatten().any(|e| lvalue_is_state_var(gcx, hir, e)),
191        _ => false,
192    }
193}