1use super::UnsafeOzErc721Mint;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint, analysis::primitives::is_require_or_assert},
5};
6use alloy_primitives::U256;
7use solar::{
8 ast::{ElementaryType, LitKind, StateMutability, Visibility},
9 interface::{kw, source_map::FileName},
10 sema::{
11 Gcx,
12 hir::{self, Expr, ExprKind, FunctionId, Hir, Visit},
13 ty::TyKind,
14 },
15};
16use std::{convert::Infallible, ops::ControlFlow};
17
18declare_forge_lint!(
19 UNSAFE_OZ_ERC721_MINT,
20 Severity::Med,
21 "unsafe-oz-erc721-mint",
22 "`ERC721._mint` does not check that the recipient can receive the token; use `_safeMint`"
23);
24
25impl<'hir> LateLintPass<'hir> for UnsafeOzErc721Mint {
26 fn check_function(
27 &mut self,
28 ctx: &LintContext,
29 gcx: Gcx<'hir>,
30 hir: &'hir Hir<'hir>,
31 func: &'hir hir::Function<'hir>,
32 ) {
33 if func.name.is_some_and(|name| name.as_str() == "_safeMint")
37 && func.contract.is_some_and(|id| is_canonical_erc721(hir.contract(id).name.as_str()))
38 && is_openzeppelin_source(hir, func.source)
39 {
40 return;
41 }
42 if func.name.is_some_and(|name| name.as_str() == "_mint") && func.override_ {
47 return;
48 }
49 if let Some(body) = &func.body {
53 let suppress_direct_mint = is_override_delegation_helper(gcx, hir, func);
54 let mut finder = MintCallFinder { gcx, hir, ctx, suppress_direct_mint };
55 for stmt in body.stmts {
56 let _ = finder.visit_stmt(stmt);
57 }
58 }
59 }
60}
61
62struct MintCallFinder<'ctx, 's, 'c, 'hir> {
63 gcx: Gcx<'hir>,
64 hir: &'hir Hir<'hir>,
65 ctx: &'ctx LintContext<'s, 'c>,
66 suppress_direct_mint: bool,
67}
68
69impl<'hir> Visit<'hir> for MintCallFinder<'_, '_, '_, 'hir> {
70 type BreakValue = Infallible;
71
72 fn hir(&self) -> &'hir Hir<'hir> {
73 self.hir
74 }
75
76 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
77 if let Some(function_id) = resolved_callee(self.gcx, expr)
82 && self.is_erc721_mint(function_id)
83 && !self.suppress_direct_mint
84 {
85 self.ctx.emit(&UNSAFE_OZ_ERC721_MINT, expr.span);
86 }
87 self.walk_expr(expr)
88 }
89}
90
91fn is_override_delegation_helper<'hir>(
92 gcx: Gcx<'hir>,
93 hir: &'hir Hir<'hir>,
94 function: &'hir hir::Function<'hir>,
95) -> bool {
96 if !matches!(function.visibility, Visibility::Internal | Visibility::Private)
97 || (function.override_ && function.name.is_some_and(|name| name.as_str() == "_mint"))
98 {
99 return false;
100 }
101 let Some(contract_id) = function.contract else { return false };
102 let contract = hir.contract(contract_id);
103 let Some(function_id) = contract
104 .all_functions()
105 .find(|&function_id| std::ptr::eq(hir.function(function_id), function))
106 else {
107 return false;
108 };
109
110 hir.contract_ids().any(|candidate_contract_id| {
111 let candidate_contract = hir.contract(candidate_contract_id);
112 candidate_contract.linearized_bases.contains(&contract_id)
113 && candidate_contract.all_functions().any(|candidate| {
114 let candidate_function = hir.function(candidate);
115 candidate_function.override_
116 && candidate_function.name.is_some_and(|name| name.as_str() == "_mint")
117 && function_reaches(gcx, hir, candidate, function_id, &mut Vec::new())
118 })
119 })
120}
121
122fn function_reaches<'hir>(
123 gcx: Gcx<'hir>,
124 hir: &'hir Hir<'hir>,
125 function_id: FunctionId,
126 target: FunctionId,
127 seen: &mut Vec<FunctionId>,
128) -> bool {
129 if seen.contains(&function_id) {
130 return false;
131 }
132 seen.push(function_id);
133 let Some(body) = hir.function(function_id).body else { return false };
134 let mut scan = CalleeCollector { gcx, hir, calls: Vec::new() };
135 for stmt in body.stmts {
136 let _ = scan.visit_stmt(stmt);
137 }
138 scan.calls.iter().any(|(callee, _)| {
139 *callee == target
140 || (matches!(
141 hir.function(*callee).visibility,
142 Visibility::Internal | Visibility::Private
143 ) && function_reaches(gcx, hir, *callee, target, seen))
144 })
145}
146
147impl MintCallFinder<'_, '_, '_, '_> {
148 fn is_erc721_mint(&self, function_id: FunctionId) -> bool {
151 let function = self.hir.function(function_id);
152 let helper = is_override_delegation_helper(self.gcx, self.hir, function);
153 self.unsafe_mint_target(function_id, helper, &mut Vec::new()).is_some()
154 }
155
156 fn unsafe_mint_target(
163 &self,
164 function_id: FunctionId,
165 helper: bool,
166 seen: &mut Vec<FunctionId>,
167 ) -> Option<UnsafeMintTarget> {
168 if seen.contains(&function_id) {
170 return None;
171 }
172 seen.push(function_id);
173 let function = self.hir.function(function_id);
174 let is_mint = function.name.is_some_and(|name| name.as_str() == "_mint");
175 let is_safe_mint = function.name.is_some_and(|name| name.as_str() == "_safeMint");
176 if !is_mint
177 && (!helper
178 || !matches!(function.visibility, Visibility::Internal | Visibility::Private))
179 {
180 return None;
181 }
182 let contract_id = function.contract?;
183 let contract = self.hir.contract(contract_id);
184 if contract.kind.is_library() {
185 return None;
186 }
187 if is_safe_mint
188 && is_canonical_erc721(contract.name.as_str())
189 && is_openzeppelin_source(self.hir, function.source)
190 {
191 return None;
192 }
193 if is_mint
197 && is_canonical_erc721(contract.name.as_str())
198 && is_openzeppelin_source(self.hir, function.source)
199 {
200 return Some(UnsafeMintTarget {
201 preserves_recipient: true,
202 preserves_token: true,
203 preserves_code_length: true,
204 });
205 }
206 if (function.override_ || helper)
211 && let Some(body) = &function.body
212 {
213 let recipient = function.parameters.iter().copied().find(|&vid| {
215 matches!(
216 self.hir.variable(vid).ty.kind,
217 hir::TypeKind::Elementary(ElementaryType::Address(_))
218 )
219 });
220 let mut scan = CalleeCollector { gcx: self.gcx, hir: self.hir, calls: Vec::new() };
221 for stmt in body.stmts {
222 let _ = scan.visit_stmt(stmt);
223 }
224 let mut unsafe_targets: Vec<FunctionId> = Vec::new();
228 let mut unstable_code_targets: Vec<FunctionId> = Vec::new();
229 let mut judged: Vec<FunctionId> = Vec::new();
230 let mut targets_preserve_recipient = true;
231 let mut targets_preserve_token = true;
232 for (callee, _) in &scan.calls {
233 if judged.contains(callee) {
234 continue;
235 }
236 judged.push(*callee);
237 let mut branch = seen.clone();
240 if let Some(target) = self.unsafe_mint_target(*callee, true, &mut branch) {
241 unsafe_targets.push(*callee);
242 if !target.preserves_code_length {
243 unstable_code_targets.push(*callee);
244 }
245 targets_preserve_recipient &= target.preserves_recipient;
246 targets_preserve_token &= target.preserves_token;
247 }
248 }
249 let mut delegates = false;
250 let mut only_to_recipient = true;
251 let mut token = None;
256 let mut token_consistent = true;
257 for (callee, args) in &scan.calls {
258 if !unsafe_targets.contains(callee) {
259 continue;
260 }
261 delegates = true;
262 let handed_the_recipient = recipient.is_some_and(|recipient| {
267 argument_bound_to_parameter(self.hir, *callee, args, 0)
268 .is_some_and(|argument| is_exactly_var(argument, recipient))
269 });
270 if !handed_the_recipient {
271 only_to_recipient = false;
272 }
273 match argument_bound_to_parameter(self.hir, *callee, args, 1)
274 .and_then(variable_of)
275 .filter(|&minted| keeps_its_value(self.hir, minted))
276 {
277 Some(minted) => {
278 if token.is_some_and(|token| token != minted) {
279 token_consistent = false;
280 }
281 token = Some(minted);
282 }
283 None => token_consistent = false,
288 }
289 }
290 if !delegates {
291 return None;
292 }
293 if only_to_recipient
294 && targets_preserve_recipient
295 && let Some(recipient) = recipient
296 {
297 let coverage = modifier_coverage_at_body(
304 self.gcx,
305 self.hir,
306 function,
307 recipient,
308 recipient,
309 GuardCoverage::None,
310 );
311 let mut walk = GuardWalk {
312 coverage: coverage.coverage,
313 future_coverage: coverage.future_coverage,
314 ..GuardWalk::default()
315 };
316 walk_guards(
317 self.gcx,
318 self.hir,
319 body.stmts,
320 recipient,
321 recipient,
322 &unsafe_targets,
323 &unstable_code_targets,
324 false,
325 &mut Vec::new(),
326 &mut walk,
327 );
328 if !walk.failed && !walk.pending {
329 return None;
330 }
331 }
332 if only_to_recipient
333 && token_consistent
334 && targets_preserve_recipient
335 && targets_preserve_token
336 && let Some(recipient) = recipient
337 && let Some(token) = token
338 {
339 let coverage = modifier_coverage_at_body(
344 self.gcx,
345 self.hir,
346 function,
347 recipient,
348 token,
349 GuardCoverage::None,
350 );
351 let mut walk = GuardWalk {
352 coverage: coverage.coverage,
353 future_coverage: coverage.future_coverage,
354 ..GuardWalk::default()
355 };
356 walk_guards(
357 self.gcx,
358 self.hir,
359 body.stmts,
360 recipient,
361 token,
362 &unsafe_targets,
363 &unstable_code_targets,
364 false,
365 &mut Vec::new(),
366 &mut walk,
367 );
368 if !walk.failed && !walk.pending {
369 return None;
370 }
371 }
372 let recipient_parameter = function.parameters.first().copied();
378 let token_parameter = function.parameters.get(1).copied();
379 let recipient_unchanged = recipient_parameter.is_some_and(|recipient| {
380 !body.stmts.iter().any(|stmt| mutates_var(self.hir, stmt, recipient))
381 && !function.modifiers.iter().any(|modifier| {
382 modifier.args.exprs().any(|arg| expr_mutates_var(self.hir, arg, recipient))
383 })
384 });
385 let token_unchanged = token_parameter.is_some_and(|token| {
386 !body.stmts.iter().any(|stmt| mutates_var(self.hir, stmt, token))
387 && !function.modifiers.iter().any(|modifier| {
388 modifier.args.exprs().any(|arg| expr_mutates_var(self.hir, arg, token))
389 })
390 });
391 let forwards_recipient = recipient_parameter.is_some_and(|recipient| {
392 scan.calls.iter().filter(|(callee, _)| unsafe_targets.contains(callee)).all(
393 |(callee, args)| {
394 argument_bound_to_parameter(self.hir, *callee, args, 0)
395 .is_some_and(|argument| is_exactly_var(argument, recipient))
396 },
397 )
398 });
399 let forwards_token = token_parameter.is_some_and(|token| {
400 scan.calls.iter().filter(|(callee, _)| unsafe_targets.contains(callee)).all(
401 |(callee, args)| {
402 argument_bound_to_parameter(self.hir, *callee, args, 1)
403 .is_some_and(|argument| is_exactly_var(argument, token))
404 },
405 )
406 });
407 let preserves_code_length = recipient.is_some_and(|recipient| {
412 let coverage = modifier_coverage_at_body(
413 self.gcx,
414 self.hir,
415 function,
416 recipient,
417 recipient,
418 GuardCoverage::CodeLess,
419 );
420 let mut code_length_walk = GuardWalk {
421 coverage: coverage.coverage,
422 future_coverage: coverage.future_coverage,
423 ..GuardWalk::default()
424 };
425 walk_guards(
426 self.gcx,
427 self.hir,
428 body.stmts,
429 recipient,
430 recipient,
431 &unsafe_targets,
432 &unstable_code_targets,
433 false,
434 &mut Vec::new(),
435 &mut code_length_walk,
436 );
437 !code_length_walk.failed && !code_length_walk.pending
438 });
439 return Some(UnsafeMintTarget {
440 preserves_recipient: targets_preserve_recipient
441 && recipient_unchanged
442 && forwards_recipient,
443 preserves_token: targets_preserve_token && token_unchanged && forwards_token,
444 preserves_code_length,
445 });
446 }
447 None
448 }
449}
450
451#[derive(Clone, Copy)]
455struct UnsafeMintTarget {
456 preserves_recipient: bool,
457 preserves_token: bool,
458 preserves_code_length: bool,
459}
460
461const OPENZEPPELIN_PACKAGE_ROOTS: [&str; 3] =
465 ["@openzeppelin", "openzeppelin-contracts", "openzeppelin-contracts-upgradeable"];
466
467fn is_openzeppelin_source(hir: &Hir<'_>, source_id: hir::SourceId) -> bool {
469 match &hir.source(source_id).file.name {
470 FileName::Real(path) => path.components().any(|component| {
471 matches!(component, std::path::Component::Normal(name)
472 if OPENZEPPELIN_PACKAGE_ROOTS.iter().any(|root| name.eq_ignore_ascii_case(root)))
473 }),
474 _ => false,
475 }
476}
477
478struct CalleeCollector<'hir> {
481 gcx: Gcx<'hir>,
482 hir: &'hir Hir<'hir>,
483 calls: Vec<(FunctionId, &'hir hir::CallArgs<'hir>)>,
484}
485
486impl<'hir> Visit<'hir> for CalleeCollector<'hir> {
487 type BreakValue = Infallible;
488
489 fn hir(&self) -> &'hir Hir<'hir> {
490 self.hir
491 }
492
493 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
494 if let Some(function_id) = resolved_callee(self.gcx, expr)
495 && let ExprKind::Call(_, args, _) = &expr.kind
496 {
497 self.calls.push((function_id, args));
498 }
499 self.walk_expr(expr)
500 }
501}
502
503fn argument_bound_to_parameter<'hir>(
507 hir: &'hir Hir<'hir>,
508 function_id: FunctionId,
509 args: &'hir hir::CallArgs<'hir>,
510 index: usize,
511) -> Option<&'hir Expr<'hir>> {
512 match &args.kind {
513 hir::CallArgsKind::Unnamed(exprs) => exprs.get(index),
514 hir::CallArgsKind::Named(named) => {
515 let parameter = *hir.function(function_id).parameters.get(index)?;
516 let name = hir.variable(parameter).name?;
517 named
518 .iter()
519 .find(|argument| argument.name.as_str() == name.as_str())
520 .map(|argument| &argument.value)
521 }
522 }
523}
524
525fn resolved_function(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<FunctionId> {
527 let ty = gcx.type_of_expr(expr.peel_parens().id)?;
528 let TyKind::Fn(function_ty) = ty.kind else { return None };
529 function_ty.function_id
530}
531
532fn resolved_callee(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<FunctionId> {
534 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
535 resolved_function(gcx, callee)
536}
537
538fn resolved_internal_callee(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<FunctionId> {
542 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
543 let ty = gcx.type_of_expr(callee.peel_parens().id)?;
544 let TyKind::Fn(function_ty) = ty.kind else { return None };
545 function_ty.is_internal().then_some(function_ty.function_id).flatten()
546}
547
548fn is_unresolved_internal_pointer_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
552 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
553 let Some(ty) = gcx.type_of_expr(callee.peel_parens().id) else { return false };
554 let TyKind::Fn(function_ty) = ty.kind else { return false };
555 function_ty.is_internal()
556 && function_ty.function_id.is_none()
557 && matches!(&callee.peel_parens().kind, ExprKind::Ident(resolutions)
558 if resolutions.iter().any(|resolution| matches!(
559 resolution,
560 hir::Res::Item(hir::ItemId::Variable(_))
561 )))
562}
563
564fn is_receiver_hook(hir: &Hir<'_>, function_id: FunctionId) -> bool {
571 let function = hir.function(function_id);
572 let Some(name) = function.name else { return false };
573 if name.as_str() != "onERC721Received" {
574 return false;
575 }
576 let Some(contract_id) = function.contract else { return false };
577 if hir.contract(contract_id).kind.is_library() {
578 return false;
579 }
580 if !matches!(function.visibility, Visibility::Public | Visibility::External) {
581 return false;
582 }
583 let params = function.parameters;
584 if params.len() != 4 {
585 return false;
586 }
587 let is = |index: usize, expected: fn(&hir::TypeKind<'_>) -> bool| {
588 expected(&hir.variable(params[index]).ty.kind)
589 };
590 is(0, |kind| matches!(kind, hir::TypeKind::Elementary(ElementaryType::Address(_))))
591 && is(1, |kind| matches!(kind, hir::TypeKind::Elementary(ElementaryType::Address(_))))
592 && is(2, |kind| matches!(kind, hir::TypeKind::Elementary(ElementaryType::UInt(_))))
593 && is(3, |kind| matches!(kind, hir::TypeKind::Elementary(ElementaryType::Bytes)))
594}
595
596fn peel_value_preserving<'hir>(expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
601 let expr = expr.peel_parens();
602 match &expr.kind {
603 ExprKind::Payable(inner) => peel_value_preserving(inner),
604 ExprKind::Call(callee, args, _)
607 if matches!(&callee.peel_parens().kind,
608 ExprKind::Type(ty)
609 if matches!(ty.kind, hir::TypeKind::Elementary(ElementaryType::Address(_))))
610 || matches!(
611 &callee.peel_parens().kind,
612 ExprKind::Ident([hir::Res::Item(hir::ItemId::Contract(_)), ..])
613 ) =>
614 {
615 let mut operands = args.exprs();
616 if let Some(inner) = operands.next()
617 && operands.next().is_none()
618 {
619 return peel_value_preserving(inner);
620 }
621 expr
622 }
623 _ => expr,
624 }
625}
626
627fn is_exactly_var<'hir>(expr: &'hir Expr<'hir>, variable: hir::VariableId) -> bool {
632 if let ExprKind::Ident(resolutions) = &peel_value_preserving(expr).kind {
633 return resolutions.iter().any(
634 |res| matches!(res, hir::Res::Item(hir::ItemId::Variable(vid)) if *vid == variable),
635 );
636 }
637 false
638}
639
640fn keeps_its_value(hir: &Hir<'_>, variable: hir::VariableId) -> bool {
645 let variable = hir.variable(variable);
646 !variable.is_state_variable() || variable.is_constant() || variable.is_immutable()
647}
648
649fn variable_of(expr: &Expr<'_>) -> Option<hir::VariableId> {
652 if let ExprKind::Ident(resolutions) = &peel_value_preserving(expr).kind {
653 return resolutions.iter().find_map(|res| match res {
654 hir::Res::Item(hir::ItemId::Variable(vid)) => Some(*vid),
655 _ => None,
656 });
657 }
658 None
659}
660
661fn is_hook_call_on<'hir>(
665 gcx: Gcx<'hir>,
666 hir: &'hir Hir<'hir>,
667 expr: &'hir Expr<'hir>,
668 recipient: hir::VariableId,
669 token: hir::VariableId,
670) -> bool {
671 let expr = expr.peel_parens();
672 let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
673 let Some(function_id) = resolved_callee(gcx, expr) else { return false };
674 if !is_receiver_hook(hir, function_id) {
675 return false;
676 }
677 let ExprKind::Member(receiver, _) = &callee.peel_parens().kind else { return false };
678 is_exactly_var(receiver, recipient)
679 && argument_bound_to_parameter(hir, function_id, args, 2)
680 .is_some_and(|asked| is_exactly_var(asked, token))
681}
682
683const ERC721_RECEIVED: u64 = 0x150b_7a02;
685
686fn is_received_selector<'hir>(gcx: Gcx<'hir>, hir: &'hir Hir<'hir>, expr: &Expr<'hir>) -> bool {
695 let expr = expr.peel_parens();
696 match &expr.kind {
697 ExprKind::Lit(lit) => {
698 matches!(&lit.kind, LitKind::Number(value) if *value == U256::from(ERC721_RECEIVED))
699 }
700 ExprKind::Call(callee, args, _)
704 if matches!(callee.peel_parens().kind, ExprKind::Type(..)) =>
705 {
706 let mut operands = args.exprs();
707 match (operands.next(), operands.next()) {
708 (Some(inner), None) => {
709 selector_cast_preserves(gcx, expr, inner)
710 && is_received_selector(gcx, hir, inner)
711 }
712 _ => false,
713 }
714 }
715 ExprKind::Member(base, member) => {
717 member.as_str() == "selector"
718 && resolved_function(gcx, base)
719 .is_some_and(|function_id| is_receiver_hook(hir, function_id))
720 }
721 ExprKind::Ident(resolutions) => resolutions.iter().any(|res| match res {
723 hir::Res::Item(hir::ItemId::Variable(vid)) => {
724 let variable = hir.variable(*vid);
725 variable.is_constant()
726 && variable
727 .initializer
728 .is_some_and(|initializer| is_received_selector(gcx, hir, initializer))
729 }
730 _ => false,
731 }),
732 _ => false,
733 }
734}
735
736#[derive(Clone, Copy)]
738enum SelectorEncoding {
739 Literal,
740 Integer(u16),
741 FixedBytes(u8),
742}
743
744fn selector_cast_preserves(gcx: Gcx<'_>, cast: &Expr<'_>, inner: &Expr<'_>) -> bool {
749 let encoding = |expr: &Expr<'_>| match gcx.type_of_expr(expr.peel_parens().id)?.kind {
750 TyKind::IntLiteral(..) => Some(SelectorEncoding::Literal),
751 TyKind::Elementary(ElementaryType::Int(size) | ElementaryType::UInt(size)) => {
752 Some(SelectorEncoding::Integer(size.bits()))
753 }
754 TyKind::Elementary(ElementaryType::FixedBytes(size)) => {
755 Some(SelectorEncoding::FixedBytes(size.bytes()))
756 }
757 _ => None,
758 };
759 matches!(
760 (encoding(inner), encoding(cast)),
761 (
762 Some(SelectorEncoding::Literal | SelectorEncoding::Integer(_)),
763 Some(SelectorEncoding::Integer(32..) | SelectorEncoding::FixedBytes(4))
764 ) | (Some(SelectorEncoding::FixedBytes(4)), Some(SelectorEncoding::Integer(32)))
765 | (Some(SelectorEncoding::FixedBytes(4..)), Some(SelectorEncoding::FixedBytes(4..)))
766 )
767}
768
769fn is_hook_comparison<'hir>(
774 gcx: Gcx<'hir>,
775 hir: &'hir Hir<'hir>,
776 expr: &'hir Expr<'hir>,
777 recipient: hir::VariableId,
778 token: hir::VariableId,
779 want: hir::BinOpKind,
780) -> bool {
781 let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return false };
782 if op.kind != want {
783 return false;
784 }
785 let compares = |hook: &'hir Expr<'hir>, answer: &'hir Expr<'hir>| {
786 is_hook_call_on(gcx, hir, hook, recipient, token)
787 && !is_hook_call_on(gcx, hir, answer, recipient, token)
788 && is_received_selector(gcx, hir, answer)
789 };
790 compares(lhs, rhs) || compares(rhs, lhs)
791}
792
793fn branch_always_reverts<'hir>(
797 gcx: Gcx<'hir>,
798 hir: &'hir Hir<'hir>,
799 stmt: &'hir hir::Stmt<'hir>,
800) -> bool {
801 match &stmt.kind {
802 hir::StmtKind::Revert(_) => !may_return(gcx, hir, stmt),
803 hir::StmtKind::Expr(expr) => is_revert_call(expr) && !may_return(gcx, hir, stmt),
804 hir::StmtKind::Block(block) | hir::StmtKind::UncheckedBlock(block) => {
805 for stmt in block.stmts {
808 if branch_always_reverts(gcx, hir, stmt) {
809 return true;
810 }
811 if may_return(gcx, hir, stmt) {
812 return false;
813 }
814 }
815 false
816 }
817 hir::StmtKind::If(cond, then, Some(else_)) => {
818 !expr_contains_frame_ending_assembly(gcx, hir, cond)
819 && branch_always_reverts(gcx, hir, then)
820 && branch_always_reverts(gcx, hir, else_)
821 }
822 _ => false,
823 }
824}
825
826fn may_return<'hir>(gcx: Gcx<'hir>, hir: &'hir Hir<'hir>, stmt: &'hir hir::Stmt<'hir>) -> bool {
831 if contains_frame_ending_assembly(gcx, hir, std::slice::from_ref(stmt), &mut Vec::new()) {
832 return true;
833 }
834 match &stmt.kind {
835 hir::StmtKind::DeclSingle(_)
836 | hir::StmtKind::DeclMulti(..)
837 | hir::StmtKind::Emit(_)
838 | hir::StmtKind::Revert(_)
839 | hir::StmtKind::Break
840 | hir::StmtKind::Continue
841 | hir::StmtKind::Expr(_)
842 | hir::StmtKind::Placeholder
843 | hir::StmtKind::Err(_) => false,
844 hir::StmtKind::Block(block) | hir::StmtKind::UncheckedBlock(block) => {
845 block.stmts.iter().any(|stmt| may_return(gcx, hir, stmt))
846 }
847 hir::StmtKind::If(_, then, else_) => {
848 may_return(gcx, hir, then) || else_.is_some_and(|else_| may_return(gcx, hir, else_))
849 }
850 hir::StmtKind::Loop(block, _) => block.stmts.iter().any(|stmt| may_return(gcx, hir, stmt)),
851 hir::StmtKind::Return(_) | hir::StmtKind::AssemblyBlock(_) => true,
852 hir::StmtKind::Try(_) | hir::StmtKind::Switch(_) => true,
853 }
854}
855
856fn is_revert_call(expr: &Expr<'_>) -> bool {
858 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
859 if matches!(&callee.peel_parens().kind, ExprKind::Ident(idents)
860 if idents.iter().any(|res| matches!(res, hir::Res::Builtin(builtin) if builtin.name() == kw::Revert)))
861 {
862 return true;
863 }
864 is_require_or_assert(callee)
865 && args.exprs().next().is_some_and(|first| {
866 matches!(&first.peel_parens().kind, ExprKind::Lit(lit)
867 if matches!(lit.kind, LitKind::Bool(false)))
868 })
869}
870
871fn is_recipient_code_length<'hir>(expr: &'hir Expr<'hir>, recipient: hir::VariableId) -> bool {
873 let ExprKind::Member(code, length) = &expr.peel_parens().kind else { return false };
874 if length.as_str() != "length" {
875 return false;
876 }
877 let ExprKind::Member(base, member) = &code.peel_parens().kind else { return false };
878 member.as_str() == "code" && is_exactly_var(base, recipient)
879}
880
881fn is_code_length_test<'hir>(
884 expr: &'hir Expr<'hir>,
885 recipient: hir::VariableId,
886 has_code: bool,
887) -> bool {
888 let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return false };
889 let literal = |e: &Expr<'_>| match &e.peel_parens().kind {
890 ExprKind::Lit(lit) => match &lit.kind {
891 LitKind::Number(value) => u8::try_from(*value).ok(),
892 _ => None,
893 },
894 _ => None,
895 };
896 let (bound, flipped) = if is_recipient_code_length(lhs, recipient) {
897 (literal(rhs), false)
898 } else if is_recipient_code_length(rhs, recipient) {
899 (literal(lhs), true)
900 } else {
901 return false;
902 };
903 let Some(bound) = bound else { return false };
904 if has_code {
907 match (op.kind, flipped) {
908 (hir::BinOpKind::Ne, _) => bound == 0,
909 (hir::BinOpKind::Gt, false) | (hir::BinOpKind::Lt, true) => bound == 0,
910 (hir::BinOpKind::Ge, false) | (hir::BinOpKind::Le, true) => bound == 1,
911 _ => false,
912 }
913 } else {
914 match (op.kind, flipped) {
915 (hir::BinOpKind::Eq, _) => bound == 0,
916 (hir::BinOpKind::Lt, false) | (hir::BinOpKind::Gt, true) => bound == 1,
917 (hir::BinOpKind::Le, false) | (hir::BinOpKind::Ge, true) => bound == 0,
918 _ => false,
919 }
920 }
921}
922
923fn acceptance_coverage<'hir>(
927 gcx: Gcx<'hir>,
928 hir: &'hir Hir<'hir>,
929 cond: &'hir Expr<'hir>,
930 recipient: hir::VariableId,
931 token: hir::VariableId,
932) -> GuardCoverage {
933 let cond = cond.peel_parens();
934 if is_code_length_test(cond, recipient, false) {
935 return GuardCoverage::CodeLess;
936 }
937 if is_hook_comparison(gcx, hir, cond, recipient, token, hir::BinOpKind::Eq) {
938 return GuardCoverage::Callback;
939 }
940 let ExprKind::Binary(lhs, op, rhs) = &cond.kind else { return GuardCoverage::None };
941 if op.kind != hir::BinOpKind::Or {
942 return GuardCoverage::None;
943 }
944 let accepts = |skip: &'hir Expr<'hir>, check: &'hir Expr<'hir>| {
945 is_code_length_test(skip, recipient, false)
946 && is_hook_comparison(gcx, hir, check, recipient, token, hir::BinOpKind::Eq)
947 };
948 if accepts(lhs, rhs) || accepts(rhs, lhs) {
949 GuardCoverage::CallbackOrCodeLess
950 } else {
951 GuardCoverage::None
952 }
953}
954
955#[expect(clippy::too_many_arguments)]
961fn callee_guards_recipient<'hir>(
962 gcx: Gcx<'hir>,
963 hir: &'hir Hir<'hir>,
964 function_id: FunctionId,
965 args: &'hir hir::CallArgs<'hir>,
966 recipient: hir::VariableId,
967 token: hir::VariableId,
968 bypass: bool,
969 seen: &mut Vec<FunctionId>,
970) -> GuardCoverage {
971 let Some((recipient, token)) = bound_guard_parameters(hir, function_id, args, recipient, token)
972 else {
973 return GuardCoverage::None;
974 };
975 body_guards(gcx, hir, function_id, recipient, token, bypass, seen)
976}
977
978fn bound_guard_parameters<'hir>(
980 hir: &'hir Hir<'hir>,
981 function_id: FunctionId,
982 args: &'hir hir::CallArgs<'hir>,
983 recipient: hir::VariableId,
984 token: hir::VariableId,
985) -> Option<(hir::VariableId, hir::VariableId)> {
986 let parameters = hir.function(function_id).parameters;
987 let mut recipient_parameter = None;
988 let mut token_parameter = None;
989 for (index, ¶meter) in parameters.iter().enumerate() {
992 let Some(argument) = argument_bound_to_parameter(hir, function_id, args, index) else {
993 continue;
994 };
995 if recipient_parameter.is_none() && is_exactly_var(argument, recipient) {
996 recipient_parameter = Some(parameter);
997 }
998 if token_parameter.is_none() && is_exactly_var(argument, token) {
999 token_parameter = Some(parameter);
1000 }
1001 }
1002 recipient_parameter.zip(token_parameter)
1003}
1004
1005#[derive(Clone, Copy, Default, PartialEq, Eq)]
1010enum GuardCoverage {
1011 #[default]
1012 None,
1013 Callback,
1014 CodeLess,
1015 CallbackOrCodeLess,
1016}
1017
1018impl GuardCoverage {
1019 fn is_covered(self) -> bool {
1020 self != Self::None
1021 }
1022
1023 const fn relies_on_code_length(self) -> bool {
1024 matches!(self, Self::CodeLess | Self::CallbackOrCodeLess)
1025 }
1026
1027 const fn merge_paths(self, other: Self) -> Self {
1030 match (self, other) {
1031 (Self::None, _) | (_, Self::None) => Self::None,
1032 (Self::Callback, Self::Callback) => Self::Callback,
1033 (Self::CodeLess, Self::CodeLess) => Self::CodeLess,
1034 _ => Self::CallbackOrCodeLess,
1035 }
1036 }
1037
1038 const fn combine_guards(self, other: Self) -> Self {
1041 match (self, other) {
1042 (Self::Callback, _) | (_, Self::Callback) => Self::Callback,
1043 (Self::CallbackOrCodeLess, _) | (_, Self::CallbackOrCodeLess) => {
1044 Self::CallbackOrCodeLess
1045 }
1046 (Self::CodeLess, _) | (_, Self::CodeLess) => Self::CodeLess,
1047 _ => Self::None,
1048 }
1049 }
1050}
1051
1052#[derive(Clone, Default)]
1057struct GuardWalk {
1058 coverage: GuardCoverage,
1060 future_coverage: GuardCoverage,
1064 pending: bool,
1065 failed: bool,
1066 escaped: bool,
1067}
1068
1069#[expect(clippy::too_many_arguments)]
1093fn walk_guards<'hir>(
1094 gcx: Gcx<'hir>,
1095 hir: &'hir Hir<'hir>,
1096 stmts: &'hir [hir::Stmt<'hir>],
1097 recipient: hir::VariableId,
1098 token: hir::VariableId,
1099 delegations: &[FunctionId],
1100 unstable_code_delegations: &[FunctionId],
1101 bypass: bool,
1102 seen: &mut Vec<FunctionId>,
1103 walk: &mut GuardWalk,
1104) {
1105 for stmt in stmts {
1108 match &stmt.kind {
1109 hir::StmtKind::Block(block) | hir::StmtKind::UncheckedBlock(block) => {
1110 walk_guards(
1111 gcx,
1112 hir,
1113 block.stmts,
1114 recipient,
1115 token,
1116 delegations,
1117 unstable_code_delegations,
1118 bypass,
1119 seen,
1120 walk,
1121 );
1122 }
1123 hir::StmtKind::Expr(expr)
1124 if guard_expr_coverage(gcx, hir, expr, recipient, token, seen).is_covered() =>
1125 {
1126 let guard_coverage = guard_expr_coverage(gcx, hir, expr, recipient, token, seen);
1127 let mutates = mutates_var(hir, stmt, recipient) || mutates_var(hir, stmt, token);
1134 let escapes = may_return(gcx, hir, stmt);
1135 let changes_code = guard_coverage.relies_on_code_length()
1136 && guard_extra_args_may_change_account_code(
1137 gcx,
1138 hir,
1139 expr,
1140 delegations,
1141 unstable_code_delegations,
1142 );
1143 if mutates {
1144 if walk.pending {
1145 walk.failed = true;
1146 }
1147 walk.coverage = GuardCoverage::None;
1148 walk.future_coverage = GuardCoverage::None;
1149 } else if escapes {
1150 if walk.pending {
1151 walk.failed = true;
1152 }
1153 if !walk.coverage.is_covered() {
1154 walk.escaped = true;
1155 }
1156 } else if changes_code {
1157 if walk.future_coverage.relies_on_code_length() {
1158 walk.future_coverage = GuardCoverage::None;
1159 }
1160 } else {
1161 walk.coverage = walk.coverage.combine_guards(guard_coverage);
1162 if guard_coverage == GuardCoverage::CodeLess {
1163 walk.future_coverage = walk.future_coverage.combine_guards(guard_coverage);
1164 }
1165 walk.pending = false;
1166 }
1167 }
1168 hir::StmtKind::If(cond, then, else_) => {
1169 let condition_mutates =
1174 expr_mutates_var(hir, cond, recipient) || expr_mutates_var(hir, cond, token);
1175 let condition_escapes = expr_contains_frame_ending_assembly(gcx, hir, cond);
1176 if condition_mutates {
1177 if walk.pending {
1178 walk.failed = true;
1179 }
1180 walk.coverage = GuardCoverage::None;
1181 walk.future_coverage = GuardCoverage::None;
1182 }
1183 let future_relies_on_code = walk.future_coverage.relies_on_code_length();
1184 if future_relies_on_code
1185 && expr_may_change_account_code(
1186 gcx,
1187 hir,
1188 cond,
1189 delegations,
1190 unstable_code_delegations,
1191 )
1192 {
1193 walk.future_coverage = GuardCoverage::None;
1194 }
1195 if condition_escapes {
1196 if walk.pending {
1197 walk.failed = true;
1198 }
1199 if !walk.coverage.is_covered() {
1200 walk.escaped = true;
1201 }
1202 }
1203 let refusal_then = !condition_mutates
1208 && is_hook_comparison(gcx, hir, cond, recipient, token, hir::BinOpKind::Ne)
1209 && branch_always_reverts(gcx, hir, then);
1210 let refusal_else = !condition_mutates
1211 && is_hook_comparison(gcx, hir, cond, recipient, token, hir::BinOpKind::Eq)
1212 && else_.is_some_and(|else_| branch_always_reverts(gcx, hir, else_));
1213 if refusal_then || refusal_else {
1214 walk.coverage = walk.coverage.combine_guards(GuardCoverage::Callback);
1215 walk.pending = false;
1216 if refusal_then {
1217 if let Some(accepted) = else_ {
1218 walk_one(
1219 gcx,
1220 hir,
1221 accepted,
1222 recipient,
1223 token,
1224 delegations,
1225 unstable_code_delegations,
1226 bypass,
1227 seen,
1228 walk,
1229 );
1230 }
1231 } else {
1232 walk_one(
1233 gcx,
1234 hir,
1235 then,
1236 recipient,
1237 token,
1238 delegations,
1239 unstable_code_delegations,
1240 bypass,
1241 seen,
1242 walk,
1243 );
1244 }
1245 continue;
1246 }
1247 let mut then_walk = walk.clone();
1252 let mut else_walk = walk.clone();
1253 if is_code_length_test(cond, recipient, true) {
1254 else_walk.coverage = else_walk.coverage.combine_guards(GuardCoverage::CodeLess);
1255 else_walk.future_coverage =
1256 else_walk.future_coverage.combine_guards(GuardCoverage::CodeLess);
1257 else_walk.pending = false;
1258 } else if is_code_length_test(cond, recipient, false) {
1259 then_walk.coverage = then_walk.coverage.combine_guards(GuardCoverage::CodeLess);
1260 then_walk.future_coverage =
1261 then_walk.future_coverage.combine_guards(GuardCoverage::CodeLess);
1262 then_walk.pending = false;
1263 }
1264 walk_one(
1265 gcx,
1266 hir,
1267 then,
1268 recipient,
1269 token,
1270 delegations,
1271 unstable_code_delegations,
1272 bypass,
1273 seen,
1274 &mut then_walk,
1275 );
1276 if let Some(else_) = else_ {
1277 walk_one(
1278 gcx,
1279 hir,
1280 else_,
1281 recipient,
1282 token,
1283 delegations,
1284 unstable_code_delegations,
1285 bypass,
1286 seen,
1287 &mut else_walk,
1288 );
1289 }
1290 walk.coverage = then_walk.coverage.merge_paths(else_walk.coverage);
1293 walk.future_coverage =
1294 then_walk.future_coverage.merge_paths(else_walk.future_coverage);
1295 walk.pending = then_walk.pending || else_walk.pending;
1296 walk.failed = then_walk.failed || else_walk.failed;
1297 walk.escaped = then_walk.escaped || else_walk.escaped;
1298 }
1299 _ => {
1300 if mutates_var(hir, stmt, recipient) || mutates_var(hir, stmt, token) {
1307 if walk.pending {
1308 walk.failed = true;
1309 }
1310 walk.coverage = GuardCoverage::None;
1311 walk.future_coverage = GuardCoverage::None;
1312 }
1313 let future_relies_on_code = walk.future_coverage.relies_on_code_length();
1318 if future_relies_on_code
1319 && stmt_may_change_account_code(
1320 gcx,
1321 hir,
1322 stmt,
1323 delegations,
1324 unstable_code_delegations,
1325 )
1326 {
1327 walk.future_coverage = GuardCoverage::None;
1328 }
1329 if !walk.future_coverage.is_covered()
1335 && contains_delegation(gcx, hir, stmt, delegations)
1336 {
1337 walk.pending = true;
1338 }
1339 if may_return(gcx, hir, stmt) || (bypass && contains_placeholder(hir, stmt)) {
1340 if walk.pending {
1341 walk.failed = true;
1342 }
1343 if !walk.coverage.is_covered() {
1344 walk.escaped = true;
1345 }
1346 }
1347 }
1348 }
1349 }
1350}
1351
1352#[expect(clippy::too_many_arguments)]
1354fn walk_one<'hir>(
1355 gcx: Gcx<'hir>,
1356 hir: &'hir Hir<'hir>,
1357 stmt: &'hir hir::Stmt<'hir>,
1358 recipient: hir::VariableId,
1359 token: hir::VariableId,
1360 delegations: &[FunctionId],
1361 unstable_code_delegations: &[FunctionId],
1362 bypass: bool,
1363 seen: &mut Vec<FunctionId>,
1364 walk: &mut GuardWalk,
1365) {
1366 walk_guards(
1367 gcx,
1368 hir,
1369 std::slice::from_ref(stmt),
1370 recipient,
1371 token,
1372 delegations,
1373 unstable_code_delegations,
1374 bypass,
1375 seen,
1376 walk,
1377 );
1378}
1379
1380fn contains_placeholder<'hir>(hir: &'hir Hir<'hir>, stmt: &'hir hir::Stmt<'hir>) -> bool {
1382 struct PlaceholderFinder<'hir> {
1383 hir: &'hir Hir<'hir>,
1384 }
1385 impl<'hir> Visit<'hir> for PlaceholderFinder<'hir> {
1386 type BreakValue = ();
1387
1388 fn hir(&self) -> &'hir Hir<'hir> {
1389 self.hir
1390 }
1391
1392 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
1393 if matches!(stmt.kind, hir::StmtKind::Placeholder) {
1394 return ControlFlow::Break(());
1395 }
1396 self.walk_stmt(stmt)
1397 }
1398 }
1399 let mut finder = PlaceholderFinder { hir };
1400 finder.visit_stmt(stmt).is_break()
1401}
1402
1403fn contains_frame_ending_assembly<'hir>(
1408 gcx: Gcx<'hir>,
1409 hir: &'hir Hir<'hir>,
1410 stmts: &'hir [hir::Stmt<'hir>],
1411 seen: &mut Vec<FunctionId>,
1412) -> bool {
1413 struct AssemblyFinder<'a, 'hir> {
1414 gcx: Gcx<'hir>,
1415 hir: &'hir Hir<'hir>,
1416 seen: &'a mut Vec<FunctionId>,
1417 }
1418 impl<'hir> Visit<'hir> for AssemblyFinder<'_, 'hir> {
1419 type BreakValue = ();
1420
1421 fn hir(&self) -> &'hir Hir<'hir> {
1422 self.hir
1423 }
1424
1425 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
1426 if matches!(stmt.kind, hir::StmtKind::AssemblyBlock(_)) {
1427 return ControlFlow::Break(());
1428 }
1429 self.walk_stmt(stmt)
1430 }
1431
1432 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1433 if is_unresolved_internal_pointer_call(self.gcx, expr)
1434 || resolved_internal_callee(self.gcx, expr).is_some_and(|function_id| {
1435 callable_contains_frame_ending_assembly(
1436 self.gcx,
1437 self.hir,
1438 function_id,
1439 self.seen,
1440 )
1441 })
1442 {
1443 return ControlFlow::Break(());
1444 }
1445 self.walk_expr(expr)
1446 }
1447 }
1448 let mut finder = AssemblyFinder { gcx, hir, seen };
1449 stmts.iter().any(|stmt| finder.visit_stmt(stmt).is_break())
1450}
1451
1452fn expr_contains_frame_ending_assembly<'hir>(
1455 gcx: Gcx<'hir>,
1456 hir: &'hir Hir<'hir>,
1457 expr: &'hir Expr<'hir>,
1458) -> bool {
1459 struct ExprAssemblyFinder<'a, 'hir> {
1460 gcx: Gcx<'hir>,
1461 hir: &'hir Hir<'hir>,
1462 seen: &'a mut Vec<FunctionId>,
1463 }
1464 impl<'hir> Visit<'hir> for ExprAssemblyFinder<'_, 'hir> {
1465 type BreakValue = ();
1466
1467 fn hir(&self) -> &'hir Hir<'hir> {
1468 self.hir
1469 }
1470
1471 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1472 if is_unresolved_internal_pointer_call(self.gcx, expr)
1473 || resolved_internal_callee(self.gcx, expr).is_some_and(|function_id| {
1474 callable_contains_frame_ending_assembly(
1475 self.gcx,
1476 self.hir,
1477 function_id,
1478 self.seen,
1479 )
1480 })
1481 {
1482 return ControlFlow::Break(());
1483 }
1484 self.walk_expr(expr)
1485 }
1486 }
1487 let mut seen = Vec::new();
1488 let mut finder = ExprAssemblyFinder { gcx, hir, seen: &mut seen };
1489 finder.visit_expr(expr).is_break()
1490}
1491
1492fn callable_contains_frame_ending_assembly<'hir>(
1495 gcx: Gcx<'hir>,
1496 hir: &'hir Hir<'hir>,
1497 function_id: FunctionId,
1498 seen: &mut Vec<FunctionId>,
1499) -> bool {
1500 if seen.contains(&function_id) {
1501 return false;
1502 }
1503 seen.push(function_id);
1504 let function = hir.function(function_id);
1505 let in_modifiers = function.modifiers.iter().any(|modifier| {
1506 matches!(modifier.id, hir::ItemId::Function(id)
1507 if callable_contains_frame_ending_assembly(gcx, hir, id, seen))
1508 });
1509 let in_body = function
1510 .body
1511 .as_ref()
1512 .is_some_and(|body| contains_frame_ending_assembly(gcx, hir, body.stmts, seen));
1513 seen.pop();
1514 in_modifiers || in_body
1515}
1516
1517fn guard_expr_coverage<'hir>(
1523 gcx: Gcx<'hir>,
1524 hir: &'hir Hir<'hir>,
1525 expr: &'hir Expr<'hir>,
1526 recipient: hir::VariableId,
1527 token: hir::VariableId,
1528 seen: &mut Vec<FunctionId>,
1529) -> GuardCoverage {
1530 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else {
1531 return GuardCoverage::None;
1532 };
1533 if is_require_or_assert(callee) {
1534 return args.exprs().next().map_or(GuardCoverage::None, |cond| {
1535 acceptance_coverage(gcx, hir, cond, recipient, token)
1536 });
1537 }
1538 resolved_internal_callee(gcx, expr.peel_parens()).map_or(GuardCoverage::None, |function_id| {
1539 callee_guards_recipient(gcx, hir, function_id, args, recipient, token, false, seen)
1540 })
1541}
1542
1543fn guard_extra_args_may_change_account_code<'hir>(
1548 gcx: Gcx<'hir>,
1549 hir: &'hir Hir<'hir>,
1550 expr: &'hir Expr<'hir>,
1551 delegations: &[FunctionId],
1552 unstable_code_delegations: &[FunctionId],
1553) -> bool {
1554 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
1555 if !is_require_or_assert(callee) {
1556 return false;
1557 }
1558 args.exprs().skip(1).any(|arg| {
1559 expr_may_change_account_code(gcx, hir, arg, delegations, unstable_code_delegations)
1560 })
1561}
1562
1563fn mutates_var<'hir>(
1570 hir: &'hir Hir<'hir>,
1571 stmt: &'hir hir::Stmt<'hir>,
1572 var: hir::VariableId,
1573) -> bool {
1574 let mut finder = MutationFinder { hir, var };
1575 finder.visit_stmt(stmt).is_break()
1576}
1577
1578fn expr_mutates_var<'hir>(
1582 hir: &'hir Hir<'hir>,
1583 expr: &'hir Expr<'hir>,
1584 var: hir::VariableId,
1585) -> bool {
1586 let mut finder = MutationFinder { hir, var };
1587 finder.visit_expr(expr).is_break()
1588}
1589
1590struct MutationFinder<'hir> {
1592 hir: &'hir Hir<'hir>,
1593 var: hir::VariableId,
1594}
1595
1596impl<'hir> Visit<'hir> for MutationFinder<'hir> {
1597 type BreakValue = ();
1598
1599 fn hir(&self) -> &'hir Hir<'hir> {
1600 self.hir
1601 }
1602
1603 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
1604 if matches!(stmt.kind, hir::StmtKind::AssemblyBlock(_)) {
1608 return ControlFlow::Break(());
1609 }
1610 self.walk_stmt(stmt)
1611 }
1612
1613 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1614 let target = match &expr.kind {
1615 ExprKind::Assign(lhs, _, _) => Some(*lhs),
1616 ExprKind::Delete(inner) => Some(*inner),
1617 ExprKind::Unary(op, inner)
1618 if matches!(
1619 op.kind,
1620 hir::UnOpKind::PreInc
1621 | hir::UnOpKind::PreDec
1622 | hir::UnOpKind::PostInc
1623 | hir::UnOpKind::PostDec
1624 ) =>
1625 {
1626 Some(*inner)
1627 }
1628 _ => None,
1629 };
1630 if let Some(target) = target
1631 && assigns_to(target, self.var)
1632 {
1633 return ControlFlow::Break(());
1634 }
1635 self.walk_expr(expr)
1636 }
1637}
1638
1639fn assigns_to(target: &Expr<'_>, var: hir::VariableId) -> bool {
1641 match &target.peel_parens().kind {
1642 ExprKind::Ident(resolutions) => resolutions
1643 .iter()
1644 .any(|res| matches!(res, hir::Res::Item(hir::ItemId::Variable(vid)) if *vid == var)),
1645 ExprKind::Tuple(elements) => {
1646 elements.iter().any(|element| element.is_some_and(|inner| assigns_to(inner, var)))
1647 }
1648 _ => false,
1649 }
1650}
1651
1652fn expr_may_change_account_code<'hir>(
1658 gcx: Gcx<'hir>,
1659 hir: &'hir Hir<'hir>,
1660 expr: &'hir Expr<'hir>,
1661 delegations: &[FunctionId],
1662 unstable_code_delegations: &[FunctionId],
1663) -> bool {
1664 let mut seen = Vec::new();
1665 let mut finder = AccountCodeChangeFinder {
1666 gcx,
1667 hir,
1668 delegations,
1669 unstable_code_delegations,
1670 seen: &mut seen,
1671 };
1672 finder.visit_expr(expr).is_break()
1673}
1674
1675fn stmt_may_change_account_code<'hir>(
1678 gcx: Gcx<'hir>,
1679 hir: &'hir Hir<'hir>,
1680 stmt: &'hir hir::Stmt<'hir>,
1681 delegations: &[FunctionId],
1682 unstable_code_delegations: &[FunctionId],
1683) -> bool {
1684 let mut seen = Vec::new();
1685 let mut finder = AccountCodeChangeFinder {
1686 gcx,
1687 hir,
1688 delegations,
1689 unstable_code_delegations,
1690 seen: &mut seen,
1691 };
1692 finder.visit_stmt(stmt).is_break()
1693}
1694
1695struct AccountCodeChangeFinder<'a, 'hir> {
1696 gcx: Gcx<'hir>,
1697 hir: &'hir Hir<'hir>,
1698 delegations: &'a [FunctionId],
1699 unstable_code_delegations: &'a [FunctionId],
1700 seen: &'a mut Vec<FunctionId>,
1701}
1702
1703impl<'hir> Visit<'hir> for AccountCodeChangeFinder<'_, 'hir> {
1704 type BreakValue = ();
1705
1706 fn hir(&self) -> &'hir Hir<'hir> {
1707 self.hir
1708 }
1709
1710 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
1711 if matches!(stmt.kind, hir::StmtKind::AssemblyBlock(_)) {
1712 return ControlFlow::Break(());
1713 }
1714 self.walk_stmt(stmt)
1715 }
1716
1717 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1718 if let ExprKind::Call(callee, ..) = &expr.kind {
1719 let is_delegation = resolved_callee(self.gcx, expr)
1720 .is_some_and(|function_id| self.delegations.contains(&function_id));
1721 let is_unstable_delegation = resolved_callee(self.gcx, expr)
1722 .is_some_and(|function_id| self.unstable_code_delegations.contains(&function_id));
1723 if !is_delegation || is_unstable_delegation {
1724 if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
1725 return ControlFlow::Break(());
1726 }
1727 if let Some(ty) = self.gcx.type_of_expr(callee.peel_parens().id)
1728 && let TyKind::Fn(function_ty) = ty.kind
1729 && matches!(
1730 function_ty.state_mutability,
1731 StateMutability::NonPayable | StateMutability::Payable
1732 )
1733 {
1734 if let Some(function_id) = resolved_internal_callee(self.gcx, expr)
1735 && !self.hir.function(function_id).virtual_
1736 {
1737 if callable_may_change_account_code(
1738 self.gcx,
1739 self.hir,
1740 function_id,
1741 self.seen,
1742 ) {
1743 return ControlFlow::Break(());
1744 }
1745 } else {
1746 return ControlFlow::Break(());
1747 }
1748 }
1749 }
1750 }
1751 self.walk_expr(expr)
1752 }
1753}
1754
1755fn callable_may_change_account_code<'hir>(
1759 gcx: Gcx<'hir>,
1760 hir: &'hir Hir<'hir>,
1761 function_id: FunctionId,
1762 seen: &mut Vec<FunctionId>,
1763) -> bool {
1764 if seen.contains(&function_id) {
1765 return false;
1766 }
1767 seen.push(function_id);
1768 let function = hir.function(function_id);
1769 let mut finder = AccountCodeChangeFinder {
1770 gcx,
1771 hir,
1772 delegations: &[],
1773 unstable_code_delegations: &[],
1774 seen,
1775 };
1776 let in_modifier_args = function
1777 .modifiers
1778 .iter()
1779 .any(|modifier| modifier.args.exprs().any(|arg| finder.visit_expr(arg).is_break()));
1780 let in_modifiers = !in_modifier_args
1781 && function.modifiers.iter().any(|modifier| {
1782 matches!(modifier.id, hir::ItemId::Function(id)
1783 if callable_may_change_account_code(gcx, hir, id, finder.seen))
1784 });
1785 let in_body = !in_modifier_args
1786 && !in_modifiers
1787 && function
1788 .body
1789 .as_ref()
1790 .is_some_and(|body| body.stmts.iter().any(|stmt| finder.visit_stmt(stmt).is_break()));
1791 finder.seen.pop();
1792 in_modifier_args || in_modifiers || in_body
1793}
1794
1795fn contains_delegation<'hir>(
1797 gcx: Gcx<'hir>,
1798 hir: &'hir Hir<'hir>,
1799 stmt: &'hir hir::Stmt<'hir>,
1800 delegations: &[FunctionId],
1801) -> bool {
1802 struct DelegationFinder<'a, 'hir> {
1803 gcx: Gcx<'hir>,
1804 hir: &'hir Hir<'hir>,
1805 delegations: &'a [FunctionId],
1806 }
1807 impl<'hir> Visit<'hir> for DelegationFinder<'_, 'hir> {
1808 type BreakValue = ();
1809
1810 fn hir(&self) -> &'hir Hir<'hir> {
1811 self.hir
1812 }
1813
1814 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1815 if let Some(function_id) = resolved_callee(self.gcx, expr)
1816 && self.delegations.contains(&function_id)
1817 {
1818 return ControlFlow::Break(());
1819 }
1820 self.walk_expr(expr)
1821 }
1822 }
1823 let mut finder = DelegationFinder { gcx, hir, delegations };
1824 finder.visit_stmt(stmt).is_break()
1825}
1826
1827fn body_guards<'hir>(
1832 gcx: Gcx<'hir>,
1833 hir: &'hir Hir<'hir>,
1834 function_id: FunctionId,
1835 recipient: hir::VariableId,
1836 token: hir::VariableId,
1837 bypass: bool,
1838 seen: &mut Vec<FunctionId>,
1839) -> GuardCoverage {
1840 if seen.contains(&function_id) {
1841 return GuardCoverage::None;
1842 }
1843 seen.push(function_id);
1844 let function = hir.function(function_id);
1845 let guarded = if function.virtual_ || !function.modifiers.is_empty() {
1850 GuardCoverage::None
1851 } else if let Some(body) = &function.body {
1852 let parameters_unchanged = !body
1856 .stmts
1857 .iter()
1858 .any(|stmt| mutates_var(hir, stmt, recipient) || mutates_var(hir, stmt, token));
1859 if parameters_unchanged {
1860 let mut walk = GuardWalk::default();
1861 walk_guards(gcx, hir, body.stmts, recipient, token, &[], &[], bypass, seen, &mut walk);
1862 if walk.escaped {
1863 GuardCoverage::None
1864 } else if walk.future_coverage == GuardCoverage::CodeLess {
1865 GuardCoverage::CodeLess
1866 } else if walk.coverage == GuardCoverage::CodeLess {
1867 GuardCoverage::CallbackOrCodeLess
1871 } else {
1872 walk.coverage
1873 }
1874 } else {
1875 GuardCoverage::None
1876 }
1877 } else {
1878 GuardCoverage::None
1879 };
1880 seen.pop();
1881 guarded
1882}
1883
1884#[derive(Clone, Copy)]
1887struct ModifierCoverage {
1888 coverage: GuardCoverage,
1889 future_coverage: GuardCoverage,
1890}
1891
1892fn modifier_coverage_at_body<'hir>(
1898 gcx: Gcx<'hir>,
1899 hir: &'hir Hir<'hir>,
1900 function: &'hir hir::Function<'hir>,
1901 recipient: hir::VariableId,
1902 token: hir::VariableId,
1903 mut coverage: GuardCoverage,
1904) -> ModifierCoverage {
1905 let mut future_coverage = coverage;
1906 let body_bypass = function
1907 .body
1908 .as_ref()
1909 .is_some_and(|body| contains_frame_ending_assembly(gcx, hir, body.stmts, &mut Vec::new()));
1910 let mut has_tail_guard = false;
1911 for (index, modifier) in function.modifiers.iter().enumerate() {
1912 if modifier
1913 .args
1914 .exprs()
1915 .any(|arg| expr_mutates_var(hir, arg, recipient) || expr_mutates_var(hir, arg, token))
1916 {
1917 coverage = GuardCoverage::None;
1918 future_coverage = GuardCoverage::None;
1919 has_tail_guard = false;
1920 }
1921 let argument_may_change_code =
1922 modifier.args.exprs().any(|arg| expr_may_change_account_code(gcx, hir, arg, &[], &[]));
1923 if coverage.relies_on_code_length() && argument_may_change_code {
1924 coverage = GuardCoverage::None;
1925 }
1926 if future_coverage.relies_on_code_length() && argument_may_change_code {
1927 future_coverage = GuardCoverage::None;
1928 }
1929 let hir::ItemId::Function(modifier_id) = modifier.id else { continue };
1930 let modifier_function = hir.function(modifier_id);
1931 let Some(body) = &modifier_function.body else { continue };
1932 let Some((prefix, suffix)) = modifier_body_sides(body.stmts) else {
1933 let coverage_relies_on_code = coverage.relies_on_code_length();
1937 let future_relies_on_code = future_coverage.relies_on_code_length();
1938 if (coverage_relies_on_code || future_relies_on_code)
1939 && body
1940 .stmts
1941 .iter()
1942 .any(|stmt| stmt_may_change_account_code(gcx, hir, stmt, &[], &[]))
1943 {
1944 if coverage_relies_on_code {
1945 coverage = GuardCoverage::None;
1946 }
1947 if future_relies_on_code {
1948 future_coverage = GuardCoverage::None;
1949 }
1950 }
1951 continue;
1952 };
1953 let Some((modifier_recipient, modifier_token)) =
1954 bound_guard_parameters(hir, modifier_id, &modifier.args, recipient, token)
1955 else {
1956 let coverage_relies_on_code = coverage.relies_on_code_length();
1957 let future_relies_on_code = future_coverage.relies_on_code_length();
1958 if (coverage_relies_on_code || future_relies_on_code)
1959 && prefix.iter().any(|stmt| stmt_may_change_account_code(gcx, hir, stmt, &[], &[]))
1960 {
1961 if coverage_relies_on_code {
1962 coverage = GuardCoverage::None;
1963 }
1964 if future_relies_on_code {
1965 future_coverage = GuardCoverage::None;
1966 }
1967 }
1968 continue;
1969 };
1970 let parameters_unchanged = !body.stmts.iter().any(|stmt| {
1971 mutates_var(hir, stmt, modifier_recipient) || mutates_var(hir, stmt, modifier_token)
1972 });
1973 if parameters_unchanged {
1974 let mut prefix_walk = GuardWalk { coverage, future_coverage, ..GuardWalk::default() };
1975 walk_guards(
1976 gcx,
1977 hir,
1978 prefix,
1979 modifier_recipient,
1980 modifier_token,
1981 &[],
1982 &[],
1983 false,
1984 &mut Vec::new(),
1985 &mut prefix_walk,
1986 );
1987 coverage = prefix_walk.coverage;
1988 future_coverage = prefix_walk.future_coverage;
1989 } else {
1990 let prefix_may_change_code =
1991 prefix.iter().any(|stmt| stmt_may_change_account_code(gcx, hir, stmt, &[], &[]));
1992 if coverage.relies_on_code_length() && prefix_may_change_code {
1993 coverage = GuardCoverage::None;
1994 }
1995 if future_coverage.relies_on_code_length() && prefix_may_change_code {
1996 future_coverage = GuardCoverage::None;
1997 }
1998 }
1999
2000 let inner_modifier_bypass = function.modifiers[index + 1..].iter().any(|inner| {
2001 matches!(inner.id, hir::ItemId::Function(id)
2002 if callable_contains_frame_ending_assembly(gcx, hir, id, &mut Vec::new()))
2003 });
2004 if parameters_unchanged && !body_bypass && !inner_modifier_bypass {
2005 let mut suffix_walk = GuardWalk { pending: true, ..GuardWalk::default() };
2009 walk_guards(
2010 gcx,
2011 hir,
2012 suffix,
2013 modifier_recipient,
2014 modifier_token,
2015 &[],
2016 &[],
2017 false,
2018 &mut Vec::new(),
2019 &mut suffix_walk,
2020 );
2021 has_tail_guard |= !suffix_walk.failed && !suffix_walk.pending;
2022 }
2023 }
2024 if has_tail_guard {
2025 coverage = coverage.combine_guards(GuardCoverage::Callback);
2026 future_coverage = future_coverage.combine_guards(GuardCoverage::Callback);
2027 }
2028 ModifierCoverage { coverage, future_coverage }
2029}
2030
2031fn modifier_body_sides<'hir>(
2034 stmts: &'hir [hir::Stmt<'hir>],
2035) -> Option<(&'hir [hir::Stmt<'hir>], &'hir [hir::Stmt<'hir>])> {
2036 let mut placeholders = stmts
2037 .iter()
2038 .enumerate()
2039 .filter(|(_, stmt)| matches!(stmt.kind, hir::StmtKind::Placeholder));
2040 let (index, _) = placeholders.next()?;
2041 if placeholders.next().is_some() {
2042 return None;
2043 }
2044 Some((&stmts[..index], &stmts[index + 1..]))
2045}
2046
2047fn is_canonical_erc721(name: &str) -> bool {
2054 matches!(
2055 name,
2056 "ERC721" | "ERC721Upgradeable" | "ERC721Consecutive" | "ERC721ConsecutiveUpgradeable"
2057 )
2058}