1use super::UnsafeOzErc721Mint;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 OPENZEPPELIN_ROOTS, arg_for_param, for_each_lhs_var, is_address_type, is_builtin,
8 is_literal_false, is_require_or_assert, loop_stmts, source_in_package, underlying_var,
9 unique, write_target,
10 },
11 },
12};
13use alloy_primitives::U256;
14use solar::{
15 ast::{ElementaryType, LitKind, StateMutability, Visibility},
16 interface::{Span, kw},
17 sema::{
18 Gcx,
19 hir::{
20 self, BinOpKind, CallArgs, Expr, ExprKind, FunctionId, Hir, ItemId, Stmt, StmtKind,
21 TypeKind, VariableId, Visit,
22 },
23 ty::{TyFn, TyKind},
24 },
25};
26use std::{ops::ControlFlow, slice};
27
28declare_forge_lint!(
29 UNSAFE_OZ_ERC721_MINT,
30 Severity::Med,
31 "unsafe-oz-erc721-mint",
32 "`ERC721._mint` does not check that the recipient can receive the token; use `_safeMint`"
33);
34
35impl<'gcx> LateLintPass<'gcx> for UnsafeOzErc721Mint {
36 fn check_function(
37 &mut self,
38 ctx: &LintContext,
39 gcx: Gcx<'gcx>,
40 func: &'gcx hir::Function<'gcx>,
41 ) {
42 let cx = Cx { gcx };
43 if named(func, "_safeMint")
47 && func
48 .contract
49 .is_some_and(|id| is_canonical_erc721(gcx.hir.contract(id).name.as_str()))
50 && source_in_package(&gcx.hir, func.source, OPENZEPPELIN_ROOTS)
51 {
52 return;
53 }
54 if (named(func, "_mint") && func.override_) || cx.is_override_delegation_helper(func) {
60 return;
61 }
62 let Some(body) = &func.body else { return };
68 for (callee, _, span) in cx.calls(body.stmts) {
69 let helper = cx.is_override_delegation_helper(gcx.hir.function(callee));
70 if cx.unsafe_mint_target(callee, helper, &mut Vec::new()).is_some() {
71 ctx.emit(&UNSAFE_OZ_ERC721_MINT, span);
72 }
73 }
74 }
75}
76
77#[derive(Clone, Copy)]
81struct UnsafeMintTarget {
82 preserves_recipient: bool,
83 preserves_token: bool,
84 preserves_code_length: bool,
85}
86
87type Call<'gcx> = (FunctionId, &'gcx CallArgs<'gcx>, Span);
89
90#[derive(Clone, Copy)]
92struct Cx<'gcx> {
93 gcx: Gcx<'gcx>,
94}
95
96impl<'gcx> Cx<'gcx> {
97 fn is_override_delegation_helper(self, function: &'gcx hir::Function<'gcx>) -> bool {
100 if !is_internal(function) || (function.override_ && named(function, "_mint")) {
101 return false;
102 }
103 let Some(contract_id) = function.contract else { return false };
104 let Some(function_id) = self
105 .gcx
106 .hir
107 .contract(contract_id)
108 .all_functions()
109 .find(|&id| std::ptr::eq(self.gcx.hir.function(id), function))
110 else {
111 return false;
112 };
113 self.gcx.hir.contract_ids().any(|candidate| {
114 let candidate = self.gcx.hir.contract(candidate);
115 candidate.linearized_bases.contains(&contract_id)
116 && candidate.all_functions().any(|id| {
117 let f = self.gcx.hir.function(id);
118 f.override_
119 && named(f, "_mint")
120 && self.function_reaches(id, function_id, &mut Vec::new())
121 })
122 })
123 }
124
125 fn function_reaches(
127 self,
128 function_id: FunctionId,
129 target: FunctionId,
130 seen: &mut Vec<FunctionId>,
131 ) -> bool {
132 if seen.contains(&function_id) {
133 return false;
134 }
135 seen.push(function_id);
136 let Some(body) = self.gcx.hir.function(function_id).body else { return false };
137 self.calls(body.stmts).iter().any(|&(callee, ..)| {
138 callee == target
139 || (is_internal(self.gcx.hir.function(callee))
140 && self.function_reaches(callee, target, seen))
141 })
142 }
143
144 fn unsafe_mint_target(
152 self,
153 function_id: FunctionId,
154 helper: bool,
155 seen: &mut Vec<FunctionId>,
156 ) -> Option<UnsafeMintTarget> {
157 if seen.contains(&function_id) {
158 return None;
159 }
160 seen.push(function_id);
161 let function = self.gcx.hir.function(function_id);
162 let is_mint = named(function, "_mint");
163 if !(is_mint || (helper && is_internal(function))) {
164 return None;
165 }
166 let contract = self.gcx.hir.contract(function.contract?);
167 if contract.kind.is_library() {
168 return None;
169 }
170 let canonical = is_canonical_erc721(contract.name.as_str())
171 && source_in_package(&self.gcx.hir, function.source, OPENZEPPELIN_ROOTS);
172 if canonical && named(function, "_safeMint") {
173 return None;
174 }
175 if canonical && is_mint {
178 return Some(UnsafeMintTarget {
179 preserves_recipient: true,
180 preserves_token: true,
181 preserves_code_length: true,
182 });
183 }
184 if !(function.override_ || helper) {
185 return None;
186 }
187 let body = function.body.as_ref()?;
188 let recipient =
190 function.parameters.iter().copied().find(|&vid| is_address_type(&self.gcx.hir, vid));
191 let calls = self.calls(body.stmts);
192 let mut unsafe_targets = Vec::new();
196 let mut unstable_code_targets = Vec::new();
197 let mut judged = Vec::new();
198 let (mut targets_preserve_recipient, mut targets_preserve_token) = (true, true);
199 for &(callee, ..) in &calls {
200 if judged.contains(&callee) {
201 continue;
202 }
203 judged.push(callee);
204 if let Some(target) = self.unsafe_mint_target(callee, true, &mut seen.clone()) {
205 unsafe_targets.push(callee);
206 if !target.preserves_code_length {
207 unstable_code_targets.push(callee);
208 }
209 targets_preserve_recipient &= target.preserves_recipient;
210 targets_preserve_token &= target.preserves_token;
211 }
212 }
213 let delegations: Vec<_> =
214 calls.iter().filter(|(callee, ..)| unsafe_targets.contains(callee)).collect();
215 if delegations.is_empty() {
216 return None;
217 }
218 let forwards = |index: usize, var: Option<VariableId>| {
222 var.is_some_and(|var| {
223 delegations.iter().all(|&&(callee, args, _)| {
224 self.arg(callee, args, index).and_then(|expr| underlying_var(self.gcx, expr))
225 == Some(var)
226 })
227 })
228 };
229 let only_to_recipient = forwards(0, recipient);
230 let mut token = None;
235 let mut token_consistent = true;
236 for &&(callee, args, _) in &delegations {
237 let minted = self.arg(callee, args, 1).and_then(|expr| underlying_var(self.gcx, expr));
238 match minted.filter(|&minted| keeps_its_value(self.gcx, minted)) {
239 Some(minted) => {
240 token_consistent &= token.is_none_or(|token| token == minted);
241 token = Some(minted);
242 }
243 None => token_consistent = false,
244 }
245 }
246 let guarded = |recipient, token, seed| {
250 let mut walk = self.modifier_coverage_at_body(function, recipient, token, seed);
251 let mut walker = GuardWalker {
252 cx: self,
253 recipient,
254 token,
255 delegations: &unsafe_targets,
256 unstable_code_delegations: &unstable_code_targets,
257 seen: &mut Vec::new(),
258 };
259 walker.walk(body.stmts, &mut walk);
260 !walk.failed && !walk.pending
261 };
262 if only_to_recipient
267 && targets_preserve_recipient
268 && let Some(recipient) = recipient
269 && guarded(recipient, recipient, GuardCoverage::None)
270 {
271 return None;
272 }
273 if only_to_recipient
274 && token_consistent
275 && targets_preserve_recipient
276 && targets_preserve_token
277 && let Some(recipient) = recipient
278 && let Some(token) = token
279 && guarded(recipient, token, GuardCoverage::None)
280 {
281 return None;
282 }
283 let preserves = |index: usize| {
288 function.parameters.get(index).is_some_and(|&var| {
289 !body.stmts.iter().any(|stmt| self.mutates_var(stmt, var))
290 && !function.modifiers.iter().any(|modifier| {
291 modifier.args.exprs().any(|arg| self.expr_mutates_var(arg, var))
292 })
293 && forwards(index, Some(var))
294 })
295 };
296 let preserves_code_length = recipient
299 .is_some_and(|recipient| guarded(recipient, recipient, GuardCoverage::CodeLess));
300 Some(UnsafeMintTarget {
301 preserves_recipient: targets_preserve_recipient && preserves(0),
302 preserves_token: targets_preserve_token && preserves(1),
303 preserves_code_length,
304 })
305 }
306
307 fn any_in_stmts(
309 self,
310 stmts: &'gcx [Stmt<'gcx>],
311 stmt_matches: impl FnMut(&'gcx Stmt<'gcx>) -> bool,
312 expr_matches: impl FnMut(&'gcx Expr<'gcx>) -> bool,
313 ) -> bool {
314 let mut finder = Finder { gcx: self.gcx, stmt_matches, expr_matches };
315 stmts.iter().any(|stmt| finder.visit_stmt(stmt).is_break())
316 }
317
318 fn any_in_expr(
319 self,
320 expr: &'gcx Expr<'gcx>,
321 expr_matches: impl FnMut(&'gcx Expr<'gcx>) -> bool,
322 ) -> bool {
323 Finder { gcx: self.gcx, stmt_matches: |_| false, expr_matches }.visit_expr(expr).is_break()
324 }
325
326 fn calls(self, stmts: &'gcx [Stmt<'gcx>]) -> Vec<Call<'gcx>> {
328 let mut calls = Vec::new();
329 self.any_in_stmts(
330 stmts,
331 |_| false,
332 |expr| {
333 if let ExprKind::Call(_, args, _) = &expr.kind
334 && let Some(function_id) = self.resolved_callee(expr)
335 {
336 calls.push((function_id, args, expr.span));
337 }
338 false
339 },
340 );
341 calls
342 }
343
344 fn resolved_callee(self, expr: &Expr<'_>) -> Option<FunctionId> {
346 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
347 self.gcx.resolved_function(callee)
348 }
349
350 fn callee_fn(self, expr: &Expr<'_>) -> Option<&'gcx TyFn<'gcx>> {
351 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
352 match self.gcx.type_of_expr(callee.peel_parens().id)?.kind {
353 TyKind::Fn(function_ty) => Some(function_ty),
354 _ => None,
355 }
356 }
357
358 fn resolved_internal_callee(self, expr: &Expr<'_>) -> Option<FunctionId> {
362 let function_ty = self.callee_fn(expr)?;
363 function_ty.is_internal().then_some(function_ty.function_id).flatten()
364 }
365
366 fn is_unresolved_internal_pointer_call(self, expr: &Expr<'_>) -> bool {
370 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
371 self.callee_fn(expr).is_some_and(|f| f.is_internal() && f.function_id.is_none())
372 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
373 && self.gcx.resolved_variable(callee).is_some()
374 }
375
376 fn arg(
378 self,
379 function_id: FunctionId,
380 args: &'gcx CallArgs<'gcx>,
381 index: usize,
382 ) -> Option<&'gcx Expr<'gcx>> {
383 let function = self.gcx.hir.function(function_id);
384 arg_for_param(self.gcx, function_id, *function.parameters.get(index)?, args)
385 }
386
387 fn is_receiver_hook(self, function_id: FunctionId) -> bool {
393 let function = self.gcx.hir.function(function_id);
394 let Some(contract) = function.contract else { return false };
395 let &[from, to, id, data] = function.parameters else { return false };
396 let kind = |vid: VariableId| &self.gcx.hir.variable(vid).ty.kind;
397 named(function, "onERC721Received")
398 && !self.gcx.hir.contract(contract).kind.is_library()
399 && matches!(function.visibility, Visibility::Public | Visibility::External)
400 && is_address_type(&self.gcx.hir, from)
401 && is_address_type(&self.gcx.hir, to)
402 && matches!(kind(id), TypeKind::Elementary(ElementaryType::UInt(_)))
403 && matches!(kind(data), TypeKind::Elementary(ElementaryType::Bytes))
404 }
405
406 fn is_received_selector(self, expr: &Expr<'gcx>) -> bool {
412 let expr = expr.peel_parens();
413 match &expr.kind {
414 ExprKind::Lit(lit) => {
415 matches!(&lit.kind, LitKind::Number(value) if *value == U256::from(ERC721_RECEIVED))
416 }
417 ExprKind::Call(callee, args, _)
418 if matches!(callee.peel_parens().kind, ExprKind::Type(..)) =>
419 {
420 args.len() == 1
421 && args.exprs().next().is_some_and(|inner| {
422 self.selector_cast_preserves(expr, inner)
423 && self.is_received_selector(inner)
424 })
425 }
426 ExprKind::Member(base, member) => {
427 member.as_str() == "selector"
428 && self.gcx.resolved_function(base).is_some_and(|id| self.is_receiver_hook(id))
429 }
430 ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|vid| {
432 let variable = self.gcx.hir.variable(vid);
433 variable.is_constant()
434 && variable.initializer.is_some_and(|init| self.is_received_selector(init))
435 }),
436 _ => false,
437 }
438 }
439
440 fn selector_cast_preserves(self, cast: &Expr<'_>, inner: &Expr<'_>) -> bool {
445 let encoding = |expr: &Expr<'_>| match self.gcx.type_of_expr(expr.peel_parens().id)?.kind {
446 TyKind::IntLiteral(..) => Some(SelectorEncoding::Literal),
447 TyKind::Elementary(ElementaryType::Int(size) | ElementaryType::UInt(size)) => {
448 Some(SelectorEncoding::Integer(size.bits()))
449 }
450 TyKind::Elementary(ElementaryType::FixedBytes(size)) => {
451 Some(SelectorEncoding::FixedBytes(size.bytes()))
452 }
453 _ => None,
454 };
455 matches!(
456 (encoding(inner), encoding(cast)),
457 (
458 Some(SelectorEncoding::Literal | SelectorEncoding::Integer(_)),
459 Some(SelectorEncoding::Integer(32..) | SelectorEncoding::FixedBytes(4))
460 ) | (Some(SelectorEncoding::FixedBytes(4)), Some(SelectorEncoding::Integer(32)))
461 | (
462 Some(SelectorEncoding::FixedBytes(4..)),
463 Some(SelectorEncoding::FixedBytes(4..))
464 )
465 )
466 }
467
468 fn branch_always_reverts(self, stmt: &'gcx Stmt<'gcx>) -> bool {
472 match &stmt.kind {
473 StmtKind::Revert(_) => !self.may_return(stmt),
474 StmtKind::Expr(expr) => is_revert_call(self.gcx, expr) && !self.may_return(stmt),
475 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => block
478 .stmts
479 .iter()
480 .find_map(|stmt| {
481 self.branch_always_reverts(stmt)
482 .then_some(true)
483 .or_else(|| self.may_return(stmt).then_some(false))
484 })
485 .unwrap_or(false),
486 StmtKind::If(cond, then, Some(otherwise)) => {
487 !self.expr_contains_frame_ending_assembly(cond)
488 && self.branch_always_reverts(then)
489 && self.branch_always_reverts(otherwise)
490 }
491 _ => false,
492 }
493 }
494
495 fn may_return(self, stmt: &'gcx Stmt<'gcx>) -> bool {
499 self.contains_frame_ending_assembly(slice::from_ref(stmt), &mut Vec::new())
500 || match &stmt.kind {
501 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
502 block.stmts.iter().any(|stmt| self.may_return(stmt))
503 }
504 StmtKind::Loop(block, source) => {
505 loop_stmts(*block, *source).any(|stmt| self.may_return(stmt))
506 }
507 StmtKind::If(_, then, otherwise) => {
508 self.may_return(then) || otherwise.is_some_and(|stmt| self.may_return(stmt))
509 }
510 StmtKind::Return(_)
511 | StmtKind::AssemblyBlock(_)
512 | StmtKind::Try(_)
513 | StmtKind::Switch(_) => true,
514 _ => false,
515 }
516 }
517
518 fn contains_frame_ending_assembly(
523 self,
524 stmts: &'gcx [Stmt<'gcx>],
525 seen: &mut Vec<FunctionId>,
526 ) -> bool {
527 self.any_in_stmts(stmts, is_assembly, |expr| self.call_leaves_frame(expr, seen))
528 }
529
530 fn expr_contains_frame_ending_assembly(self, expr: &'gcx Expr<'gcx>) -> bool {
531 let mut seen = Vec::new();
532 self.any_in_expr(expr, |expr| self.call_leaves_frame(expr, &mut seen))
533 }
534
535 fn call_leaves_frame(self, expr: &Expr<'_>, seen: &mut Vec<FunctionId>) -> bool {
536 self.is_unresolved_internal_pointer_call(expr)
537 || self
538 .resolved_internal_callee(expr)
539 .is_some_and(|id| self.callable_contains_frame_ending_assembly(id, seen))
540 }
541
542 fn callable_contains_frame_ending_assembly(
545 self,
546 function_id: FunctionId,
547 seen: &mut Vec<FunctionId>,
548 ) -> bool {
549 if seen.contains(&function_id) {
550 return false;
551 }
552 seen.push(function_id);
553 let function = self.gcx.hir.function(function_id);
554 let in_modifiers = function.modifiers.iter().any(|modifier| {
555 matches!(modifier.id, ItemId::Function(id)
556 if self.callable_contains_frame_ending_assembly(id, seen))
557 });
558 let in_body = function
559 .body
560 .as_ref()
561 .is_some_and(|body| self.contains_frame_ending_assembly(body.stmts, seen));
562 seen.pop();
563 in_modifiers || in_body
564 }
565
566 fn mutates_var(self, stmt: &'gcx Stmt<'gcx>, var: VariableId) -> bool {
572 self.any_in_stmts(slice::from_ref(stmt), is_assembly, |expr| {
573 assigns_to(self.gcx, expr, var)
574 })
575 }
576
577 fn expr_mutates_var(self, expr: &'gcx Expr<'gcx>, var: VariableId) -> bool {
578 self.any_in_expr(expr, |expr| assigns_to(self.gcx, expr, var))
579 }
580
581 fn stmts_may_change_account_code(
584 self,
585 stmts: &'gcx [Stmt<'gcx>],
586 delegations: &[FunctionId],
587 unstable_code_delegations: &[FunctionId],
588 seen: &mut Vec<FunctionId>,
589 ) -> bool {
590 self.any_in_stmts(stmts, is_assembly, |expr| {
591 self.call_may_change_account_code(expr, delegations, unstable_code_delegations, seen)
592 })
593 }
594
595 fn expr_may_change_account_code(
596 self,
597 expr: &'gcx Expr<'gcx>,
598 delegations: &[FunctionId],
599 unstable_code_delegations: &[FunctionId],
600 seen: &mut Vec<FunctionId>,
601 ) -> bool {
602 self.any_in_expr(expr, |expr| {
603 self.call_may_change_account_code(expr, delegations, unstable_code_delegations, seen)
604 })
605 }
606
607 fn call_may_change_account_code(
613 self,
614 expr: &Expr<'_>,
615 delegations: &[FunctionId],
616 unstable_code_delegations: &[FunctionId],
617 seen: &mut Vec<FunctionId>,
618 ) -> bool {
619 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
620 let resolved = self.resolved_callee(expr);
621 if resolved.is_some_and(|id| delegations.contains(&id))
622 && !resolved.is_some_and(|id| unstable_code_delegations.contains(&id))
623 {
624 return false;
625 }
626 if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
627 return true;
628 }
629 if !self.callee_fn(expr).is_some_and(|f| {
630 matches!(f.state_mutability, StateMutability::NonPayable | StateMutability::Payable)
631 }) {
632 return false;
633 }
634 match self.resolved_internal_callee(expr).filter(|&id| !self.gcx.hir.function(id).virtual_)
635 {
636 Some(id) => self.callable_may_change_account_code(id, seen),
637 None => true,
638 }
639 }
640
641 fn callable_may_change_account_code(
645 self,
646 function_id: FunctionId,
647 seen: &mut Vec<FunctionId>,
648 ) -> bool {
649 if seen.contains(&function_id) {
650 return false;
651 }
652 seen.push(function_id);
653 let function = self.gcx.hir.function(function_id);
654 let may_change = function.modifiers.iter().any(|modifier| {
655 modifier.args.exprs().any(|arg| self.expr_may_change_account_code(arg, &[], &[], seen))
656 }) || function.modifiers.iter().any(|modifier| {
657 matches!(modifier.id, ItemId::Function(id)
658 if self.callable_may_change_account_code(id, seen))
659 }) || function
660 .body
661 .as_ref()
662 .is_some_and(|body| self.stmts_may_change_account_code(body.stmts, &[], &[], seen));
663 seen.pop();
664 may_change
665 }
666
667 fn bound_guard_parameters(
669 self,
670 function_id: FunctionId,
671 args: &'gcx CallArgs<'gcx>,
672 recipient: VariableId,
673 token: VariableId,
674 ) -> Option<(VariableId, VariableId)> {
675 let parameters = self.gcx.hir.function(function_id).parameters;
676 let bound_to = |var| {
677 parameters
678 .iter()
679 .enumerate()
680 .find(|&(index, _)| {
681 self.arg(function_id, args, index)
682 .and_then(|expr| underlying_var(self.gcx, expr))
683 == Some(var)
684 })
685 .map(|(_, ¶meter)| parameter)
686 };
687 bound_to(recipient).zip(bound_to(token))
688 }
689
690 fn body_guards(
694 self,
695 function_id: FunctionId,
696 recipient: VariableId,
697 token: VariableId,
698 seen: &mut Vec<FunctionId>,
699 ) -> GuardCoverage {
700 if seen.contains(&function_id) {
701 return GuardCoverage::None;
702 }
703 seen.push(function_id);
704 let function = self.gcx.hir.function(function_id);
705 let guarded = match &function.body {
711 Some(body)
712 if !function.virtual_
713 && function.modifiers.is_empty()
714 && !body.stmts.iter().any(|stmt| {
715 self.mutates_var(stmt, recipient) || self.mutates_var(stmt, token)
716 }) =>
717 {
718 let mut walk = GuardWalk::default();
719 let mut walker = GuardWalker {
720 cx: self,
721 recipient,
722 token,
723 delegations: &[],
724 unstable_code_delegations: &[],
725 seen,
726 };
727 walker.walk(body.stmts, &mut walk);
728 if walk.escaped {
729 GuardCoverage::None
730 } else if walk.future_coverage == GuardCoverage::CodeLess {
731 GuardCoverage::CodeLess
732 } else if walk.coverage == GuardCoverage::CodeLess {
733 GuardCoverage::CallbackOrCodeLess
737 } else {
738 walk.coverage
739 }
740 }
741 _ => GuardCoverage::None,
742 };
743 seen.pop();
744 guarded
745 }
746
747 fn modifier_coverage_at_body(
753 self,
754 function: &'gcx hir::Function<'gcx>,
755 recipient: VariableId,
756 token: VariableId,
757 seed: GuardCoverage,
758 ) -> GuardWalk {
759 let mut state = GuardWalk { coverage: seed, future_coverage: seed, ..GuardWalk::default() };
760 let body_bypass = function
761 .body
762 .as_ref()
763 .is_some_and(|body| self.contains_frame_ending_assembly(body.stmts, &mut Vec::new()));
764 let mut has_tail_guard = false;
765 for (index, modifier) in function.modifiers.iter().enumerate() {
766 if modifier.args.exprs().any(|arg| {
767 self.expr_mutates_var(arg, recipient) || self.expr_mutates_var(arg, token)
768 }) {
769 state.coverage = GuardCoverage::None;
770 state.future_coverage = GuardCoverage::None;
771 has_tail_guard = false;
772 }
773 state.retire_code_snapshots_if(|| {
774 modifier
775 .args
776 .exprs()
777 .any(|arg| self.expr_may_change_account_code(arg, &[], &[], &mut Vec::new()))
778 });
779 let ItemId::Function(modifier_id) = modifier.id else { continue };
780 let Some(body) = &self.gcx.hir.function(modifier_id).body else { continue };
781 let Some((prefix, suffix)) = modifier_body_sides(body.stmts) else {
782 state.retire_code_snapshots_if(|| {
786 self.stmts_may_change_account_code(body.stmts, &[], &[], &mut Vec::new())
787 });
788 continue;
789 };
790 let prefix_may_change_code =
791 || self.stmts_may_change_account_code(prefix, &[], &[], &mut Vec::new());
792 let Some((modifier_recipient, modifier_token)) =
793 self.bound_guard_parameters(modifier_id, &modifier.args, recipient, token)
794 else {
795 state.retire_code_snapshots_if(prefix_may_change_code);
796 continue;
797 };
798 let parameters_unchanged = !body.stmts.iter().any(|stmt| {
799 self.mutates_var(stmt, modifier_recipient) || self.mutates_var(stmt, modifier_token)
800 });
801 let mut walker = GuardWalker {
802 cx: self,
803 recipient: modifier_recipient,
804 token: modifier_token,
805 delegations: &[],
806 unstable_code_delegations: &[],
807 seen: &mut Vec::new(),
808 };
809 if parameters_unchanged {
810 walker.walk(prefix, &mut state);
811 } else {
812 state.retire_code_snapshots_if(prefix_may_change_code);
813 }
814 let inner_modifier_bypass = function.modifiers[index + 1..].iter().any(|inner| {
815 matches!(inner.id, ItemId::Function(id)
816 if self.callable_contains_frame_ending_assembly(id, &mut Vec::new()))
817 });
818 if parameters_unchanged && !body_bypass && !inner_modifier_bypass {
819 let mut suffix_walk = GuardWalk { pending: true, ..GuardWalk::default() };
823 walker.walk(suffix, &mut suffix_walk);
824 has_tail_guard |= !suffix_walk.failed && !suffix_walk.pending;
825 }
826 }
827 if has_tail_guard {
828 state.cover(GuardCoverage::Callback, true);
829 }
830 GuardWalk {
831 coverage: state.coverage,
832 future_coverage: state.future_coverage,
833 ..GuardWalk::default()
834 }
835 }
836}
837
838struct Finder<'gcx, S, E> {
840 gcx: Gcx<'gcx>,
841 stmt_matches: S,
842 expr_matches: E,
843}
844
845impl<'gcx, S, E> Visit<'gcx> for Finder<'gcx, S, E>
846where
847 S: FnMut(&'gcx Stmt<'gcx>) -> bool,
848 E: FnMut(&'gcx Expr<'gcx>) -> bool,
849{
850 type BreakValue = ();
851
852 fn hir(&self) -> &'gcx Hir<'gcx> {
853 &self.gcx.hir
854 }
855
856 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
857 if (self.stmt_matches)(stmt) { ControlFlow::Break(()) } else { self.walk_stmt(stmt) }
858 }
859
860 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
861 if (self.expr_matches)(expr) { ControlFlow::Break(()) } else { self.walk_expr(expr) }
862 }
863}
864
865#[derive(Clone, Copy, Default, PartialEq, Eq)]
870enum GuardCoverage {
871 #[default]
872 None,
873 Callback,
874 CodeLess,
875 CallbackOrCodeLess,
876}
877
878impl GuardCoverage {
879 fn is_covered(self) -> bool {
880 self != Self::None
881 }
882
883 const fn relies_on_code_length(self) -> bool {
884 matches!(self, Self::CodeLess | Self::CallbackOrCodeLess)
885 }
886
887 const fn merge_paths(self, other: Self) -> Self {
890 match (self, other) {
891 (Self::None, _) | (_, Self::None) => Self::None,
892 (Self::Callback, Self::Callback) => Self::Callback,
893 (Self::CodeLess, Self::CodeLess) => Self::CodeLess,
894 _ => Self::CallbackOrCodeLess,
895 }
896 }
897
898 const fn combine_guards(self, other: Self) -> Self {
901 match (self, other) {
902 (Self::Callback, _) | (_, Self::Callback) => Self::Callback,
903 (Self::CallbackOrCodeLess, _) | (_, Self::CallbackOrCodeLess) => {
904 Self::CallbackOrCodeLess
905 }
906 (Self::CodeLess, _) | (_, Self::CodeLess) => Self::CodeLess,
907 _ => Self::None,
908 }
909 }
910}
911
912#[derive(Clone, Default)]
917struct GuardWalk {
918 coverage: GuardCoverage,
920 future_coverage: GuardCoverage,
924 pending: bool,
925 failed: bool,
926 escaped: bool,
927}
928
929impl GuardWalk {
930 const fn cover(&mut self, coverage: GuardCoverage, future: bool) {
932 self.coverage = self.coverage.combine_guards(coverage);
933 if future {
934 self.future_coverage = self.future_coverage.combine_guards(coverage);
935 }
936 self.pending = false;
937 }
938
939 const fn retire(&mut self) {
943 self.failed |= self.pending;
944 self.coverage = GuardCoverage::None;
945 self.future_coverage = GuardCoverage::None;
946 }
947
948 fn escape(&mut self) {
950 self.failed |= self.pending;
951 self.escaped |= !self.coverage.is_covered();
952 }
953
954 fn retire_code_snapshots_if(&mut self, may_change_code: impl FnOnce() -> bool) {
957 let (coverage, future) =
958 (self.coverage.relies_on_code_length(), self.future_coverage.relies_on_code_length());
959 if (coverage || future) && may_change_code() {
960 if coverage {
961 self.coverage = GuardCoverage::None;
962 }
963 if future {
964 self.future_coverage = GuardCoverage::None;
965 }
966 }
967 }
968
969 const fn merge(self, other: Self) -> Self {
972 Self {
973 coverage: self.coverage.merge_paths(other.coverage),
974 future_coverage: self.future_coverage.merge_paths(other.future_coverage),
975 pending: self.pending || other.pending,
976 failed: self.failed || other.failed,
977 escaped: self.escaped || other.escaped,
978 }
979 }
980}
981
982struct GuardWalker<'a, 'gcx> {
1006 cx: Cx<'gcx>,
1007 recipient: VariableId,
1008 token: VariableId,
1009 delegations: &'a [FunctionId],
1011 unstable_code_delegations: &'a [FunctionId],
1012 seen: &'a mut Vec<FunctionId>,
1013}
1014
1015impl<'gcx> GuardWalker<'_, 'gcx> {
1016 fn walk(&mut self, stmts: &'gcx [Stmt<'gcx>], walk: &mut GuardWalk) {
1017 let cx = self.cx;
1018 for stmt in stmts {
1021 let guard = match &stmt.kind {
1022 StmtKind::Expr(expr) => self.guard_expr_coverage(expr),
1023 _ => GuardCoverage::None,
1024 };
1025 match &stmt.kind {
1026 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
1027 self.walk(block.stmts, walk);
1028 }
1029 StmtKind::Expr(expr) if guard.is_covered() => {
1030 if self.mutates(stmt) {
1036 walk.retire();
1037 } else if cx.may_return(stmt) {
1038 walk.escape();
1039 } else if guard.relies_on_code_length()
1040 && self.guard_extra_args_may_change_account_code(expr)
1041 {
1042 if walk.future_coverage.relies_on_code_length() {
1043 walk.future_coverage = GuardCoverage::None;
1044 }
1045 } else {
1046 walk.cover(guard, guard == GuardCoverage::CodeLess);
1047 }
1048 }
1049 StmtKind::If(cond, then, otherwise) => {
1050 let condition_mutates = cx.expr_mutates_var(cond, self.recipient)
1054 || cx.expr_mutates_var(cond, self.token);
1055 if condition_mutates {
1056 walk.retire();
1057 }
1058 if walk.future_coverage.relies_on_code_length()
1059 && self.may_change_account_code(slice::from_ref(stmt), Some(cond))
1060 {
1061 walk.future_coverage = GuardCoverage::None;
1062 }
1063 if cx.expr_contains_frame_ending_assembly(cond) {
1064 walk.escape();
1065 }
1066 let refusal_then = !condition_mutates
1070 && self.is_hook_comparison(cond, BinOpKind::Ne)
1071 && cx.branch_always_reverts(then);
1072 let refusal_else = !condition_mutates
1073 && self.is_hook_comparison(cond, BinOpKind::Eq)
1074 && otherwise.is_some_and(|otherwise| cx.branch_always_reverts(otherwise));
1075 if refusal_then || refusal_else {
1076 walk.cover(GuardCoverage::Callback, false);
1077 let accepted = if refusal_then { *otherwise } else { Some(*then) };
1078 if let Some(accepted) = accepted {
1079 self.walk(slice::from_ref(accepted), walk);
1080 }
1081 continue;
1082 }
1083 let mut then_walk = walk.clone();
1088 let mut else_walk = walk.clone();
1089 if self.is_code_length_test(cond, true) {
1090 else_walk.cover(GuardCoverage::CodeLess, true);
1091 } else if self.is_code_length_test(cond, false) {
1092 then_walk.cover(GuardCoverage::CodeLess, true);
1093 }
1094 self.walk(slice::from_ref(then), &mut then_walk);
1095 if let Some(otherwise) = otherwise {
1096 self.walk(slice::from_ref(otherwise), &mut else_walk);
1097 }
1098 *walk = then_walk.merge(else_walk);
1099 }
1100 _ => {
1101 if self.mutates(stmt) {
1104 walk.retire();
1105 }
1106 if walk.future_coverage.relies_on_code_length()
1111 && self.may_change_account_code(slice::from_ref(stmt), None)
1112 {
1113 walk.future_coverage = GuardCoverage::None;
1114 }
1115 let delegations = self.delegations;
1119 if !walk.future_coverage.is_covered()
1120 && cx.any_in_stmts(
1121 slice::from_ref(stmt),
1122 |_| false,
1123 |expr| {
1124 cx.resolved_callee(expr).is_some_and(|id| delegations.contains(&id))
1125 },
1126 )
1127 {
1128 walk.pending = true;
1129 }
1130 if cx.may_return(stmt) {
1131 walk.escape();
1132 }
1133 }
1134 }
1135 }
1136 }
1137
1138 fn mutates(&self, stmt: &'gcx Stmt<'gcx>) -> bool {
1139 self.cx.mutates_var(stmt, self.recipient) || self.cx.mutates_var(stmt, self.token)
1140 }
1141
1142 fn may_change_account_code(
1145 &self,
1146 stmts: &'gcx [Stmt<'gcx>],
1147 expr: Option<&'gcx Expr<'gcx>>,
1148 ) -> bool {
1149 let (delegations, unstable) = (self.delegations, self.unstable_code_delegations);
1150 let mut seen = Vec::new();
1151 match expr {
1152 Some(expr) => {
1153 self.cx.expr_may_change_account_code(expr, delegations, unstable, &mut seen)
1154 }
1155 None => self.cx.stmts_may_change_account_code(stmts, delegations, unstable, &mut seen),
1156 }
1157 }
1158
1159 fn guard_expr_coverage(&mut self, expr: &'gcx Expr<'gcx>) -> GuardCoverage {
1165 let expr = expr.peel_parens();
1166 let ExprKind::Call(callee, args, _) = &expr.kind else { return GuardCoverage::None };
1167 if is_require_or_assert(self.cx.gcx, callee) {
1168 return args
1169 .exprs()
1170 .next()
1171 .map_or(GuardCoverage::None, |cond| self.acceptance_coverage(cond));
1172 }
1173 let Some(function_id) = self.cx.resolved_internal_callee(expr) else {
1174 return GuardCoverage::None;
1175 };
1176 let Some((recipient, token)) =
1177 self.cx.bound_guard_parameters(function_id, args, self.recipient, self.token)
1178 else {
1179 return GuardCoverage::None;
1180 };
1181 self.cx.body_guards(function_id, recipient, token, self.seen)
1182 }
1183
1184 fn guard_extra_args_may_change_account_code(&self, expr: &'gcx Expr<'gcx>) -> bool {
1189 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
1190 is_require_or_assert(self.cx.gcx, callee)
1191 && args.exprs().skip(1).any(|arg| self.may_change_account_code(&[], Some(arg)))
1192 }
1193
1194 fn acceptance_coverage(&self, cond: &'gcx Expr<'gcx>) -> GuardCoverage {
1198 let cond = cond.peel_parens();
1199 if self.is_code_length_test(cond, false) {
1200 return GuardCoverage::CodeLess;
1201 }
1202 if self.is_hook_comparison(cond, BinOpKind::Eq) {
1203 return GuardCoverage::Callback;
1204 }
1205 let ExprKind::Binary(lhs, op, rhs) = &cond.kind else { return GuardCoverage::None };
1206 let accepts = |skip, check| {
1207 self.is_code_length_test(skip, false) && self.is_hook_comparison(check, BinOpKind::Eq)
1208 };
1209 if op.kind == BinOpKind::Or && (accepts(lhs, rhs) || accepts(rhs, lhs)) {
1210 GuardCoverage::CallbackOrCodeLess
1211 } else {
1212 GuardCoverage::None
1213 }
1214 }
1215
1216 fn is_hook_call_on(&self, expr: &'gcx Expr<'gcx>) -> bool {
1220 let expr = expr.peel_parens();
1221 let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
1222 let ExprKind::Member(receiver, _) = &callee.peel_parens().kind else { return false };
1223 let Some(function_id) = self.cx.resolved_callee(expr) else { return false };
1224 self.cx.is_receiver_hook(function_id)
1225 && underlying_var(self.cx.gcx, receiver) == Some(self.recipient)
1226 && self.cx.arg(function_id, args, 2).and_then(|expr| underlying_var(self.cx.gcx, expr))
1227 == Some(self.token)
1228 }
1229
1230 fn is_hook_comparison(&self, expr: &'gcx Expr<'gcx>, want: BinOpKind) -> bool {
1235 let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return false };
1236 let compares = |hook, answer| {
1237 self.is_hook_call_on(hook)
1238 && !self.is_hook_call_on(answer)
1239 && self.cx.is_received_selector(answer)
1240 };
1241 op.kind == want && (compares(lhs, rhs) || compares(rhs, lhs))
1242 }
1243
1244 fn is_code_length_test(&self, expr: &'gcx Expr<'gcx>, has_code: bool) -> bool {
1248 let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return false };
1249 let is_code_length = |expr: &Expr<'_>| {
1250 let ExprKind::Member(code, length) = &expr.peel_parens().kind else { return false };
1251 let ExprKind::Member(base, member) = &code.peel_parens().kind else { return false };
1252 length.as_str() == "length"
1253 && member.as_str() == "code"
1254 && underlying_var(self.cx.gcx, base) == Some(self.recipient)
1255 };
1256 let literal = |expr: &Expr<'_>| match &expr.peel_parens().kind {
1257 ExprKind::Lit(lit) => match &lit.kind {
1258 LitKind::Number(value) => u8::try_from(*value).ok(),
1259 _ => None,
1260 },
1261 _ => None,
1262 };
1263 let (bound, flipped) = if is_code_length(lhs) {
1264 (literal(rhs), false)
1265 } else if is_code_length(rhs) {
1266 (literal(lhs), true)
1267 } else {
1268 return false;
1269 };
1270 let Some(bound) = bound else { return false };
1271 match (has_code, op.kind, flipped) {
1275 (true, BinOpKind::Ne, _)
1276 | (true, BinOpKind::Gt, false)
1277 | (true, BinOpKind::Lt, true)
1278 | (false, BinOpKind::Eq, _)
1279 | (false, BinOpKind::Le, false)
1280 | (false, BinOpKind::Ge, true) => bound == 0,
1281 (true, BinOpKind::Ge, false)
1282 | (true, BinOpKind::Le, true)
1283 | (false, BinOpKind::Lt, false)
1284 | (false, BinOpKind::Gt, true) => bound == 1,
1285 _ => false,
1286 }
1287 }
1288}
1289
1290#[derive(Clone, Copy)]
1292enum SelectorEncoding {
1293 Literal,
1294 Integer(u16),
1295 FixedBytes(u8),
1296}
1297
1298const ERC721_RECEIVED: u64 = 0x150b_7a02;
1300
1301fn is_canonical_erc721(name: &str) -> bool {
1307 matches!(
1308 name,
1309 "ERC721" | "ERC721Upgradeable" | "ERC721Consecutive" | "ERC721ConsecutiveUpgradeable"
1310 )
1311}
1312
1313fn named(function: &hir::Function<'_>, name: &str) -> bool {
1314 function.name.is_some_and(|n| n.as_str() == name)
1315}
1316
1317const fn is_internal(function: &hir::Function<'_>) -> bool {
1318 matches!(function.visibility, Visibility::Internal | Visibility::Private)
1319}
1320
1321const fn is_assembly(stmt: &Stmt<'_>) -> bool {
1322 matches!(stmt.kind, StmtKind::AssemblyBlock(_))
1323}
1324
1325fn keeps_its_value(gcx: Gcx<'_>, variable: VariableId) -> bool {
1329 let variable = gcx.hir.variable(variable);
1330 !variable.kind.is_state() || variable.mutability.is_some()
1331}
1332
1333fn assigns_to(gcx: Gcx<'_>, expr: &Expr<'_>, var: VariableId) -> bool {
1336 let Some(target) = write_target(expr) else { return false };
1337 let mut hit = false;
1338 for_each_lhs_var(gcx, target, &mut |vid| hit |= vid == var);
1339 hit
1340}
1341
1342fn is_revert_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
1344 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
1345 is_builtin(gcx, callee, kw::Revert)
1346 || (is_require_or_assert(gcx, callee) && args.exprs().next().is_some_and(is_literal_false))
1347}
1348
1349fn modifier_body_sides<'gcx>(
1352 stmts: &'gcx [Stmt<'gcx>],
1353) -> Option<(&'gcx [Stmt<'gcx>], &'gcx [Stmt<'gcx>])> {
1354 let placeholders =
1355 stmts.iter().enumerate().filter(|(_, stmt)| matches!(stmt.kind, StmtKind::Placeholder));
1356 let index = unique(placeholders.map(|(index, _)| index))?;
1357 Some((&stmts[..index], &stmts[index + 1..]))
1358}