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, ¶ms);
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 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 ¶ms {
78 if a.sinks.contains(&p) {
79 ctx.emit(&MISSING_ZERO_CHECK, gcx.hir.variable(p).span);
80 }
81 }
82 }
83}
84
85struct Analyzer<'gcx> {
87 gcx: Gcx<'gcx>,
88 taint: HashMap<VariableId, HashSet<VariableId>>,
91 sinks: HashSet<VariableId>,
93 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 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 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 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 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 StmtKind::Loop(block, source) => {
216 self.scoped_guards(loop_stmts(block, source));
217 return ControlFlow::Continue(());
218 }
219 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 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 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 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}