Skip to main content

forge_lint/sol/low/
calls_loop.rs

1use super::{
2    CallsLoop,
3    payable_loop::{LoopItem, for_each_loop_item},
4};
5use crate::{
6    linter::{LateLintPass, LintContext},
7    sol::{Severity, SolLint},
8};
9use solar::{
10    ast::StateMutability,
11    sema::{
12        Gcx,
13        builtins::Builtin,
14        hir::{Expr, ExprKind, Function},
15        ty::{TyFnKind, TyKind},
16    },
17};
18
19declare_forge_lint!(CALLS_LOOP, Severity::Low, "calls-loop", "external call inside a loop");
20
21impl<'gcx> LateLintPass<'gcx> for CallsLoop {
22    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
23        for_each_loop_item(gcx, func, false, |item| {
24            if let LoopItem::Expr(expr) = item
25                && let ExprKind::Call(callee, ..) = &expr.kind
26                && is_external_call(gcx, callee)
27            {
28                ctx.emit(&CALLS_LOOP, expr.span);
29            }
30        });
31    }
32}
33
34/// An interaction with another contract.
35enum ExternalCall {
36    /// Contract creation or a mutating address builtin.
37    Opaque,
38    /// `.staticcall` on an address.
39    Static,
40    /// High-level external or library call, including an external function pointer.
41    Member(StateMutability),
42}
43
44/// Classifies calls by their checked function kind, including function pointers and library
45/// delegate calls. Internal `using for` bindings and `super` dispatch stay internal.
46fn classify<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> Option<ExternalCall> {
47    let callee = callee.peel_parens();
48    if matches!(
49        gcx.resolved_builtin(callee),
50        Some(Builtin::AddressPayableSend | Builtin::AddressPayableTransfer)
51    ) {
52        return Some(ExternalCall::Opaque);
53    }
54    let TyKind::Fn(function) = gcx.type_of_expr(callee.id)?.kind else { return None };
55    match function.kind() {
56        TyFnKind::External | TyFnKind::DelegateCall => {
57            Some(ExternalCall::Member(function.state_mutability))
58        }
59        TyFnKind::BareStaticCall => Some(ExternalCall::Static),
60        TyFnKind::BareCall | TyFnKind::BareDelegateCall | TyFnKind::Creation => {
61            Some(ExternalCall::Opaque)
62        }
63        _ => None,
64    }
65}
66
67/// True if calling `callee` interacts with another contract (or deploys one).
68pub(super) fn is_external_call<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> bool {
69    classify(gcx, callee).is_some()
70}
71
72/// Like [`is_external_call`], but excludes calls that cannot affect log ordering or observable
73/// state: `staticcall` and high-level `view`/`pure` callees (including `this.*`).
74pub(super) fn is_state_mutating_external_call<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> bool {
75    match classify(gcx, callee) {
76        Some(ExternalCall::Opaque) => true,
77        Some(ExternalCall::Member(mutability)) => {
78            !matches!(mutability, StateMutability::View | StateMutability::Pure)
79        }
80        Some(ExternalCall::Static) | None => false,
81    }
82}