1use super::ControlledDelegatecall;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 arg_for_param, branch_always_exits, count_placeholders, do_while_user_stmts,
8 has_side_effect, is_address_like_cast, is_loop_termination_if, is_require_or_assert,
9 loop_update, stmts_before_placeholder, stmts_break_or_continue, tuple_elems,
10 var_is_address_like,
11 },
12 },
13};
14use solar::{
15 ast::{BinOpKind, LitKind, UnOpKind},
16 interface::{Span, sym},
17 sema::{
18 Gcx,
19 builtins::Builtin,
20 hir::{
21 self, ElementaryType, Expr, ExprKind, FunctionKind, ItemId, LoopSource, Res, Stmt,
22 StmtKind, TypeKind, VariableId, Visit,
23 },
24 },
25};
26use std::{collections::HashSet, ops::ControlFlow};
27
28declare_forge_lint!(
29 CONTROLLED_DELEGATECALL,
30 Severity::High,
31 "controlled-delegatecall",
32 "`delegatecall` target is not provably trusted"
33);
34
35const HELPER_DEPTH: u8 = 3;
37
38impl<'gcx> LateLintPass<'gcx> for ControlledDelegatecall {
39 fn check_function(
40 &mut self,
41 ctx: &LintContext,
42 gcx: Gcx<'gcx>,
43 func: &'gcx hir::Function<'gcx>,
44 ) {
45 let Some(body) = func.body else { return };
46 let mut analyzer = Analyzer::new(gcx);
47 for modifier in func.modifiers {
48 analyzer.safe_vars.extend(modifier_safe_vars(gcx, modifier));
49 }
50 let _ = analyzer.visit_stmts(body.stmts);
51 for span in analyzer.hits {
52 ctx.emit(&CONTROLLED_DELEGATECALL, span);
53 }
54 }
55}
56
57struct Analyzer<'gcx> {
61 gcx: Gcx<'gcx>,
62 safe_vars: HashSet<VariableId>,
63 assigned: HashSet<VariableId>,
65 loop_exits: Vec<Vec<HashSet<VariableId>>>,
67 hits: Vec<Span>,
68}
69
70fn intersect(a: &HashSet<VariableId>, b: &HashSet<VariableId>) -> HashSet<VariableId> {
71 a.intersection(b).copied().collect()
72}
73
74impl<'gcx> Analyzer<'gcx> {
75 fn new(gcx: Gcx<'gcx>) -> Self {
76 Self {
77 gcx,
78 safe_vars: HashSet::new(),
79 assigned: HashSet::new(),
80 loop_exits: Vec::new(),
81 hits: Vec::new(),
82 }
83 }
84
85 fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> ControlFlow<()> {
86 stmts.iter().try_for_each(|stmt| self.visit_stmt(stmt))
87 }
88
89 fn is_trusted_target(&self, expr: &'gcx Expr<'gcx>) -> bool {
90 self.is_trusted_target_inner(expr, HELPER_DEPTH)
91 }
92
93 fn is_trusted_target_inner(&self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
94 match &expr.peel_parens().kind {
95 ExprKind::Lit(lit) => match &lit.kind {
96 LitKind::Address(_) => true,
97 LitKind::Number(n) => n.is_zero(),
98 _ => false,
99 },
100 ExprKind::Ident(_) => self.gcx.resolved_expr(expr).is_some_and(|res| match res {
101 Res::Builtin(builtin) => builtin.name() == sym::this,
102 Res::Item(ItemId::Variable(vid)) => {
103 let var = self.gcx.hir.variable(vid);
104 (var.is_constant() && var_is_address_like(var)) || self.safe_vars.contains(&vid)
105 }
106 _ => false,
107 }),
108 ExprKind::Call(callee, args, _) if is_cast(self.gcx, callee) => {
109 args.exprs().next().is_some_and(|arg| self.is_trusted_target_inner(arg, depth))
110 }
111 ExprKind::Payable(inner) => self.is_trusted_target_inner(inner, depth),
112 ExprKind::Ternary(_, if_true, if_false) => {
113 self.is_trusted_target_inner(if_true, depth)
114 && self.is_trusted_target_inner(if_false, depth)
115 }
116 ExprKind::Assign(_, _, rhs) => self.is_trusted_target_inner(rhs, depth),
117 ExprKind::Call(callee, args, _) => {
118 depth > 0
119 && args.exprs().next().is_none()
120 && no_arg_helper_return(self.gcx, callee)
121 .is_some_and(|ret| self.is_trusted_target_inner(ret, depth - 1))
122 }
123 _ => false,
124 }
125 }
126
127 fn is_trusted_fact_target(&self, var: VariableId) -> bool {
129 let variable = self.gcx.hir.variable(var);
130 (!variable.kind.is_state() || variable.is_constant()) && var_is_address_like(variable)
131 }
132
133 fn assign(&mut self, var: VariableId, trusted: bool) {
135 self.assigned.insert(var);
136 self.safe_vars.remove(&var);
137 let variable = self.gcx.hir.variable(var);
138 if trusted && !variable.kind.is_state() && var_is_address_like(variable) {
139 self.safe_vars.insert(var);
140 }
141 }
142
143 fn assign_expr(&mut self, lhs: &'gcx Expr<'gcx>, rhs: Option<&'gcx Expr<'gcx>>) {
144 if let Some(var) = underlying_var(self.gcx, lhs) {
145 self.assign(var, rhs.is_some_and(|rhs| self.is_trusted_target(rhs)));
146 }
147 }
148
149 fn handle_assign(
150 &mut self,
151 lhs: &'gcx Expr<'gcx>,
152 op: Option<hir::BinOp>,
153 rhs: &'gcx Expr<'gcx>,
154 ) {
155 let rhs = op.is_none().then_some(rhs);
156 let Some(lhs_elems) = tuple_elems(lhs) else { return self.assign_expr(lhs, rhs) };
157 let rhs_elems = rhs.and_then(tuple_elems);
158 for (i, lhs_elem) in lhs_elems.iter().enumerate() {
159 if let Some(lhs_elem) = lhs_elem {
160 let rhs_elem = rhs_elems.and_then(|elems| elems.get(i).copied().flatten());
161 self.assign_expr(lhs_elem, rhs_elem);
162 }
163 }
164 }
165
166 fn is_controlled_delegatecall(&self, expr: &'gcx Expr<'gcx>) -> bool {
167 let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return false };
168 let ExprKind::Member(receiver, _) = &callee.peel_parens().kind else { return false };
169 self.gcx.resolved_builtin(callee) == Some(Builtin::AddressDelegatecall)
170 && !self.is_trusted_target(receiver)
171 }
172
173 fn add_facts(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
175 if !has_side_effect(pred) {
176 self.add_facts_unchecked(pred, negate);
177 }
178 }
179
180 fn add_facts_unchecked(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
181 match &pred.peel_parens().kind {
182 ExprKind::Binary(lhs, op, rhs) => {
183 let (eq, and_op, or_op) = if negate {
184 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
185 } else {
186 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
187 };
188 if op.kind == and_op {
189 self.add_facts_unchecked(lhs, negate);
190 self.add_facts_unchecked(rhs, negate);
191 } else if op.kind == or_op {
192 let baseline = self.safe_vars.clone();
194 self.add_facts_unchecked(lhs, negate);
195 let lhs_added = std::mem::replace(&mut self.safe_vars, baseline.clone());
196 self.add_facts_unchecked(rhs, negate);
197 let rhs_added = std::mem::replace(&mut self.safe_vars, baseline);
198 self.safe_vars.extend(intersect(&lhs_added, &rhs_added));
199 } else if op.kind == eq {
200 for (safe, candidate) in [(lhs, rhs), (rhs, lhs)] {
201 if self.is_trusted_target(safe)
202 && let Some(var) = underlying_var(self.gcx, candidate)
203 && self.is_trusted_fact_target(var)
204 {
205 self.safe_vars.insert(var);
206 }
207 }
208 }
209 }
210 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
211 self.add_facts_unchecked(inner, !negate);
212 }
213 _ => {}
214 }
215 }
216
217 fn visit_arm(
220 &mut self,
221 cond: &'gcx Expr<'gcx>,
222 negate: bool,
223 arm: impl FnOnce(&mut Self) -> ControlFlow<()>,
224 ) -> Option<HashSet<VariableId>> {
225 self.add_facts(cond, negate);
226 arm(self).is_continue().then(|| self.safe_vars.clone())
227 }
228
229 fn join(
231 &mut self,
232 a: Option<HashSet<VariableId>>,
233 b: Option<HashSet<VariableId>>,
234 ) -> ControlFlow<()> {
235 match (a, b) {
236 (Some(a), Some(b)) => self.safe_vars = intersect(&a, &b),
237 (Some(state), None) | (None, Some(state)) => self.safe_vars = state,
238 (None, None) => return ControlFlow::Break(()),
239 }
240 ControlFlow::Continue(())
241 }
242}
243
244impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
245 type BreakValue = ();
246
247 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
248 &self.gcx.hir
249 }
250
251 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
252 match &stmt.kind {
253 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
254 self.visit_stmts(block.stmts)
255 }
256 StmtKind::If(cond, then, else_) => {
257 let _ = self.visit_expr(cond);
258 let baseline = self.safe_vars.clone();
259 let then_state = self.visit_arm(cond, false, |this| this.visit_stmt(then));
260 self.safe_vars = baseline;
261 let else_state = self.visit_arm(cond, true, |this| match else_ {
262 Some(else_) => this.visit_stmt(else_),
263 None => ControlFlow::Continue(()),
264 });
265 self.join(then_state, else_state)
266 }
267 StmtKind::Loop(block, LoopSource::DoWhile)
268 if !stmts_break_or_continue(do_while_user_stmts(block.stmts)) =>
269 {
270 self.visit_stmts(do_while_user_stmts(block.stmts))?;
273 if let Some(last) = block.stmts.last()
274 && is_loop_termination_if(last)
275 && let StmtKind::If(cond, ..) = &last.kind
276 {
277 let _ = self.visit_expr(cond);
278 }
279 ControlFlow::Continue(())
280 }
281 StmtKind::Loop(block, source) => {
282 self.loop_exits.push(vec![self.safe_vars.clone()]);
285 let falls_through = self.visit_stmts(block.stmts).is_continue()
286 && loop_update(*source)
287 .is_none_or(|update| self.visit_stmt(update).is_continue());
288 let mut exits = self.loop_exits.pop().expect("loop frame");
289 if falls_through {
290 exits.push(self.safe_vars.clone());
291 }
292 self.safe_vars =
293 exits.iter().skip(1).fold(exits[0].clone(), |a, b| intersect(&a, b));
294 ControlFlow::Continue(())
295 }
296 StmtKind::Break | StmtKind::Continue => {
297 if let Some(exits) = self.loop_exits.last_mut() {
298 exits.push(self.safe_vars.clone());
299 }
300 ControlFlow::Break(())
301 }
302 StmtKind::Try(stmt_try) => {
303 let _ = self.visit_expr(&stmt_try.expr);
304 let outer = self.safe_vars.clone();
305 let mut joined = None;
306 for clause in stmt_try.clauses {
307 self.safe_vars = outer.clone();
308 if self.visit_stmts(clause.block.stmts).is_continue() {
309 joined = Some(match joined {
310 Some(state) => intersect(&state, &self.safe_vars),
311 None => self.safe_vars.clone(),
312 });
313 }
314 }
315 self.safe_vars = joined.unwrap_or(outer);
316 ControlFlow::Continue(())
317 }
318 StmtKind::Err(_) => {
319 self.safe_vars.clear();
320 ControlFlow::Continue(())
321 }
322 StmtKind::DeclSingle(var) => {
323 let init = self.gcx.hir.variable(*var).initializer;
324 self.assign(*var, init.is_some_and(|init| self.is_trusted_target(init)));
325 self.walk_stmt(stmt)
326 }
327 StmtKind::DeclMulti(vars, init) => {
328 let inits = tuple_elems(init);
329 for (i, var) in vars.iter().enumerate() {
330 if let Some(var) = var {
331 let init = inits.and_then(|elems| elems.get(i).copied().flatten());
332 self.assign(*var, init.is_some_and(|init| self.is_trusted_target(init)));
333 }
334 }
335 self.walk_stmt(stmt)
336 }
337 _ => {
338 let _ = self.walk_stmt(stmt);
339 if branch_always_exits(self.gcx, stmt) {
340 ControlFlow::Break(())
341 } else {
342 ControlFlow::Continue(())
343 }
344 }
345 }
346 }
347
348 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
349 if self.is_controlled_delegatecall(expr) {
350 self.hits.push(expr.span);
351 }
352 match &expr.kind {
353 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
354 let _ = self.visit_expr(lhs);
355 let skipped_rhs = self.safe_vars.clone();
356 let ran_rhs =
357 self.visit_arm(lhs, op.kind == BinOpKind::Or, |this| this.visit_expr(rhs));
358 self.join(Some(skipped_rhs), ran_rhs)
359 }
360 ExprKind::Ternary(cond, if_true, if_false) => {
361 let _ = self.visit_expr(cond);
362 let baseline = self.safe_vars.clone();
363 let true_state = self.visit_arm(cond, false, |this| this.visit_expr(if_true));
364 self.safe_vars = baseline;
365 let false_state = self.visit_arm(cond, true, |this| this.visit_expr(if_false));
366 self.join(true_state, false_state)
367 }
368 ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
369 let _ = self.walk_expr(expr);
370 let mut args = args.exprs();
371 if let Some(cond) = args.next()
372 && !args.any(has_side_effect)
373 {
374 self.add_facts(cond, false);
375 }
376 ControlFlow::Continue(())
377 }
378 ExprKind::Assign(lhs, op, rhs) => {
379 let _ = self.walk_expr(expr);
380 self.handle_assign(lhs, *op, rhs);
381 ControlFlow::Continue(())
382 }
383 ExprKind::Delete(target) => {
384 if let Some(var) = underlying_var(self.gcx, target) {
386 self.assign(var, true);
387 }
388 self.walk_expr(expr)
389 }
390 _ => self.walk_expr(expr),
391 }
392 }
393}
394
395fn underlying_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<VariableId> {
398 match &expr.peel_parens().kind {
399 ExprKind::Ident(_) => gcx.resolved_variable(expr),
400 ExprKind::Call(callee, args, _) if is_cast(gcx, callee) => {
401 args.exprs().next().and_then(|expr| underlying_var(gcx, expr))
402 }
403 ExprKind::Payable(inner) => underlying_var(gcx, inner),
404 _ => None,
405 }
406}
407
408fn is_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
410 is_address_like_cast(gcx, callee)
411 || matches!(
412 &callee.peel_parens().kind,
413 ExprKind::Type(hir::Type {
414 kind: TypeKind::Elementary(
415 ElementaryType::Int(_) | ElementaryType::UInt(_) | ElementaryType::Bytes
416 ),
417 ..
418 })
419 )
420}
421
422fn no_arg_helper_return<'gcx>(
425 gcx: Gcx<'gcx>,
426 callee: &'gcx Expr<'gcx>,
427) -> Option<&'gcx Expr<'gcx>> {
428 let fid = gcx
429 .resolved_function(callee)
430 .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))?;
431 let func = gcx.hir.function(fid);
432 if func.virtual_ || func.override_ || !func.parameters.is_empty() {
433 return None;
434 }
435 let body = func.body?;
436 let stmts = match body.stmts.split_last() {
437 Some((last, rest)) if matches!(last.kind, StmtKind::Return(None)) => rest,
438 _ => body.stmts,
439 };
440 let [stmt] = stmts else { return None };
441 match &stmt.kind {
442 StmtKind::Return(Some(expr)) => Some(expr),
443 StmtKind::Expr(expr) => match &expr.peel_parens().kind {
444 ExprKind::Assign(lhs, None, rhs)
445 if func.returns.len() == 1 && underlying_var(gcx, lhs) == Some(func.returns[0]) =>
446 {
447 Some(rhs)
448 }
449 _ => None,
450 },
451 _ => None,
452 }
453}
454
455fn modifier_safe_vars<'gcx>(
458 gcx: Gcx<'gcx>,
459 invocation: &'gcx hir::Modifier<'gcx>,
460) -> Vec<VariableId> {
461 let Some(fid) = invocation.id.as_function() else { return Vec::new() };
462 let modifier = gcx.hir.function(fid);
463 let Some(body) = modifier.body else { return Vec::new() };
464 let mut prefix = Vec::new();
465 if modifier.kind != FunctionKind::Modifier
466 || count_placeholders(body.stmts) != 1
467 || stmts_before_placeholder(body.stmts, &mut prefix).is_none()
468 {
469 return Vec::new();
470 }
471 let bindings: Vec<_> = modifier
472 .parameters
473 .iter()
474 .filter_map(|¶m| {
475 let arg = arg_for_param(gcx, fid, param, &invocation.args)?;
476 Some((param, underlying_var(gcx, arg)?))
477 })
478 .collect();
479 if bindings.is_empty() {
480 return Vec::new();
481 }
482
483 let mut analyzer = Analyzer::new(gcx);
484 let _ = prefix.iter().try_for_each(|stmt| analyzer.visit_stmt(stmt));
485 bindings
486 .into_iter()
487 .filter(|&(param, caller_var)| {
488 !analyzer.assigned.contains(¶m)
489 && analyzer.safe_vars.contains(¶m)
490 && analyzer.is_trusted_fact_target(caller_var)
491 })
492 .map(|(_, caller_var)| caller_var)
493 .collect()
494}