diff --git a/crates/lint/src/sol/info/missing_zero_check.rs b/crates/lint/src/sol/info/missing_zero_check.rs new file mode 100644 index 0000000000000..def234e9d1582 --- /dev/null +++ b/crates/lint/src/sol/info/missing_zero_check.rs @@ -0,0 +1,443 @@ +use super::MissingZeroCheck; +use crate::{ + linter::{LateLintPass, LintContext}, + sol::{Severity, SolLint}, +}; +use solar::{ + ast, + interface::{data_structures::Never, kw, sym}, + sema::hir::{self, ElementaryType, ExprKind, ItemId, Res, StmtKind, TypeKind, Visit}, +}; +use std::{ + collections::{HashMap, HashSet}, + ops::ControlFlow, +}; + +declare_forge_lint!( + MISSING_ZERO_CHECK, + Severity::Low, + "missing-zero-check", + "address parameter is used in a state write or value transfer without a zero-address check" +); + +impl<'hir> LateLintPass<'hir> for MissingZeroCheck { + fn check_function( + &mut self, + ctx: &LintContext, + hir: &'hir hir::Hir<'hir>, + func: &'hir hir::Function<'hir>, + ) { + if !is_entry_point(func) { + return; + } + + let params: HashSet = + func.parameters.iter().copied().filter(|id| is_address(hir, *id)).collect(); + + if params.is_empty() { + return; + } + + let Some(body) = func.body else { return }; + + let mut a = Analyzer::new(hir, ¶ms); + + for m in func.modifiers { + collect_modifier_guards(hir, m, ¶ms, &mut a.guarded); + } + + for stmt in body.stmts { + let _ = a.visit_stmt(stmt); + } + + for &p in ¶ms { + if a.sinks.contains(&p) { + ctx.emit(&MISSING_ZERO_CHECK, hir.variable(p).span); + } + } + } +} + +/// Externally callable, state-mutating functions and constructors. +fn is_entry_point(func: &hir::Function<'_>) -> bool { + if matches!(func.state_mutability, ast::StateMutability::Pure | ast::StateMutability::View) { + return false; + } + if func.is_constructor() { + return true; + } + func.kind.is_function() + && matches!(func.visibility, ast::Visibility::Public | ast::Visibility::External) +} + +fn is_address(hir: &hir::Hir<'_>, id: hir::VariableId) -> bool { + matches!(hir.variable(id).ty.kind, TypeKind::Elementary(ElementaryType::Address(_))) +} + +/// Tracks address-parameter taint, sinks reached, and guards observed in a function body. +struct Analyzer<'hir> { + hir: &'hir hir::Hir<'hir>, + /// Variables transitively derived from candidate parameters, mapped to their sources. + /// Each parameter is initially mapped to itself. + taint: HashMap>, + /// Source parameters that reached a sink. + sinks: HashSet, + /// Source parameters read inside an `if`/`require`/`assert` predicate. + guarded: HashSet, + guard_depth: u32, + sink_depth: u32, +} + +impl<'hir> Analyzer<'hir> { + fn new(hir: &'hir hir::Hir<'hir>, params: &HashSet) -> Self { + let mut taint = HashMap::with_capacity(params.len()); + for &p in params { + taint.insert(p, HashSet::from([p])); + } + Self { + hir, + taint, + sinks: HashSet::new(), + guarded: HashSet::new(), + guard_depth: 0, + sink_depth: 0, + } + } + + fn taint_sources(&self, expr: &hir::Expr<'_>) -> HashSet { + let mut out = HashSet::new(); + collect_taint_sources(&self.taint, expr, &mut out); + out + } +} + +fn collect_taint_sources( + taint: &HashMap>, + expr: &hir::Expr<'_>, + out: &mut HashSet, +) { + match &expr.kind { + ExprKind::Ident(reses) => { + for res in *reses { + if let Res::Item(ItemId::Variable(vid)) = res + && let Some(srcs) = taint.get(vid) + { + out.extend(srcs.iter().copied()); + } + } + } + ExprKind::Assign(_, _, rhs) => collect_taint_sources(taint, rhs, out), + ExprKind::Binary(lhs, _, rhs) => { + collect_taint_sources(taint, lhs, out); + collect_taint_sources(taint, rhs, out); + } + ExprKind::Unary(_, e) + | ExprKind::Delete(e) + | ExprKind::Member(e, _) + | ExprKind::Payable(e) => collect_taint_sources(taint, e, out), + ExprKind::Ternary(_, t, f) => { + collect_taint_sources(taint, t, out); + collect_taint_sources(taint, f, out); + } + ExprKind::Tuple(elems) => { + for e in elems.iter().copied().flatten() { + collect_taint_sources(taint, e, out); + } + } + ExprKind::Array(elems) => { + for e in *elems { + collect_taint_sources(taint, e, out); + } + } + ExprKind::Index(base, idx) => { + collect_taint_sources(taint, base, out); + if let Some(i) = idx { + collect_taint_sources(taint, i, out); + } + } + // Covers type casts (`address(x)`), method calls, and ordinary calls; conservative. + ExprKind::Call(callee, args, _) => { + collect_taint_sources(taint, callee, out); + for a in args.exprs() { + collect_taint_sources(taint, a, out); + } + } + _ => {} + } +} + +/// Returns the underlying local `VariableId` if `lhs` is a direct identifier reference to a +/// non-state variable. +fn lhs_local_var(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> Option { + if let ExprKind::Ident(reses) = &lhs.kind { + for res in *reses { + if let Res::Item(ItemId::Variable(vid)) = res + && !hir.variable(*vid).kind.is_state() + { + return Some(*vid); + } + } + } + None +} + +impl<'hir> Visit<'hir> for Analyzer<'hir> { + type BreakValue = Never; + + fn hir(&self) -> &'hir hir::Hir<'hir> { + self.hir + } + + fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow { + match stmt.kind { + StmtKind::If(cond, then, else_) => { + self.guard_depth += 1; + let _ = self.visit_expr(cond); + self.guard_depth -= 1; + + let baseline = self.guarded.clone(); + let _ = self.visit_stmt(then); + let then_added: HashSet = + self.guarded.difference(&baseline).copied().collect(); + let then_exits = branch_always_exits(then); + + let (else_added, else_exits) = if let Some(e) = else_ { + self.guarded = baseline.clone(); + let _ = self.visit_stmt(e); + let added: HashSet = + self.guarded.difference(&baseline).copied().collect(); + (added, branch_always_exits(e)) + } else { + (HashSet::new(), false) + }; + + self.guarded = baseline; + let to_add: HashSet = match (then_exits, else_exits) { + (true, true) => then_added.union(&else_added).copied().collect(), + (true, false) => else_added, + (false, true) => then_added, + (false, false) => then_added.intersection(&else_added).copied().collect(), + }; + self.guarded.extend(to_add); + + return ControlFlow::Continue(()); + } + // Loop bodies may execute zero times, so guards inside must not persist. + StmtKind::Loop(block, _) => { + let baseline = self.guarded.clone(); + for s in block.stmts { + let _ = self.visit_stmt(s); + } + self.guarded = baseline; + return ControlFlow::Continue(()); + } + // Each try/catch clause is taken on a single path; discard clause-local guards. + StmtKind::Try(t) => { + let _ = self.visit_expr(&t.expr); + for clause in t.clauses { + let baseline = self.guarded.clone(); + for s in clause.block.stmts { + let _ = self.visit_stmt(s); + } + self.guarded = baseline; + } + return ControlFlow::Continue(()); + } + // Propagate taint through address-typed local declarations only; this avoids + // marking unrelated values (e.g. `bool ok = a.send(1)`) as derived from `a`. + StmtKind::DeclSingle(var_id) => { + let v = self.hir.variable(var_id); + if let Some(init) = v.initializer + && is_address(self.hir, var_id) + { + let srcs = self.taint_sources(init); + if !srcs.is_empty() { + self.taint.entry(var_id).or_default().extend(srcs); + } + } + } + _ => {} + } + self.walk_stmt(stmt) + } + + fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow { + match &expr.kind { + // `require(cond, ..)` / `assert(cond)`: only the first arg is a guard predicate. + ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => { + let mut iter = args.exprs(); + if let Some(cond) = iter.next() { + self.guard_depth += 1; + let _ = self.visit_expr(cond); + self.guard_depth -= 1; + } + for rest in iter { + let _ = self.visit_expr(rest); + } + return ControlFlow::Continue(()); + } + + // `.call/.delegatecall/.transfer/.send(..)`: receiver is the sink. + ExprKind::Call(callee, args, _) => { + if let Some(receiver) = address_call_receiver(callee) { + self.sink_depth += 1; + let _ = self.visit_expr(receiver); + self.sink_depth -= 1; + let _ = self.visit_call_args(args); + return ControlFlow::Continue(()); + } + } + + ExprKind::Assign(lhs, _, rhs) => { + // Sink: assignment to an address state variable. + if is_address_state_var_lhs(self.hir, lhs) { + let _ = self.visit_expr(lhs); + self.sink_depth += 1; + let _ = self.visit_expr(rhs); + self.sink_depth -= 1; + return ControlFlow::Continue(()); + } + // Taint propagation: assignment to an address local. + if let Some(local) = lhs_local_var(self.hir, lhs) + && is_address(self.hir, local) + { + let srcs = self.taint_sources(rhs); + if !srcs.is_empty() { + self.taint.entry(local).or_default().extend(srcs); + } + } + } + + // Identifier reads contribute to whichever contexts are currently active. + ExprKind::Ident(reses) => { + for res in *reses { + if let Res::Item(ItemId::Variable(vid)) = res + && let Some(srcs) = self.taint.get(vid) + { + if self.guard_depth > 0 { + self.guarded.extend(srcs.iter().copied()); + } + if self.sink_depth > 0 { + for &src in srcs { + if !self.guarded.contains(&src) { + self.sinks.insert(src); + } + } + } + } + } + } + + _ => {} + } + self.walk_expr(expr) + } +} + +fn is_require_or_assert(callee: &hir::Expr<'_>) -> bool { + if let ExprKind::Ident(reses) = &callee.kind { + return reses.iter().any(|r| { + if let Res::Builtin(b) = r { + let n = b.name(); + n == sym::require || n == sym::assert + } else { + false + } + }); + } + false +} + +/// If `callee` is `.{call,delegatecall,transfer,send}` (with or without +/// call options), returns the `` expression. +fn address_call_receiver<'hir>(callee: &'hir hir::Expr<'hir>) -> Option<&'hir hir::Expr<'hir>> { + // `addr.call{value: x}(..)` lowers as `Call(Member(receiver, "call"), ..)` — peel an + // outer call layer so the inner Member is reachable. + let inner = match &callee.kind { + ExprKind::Call(inner, ..) => inner, + _ => callee, + }; + let target = if matches!(inner.kind, ExprKind::Member(..)) { inner } else { callee }; + if let ExprKind::Member(receiver, name) = &target.kind { + let n = name.name; + if n == kw::Call || n == kw::Delegatecall || n == sym::transfer || n == sym::send { + return Some(receiver); + } + } + None +} + +fn branch_always_exits(stmt: &hir::Stmt<'_>) -> bool { + match &stmt.kind { + StmtKind::Return(_) | StmtKind::Revert(_) => true, + StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => { + block.stmts.last().is_some_and(branch_always_exits) + } + StmtKind::If(_, t, Some(e)) => branch_always_exits(t) && branch_always_exits(e), + _ => false, + } +} + +fn is_address_state_var_lhs(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> bool { + if let ExprKind::Ident(reses) = &lhs.kind { + for res in *reses { + if let Res::Item(ItemId::Variable(vid)) = res { + let v = hir.variable(*vid); + if v.kind.is_state() + && matches!(v.ty.kind, TypeKind::Elementary(ElementaryType::Address(_))) + { + return true; + } + } + } + } + false +} + +/// Maps each direct-ident modifier argument back to its caller-side parameter, runs the same guard +/// analysis on the modifier body, and records any caller params whose mapped modifier parameter is +/// guarded. +fn collect_modifier_guards( + hir: &hir::Hir<'_>, + invocation: &hir::Modifier<'_>, + caller_params: &HashSet, + guarded: &mut HashSet, +) { + let ItemId::Function(fid) = invocation.id else { return }; + let modifier = hir.function(fid); + if !matches!(modifier.kind, hir::FunctionKind::Modifier) { + return; + } + + let mod_params = modifier.parameters; + let mut mapping: HashSet = HashSet::new(); + let mut caller_for_modparam: HashMap = HashMap::new(); + for (i, arg_expr) in invocation.args.exprs().enumerate() { + if let ExprKind::Ident(reses) = &arg_expr.kind { + for res in *reses { + if let Res::Item(ItemId::Variable(vid)) = res + && caller_params.contains(vid) + && let Some(&mp) = mod_params.get(i) + { + caller_for_modparam.insert(mp, *vid); + mapping.insert(mp); + } + } + } + } + if mapping.is_empty() { + return; + } + + let Some(body) = modifier.body else { return }; + let mut a = Analyzer::new(hir, &mapping); + for stmt in body.stmts { + let _ = a.visit_stmt(stmt); + } + + for mp in a.guarded { + if let Some(caller_vid) = caller_for_modparam.get(&mp) { + guarded.insert(*caller_vid); + } + } +} diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index 28b7648b7dad5..efc59f208623b 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -27,6 +27,9 @@ use block_timestamp::BLOCK_TIMESTAMP; mod interface_naming; use interface_naming::{INTERFACE_FILE_NAMING, INTERFACE_NAMING}; +mod missing_zero_check; +use missing_zero_check::MISSING_ZERO_CHECK; + register_lints!( (PascalCaseStruct, early, (PASCAL_CASE_STRUCT)), (MixedCaseVariable, early, (MIXED_CASE_VARIABLE)), @@ -38,4 +41,5 @@ register_lints!( (MultiContractFile, early, (MULTI_CONTRACT_FILE)), (InterfaceFileNaming, early, (INTERFACE_FILE_NAMING, INTERFACE_NAMING)), (BlockTimestamp, early, (BLOCK_TIMESTAMP)), + (MissingZeroCheck, late, (MISSING_ZERO_CHECK)), ); diff --git a/crates/lint/testdata/MissingZeroCheck.sol b/crates/lint/testdata/MissingZeroCheck.sol new file mode 100644 index 0000000000000..c1af9bb2f06ae --- /dev/null +++ b/crates/lint/testdata/MissingZeroCheck.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface IExternal { + function ping() external; +} + +contract MissingZeroCheck { + address public owner; + address payable public recipient; + uint256 public n; + + modifier nonZero(address a) { + require(a != address(0), "zero"); + _; + } + + modifier doesNothing(address a) { + _; + } + + // SHOULD FAIL: + + function setOwner(address newOwner) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = newOwner; + } + + constructor(address initialOwner) { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = initialOwner; + } + + function pay(address payable to) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + to.transfer(1); + } + + function lowLevel(address payable to, bytes calldata data) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + (bool ok,) = to.call(data); + require(ok); + } + + function withUselessModifier(address a) external doesNothing(a) { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = a; + } + + function setOwnerViaAlias(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + address tmp = a; + owner = tmp; + } + + function setOwnerViaReassign(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + address tmp; + tmp = a; + owner = tmp; + } + + function setOwnerViaCast(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = address(uint160(a)); + } + + function payViaAlias(address payable a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + address payable tmp = a; + tmp.transfer(1); + } + + // Only `b` should be flagged; `a` is guarded. + function mixedParams(address a, address b) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + require(a != address(0)); + owner = a; + recipient = payable(b); + } + + // Same param feeds two sinks: should produce a single diagnostic. + function bothSinks(address payable a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + recipient = a; + a.transfer(1); + } + + function ternaryAlias(address a, bool flag) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + address tmp = flag ? a : address(0); + owner = tmp; + } + + function payableWrap(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + payable(a).transfer(1); + } + + // Modifier called with an expression, not a direct ident: we cannot prove the guard + // applies, so we should still flag. + function modifierWithExpr(address a) external nonZero(addrIdentity(a)) { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = a; + } + + function delegateCallSink(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + (bool ok,) = a.delegatecall(""); + require(ok); + } + + function sendSinkStmt(address payable a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + a.send(1); + } + + function sendSinkDecl(address payable a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + bool ok = a.send(1); + require(ok); + } + + function multiHopTaint(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + address x = a; + address y = x; + owner = y; + } + + function guardAfterSink(address a) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + owner = a; + require(a != address(0)); + } + + function guardOnOneBranch(address a, bool flag) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + if (flag) { + require(a != address(0)); + } + owner = a; + } + + function guardInForLoop(address a, uint256 n) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + for (uint256 i = 0; i < n; i++) { + require(a != address(0)); + } + owner = a; + } + + function guardInWhileLoop(address a, bool flag) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + while (flag) { + require(a != address(0)); + flag = false; + } + owner = a; + } + + function guardInTryClause(address a, address payable target) external { //~WARN: address parameter is used in a state write or value transfer without a zero-address check + try IExternal(target).ping() { + require(a != address(0)); + } catch { + require(a != address(0)); + } + owner = a; + } + + // SHOULD PASS: + + function setOwnerGuarded(address newOwner) external { + require(newOwner != address(0), "zero"); + owner = newOwner; + } + + function setOwnerIfGuarded(address newOwner) external { + if (newOwner == address(0)) revert(); + owner = newOwner; + } + + function setOwnerAssertGuarded(address newOwner) external { + assert(newOwner != address(0)); + owner = newOwner; + } + + function setOwnerWithModifier(address newOwner) external nonZero(newOwner) { + owner = newOwner; + } + + function setN(uint256 v) external { + n = v; + } + + function viewer(address a) external view returns (address) { + return a; + } + + function pureFn(address a) external pure returns (address) { + return a; + } + + function internalHelper(address a) internal { + owner = a; + } + + function callsHelper(address a) external { + require(a != address(0)); + internalHelper(a); + } + + function setOwnerViaAliasGuarded(address a) external { + require(a != address(0)); + address tmp = a; + owner = tmp; + } + + function privateHelper2(address a) private { + owner = a; + } + + event Deposit(address indexed from); + error ZeroAddress(); + + function emitOnly(address a) external { + emit Deposit(a); + } + + function guardViaCustomRevert(address a) external { + if (a == address(0)) revert ZeroAddress(); + owner = a; + } + + function noSinkJustPassthrough(address a) external returns (address) { + return a; + } + + function addrIdentity(address x) internal pure returns (address) { + return x; + } + + function staticCallOnly(address a) external { + (bool ok,) = a.staticcall(""); + require(ok); + } + + // Symmetric guard on both branches: universally checked. + function guardOnBothBranches(address a, bool flag) external { + if (flag) { + require(a != address(0)); + } else { + require(a != address(0)); + } + owner = a; + } + + // Inner zero-check guards `a` for the rest of the enclosing branch via early revert. + function nestedGuardWithRevert(address a, bool flag) external { + if (flag) { + if (a == address(0)) revert(); + owner = a; + } + } +} diff --git a/crates/lint/testdata/MissingZeroCheck.stderr b/crates/lint/testdata/MissingZeroCheck.stderr new file mode 100644 index 0000000000000..b55a902547fcf --- /dev/null +++ b/crates/lint/testdata/MissingZeroCheck.stderr @@ -0,0 +1,184 @@ +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function setOwner(address newOwner) external { + │ ━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ constructor(address initialOwner) { + │ ━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function pay(address payable to) external { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function lowLevel(address payable to, bytes calldata data) external { + │ ━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function withUselessModifier(address a) external doesNothing(a) { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function setOwnerViaAlias(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function setOwnerViaReassign(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function setOwnerViaCast(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function payViaAlias(address payable a) external { + │ ━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function mixedParams(address a, address b) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function bothSinks(address payable a) external { + │ ━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function ternaryAlias(address a, bool flag) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function payableWrap(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function modifierWithExpr(address a) external nonZero(addrIdentity(a)) { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function delegateCallSink(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function sendSinkStmt(address payable a) external { + │ ━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function sendSinkDecl(address payable a) external { + │ ━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function multiHopTaint(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function guardAfterSink(address a) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function guardOnOneBranch(address a, bool flag) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function guardInForLoop(address a, uint256 n) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function guardInWhileLoop(address a, bool flag) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + +warning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check + ╭▸ ROOT/testdata/MissingZeroCheck.sol:LL:CC + │ +LL │ function guardInTryClause(address a, address payable target) external { + │ ━━━━━━━━━ + │ + ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#missing-zero-check + diff --git a/crates/lint/testdata/UncheckedCall.sol b/crates/lint/testdata/UncheckedCall.sol index d495a61a8cf99..da9e9cf3a0fe4 100644 --- a/crates/lint/testdata/UncheckedCall.sol +++ b/crates/lint/testdata/UncheckedCall.sol @@ -1,3 +1,5 @@ +//@compile-flags: --severity high + // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; diff --git a/crates/lint/testdata/UncheckedTransferERC20.sol b/crates/lint/testdata/UncheckedTransferERC20.sol index a7d0d1d37bf1c..ee600aa956903 100644 --- a/crates/lint/testdata/UncheckedTransferERC20.sol +++ b/crates/lint/testdata/UncheckedTransferERC20.sol @@ -1,4 +1,4 @@ -//@compile-flags: --severity high med low info +//@compile-flags: --severity high info // SPDX-License-Identifier: MIT pragma solidity ^0.8.18;