Skip to main content

forge_lint/sol/gas/
external_function.rs

1use super::ExternalFunction;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::is_builtin},
5};
6use solar::{
7    ast::{ContractKind, DataLocation, Visibility},
8    interface::{data_structures::Never, sym},
9    sema::{
10        Gcx,
11        hir::{
12            self, ContractId, Expr, ExprKind, FunctionId, Stmt, StmtKind, VariableId, Visit as _,
13        },
14        ty::TyKind,
15    },
16};
17use std::{
18    cell::RefCell,
19    collections::{HashMap, HashSet},
20    ops::ControlFlow,
21    rc::Rc,
22};
23
24declare_forge_lint!(
25    EXTERNAL_FUNCTION,
26    Severity::Gas,
27    "external-function",
28    "`public` function can be declared `external`"
29);
30
31#[derive(Default)]
32struct ProjectIndex {
33    /// Functions referenced by name anywhere in the project: internal calls (`foo()`) and
34    /// function pointers (`fn = foo;`).
35    referenced: HashSet<FunctionId>,
36    /// Contracts containing a `super` access, keyed by its resolved function.
37    super_called: HashMap<FunctionId, HashSet<ContractId>>,
38}
39
40thread_local! {
41    /// Project index keyed by the HIR address, which is stable for the whole lint run, so the
42    /// index is built once instead of once per contract.
43    static PROJECT_INDEX: RefCell<Option<(usize, Rc<ProjectIndex>)>> = const { RefCell::new(None) };
44}
45
46fn project_index(gcx: Gcx<'_>) -> Rc<ProjectIndex> {
47    let key = std::ptr::from_ref(&gcx.hir) as usize;
48    PROJECT_INDEX.with_borrow_mut(|slot| match slot {
49        Some((cached_key, index)) if *cached_key == key => index.clone(),
50        _ => slot.insert((key, Rc::new(build_project_index(gcx)))).1.clone(),
51    })
52}
53
54fn build_project_index(gcx: Gcx<'_>) -> ProjectIndex {
55    let hir = &gcx.hir;
56    let mut builder = IndexBuilder { gcx, index: ProjectIndex::default(), contract: None };
57    for func in hir.functions() {
58        builder.contract = func.contract;
59        let _ = builder.visit_function(func);
60    }
61    // State variable initializers run in the synthesized constructor.
62    for var in hir.variables().filter(|var| var.is_state_variable()) {
63        builder.contract = var.contract;
64        let _ = builder.visit_var(var);
65    }
66    builder.index
67}
68
69struct IndexBuilder<'gcx> {
70    gcx: Gcx<'gcx>,
71    index: ProjectIndex,
72    /// Contract being walked, to attribute `super.<name>` accesses to the caller.
73    contract: Option<ContractId>,
74}
75
76impl<'gcx> hir::Visit<'gcx> for IndexBuilder<'gcx> {
77    type BreakValue = Never;
78
79    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
80        &self.gcx.hir
81    }
82
83    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
84        match &expr.kind {
85            ExprKind::Ident(_) => self.index.referenced.extend(self.gcx.resolved_function(expr)),
86            ExprKind::Member(base, _) if is_builtin(self.gcx, base, sym::super_) => {
87                if let Some(cid) = self.contract
88                    && let Some(fid) = self.gcx.resolved_function(expr)
89                {
90                    self.index.super_called.entry(fid).or_default().insert(cid);
91                }
92            }
93            ExprKind::Member(base, _)
94                if matches!(self.gcx.type_of_expr(base.id).map(|ty| ty.kind),
95                    Some(TyKind::Type(ty)) if matches!(ty.kind, TyKind::Contract(_))) =>
96            {
97                self.index.referenced.extend(self.gcx.resolved_function(expr));
98            }
99            _ => {}
100        }
101        self.walk_expr(expr)
102    }
103}
104
105impl<'gcx> LateLintPass<'gcx> for ExternalFunction {
106    fn check_nested_contract(
107        &mut self,
108        ctx: &LintContext,
109        gcx: Gcx<'gcx>,
110        contract_id: ContractId,
111    ) {
112        let contract = gcx.hir.contract(contract_id);
113        // Libraries have different `external` semantics (delegatecall vs inlining); interfaces
114        // have no bodies.
115        if !ctx.is_lint_enabled(EXTERNAL_FUNCTION.id)
116            || !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract)
117            || contract.linearization_failed()
118        {
119            return;
120        }
121        let index = project_index(gcx);
122
123        for fid in contract.functions() {
124            let func = gcx.hir.function(fid);
125            // Overrides can only widen visibility (`external` -> `public`), so the base chain is
126            // flagged instead; abstract declarations must stay `public` to be overridable.
127            let Some(name) = func.name else { continue };
128            if func.visibility != Visibility::Public
129                || !func.is_ordinary()
130                || func.override_
131                || func.body.is_none()
132            {
133                continue;
134            }
135            // Only reference parameters currently in `memory` yield meaningful savings.
136            if !func.parameters.iter().any(|&p| is_memory_reference(gcx, p)) {
137                continue;
138            }
139            let mut finder = ParamEscapeFinder { gcx, params: func.parameters };
140            if finder.visit_function(func).is_break() {
141                continue;
142            }
143            let super_called = index.super_called.iter().any(|(&target, callers)| {
144                callers.iter().any(|&caller| {
145                    gcx.hir.contracts_enumerated().any(|(cid, contract)| {
146                        contract.linearized_bases.contains(&caller)
147                            && gcx.resolve_super_function(cid, caller, target) == fid
148                    })
149                })
150            });
151            // A reference to this function's virtual target in a descendant also uses its slot.
152            let override_referenced = gcx
153                .hir
154                .contracts_enumerated()
155                .filter(|(cid, c)| *cid == contract_id || c.linearized_bases.contains(&contract_id))
156                .any(|(cid, _)| index.referenced.contains(&gcx.resolve_virtual_function(cid, fid)));
157            if !super_called && !override_referenced {
158                ctx.emit(&EXTERNAL_FUNCTION, name.span);
159            }
160        }
161    }
162}
163
164/// Breaks when a parameter is written, aliased, passed to a callee or modifier that could mutate
165/// it through the internal-call memory-reference aliasing rule.
166struct ParamEscapeFinder<'a, 'gcx> {
167    gcx: Gcx<'gcx>,
168    params: &'a [VariableId],
169}
170
171impl ParamEscapeFinder<'_, '_> {
172    fn is_param(&self, expr: &Expr<'_>) -> bool {
173        root_var_is(self.gcx, expr, &|v| self.params.contains(&v))
174    }
175}
176
177impl<'gcx> hir::Visit<'gcx> for ParamEscapeFinder<'_, 'gcx> {
178    type BreakValue = ();
179
180    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
181        &self.gcx.hir
182    }
183
184    fn visit_modifier(&mut self, modifier: &'gcx hir::Modifier<'gcx>) -> ControlFlow<()> {
185        if modifier.args.exprs().any(|arg| self.is_param(arg)) {
186            return ControlFlow::Break(());
187        }
188        self.walk_modifier(modifier)
189    }
190
191    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
192        if let StmtKind::DeclSingle(vid) = &stmt.kind
193            && let var = self.gcx.hir.variable(*vid)
194            && is_memory_reference(self.gcx, *vid)
195            && var.initializer.is_some_and(|init| self.is_param(init))
196        {
197            return ControlFlow::Break(());
198        }
199        self.walk_stmt(stmt)
200    }
201
202    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
203        let escapes = match &expr.kind {
204            ExprKind::Assign(lhs, op, rhs) => {
205                self.is_param(lhs)
206                    || (op.is_none()
207                        && root_var_is(self.gcx, lhs, &|v| {
208                            let var = self.gcx.hir.variable(v);
209                            var.is_local_variable() && is_memory_reference(self.gcx, v)
210                        })
211                        && self.is_param(rhs))
212            }
213            ExprKind::Delete(inner) => self.is_param(inner),
214            ExprKind::Unary(op, inner) => op.kind.has_side_effects() && self.is_param(inner),
215            ExprKind::Call(callee, args, opts) => {
216                !self
217                    .gcx
218                    .type_of_expr(callee.id)
219                    .is_some_and(|ty| matches!(ty.kind, TyKind::Type(_)))
220                    && (args.exprs().any(|arg| self.is_param(arg))
221                        || opts.is_some_and(|opts| {
222                            opts.args.iter().any(|opt| self.is_param(&opt.value))
223                        })
224                        || matches!(&callee.peel_parens().kind, ExprKind::Member(receiver, _)
225                            if self.is_param(receiver)))
226            }
227            _ => false,
228        };
229        if escapes {
230            return ControlFlow::Break(());
231        }
232        self.walk_expr(expr)
233    }
234}
235
236fn is_memory_reference(gcx: Gcx<'_>, var: VariableId) -> bool {
237    gcx.type_of_item(var.into()).loc() == Some(DataLocation::Memory)
238}
239
240/// Whether the variable at the root of `expr` (through parens, members, indexes and slices)
241/// satisfies `pred`.
242fn root_var_is(gcx: Gcx<'_>, expr: &Expr<'_>, pred: &impl Fn(VariableId) -> bool) -> bool {
243    match &expr.peel_parens().kind {
244        ExprKind::Ident(_) => gcx.resolved_variable(expr).is_some_and(pred),
245        ExprKind::Member(base, _)
246        | ExprKind::Payable(base)
247        | ExprKind::Index(base, _)
248        | ExprKind::Slice(base, ..) => root_var_is(gcx, base, pred),
249        _ => false,
250    }
251}