Skip to main content

forge_lint/sol/low/
missing_zero_check.rs

1use super::MissingZeroCheck;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{
7            address_call_receiver, branch_always_exits, is_address_type, is_require_or_assert,
8            is_zero_value, lhs_local_var, loop_stmts, underlying_var,
9        },
10    },
11};
12use solar::{
13    ast::{self, BinOpKind, UnOpKind},
14    interface::data_structures::Never,
15    sema::{
16        Gcx,
17        hir::{self, ExprKind, StmtKind, VariableId, Visit},
18    },
19};
20use std::{
21    collections::{HashMap, HashSet},
22    ops::ControlFlow,
23    slice,
24};
25
26declare_forge_lint!(
27    MISSING_ZERO_CHECK,
28    Severity::Low,
29    "missing-zero-check",
30    "address parameter is used in a state write or value transfer without a zero-address check"
31);
32
33impl<'gcx> LateLintPass<'gcx> for MissingZeroCheck {
34    fn check_function(
35        &mut self,
36        ctx: &LintContext,
37        gcx: Gcx<'gcx>,
38        func: &'gcx hir::Function<'gcx>,
39    ) {
40        let is_entry_point = !matches!(
41            func.state_mutability,
42            ast::StateMutability::Pure | ast::StateMutability::View
43        ) && (func.is_constructor()
44            || (func.kind.is_function()
45                && matches!(func.visibility, ast::Visibility::Public | ast::Visibility::External)));
46        let Some(body) = func.body.filter(|_| is_entry_point) else { return };
47
48        let params: HashSet<_> =
49            func.parameters.iter().copied().filter(|&id| is_address_type(&gcx.hir, id)).collect();
50        if params.is_empty() {
51            return;
52        }
53
54        let mut a = Analyzer::new(gcx, &params);
55        for m in func.modifiers {
56            let Some(modifier_id) = m.id.as_function() else { continue };
57            let modifier = gcx.hir.function(modifier_id);
58            // Map each direct-ident argument back to the caller's parameter and analyze the
59            // modifier body as if it were a prefix of the function.
60            let mapping: HashMap<_, _> = modifier
61                .parameters
62                .iter()
63                .zip(m.args.exprs())
64                .filter_map(|(&mp, arg)| {
65                    let caller = underlying_var(gcx, arg).filter(|v| params.contains(v))?;
66                    Some((mp, caller))
67                })
68                .collect();
69            if let Some(body) = modifier.body.filter(|_| !mapping.is_empty()) {
70                let mut ma = Analyzer::new(gcx, &mapping.keys().copied().collect());
71                ma.visit_stmts(body.stmts);
72                a.guarded.extend(ma.guarded.iter().filter_map(|mp| mapping.get(mp)));
73            }
74        }
75        a.visit_stmts(body.stmts);
76
77        for &p in &params {
78            if a.sinks.contains(&p) {
79                ctx.emit(&MISSING_ZERO_CHECK, gcx.hir.variable(p).span);
80            }
81        }
82    }
83}
84
85/// Tracks address-parameter taint, sinks reached, and guards observed in a function body.
86struct Analyzer<'gcx> {
87    gcx: Gcx<'gcx>,
88    /// Variables transitively derived from candidate parameters, mapped to their sources.
89    /// Each parameter is initially mapped to itself.
90    taint: HashMap<VariableId, HashSet<VariableId>>,
91    /// Source parameters that reached a sink.
92    sinks: HashSet<VariableId>,
93    /// Source parameters proven non-zero so far.
94    guarded: HashSet<VariableId>,
95    sink_depth: u32,
96}
97
98impl<'gcx> Analyzer<'gcx> {
99    fn new(gcx: Gcx<'gcx>, params: &HashSet<VariableId>) -> Self {
100        Self {
101            gcx,
102            taint: params.iter().map(|&p| (p, HashSet::from([p]))).collect(),
103            sinks: HashSet::new(),
104            guarded: HashSet::new(),
105            sink_depth: 0,
106        }
107    }
108
109    fn visit_stmts(&mut self, stmts: impl IntoIterator<Item = &'gcx hir::Stmt<'gcx>>) {
110        for s in stmts {
111            let _ = self.visit_stmt(s);
112        }
113    }
114
115    /// Visits `stmts` without letting their guards escape.
116    fn scoped_guards(
117        &mut self,
118        stmts: impl IntoIterator<Item = &'gcx hir::Stmt<'gcx>>,
119    ) -> HashSet<VariableId> {
120        let baseline = self.guarded.clone();
121        self.visit_stmts(stmts);
122        std::mem::replace(&mut self.guarded, baseline)
123    }
124
125    /// Sources proven non-zero when `pred` evaluates to `!negate`.
126    fn nonzero_facts(&self, pred: &'gcx hir::Expr<'gcx>, negate: bool) -> HashSet<VariableId> {
127        let nonzero_cmp = if negate { BinOpKind::Eq } else { BinOpKind::Ne };
128        match &pred.peel_parens().kind {
129            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
130                self.nonzero_facts(inner, !negate)
131            }
132            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
133                let lhs = self.nonzero_facts(lhs, negate);
134                let rhs = self.nonzero_facts(rhs, negate);
135                if matches!((op.kind, negate), (BinOpKind::And, false) | (BinOpKind::Or, true)) {
136                    &lhs | &rhs
137                } else {
138                    &lhs & &rhs
139                }
140            }
141            ExprKind::Binary(lhs, op, rhs) if op.kind == nonzero_cmp => {
142                let mut facts = HashSet::new();
143                for (candidate, zero) in [(lhs, rhs), (rhs, lhs)] {
144                    if is_zero_value(zero)
145                        && let Some(sources) =
146                            underlying_var(self.gcx, candidate).and_then(|v| self.taint.get(&v))
147                    {
148                        facts.extend(sources);
149                    }
150                }
151                facts
152            }
153            _ => HashSet::new(),
154        }
155    }
156
157    fn taint_sources(&self, expr: &hir::Expr<'_>) -> HashSet<VariableId> {
158        let mut out = HashSet::new();
159        let _ = expr.visit(&mut |e| {
160            if let Some(srcs) = underlying_var(self.gcx, e).and_then(|v| self.taint.get(&v)) {
161                out.extend(srcs);
162            }
163            ControlFlow::<Never>::Continue(())
164        });
165        out
166    }
167
168    fn propagate(&mut self, local: VariableId, value: &hir::Expr<'_>) {
169        // Propagate taint through address-typed locals only; this avoids marking unrelated
170        // values (e.g. `bool ok = a.send(1)`) as derived from `a`.
171        if is_address_type(&self.gcx.hir, local) {
172            let srcs = self.taint_sources(value);
173            if !srcs.is_empty() {
174                self.taint.entry(local).or_default().extend(srcs);
175            }
176        }
177    }
178}
179
180impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
181    type BreakValue = Never;
182
183    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
184        &self.gcx.hir
185    }
186
187    fn visit_stmt(&mut self, stmt: &'gcx hir::Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
188        match stmt.kind {
189            StmtKind::If(cond, then, else_) => {
190                let _ = self.visit_expr(cond);
191                let baseline = self.guarded.clone();
192
193                self.guarded.extend(self.nonzero_facts(cond, false));
194                let then_guards = self.scoped_guards(slice::from_ref(then));
195
196                self.guarded = baseline;
197                self.guarded.extend(self.nonzero_facts(cond, true));
198                let else_guards = else_
199                    .map(|e| self.scoped_guards(slice::from_ref(e)))
200                    .unwrap_or_else(|| self.guarded.clone());
201
202                // A guard in an exiting branch holds for everything after the `if`; otherwise it
203                // must hold on both branches.
204                let then_exits = branch_always_exits(self.gcx, then);
205                let else_exits = else_.is_some_and(|expr| branch_always_exits(self.gcx, expr));
206                self.guarded = match (then_exits, else_exits) {
207                    (true, true) => &then_guards | &else_guards,
208                    (true, false) => else_guards,
209                    (false, true) => then_guards,
210                    (false, false) => &then_guards & &else_guards,
211                };
212                return ControlFlow::Continue(());
213            }
214            // Loop bodies may execute zero times, so guards inside must not persist.
215            StmtKind::Loop(block, source) => {
216                self.scoped_guards(loop_stmts(block, source));
217                return ControlFlow::Continue(());
218            }
219            // Each try/catch clause is taken on a single path; discard clause-local guards.
220            StmtKind::Try(t) => {
221                let _ = self.visit_expr(&t.expr);
222                for clause in t.clauses {
223                    self.scoped_guards(clause.block.stmts);
224                }
225                return ControlFlow::Continue(());
226            }
227            StmtKind::DeclSingle(var_id) => {
228                if let Some(init) = self.gcx.hir.variable(var_id).initializer {
229                    self.propagate(var_id, init);
230                }
231            }
232            _ => {}
233        }
234        self.walk_stmt(stmt)
235    }
236
237    fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
238        match &expr.kind {
239            // `require(cond, ..)` / `assert(cond)`: only the first arg is a guard predicate.
240            ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
241                let mut iter = args.exprs();
242                if let Some(cond) = iter.next() {
243                    self.guarded.extend(self.nonzero_facts(cond, false));
244                    let _ = self.visit_expr(cond);
245                }
246                for rest in iter {
247                    let _ = self.visit_expr(rest);
248                }
249                return ControlFlow::Continue(());
250            }
251            // `<addr>.call/.delegatecall/.transfer/.send(..)`: receiver is the sink.
252            ExprKind::Call(callee, args, _) => {
253                if let Some(receiver) = address_call_receiver(callee) {
254                    self.sink_depth += 1;
255                    let _ = self.visit_expr(receiver);
256                    self.sink_depth -= 1;
257                    return self.visit_call_args(args);
258                }
259            }
260            ExprKind::Assign(lhs, _, rhs) => {
261                // Sink: assignment to an address state variable.
262                if let Some(v) = underlying_var(self.gcx, lhs)
263                    && self.gcx.hir.variable(v).kind.is_state()
264                    && is_address_type(&self.gcx.hir, v)
265                {
266                    self.sink_depth += 1;
267                    let _ = self.visit_expr(rhs);
268                    self.sink_depth -= 1;
269                    return ControlFlow::Continue(());
270                }
271                if let Some(local) = lhs_local_var(self.gcx, lhs) {
272                    self.propagate(local, rhs);
273                }
274            }
275            ExprKind::Ident(_) => {
276                if self.sink_depth > 0
277                    && let Some(srcs) =
278                        underlying_var(self.gcx, expr).and_then(|v| self.taint.get(&v))
279                {
280                    self.sinks.extend(srcs.iter().filter(|src| !self.guarded.contains(src)));
281                }
282            }
283            _ => {}
284        }
285        self.walk_expr(expr)
286    }
287}