diff --git a/crates/lint/README.md b/crates/lint/README.md index 143302994be33..7b25bdbbcd457 100644 --- a/crates/lint/README.md +++ b/crates/lint/README.md @@ -71,6 +71,7 @@ It helps enforce best practices and improve code quality within Foundry projects - `low-level-calls`: Direct use of low-level calls should be avoided. - `event-fields`: `address` event parameters should be `indexed` for efficient log filtering. - `todo-comment`: Detects unresolved `TODO` and `FIXME` markers in comments. + - `reentrancy-unlimited-gas`: Flags state changes or event emissions after `transfer`/`send`, whose fixed gas stipend may become reentrant after gas repricing. - `unused-error`: Custom error declarations that are never referenced should be removed. - `literal-instead-of-constant`: A literal value repeated inside a contract should be a named constant. - `function-init-state`: State variable initializers run before the constructor; depending on a non-pure function or another state variable there observes partial state. diff --git a/crates/lint/docs/reentrancy-unlimited-gas.md b/crates/lint/docs/reentrancy-unlimited-gas.md new file mode 100644 index 0000000000000..3bae205543461 --- /dev/null +++ b/crates/lint/docs/reentrancy-unlimited-gas.md @@ -0,0 +1,61 @@ +# Reentrancy through fixed-gas ETH transfers + +**Severity**: `Info` +**ID**: `reentrancy-unlimited-gas` + +Flags state changes and event emissions that occur after Solidity's built-in `transfer` or `send` +on the same reachable path. These operations forward a fixed gas stipend, but opcode repricing can +make new callback behavior affordable within that stipend. + +## What it does + +The lint tracks Solidity's built-in `transfer` and `send` calls through public and external entry +points, modifiers, inherited helpers, and other internal calls. It follows branches and loop +backedges and warns when a later reachable operation writes contract state or emits an event. +Writes through storage references, storage-array `push`/`pop` calls, and Yul `sstore`/`tstore` +operations count as state changes; Yul log operations count as event emissions. Both calls are +tracked regardless of the transferred amount because even a zero-value call executes recipient +code. + +For `send` used directly in a Boolean control-flow condition or through a stored result, only +continuations on which the call may have succeeded are treated as reentrant because a failed call +reverts its callee's effects. Sibling expression order is conservatively treated as unspecified, +matching Solidity's evaluation-order rules; effects on an always-reverting expression path are +discarded. + +Contract methods that merely reuse the names `transfer` or `send` are excluded by checking the +compiler-resolved call target. Low-level calls, including calls with an explicit 2,300 gas cap, are +outside this detector's Slither-compatible scope. Constructors, local-variable writes, effects +that occur only before the call, and effects on mutually exclusive paths are not reported. + +## Why is this bad? + +The fixed stipend is not a stable reentrancy boundary. Changes to EVM opcode costs can alter which +fallback operations fit within it, so code that assumes `transfer` or `send` can never call back may +become unsafe after a network upgrade. A callback before a later state write can observe stale +state, while a callback before a later event can reorder logs consumed by off-chain systems. + +Apply checks-effects-interactions: commit state and emit its corresponding event before interacting +with the recipient. Use a reentrancy guard when the interaction cannot safely be moved last. + +## Example + +### Bad + +```solidity +function withdraw(address payable recipient, uint256 amount) external { + recipient.transfer(amount); + balances[recipient] -= amount; + emit Withdrawal(recipient, amount); +} +``` + +### Good + +```solidity +function withdraw(address payable recipient, uint256 amount) external { + balances[recipient] -= amount; + emit Withdrawal(recipient, amount); + recipient.transfer(amount); +} +``` diff --git a/crates/lint/src/sol/high/mod.rs b/crates/lint/src/sol/high/mod.rs index 963e2ab705d1d..2a5ac932bc705 100644 --- a/crates/lint/src/sol/high/mod.rs +++ b/crates/lint/src/sol/high/mod.rs @@ -17,7 +17,7 @@ use controlled_delegatecall::CONTROLLED_DELEGATECALL; use encode_packed_collision::ENCODE_PACKED_COLLISION; use incorrect_exp::INCORRECT_EXP; use incorrect_shift::INCORRECT_SHIFT; -use reentrancy::{REENTRANCY_ETH, REENTRANCY_NO_ETH}; +use reentrancy::{REENTRANCY_ETH, REENTRANCY_NO_ETH, REENTRANCY_UNLIMITED_GAS}; use rtlo::RTLO; use unchecked_calls::{ERC20_UNCHECKED_TRANSFER, UNCHECKED_CALL}; use unprotected_initializer::UNPROTECTED_INITIALIZER; @@ -29,7 +29,7 @@ register_lints!( (EncodedPackedCollision, late, (ENCODE_PACKED_COLLISION)), (IncorrectExp, late, (INCORRECT_EXP)), (IncorrectShift, early, (INCORRECT_SHIFT)), - (ReentrancyEth, late, (REENTRANCY_ETH, REENTRANCY_NO_ETH)), + (ReentrancyEth, late, (REENTRANCY_ETH, REENTRANCY_NO_ETH, REENTRANCY_UNLIMITED_GAS)), (UncheckedCall, early, (UNCHECKED_CALL)), (UncheckedTransferERC20, late, (ERC20_UNCHECKED_TRANSFER)), (UnprotectedInitializer, late, (UNPROTECTED_INITIALIZER)), diff --git a/crates/lint/src/sol/high/reentrancy.rs b/crates/lint/src/sol/high/reentrancy.rs index 48326b52d95d2..23ba282862905 100644 --- a/crates/lint/src/sol/high/reentrancy.rs +++ b/crates/lint/src/sol/high/reentrancy.rs @@ -14,8 +14,10 @@ use solar::{ interface::{Span, kw, sym}, sema::{ Gcx, Ty, + builtins::Builtin, hir::{ - self, CallArgs, CallArgsKind, ExprKind, FunctionId, ItemId, Res, StmtKind, VariableId, + self, CallArgs, CallArgsKind, ExprKind, FunctionId, ItemId, LoopSource, Res, StmtKind, + VariableId, }, ty::{TyFnKind, TyKind}, }, @@ -36,6 +38,13 @@ declare_forge_lint!( "state read before external call is written after the call" ); +declare_forge_lint!( + REENTRANCY_UNLIMITED_GAS, + Severity::Info, + "reentrancy-unlimited-gas", + "state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy" +); + impl<'hir> LateLintPass<'hir> for ReentrancyEth { fn check_function( &mut self, @@ -76,6 +85,9 @@ fn is_entry_point(func: &hir::Function<'_>) -> bool { struct FlowState { state_reads: BTreeSet, pending_calls: Vec, + stored_send_results: Vec, + stored_call_results: Vec, + stored_return_results: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -83,12 +95,102 @@ struct PendingCall { span: Span, kind: ReentrantCallKind, state_reads: BTreeSet, + result_correlatable: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct SendResultSource { + call_span: Span, + succeeds_when_true: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct StoredSendResult { + variable: VariableId, + source: SendResultSource, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct StoredCallResult { + expression_span: Span, + index: usize, + source: SendResultSource, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct StoredReturnResult { + function: FunctionId, + index: usize, + source: SendResultSource, +} + +#[derive(Default)] +struct CallReturns { + function: Option, + state: Option, +} + +#[derive(Default)] +struct LoopJumps<'hir> { + breaks: Option, + continues: Option, + continue_epilogue: Option>, +} + +#[derive(Clone, Copy)] +enum ContinueEpilogue<'hir> { + Expr(&'hir hir::Expr<'hir>), + Block(hir::Block<'hir>), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] enum ReentrantCallKind { Eth, NoEth, + Stipend, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SendEvaluation { + NotEvaluated, + Failed, + Succeeded, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct BooleanOutcome { + value: bool, + send: SendEvaluation, +} + +fn unconstrained_boolean_outcomes(contains_target: bool) -> Vec { + let evaluations: &[_] = if contains_target { + &[SendEvaluation::NotEvaluated, SendEvaluation::Failed, SendEvaluation::Succeeded] + } else { + &[SendEvaluation::NotEvaluated] + }; + evaluations + .iter() + .flat_map(|&send| { + [BooleanOutcome { value: true, send }, BooleanOutcome { value: false, send }] + }) + .collect() +} + +fn merge_send_evaluations(lhs: SendEvaluation, rhs: SendEvaluation) -> Option { + match (lhs, rhs) { + (SendEvaluation::NotEvaluated, other) | (other, SendEvaluation::NotEvaluated) => { + Some(other) + } + (lhs, rhs) if lhs == rhs => Some(lhs), + _ => None, + } +} + +fn push_unique_boolean_outcome(outcomes: &mut Vec, outcome: BooleanOutcome) { + if !outcomes.contains(&outcome) { + outcomes.push(outcome); + } } impl FlowState { @@ -97,7 +199,7 @@ impl FlowState { } fn push_call(&mut self, span: Span, kind: ReentrantCallKind) { - if self.state_reads.is_empty() { + if self.state_reads.is_empty() && kind != ReentrantCallKind::Stipend { return; } @@ -105,14 +207,202 @@ impl FlowState { self.pending_calls.iter_mut().find(|call| call.span == span && call.kind == kind) { existing.state_reads.extend(self.state_reads.iter().copied()); + existing.result_correlatable = false; + self.stored_send_results.retain(|result| result.source.call_span != span); + self.stored_call_results.retain(|result| result.source.call_span != span); + self.stored_return_results.retain(|result| result.source.call_span != span); } else { self.pending_calls.push(PendingCall { span, kind, state_reads: self.state_reads.clone(), + result_correlatable: true, }); } } + + fn remove_call(&mut self, span: Span, kind: ReentrantCallKind) { + self.pending_calls.retain(|call| call.span != span || call.kind != kind); + self.stored_send_results.retain(|result| result.source.call_span != span); + self.stored_call_results.retain(|result| result.source.call_span != span); + self.stored_return_results.retain(|result| result.source.call_span != span); + } + + fn set_stored_send_results(&mut self, variable: VariableId, sources: &[SendResultSource]) { + self.stored_send_results.retain(|result| result.variable != variable); + for &source in sources { + if self.is_correlatable_source(source) { + let result = StoredSendResult { variable, source }; + if !self.stored_send_results.contains(&result) { + self.stored_send_results.push(result); + } + } + } + } + + fn clear_stored_call_results(&mut self, expression_span: Span) { + self.stored_call_results.retain(|result| result.expression_span != expression_span); + } + + fn set_stored_call_results( + &mut self, + expression_span: Span, + sources: &[Vec], + ) { + self.clear_stored_call_results(expression_span); + for (index, sources) in sources.iter().enumerate() { + for &source in sources { + if self.is_correlatable_source(source) { + let result = StoredCallResult { expression_span, index, source }; + if !self.stored_call_results.contains(&result) { + self.stored_call_results.push(result); + } + } + } + } + } + + fn clear_stored_return_results(&mut self, function: FunctionId) { + self.stored_return_results.retain(|result| result.function != function); + } + + fn set_stored_return_results( + &mut self, + function: FunctionId, + sources: &[Vec], + ) { + self.clear_stored_return_results(function); + for (index, sources) in sources.iter().enumerate() { + for &source in sources { + if self.is_correlatable_source(source) { + let result = StoredReturnResult { function, index, source }; + if !self.stored_return_results.contains(&result) { + self.stored_return_results.push(result); + } + } + } + } + } + + fn is_correlatable_source(&self, source: SendResultSource) -> bool { + self.pending_calls.iter().any(|call| { + call.span == source.call_span + && call.kind == ReentrantCallKind::Stipend + && call.result_correlatable + }) + } +} + +fn merge_optional_state(target: &mut Option, state: &FlowState) { + if let Some(target) = target { + target.merge(state); + } else { + *target = Some(state.clone()); + } +} + +/// Returns the Solidity update expression or Yul post block lowered after a `for` loop body. +/// +/// A source-level `continue` skips this synthetic statement in HIR, even though the source +/// language executes it before the next condition check. +fn for_loop_continue_epilogue<'hir>( + block: hir::Block<'hir>, + source: LoopSource, +) -> Option> { + if source != LoopSource::For || block.stmts.len() != 1 { + return None; + } + + let stmt = block.stmts.first()?; + let body = match stmt.kind { + StmtKind::If(_, then_stmt, Some(else_stmt)) + if matches!(else_stmt.kind, StmtKind::Break) => + { + then_stmt + } + _ => stmt, + }; + let StmtKind::Block(body) = body.kind else { return None }; + if body.span != block.span || body.stmts.len() < 2 { + return None; + } + + match body.stmts.last()?.kind { + StmtKind::Expr(epilogue) => Some(ContinueEpilogue::Expr(epilogue)), + StmtKind::Block(epilogue) => Some(ContinueEpilogue::Block(epilogue)), + _ => None, + } +} + +/// Returns the user statements and condition from Solar's lowered `do-while` loop. +fn do_while_parts<'hir>( + block: hir::Block<'hir>, + source: LoopSource, +) -> Option<(&'hir [hir::Stmt<'hir>], &'hir hir::Expr<'hir>)> { + if source != LoopSource::DoWhile { + return None; + } + + let (check, body) = block.stmts.split_last()?; + let StmtKind::If(condition, then_stmt, Some(else_stmt)) = check.kind else { return None }; + if !matches!(then_stmt.kind, StmtKind::Continue) || !matches!(else_stmt.kind, StmtKind::Break) { + return None; + } + Some((body, condition)) +} + +fn is_state_mutating_array_call(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool { + matches!( + gcx.builtin_callee(callee.peel_parens().id), + Some(Builtin::ArrayPush0 | Builtin::ArrayPush | Builtin::ArrayPop) + ) +} + +fn is_builtin_assertion(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool { + matches!(gcx.builtin_callee(callee.peel_parens().id), Some(Builtin::Require | Builtin::Assert)) +} + +fn is_non_returning_builtin_call(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool { + matches!( + gcx.builtin_callee(callee.peel_parens().id), + Some( + Builtin::Revert + | Builtin::RevertMsg + | Builtin::Selfdestruct + | Builtin::YulInvalid + | Builtin::YulReturn + | Builtin::YulRevert + | Builtin::YulSelfdestruct + | Builtin::YulStop + ) + ) +} + +fn is_successful_halt_builtin_call(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool { + matches!( + gcx.builtin_callee(callee.peel_parens().id), + Some( + Builtin::Selfdestruct + | Builtin::YulReturn + | Builtin::YulSelfdestruct + | Builtin::YulStop + ) + ) +} + +fn is_yul_state_effect(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool { + matches!( + gcx.builtin_callee(callee.peel_parens().id), + Some( + Builtin::YulSstore + | Builtin::YulTstore + | Builtin::YulLog0 + | Builtin::YulLog1 + | Builtin::YulLog2 + | Builtin::YulLog3 + | Builtin::YulLog4 + ) + ) } struct Analyzer<'ctx, 's, 'c, 'hir> { @@ -121,11 +411,17 @@ struct Analyzer<'ctx, 's, 'c, 'hir> { hir: &'hir hir::Hir<'hir>, emitted: HashSet, call_stack: Vec, - inline_cache: HelperAnalysisCache, + inline_cache: HelperAnalysisCache, recursive_cut_frontiers: HashMap>, direct_internal_calls: HashMap>, + loop_jumps: Vec>, + call_returns: Vec, + state_effects: usize, + completion_probe_depth: usize, + completion_probe_successful_halt: bool, reentrancy_eth_enabled: bool, reentrancy_no_eth_enabled: bool, + reentrancy_unlimited_gas_enabled: bool, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -136,6 +432,14 @@ struct InlineCallKey { state: FlowState, } +#[derive(Clone)] +struct InlineCallResult { + state: FlowState, + may_return: bool, + has_state_effect: bool, + return_sources: Vec>, +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] struct RecursiveFrontierKey { func_id: FunctionId, @@ -153,13 +457,21 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { inline_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT), recursive_cut_frontiers: HashMap::new(), direct_internal_calls: HashMap::new(), + loop_jumps: Vec::new(), + call_returns: Vec::new(), + state_effects: 0, + completion_probe_depth: 0, + completion_probe_successful_halt: false, reentrancy_eth_enabled: ctx.is_lint_enabled(REENTRANCY_ETH.id), reentrancy_no_eth_enabled: ctx.is_lint_enabled(REENTRANCY_NO_ETH.id), + reentrancy_unlimited_gas_enabled: ctx.is_lint_enabled(REENTRANCY_UNLIMITED_GAS.id), } } const fn has_enabled_lints(&self) -> bool { - self.reentrancy_eth_enabled || self.reentrancy_no_eth_enabled + self.reentrancy_eth_enabled + || self.reentrancy_no_eth_enabled + || self.reentrancy_unlimited_gas_enabled } fn analyze_callable( @@ -183,7 +495,9 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { }; for arg in modifier.args.exprs() { - self.analyze_expr(arg, state); + if !self.analyze_expr(arg, state) { + return false; + } } let Some(modifier_id) = modifier.id.as_function() else { @@ -212,7 +526,16 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { placeholder: Option<(&'hir [hir::Modifier<'hir>], usize, hir::Block<'hir>)>, state: &mut FlowState, ) -> bool { - for stmt in block.stmts { + self.analyze_stmts(block.stmts, placeholder, state) + } + + fn analyze_stmts( + &mut self, + stmts: &'hir [hir::Stmt<'hir>], + placeholder: Option<(&'hir [hir::Modifier<'hir>], usize, hir::Block<'hir>)>, + state: &mut FlowState, + ) -> bool { + for stmt in stmts { if !self.analyze_stmt(stmt, placeholder, state) { return false; } @@ -229,19 +552,38 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { match stmt.kind { StmtKind::DeclSingle(var_id) => { if let Some(init) = self.hir.variable(var_id).initializer { - self.analyze_expr(init, state); + let completes = self.analyze_expr(init, state); + if completes { + let sources = self.send_result_sources(init, state); + state.set_stored_send_results(var_id, &sources); + } + completes + } else { + true } - true } - StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => { - self.analyze_expr(expr, state); - true + StmtKind::DeclMulti(variables, expr) => { + let completes = self.analyze_expr(expr, state); + if completes { + let sources = self.send_result_sources_by_index(expr, variables.len(), state); + for (index, variable) in variables.iter().enumerate() { + if let Some(variable) = variable { + state.set_stored_send_results(*variable, &sources[index]); + } + } + } + completes } + StmtKind::Expr(expr) => self.analyze_expr(expr, state), StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => { self.analyze_block(block, placeholder, state) } StmtKind::Emit(expr) => { - self.analyze_expr(expr, state); + if !self.analyze_expr(expr, state) { + return false; + } + self.emit_pending_stipend_calls(state); + self.record_state_effect(); true } StmtKind::Revert(expr) => { @@ -249,28 +591,110 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { false } StmtKind::Return(expr) => { - if let Some(expr) = expr { - self.analyze_expr(expr, state); + let completes = expr.is_none_or(|expr| self.analyze_expr(expr, state)); + if completes { + if let Some(function) = + self.call_returns.last().and_then(|returns| returns.function) + { + let sources = self.return_result_sources(function, expr, state); + state.set_stored_return_results(function, &sources); + } + if let Some(returns) = self.call_returns.last_mut() { + merge_optional_state(&mut returns.state, state); + } + } + false + } + StmtKind::Break => { + if let Some(jumps) = self.loop_jumps.last_mut() { + merge_optional_state(&mut jumps.breaks, state); } false } - StmtKind::Break | StmtKind::Continue => false, - StmtKind::Loop(block, _) => { + StmtKind::Continue => { + let epilogue = self.loop_jumps.last().and_then(|jumps| jumps.continue_epilogue); + let completes = match epilogue { + Some(ContinueEpilogue::Expr(epilogue)) => self.analyze_expr(epilogue, state), + Some(ContinueEpilogue::Block(epilogue)) => { + self.analyze_block(epilogue, placeholder, state) + } + None => true, + }; + if completes && let Some(jumps) = self.loop_jumps.last_mut() { + merge_optional_state(&mut jumps.continues, state); + } + false + } + StmtKind::Loop(block, source) => { let before_loop = state.clone(); - let mut body_state = state.clone(); - self.analyze_block(block, placeholder, &mut body_state); - state.clear(); - state.merge(&before_loop); - state.merge(&body_state); - true + let mut header_state = before_loop.clone(); + let mut exit_state = None; + let continue_epilogue = for_loop_continue_epilogue(block, source); + let do_while_parts = do_while_parts(block, source); + + loop { + self.loop_jumps.push(LoopJumps { continue_epilogue, ..Default::default() }); + let mut body_state = header_state.clone(); + let falls_through = if let Some((body, _)) = do_while_parts { + self.analyze_stmts(body, placeholder, &mut body_state) + } else { + self.analyze_block(block, placeholder, &mut body_state) + }; + let jumps = self.loop_jumps.pop().expect("loop jump state exists"); + + if let Some(breaks) = jumps.breaks { + merge_optional_state(&mut exit_state, &breaks); + } + + let mut next_header = before_loop.clone(); + let mut backedges = None; + if falls_through { + merge_optional_state(&mut backedges, &body_state); + } + if let Some(continues) = jumps.continues { + merge_optional_state(&mut backedges, &continues); + } + + if let Some((_, condition)) = do_while_parts { + if let Some(mut condition_state) = backedges + && self.analyze_expr(condition, &mut condition_state) + { + let mut continue_state = condition_state.clone(); + self.remove_failed_send_calls(condition, true, &mut continue_state); + next_header.merge(&continue_state); + + self.remove_failed_send_calls(condition, false, &mut condition_state); + merge_optional_state(&mut exit_state, &condition_state); + } + } else if let Some(backedges) = backedges { + next_header.merge(&backedges); + } + + if next_header == header_state { + break; + } + header_state = next_header; + } + + if let Some(exit_state) = exit_state { + *state = exit_state; + true + } else { + state.clear(); + false + } } StmtKind::If(cond, then_stmt, else_stmt) => { - self.analyze_expr(cond, state); + if !self.analyze_expr(cond, state) { + return false; + } let mut then_state = state.clone(); + let mut else_state = state.clone(); + self.remove_failed_send_calls(cond, true, &mut then_state); + self.remove_failed_send_calls(cond, false, &mut else_state); let then_falls_through = self.analyze_stmt(then_stmt, placeholder, &mut then_state); - let mut else_state = state.clone(); let else_falls_through = if let Some(else_stmt) = else_stmt { self.analyze_stmt(else_stmt, placeholder, &mut else_state) } else { @@ -288,7 +712,9 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { then_falls_through || else_falls_through } StmtKind::Try(try_stmt) => { - self.analyze_expr(&try_stmt.expr, state); + if !self.analyze_expr(&try_stmt.expr, state) { + return false; + } let mut merged = FlowState::default(); let mut any_falls_through = false; @@ -307,34 +733,110 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { } StmtKind::Placeholder => { if let Some((modifiers, index, body)) = placeholder { - self.analyze_modifier_chain(modifiers, index, body, state) + let function = self.call_returns.last().and_then(|returns| returns.function); + self.call_returns.push(CallReturns { function, state: None }); + let falls_through = self.analyze_modifier_chain(modifiers, index, body, state); + let mut completions = + self.call_returns.pop().expect("modifier return state exists"); + if falls_through { + if let Some(function) = function + && !state + .stored_return_results + .iter() + .any(|result| result.function == function) + { + let sources = self.named_return_result_sources(function, state); + state.set_stored_return_results(function, &sources); + } + merge_optional_state(&mut completions.state, state); + } + if let Some(completions) = completions.state { + *state = completions; + true + } else { + state.clear(); + false + } } else { true } } - StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => true, + StmtKind::AssemblyBlock(block) => self.analyze_block(block, placeholder, state), + StmtKind::Switch(switch) => { + if !self.analyze_expr(switch.selector, state) { + return false; + } + + let before_switch = state.clone(); + let mut completions = None; + for case in switch.cases { + let mut case_state = before_switch.clone(); + if self.analyze_block(case.body, placeholder, &mut case_state) { + merge_optional_state(&mut completions, &case_state); + } + } + if !switch.cases.iter().any(|case| case.constant.is_none()) { + merge_optional_state(&mut completions, &before_switch); + } + + if let Some(completions) = completions { + *state = completions; + true + } else { + state.clear(); + false + } + } + StmtKind::Err(_) => true, } } - fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) { + fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) -> bool { match &expr.kind { ExprKind::Assign(lhs, op, rhs) => { + let mut completes = true; if op.is_some() { - self.analyze_expr(lhs, state); + completes &= self.analyze_expr(lhs, state); } - self.analyze_expr(rhs, state); - self.analyze_lhs_indices(lhs, state); - let written_vars = state_write_lhs_vars(self.hir, lhs); - if !written_vars.is_empty() { - self.emit_pending_calls(state, &written_vars); + completes &= self.analyze_expr(rhs, state); + completes &= self.analyze_lhs_indices(lhs, state); + if completes { + let assignments = if op.is_none() { + self.assignment_send_result_sources(lhs, rhs, state) + } else { + assigned_variables(lhs) + .into_iter() + .map(|variable| (variable, Vec::new())) + .collect() + }; + for (variable, sources) in assignments { + state.set_stored_send_results(variable, &sources); + } + let written_vars = state_write_lhs_vars(self.hir, lhs); + if !written_vars.is_empty() + || is_storage_write_lhs(self.gcx, self.hir, lhs, false) + { + self.emit_pending_calls(state, &written_vars); + self.record_state_effect(); + } } + completes } ExprKind::Delete(inner) => { - self.analyze_lhs_indices(inner, state); - let written_vars = state_write_lhs_vars(self.hir, inner); - if !written_vars.is_empty() { - self.emit_pending_calls(state, &written_vars); + let completes = self.analyze_lhs_indices(inner, state); + if completes { + for variable in assigned_variables(inner) { + state.set_stored_send_results(variable, &[]); + } + let written_vars = state_write_lhs_vars(self.hir, inner); + if !written_vars.is_empty() + || is_storage_write_lhs(self.gcx, self.hir, inner, true) + { + self.emit_pending_calls(state, &written_vars); + self.record_state_effect(); + } } + completes } ExprKind::Unary(op, inner) if matches!( @@ -342,81 +844,161 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec ) => { - self.analyze_expr(inner, state); - let written_vars = state_write_lhs_vars(self.hir, inner); - if !written_vars.is_empty() { - self.emit_pending_calls(state, &written_vars); + let completes = self.analyze_expr(inner, state); + if completes { + let written_vars = state_write_lhs_vars(self.hir, inner); + if !written_vars.is_empty() + || is_storage_write_lhs(self.gcx, self.hir, inner, true) + { + self.emit_pending_calls(state, &written_vars); + self.record_state_effect(); + } } + completes } - ExprKind::Unary(_, inner) => { - self.analyze_expr(inner, state); - } + ExprKind::Unary(_, inner) => self.analyze_expr(inner, state), ExprKind::Call(callee, args, opts) => { - self.analyze_expr(callee, state); + state.clear_stored_call_results(expr.span); + let mut children = + Vec::with_capacity(1 + opts.map_or(0, |opts| opts.args.len()) + args.len()); + children.push(*callee); if let Some(opts) = opts { for opt in opts.args { - self.analyze_expr(&opt.value, state); + children.push(&opt.value); } } for arg in args.exprs() { - self.analyze_expr(arg, state); + children.push(arg); } - - for func_id in resolved_function_ids(callee) { - self.analyze_internal_call(func_id, state); + let assertion_condition = + is_builtin_assertion(self.gcx, callee).then(|| args.exprs().next()).flatten(); + let mut completes = + self.analyze_unordered_exprs(&children, assertion_condition, state); + + if completes { + let func_ids = + resolved_function_ids(self.gcx, self.hir, callee).collect::>(); + if !func_ids.is_empty() { + let before_call = state.clone(); + let mut returned_state = FlowState::default(); + let mut any_returns = false; + for func_id in func_ids { + let mut candidate_state = before_call.clone(); + self.bind_call_argument_sources(func_id, args, &mut candidate_state); + if let Some(return_sources) = + self.analyze_internal_call(func_id, &mut candidate_state) + { + candidate_state.set_stored_call_results(expr.span, &return_sources); + returned_state.merge(&candidate_state); + any_returns = true; + } + } + *state = returned_state; + completes = any_returns; + } } - if !state.state_reads.is_empty() - && let Some(kind) = self.reentrant_call_kind(callee, args, *opts) - { - state.push_call(expr.span, kind); + + if completes { + if is_state_mutating_array_call(self.gcx, callee) + || is_yul_state_effect(self.gcx, callee) + { + self.emit_pending_stipend_calls(state); + self.record_state_effect(); + } + if let Some(kind) = self.reentrant_call_kind(callee, args, *opts) { + state.push_call(expr.span, kind); + } + + if is_builtin_assertion(self.gcx, callee) + && let Some(condition) = args.exprs().next() + { + self.remove_failed_send_calls(condition, true, state); + } + if is_non_returning_builtin_call(self.gcx, callee) { + if self.completion_probe_depth > 0 + && is_successful_halt_builtin_call(self.gcx, callee) + { + self.completion_probe_successful_halt = true; + } + state.clear(); + completes = false; + } } + completes } - ExprKind::Binary(lhs, _, rhs) => { - self.analyze_expr(lhs, state); - self.analyze_expr(rhs, state); + ExprKind::Binary(lhs, op, rhs) => { + if matches!(op.kind, BinOpKind::And | BinOpKind::Or) { + if !self.analyze_expr(lhs, state) { + return false; + } + + let rhs_outcome = op.kind == BinOpKind::And; + let mut short_circuit_state = state.clone(); + self.remove_failed_send_calls(lhs, !rhs_outcome, &mut short_circuit_state); + + let mut rhs_state = state.clone(); + self.remove_failed_send_calls(lhs, rhs_outcome, &mut rhs_state); + let rhs_completes = self.analyze_expr(rhs, &mut rhs_state); + + *state = short_circuit_state; + if rhs_completes { + state.merge(&rhs_state); + } + true + } else { + self.analyze_unordered_exprs(&[lhs, rhs], None, state) + } } ExprKind::Index(base, index) => { - self.analyze_expr(base, state); if let Some(index) = index { - self.analyze_expr(index, state); + self.analyze_unordered_exprs(&[base, index], None, state) + } else { + self.analyze_expr(base, state) } } ExprKind::Slice(base, start, end) => { - self.analyze_expr(base, state); + let mut children = Vec::with_capacity(3); + children.push(*base); if let Some(start) = start { - self.analyze_expr(start, state); + children.push(*start); } if let Some(end) = end { - self.analyze_expr(end, state); + children.push(*end); } + self.analyze_unordered_exprs(&children, None, state) } ExprKind::Ternary(cond, true_expr, false_expr) => { - self.analyze_expr(cond, state); + if !self.analyze_expr(cond, state) { + return false; + } let mut true_state = state.clone(); - self.analyze_expr(true_expr, &mut true_state); + self.remove_failed_send_calls(cond, true, &mut true_state); + let true_completes = self.analyze_expr(true_expr, &mut true_state); let mut false_state = state.clone(); - self.analyze_expr(false_expr, &mut false_state); + self.remove_failed_send_calls(cond, false, &mut false_state); + let false_completes = self.analyze_expr(false_expr, &mut false_state); state.clear(); - state.merge(&true_state); - state.merge(&false_state); + if true_completes { + state.merge(&true_state); + } + if false_completes { + state.merge(&false_state); + } + true_completes || false_completes } ExprKind::Array(exprs) => { - for expr in *exprs { - self.analyze_expr(expr, state); - } + let children = exprs.iter().collect::>(); + self.analyze_unordered_exprs(&children, None, state) } ExprKind::Tuple(exprs) => { - for expr in exprs.iter().copied().flatten() { - self.analyze_expr(expr, state); - } + let children = exprs.iter().copied().flatten().collect::>(); + self.analyze_unordered_exprs(&children, None, state) } - ExprKind::Member(base, _) | ExprKind::Payable(base) => { - self.analyze_expr(base, state); - } - ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {} + ExprKind::Member(base, _) | ExprKind::Payable(base) => self.analyze_expr(base, state), + ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => true, ExprKind::Ident(reses) => { for &res in *reses { if let Res::Item(ItemId::Variable(var_id)) = res @@ -425,18 +1007,565 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { state.push_read(var_id); } } + true } - ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {} + ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => true, } } - fn analyze_internal_call(&mut self, func_id: FunctionId, state: &mut FlowState) { - if self.call_stack.contains(&func_id) { + fn analyze_unordered_exprs( + &mut self, + exprs: &[&'hir hir::Expr<'hir>], + assertion_condition: Option<&'hir hir::Expr<'hir>>, + state: &mut FlowState, + ) -> bool { + if !self.reentrancy_unlimited_gas_enabled { + let mut all_complete = true; + for &expr in exprs { + all_complete &= self.analyze_expr(expr, state); + } + return all_complete; + } + + if self.completion_probe_depth == 0 && !self.unordered_exprs_can_persist(exprs, state) { + state.clear(); + return false; + } + + let mut summaries = Vec::with_capacity(exprs.len()); + let mut all_complete = true; + for &expr in exprs { + let prior_calls = state + .pending_calls + .iter() + .filter(|call| call.kind == ReentrantCallKind::Stipend) + .map(|call| call.span) + .collect::>(); + let prior_effects = self.state_effects; + all_complete &= self.analyze_expr(expr, state); + let calls = state + .pending_calls + .iter() + .filter(|call| { + call.kind == ReentrantCallKind::Stipend && !prior_calls.contains(&call.span) + }) + .map(|call| call.span) + .collect::>(); + summaries.push((calls, self.state_effects != prior_effects)); + } + + for (index, (calls, _)) in summaries.iter().enumerate() { + if summaries + .iter() + .enumerate() + .any(|(other_index, (_, has_effect))| other_index != index && *has_effect) + { + for &span in calls { + let call_can_succeed = assertion_condition.is_none_or(|condition| { + !condition.span.contains(span) + || self.send_can_succeed_for_outcome(condition, span, true, state) + }); + if call_can_succeed { + self.emit_stipend_call(span); + } + } + } + } + all_complete + } + + fn unordered_exprs_can_persist( + &mut self, + exprs: &[&'hir hir::Expr<'hir>], + state: &FlowState, + ) -> bool { + let inline_cache = std::mem::replace( + &mut self.inline_cache, + HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT), + ); + let prior_effects = self.state_effects; + let prior_successful_halt = self.completion_probe_successful_halt; + self.completion_probe_depth += 1; + self.completion_probe_successful_halt = false; + + let mut probe_state = state.clone(); + let mut all_complete = true; + for &expr in exprs { + all_complete &= self.analyze_expr(expr, &mut probe_state); + } + + let successful_halt = self.completion_probe_successful_halt; + self.completion_probe_successful_halt = prior_successful_halt; + self.completion_probe_depth -= 1; + self.state_effects = prior_effects; + self.inline_cache = inline_cache; + all_complete || successful_halt + } + + const fn record_state_effect(&mut self) { + if self.reentrancy_unlimited_gas_enabled { + self.state_effects = self.state_effects.saturating_add(1); + } + } + + fn remove_failed_send_calls( + &self, + condition: &'hir hir::Expr<'hir>, + outcome: bool, + state: &mut FlowState, + ) { + if !self.reentrancy_unlimited_gas_enabled { return; } + let calls = state + .pending_calls + .iter() + .filter(|call| { + call.kind == ReentrantCallKind::Stipend + && self.expr_contains_send_target(condition, call.span, state) + }) + .map(|call| call.span) + .collect::>(); + for span in calls { + if !self.send_can_succeed_for_outcome(condition, span, outcome, state) { + state.remove_call(span, ReentrantCallKind::Stipend); + } + } + } + + fn send_can_succeed_for_outcome( + &self, + condition: &'hir hir::Expr<'hir>, + span: Span, + outcome: bool, + state: &FlowState, + ) -> bool { + self.boolean_outcomes_for_send(condition, span, state).iter().any(|candidate| { + candidate.value == outcome && candidate.send == SendEvaluation::Succeeded + }) + } + + fn boolean_outcomes_for_send( + &self, + expr: &'hir hir::Expr<'hir>, + target: Span, + state: &FlowState, + ) -> Vec { + let expr = expr.peel_parens(); + match &expr.kind { + ExprKind::Call(callee, _, _) + if expr.span == target + && self.gcx.builtin_callee(callee.peel_parens().id) + == Some(Builtin::AddressPayableSend) => + { + vec![ + BooleanOutcome { value: true, send: SendEvaluation::Succeeded }, + BooleanOutcome { value: false, send: SendEvaluation::Failed }, + ] + } + ExprKind::Call(..) => { + let mut outcomes = Vec::new(); + for result in state.stored_call_results.iter().filter(|result| { + result.expression_span == expr.span + && result.index == 0 + && result.source.call_span == target + }) { + let (true_send, false_send) = if result.source.succeeds_when_true { + (SendEvaluation::Succeeded, SendEvaluation::Failed) + } else { + (SendEvaluation::Failed, SendEvaluation::Succeeded) + }; + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: true, send: true_send }, + ); + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: false, send: false_send }, + ); + } + if outcomes.is_empty() { + unconstrained_boolean_outcomes( + self.expr_contains_send_target(expr, target, state), + ) + } else { + outcomes + } + } + ExprKind::Lit(lit) => match lit.kind { + LitKind::Bool(value) => { + vec![BooleanOutcome { value, send: SendEvaluation::NotEvaluated }] + } + _ => unconstrained_boolean_outcomes(expr.span.contains(target)), + }, + ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => self + .boolean_outcomes_for_send(inner, target, state) + .into_iter() + .map(|outcome| BooleanOutcome { value: !outcome.value, ..outcome }) + .collect(), + ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => { + let lhs_outcomes = self.boolean_outcomes_for_send(lhs, target, state); + let rhs_outcomes = self.boolean_outcomes_for_send(rhs, target, state); + let mut outcomes = Vec::new(); + for lhs_outcome in lhs_outcomes { + let short_circuits = if op.kind == BinOpKind::And { + !lhs_outcome.value + } else { + lhs_outcome.value + }; + if short_circuits { + push_unique_boolean_outcome(&mut outcomes, lhs_outcome); + } else { + for rhs_outcome in &rhs_outcomes { + let Some(send) = + merge_send_evaluations(lhs_outcome.send, rhs_outcome.send) + else { + continue; + }; + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: rhs_outcome.value, send }, + ); + } + } + } + outcomes + } + ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => { + let lhs_outcomes = self.boolean_outcomes_for_send(lhs, target, state); + let rhs_outcomes = self.boolean_outcomes_for_send(rhs, target, state); + let mut outcomes = Vec::new(); + for lhs_outcome in lhs_outcomes { + for rhs_outcome in &rhs_outcomes { + let equal = lhs_outcome.value == rhs_outcome.value; + let Some(send) = merge_send_evaluations(lhs_outcome.send, rhs_outcome.send) + else { + continue; + }; + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { + value: if op.kind == BinOpKind::Eq { equal } else { !equal }, + send, + }, + ); + } + } + outcomes + } + ExprKind::Ternary(cond, true_expr, false_expr) => { + let condition_outcomes = self.boolean_outcomes_for_send(cond, target, state); + let true_outcomes = self.boolean_outcomes_for_send(true_expr, target, state); + let false_outcomes = self.boolean_outcomes_for_send(false_expr, target, state); + let mut outcomes = Vec::new(); + for condition_outcome in condition_outcomes { + let branch_outcomes = + if condition_outcome.value { &true_outcomes } else { &false_outcomes }; + for branch_outcome in branch_outcomes { + let Some(send) = + merge_send_evaluations(condition_outcome.send, branch_outcome.send) + else { + continue; + }; + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: branch_outcome.value, send }, + ); + } + } + outcomes + } + ExprKind::Ident(reses) => { + let mut outcomes = Vec::new(); + for variable in reses.iter().filter_map(|res| res.as_variable()) { + for result in state.stored_send_results.iter().filter(|result| { + result.variable == variable && result.source.call_span == target + }) { + let (true_send, false_send) = if result.source.succeeds_when_true { + (SendEvaluation::Succeeded, SendEvaluation::Failed) + } else { + (SendEvaluation::Failed, SendEvaluation::Succeeded) + }; + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: true, send: true_send }, + ); + push_unique_boolean_outcome( + &mut outcomes, + BooleanOutcome { value: false, send: false_send }, + ); + } + } + if outcomes.is_empty() { unconstrained_boolean_outcomes(false) } else { outcomes } + } + _ => { + unconstrained_boolean_outcomes(self.expr_contains_send_target(expr, target, state)) + } + } + } + + fn send_result_sources_by_index( + &self, + expr: &'hir hir::Expr<'hir>, + result_count: usize, + state: &FlowState, + ) -> Vec> { + if result_count == 1 { + return vec![self.send_result_sources(expr, state)]; + } + + let mut sources = vec![Vec::new(); result_count]; + match &expr.peel_parens().kind { + ExprKind::Tuple(exprs) => { + for (index, expr) in exprs.iter().take(result_count).enumerate() { + if let Some(expr) = expr { + sources[index] = self.send_result_sources(expr, state); + } + } + } + ExprKind::Call(..) => { + for result in state.stored_call_results.iter().filter(|result| { + result.expression_span == expr.span && result.index < result_count + }) { + if !sources[result.index].contains(&result.source) { + sources[result.index].push(result.source); + } + } + } + _ => {} + } + sources + } + + fn assignment_send_result_sources( + &self, + lhs: &'hir hir::Expr<'hir>, + rhs: &'hir hir::Expr<'hir>, + state: &FlowState, + ) -> Vec<(VariableId, Vec)> { + let mut assignments = Vec::new(); + self.collect_assignment_send_result_sources(lhs, rhs, state, &mut assignments); + assignments + } + + fn collect_assignment_send_result_sources( + &self, + lhs: &'hir hir::Expr<'hir>, + rhs: &'hir hir::Expr<'hir>, + state: &FlowState, + assignments: &mut Vec<(VariableId, Vec)>, + ) { + let ExprKind::Tuple(lhs_exprs) = &lhs.peel_parens().kind else { + for variable in assigned_variables(lhs) { + let sources = if assigned_variable(lhs) == Some(variable) { + self.send_result_sources(rhs, state) + } else { + Vec::new() + }; + assignments.push((variable, sources)); + } + return; + }; + + if let ExprKind::Tuple(rhs_exprs) = &rhs.peel_parens().kind { + for index in (0..lhs_exprs.len()).rev() { + if let Some(lhs) = lhs_exprs[index] + && let Some(rhs) = rhs_exprs.get(index).copied().flatten() + { + self.collect_assignment_send_result_sources(lhs, rhs, state, assignments); + } else if let Some(lhs) = lhs_exprs[index] { + assignments.extend( + assigned_variables(lhs).into_iter().map(|variable| (variable, Vec::new())), + ); + } + } + return; + } + + let sources = self.send_result_sources_by_index(rhs, lhs_exprs.len(), state); + for (index, lhs) in lhs_exprs.iter().enumerate().rev() { + if let Some(lhs) = lhs { + if let Some(variable) = assigned_variable(lhs) { + assignments.push((variable, sources[index].clone())); + } else { + assignments.extend( + assigned_variables(lhs).into_iter().map(|variable| (variable, Vec::new())), + ); + } + } + } + } + + fn named_return_result_sources( + &self, + function: FunctionId, + state: &FlowState, + ) -> Vec> { + self.hir + .function(function) + .returns + .iter() + .map(|variable| { + state + .stored_send_results + .iter() + .filter(|result| result.variable == *variable) + .map(|result| result.source) + .collect() + }) + .collect() + } + + fn return_result_sources( + &self, + function: FunctionId, + expr: Option<&'hir hir::Expr<'hir>>, + state: &FlowState, + ) -> Vec> { + if let Some(expr) = expr { + self.send_result_sources_by_index( + expr, + self.hir.function(function).returns.len(), + state, + ) + } else { + self.named_return_result_sources(function, state) + } + } + + fn send_result_sources( + &self, + expr: &'hir hir::Expr<'hir>, + state: &FlowState, + ) -> Vec { + let expr = expr.peel_parens(); + match &expr.kind { + ExprKind::Call(callee, _, _) + if self.gcx.builtin_callee(callee.peel_parens().id) + == Some(Builtin::AddressPayableSend) => + { + vec![SendResultSource { call_span: expr.span, succeeds_when_true: true }] + } + ExprKind::Call(..) => state + .stored_call_results + .iter() + .filter(|result| result.expression_span == expr.span && result.index == 0) + .map(|result| result.source) + .collect(), + ExprKind::Ident(reses) => { + let mut sources = Vec::new(); + for variable in reses.iter().filter_map(|res| res.as_variable()) { + for result in state + .stored_send_results + .iter() + .filter(|result| result.variable == variable) + { + if !sources.contains(&result.source) { + sources.push(result.source); + } + } + } + sources + } + ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => self + .send_result_sources(inner, state) + .into_iter() + .map(|source| SendResultSource { + succeeds_when_true: !source.succeeds_when_true, + ..source + }) + .collect(), + ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => { + self.path_sensitive_send_result_sources(expr, [lhs, rhs], state) + } + ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne) => { + let (source_expr, literal) = if let Some(literal) = bool_literal(rhs) { + (lhs, literal) + } else if let Some(literal) = bool_literal(lhs) { + (rhs, literal) + } else { + return Vec::new(); + }; + let invert = (op.kind == BinOpKind::Eq) != literal; + self.send_result_sources(source_expr, state) + .into_iter() + .map(|source| SendResultSource { + succeeds_when_true: source.succeeds_when_true != invert, + ..source + }) + .collect() + } + ExprKind::Ternary(condition, true_expr, false_expr) => self + .path_sensitive_send_result_sources( + expr, + [condition, true_expr, false_expr], + state, + ), + _ => Vec::new(), + } + } + + fn path_sensitive_send_result_sources( + &self, + expr: &'hir hir::Expr<'hir>, + children: [&'hir hir::Expr<'hir>; N], + state: &FlowState, + ) -> Vec { + let mut candidates = Vec::new(); + for child in children { + for source in self.send_result_sources(child, state) { + if !candidates.contains(&source) { + candidates.push(source); + } + } + } + + candidates + .into_iter() + .filter_map(|source| { + let mut succeeds_when = None; + for outcome in self.boolean_outcomes_for_send(expr, source.call_span, state) { + if outcome.send != SendEvaluation::Succeeded { + continue; + } + if succeeds_when.is_some_and(|value| value != outcome.value) { + return None; + } + succeeds_when = Some(outcome.value); + } + succeeds_when + .map(|succeeds_when_true| SendResultSource { succeeds_when_true, ..source }) + }) + .collect() + } + + fn expr_contains_send_target( + &self, + expr: &'hir hir::Expr<'hir>, + target: Span, + state: &FlowState, + ) -> bool { + expr.span.contains(target) + || state.stored_send_results.iter().any(|result| { + result.source.call_span == target && expr_references_variable(expr, result.variable) + }) + || state.stored_call_results.iter().any(|result| { + result.source.call_span == target && expr.span.contains(result.expression_span) + }) + } + + fn analyze_internal_call( + &mut self, + func_id: FunctionId, + state: &mut FlowState, + ) -> Option>> { + if self.call_stack.contains(&func_id) { + return Some(vec![Vec::new(); self.hir.function(func_id).returns.len()]); + } + let func = self.hir.function(func_id); - let Some(body) = func.body else { return }; + let Some(body) = func.body else { return Some(vec![Vec::new(); func.returns.len()]) }; let key = InlineCallKey { func_id, @@ -444,21 +1573,93 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { state: state.clone(), }; if self.inline_cache.is_in_progress(&key) { - return; + return Some(vec![Vec::new(); func.returns.len()]); } if let Some(cached) = self.inline_cache.get(&key) { - *state = cached.clone(); - return; + *state = cached.state.clone(); + let may_return = cached.may_return; + let return_sources = cached.return_sources.clone(); + if cached.has_state_effect { + self.record_state_effect(); + } + return may_return.then_some(return_sources); } let mut after = state.clone(); + after.clear_stored_return_results(func_id); + let prior_effects = self.state_effects; self.inline_cache.start(key.clone()); self.call_stack.push(func_id); - self.analyze_callable(func, body, &mut after); + self.call_returns.push(CallReturns { function: Some(func_id), state: None }); + let falls_through = self.analyze_callable(func, body, &mut after); + let mut returned_state = self.call_returns.pop().expect("call return state exists").state; self.call_stack.pop(); - self.inline_cache.finish(key, after.clone()); + if falls_through { + if !after.stored_return_results.iter().any(|result| result.function == func_id) { + let sources = self.named_return_result_sources(func_id, &after); + after.set_stored_return_results(func_id, &sources); + } + merge_optional_state(&mut returned_state, &after); + } + let may_return = returned_state.is_some(); + after = returned_state.unwrap_or_default(); + let return_sources = (0..func.returns.len()) + .map(|index| { + after + .stored_return_results + .iter() + .filter(|result| result.function == func_id && result.index == index) + .map(|result| result.source) + .collect() + }) + .collect::>(); + after.clear_stored_return_results(func_id); + + self.inline_cache.finish( + key, + InlineCallResult { + state: after.clone(), + may_return, + has_state_effect: self.state_effects != prior_effects, + return_sources: return_sources.clone(), + }, + ); *state = after; + may_return.then_some(return_sources) + } + + fn bind_call_argument_sources( + &self, + func_id: FunctionId, + args: &CallArgs<'hir>, + state: &mut FlowState, + ) { + let func = self.hir.function(func_id); + let bindings = match args.kind { + CallArgsKind::Unnamed(args) => func + .parameters + .iter() + .copied() + .zip(args) + .map(|(parameter, argument)| (parameter, self.send_result_sources(argument, state))) + .collect::>(), + CallArgsKind::Named(args) => args + .iter() + .filter_map(|argument| { + func.parameters + .iter() + .copied() + .find(|¶meter| self.hir.variable(parameter).name == Some(argument.name)) + .map(|parameter| { + (parameter, self.send_result_sources(&argument.value, state)) + }) + }) + .collect(), + }; + for (parameter, sources) in bindings { + state.set_stored_send_results(parameter, &sources); + } } fn first_recursive_cut(&mut self, func_id: FunctionId) -> Option { @@ -558,7 +1759,10 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { self.collect_direct_internal_calls_expr(expr, calls); } } - StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => { + StmtKind::Block(block) + | StmtKind::UncheckedBlock(block) + | StmtKind::AssemblyBlock(block) + | StmtKind::Loop(block, _) => { self.collect_direct_internal_calls_block(block, calls); } StmtKind::If(cond, then_stmt, else_stmt) => { @@ -574,12 +1778,13 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { self.collect_direct_internal_calls_block(clause.block, calls); } } - StmtKind::Break - | StmtKind::Continue - | StmtKind::Placeholder - | StmtKind::AssemblyBlock(_) - | StmtKind::Switch(_) - | StmtKind::Err(_) => {} + StmtKind::Switch(switch) => { + self.collect_direct_internal_calls_expr(switch.selector, calls); + for case in switch.cases { + self.collect_direct_internal_calls_block(case.body, calls); + } + } + StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder | StmtKind::Err(_) => {} } } @@ -609,7 +1814,7 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { for arg in args.exprs() { self.collect_direct_internal_calls_expr(arg, calls); } - for func_id in resolved_function_ids(callee) { + for func_id in resolved_function_ids(self.gcx, self.hir, callee) { calls.insert(func_id); } } @@ -653,36 +1858,46 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { } } - fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) { + fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut FlowState) -> bool { match &expr.kind { ExprKind::Index(base, index) => { - self.analyze_lhs_indices(base, state); + let mut completes = self.analyze_lhs_indices(base, state); if let Some(index) = index { - self.analyze_expr(index, state); + completes &= self.analyze_expr(index, state); } + completes } ExprKind::Slice(base, start, end) => { - self.analyze_lhs_indices(base, state); + let mut completes = self.analyze_lhs_indices(base, state); if let Some(start) = start { - self.analyze_expr(start, state); + completes &= self.analyze_expr(start, state); } if let Some(end) = end { - self.analyze_expr(end, state); + completes &= self.analyze_expr(end, state); } + completes } ExprKind::Member(base, _) | ExprKind::Payable(base) => { - self.analyze_lhs_indices(base, state); + self.analyze_lhs_indices(base, state) } ExprKind::Tuple(exprs) => { + let mut completes = true; for expr in exprs.iter().copied().flatten() { - self.analyze_lhs_indices(expr, state); + completes &= self.analyze_lhs_indices(expr, state); } + completes } - _ => {} + ExprKind::Call(..) => self.analyze_expr(expr, state), + _ => true, } } fn emit_pending_calls(&mut self, state: &FlowState, written_vars: &[VariableId]) { + if self.completion_probe_depth > 0 { + return; + } + self.emit_pending_stipend_calls(state); + for call in &state.pending_calls { let (lint, msg_prefix) = match call.kind { ReentrantCallKind::Eth => { @@ -691,6 +1906,7 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { ReentrantCallKind::NoEth => { (&REENTRANCY_NO_ETH, "external call can be reentered before") } + ReentrantCallKind::Stipend => continue, }; if !self.ctx.is_lint_enabled(lint.id) || self.emitted.contains(&call.span) { continue; @@ -715,12 +1931,36 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { } } + fn emit_pending_stipend_calls(&mut self, state: &FlowState) { + if !self.reentrancy_unlimited_gas_enabled { + return; + } + + for call in &state.pending_calls { + if call.kind == ReentrantCallKind::Stipend { + self.emit_stipend_call(call.span); + } + } + } + + fn emit_stipend_call(&mut self, span: Span) { + if self.completion_probe_depth == 0 + && self.reentrancy_unlimited_gas_enabled + && self.emitted.insert(span) + { + self.ctx.emit(&REENTRANCY_UNLIMITED_GAS, span); + } + } + fn reentrant_call_kind( &self, callee: &'hir hir::Expr<'hir>, args: &CallArgs<'hir>, opts: Option<&hir::CallOptions<'hir>>, ) -> Option { + if self.reentrancy_unlimited_gas_enabled && is_stipend_value_call(self.gcx, callee) { + return Some(ReentrantCallKind::Stipend); + } if self.reentrancy_eth_enabled && is_uncapped_value_call(self.hir, callee, opts) { return Some(ReentrantCallKind::Eth); } @@ -733,14 +1973,136 @@ impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> { } } +fn assigned_variable(expr: &hir::Expr<'_>) -> Option { + let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return None }; + unique(reses.iter().filter_map(|res| res.as_variable())) +} + +fn assigned_variables(expr: &hir::Expr<'_>) -> Vec { + fn collect(expr: &hir::Expr<'_>, variables: &mut Vec) { + match &expr.peel_parens().kind { + ExprKind::Ident(reses) => { + if let Some(variable) = unique(reses.iter().filter_map(|res| res.as_variable())) + && !variables.contains(&variable) + { + variables.push(variable); + } + } + ExprKind::Tuple(exprs) => { + for expr in exprs.iter().flatten() { + collect(expr, variables); + } + } + _ => {} + } + } + + let mut variables = Vec::new(); + collect(expr, &mut variables); + variables +} + +fn bool_literal(expr: &hir::Expr<'_>) -> Option { + let ExprKind::Lit(lit) = &expr.peel_parens().kind else { return None }; + let LitKind::Bool(value) = lit.kind else { return None }; + Some(value) +} + +fn expr_references_variable(expr: &hir::Expr<'_>, variable: VariableId) -> bool { + match &expr.peel_parens().kind { + ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => { + expr_references_variable(lhs, variable) || expr_references_variable(rhs, variable) + } + ExprKind::Unary(_, inner) + | ExprKind::Delete(inner) + | ExprKind::Member(inner, _) + | ExprKind::Payable(inner) => expr_references_variable(inner, variable), + ExprKind::Call(callee, args, opts) => { + expr_references_variable(callee, variable) + || opts.is_some_and(|opts| { + opts.args.iter().any(|opt| expr_references_variable(&opt.value, variable)) + }) + || args.exprs().any(|arg| expr_references_variable(arg, variable)) + } + ExprKind::Index(base, index) => { + expr_references_variable(base, variable) + || index.is_some_and(|index| expr_references_variable(index, variable)) + } + ExprKind::Slice(base, start, end) => { + expr_references_variable(base, variable) + || start.is_some_and(|start| expr_references_variable(start, variable)) + || end.is_some_and(|end| expr_references_variable(end, variable)) + } + ExprKind::Ternary(condition, true_expr, false_expr) => { + expr_references_variable(condition, variable) + || expr_references_variable(true_expr, variable) + || expr_references_variable(false_expr, variable) + } + ExprKind::Array(exprs) => exprs.iter().any(|expr| expr_references_variable(expr, variable)), + ExprKind::Tuple(exprs) => { + exprs.iter().flatten().any(|expr| expr_references_variable(expr, variable)) + } + ExprKind::Ident(reses) => reses.iter().any(|res| res.as_variable() == Some(variable)), + ExprKind::Lit(_) + | ExprKind::New(_) + | ExprKind::TypeCall(_) + | ExprKind::Type(_) + | ExprKind::YulMember(..) + | ExprKind::Err(_) => false, + } +} + +fn is_stipend_value_call<'hir>(gcx: Gcx<'hir>, callee: &'hir hir::Expr<'hir>) -> bool { + matches!( + gcx.builtin_callee(callee.peel_parens().id), + Some(Builtin::AddressPayableTransfer | Builtin::AddressPayableSend) + ) +} + impl FlowState { fn clear(&mut self) { self.state_reads.clear(); self.pending_calls.clear(); + self.stored_send_results.clear(); + self.stored_call_results.clear(); + self.stored_return_results.clear(); } fn merge(&mut self, other: &Self) { self.state_reads.extend(other.state_reads.iter().copied()); + let self_stipend_calls = self + .pending_calls + .iter() + .filter(|call| call.kind == ReentrantCallKind::Stipend) + .map(|call| call.span) + .collect::>(); + let other_stipend_calls = other + .pending_calls + .iter() + .filter(|call| call.kind == ReentrantCallKind::Stipend) + .map(|call| call.span) + .collect::>(); + let stored_send_results = merge_send_correlations( + &self.stored_send_results, + &other.stored_send_results, + &self_stipend_calls, + &other_stipend_calls, + |result| result.source, + ); + let stored_call_results = merge_send_correlations( + &self.stored_call_results, + &other.stored_call_results, + &self_stipend_calls, + &other_stipend_calls, + |result| result.source, + ); + let stored_return_results = merge_send_correlations( + &self.stored_return_results, + &other.stored_return_results, + &self_stipend_calls, + &other_stipend_calls, + |result| result.source, + ); for call in &other.pending_calls { if let Some(existing) = self .pending_calls @@ -748,11 +2110,34 @@ impl FlowState { .find(|existing| existing.span == call.span && existing.kind == call.kind) { existing.state_reads.extend(call.state_reads.iter().copied()); + existing.result_correlatable &= call.result_correlatable; } else { self.pending_calls.push(call.clone()); } } + self.stored_send_results = stored_send_results; + self.stored_call_results = stored_call_results; + self.stored_return_results = stored_return_results; + } +} + +fn merge_send_correlations( + lhs: &[T], + rhs: &[T], + lhs_calls: &HashSet, + rhs_calls: &HashSet, + source: impl Fn(&T) -> SendResultSource, +) -> Vec { + let mut merged = Vec::new(); + for &result in lhs.iter().chain(rhs) { + let source = source(&result); + let valid_on_lhs = !lhs_calls.contains(&source.call_span) || lhs.contains(&result); + let valid_on_rhs = !rhs_calls.contains(&source.call_span) || rhs.contains(&result); + if valid_on_lhs && valid_on_rhs && !merged.contains(&result) { + merged.push(result); + } } + merged } fn is_uncapped_value_call( @@ -1211,16 +2596,45 @@ fn gas_option_forwards_all(expr: &hir::Expr<'_>) -> bool { } fn resolved_function_ids<'hir>( + gcx: Gcx<'hir>, + hir: &'hir hir::Hir<'hir>, callee: &'hir hir::Expr<'hir>, -) -> impl Iterator + 'hir { +) -> impl Iterator + use<'hir> { + let resolved = + gcx.resolved_callee(callee.peel_parens().id).and_then(|resolved| match resolved.res { + Res::Item(ItemId::Function(func_id)) + if is_internal_function_dispatch(hir, callee, func_id) => + { + Some(func_id) + } + _ => None, + }); let reses = match &callee.peel_parens().kind { ExprKind::Ident(reses) => *reses, _ => &[], }; - reses.iter().filter_map(|res| match res { - Res::Item(ItemId::Function(func_id)) => Some(*func_id), + resolved.into_iter().chain(reses.iter().filter_map(move |res| match res { + Res::Item(ItemId::Function(func_id)) if resolved.is_none() => Some(*func_id), _ => None, - }) + })) +} + +fn is_internal_function_dispatch( + hir: &hir::Hir<'_>, + callee: &hir::Expr<'_>, + func_id: FunctionId, +) -> bool { + match &callee.peel_parens().kind { + ExprKind::Ident(_) => true, + ExprKind::Member(receiver, _) => { + is_super(receiver) + || matches!( + hir.function(func_id).visibility, + Visibility::Internal | Visibility::Private + ) + } + _ => false, + } } fn state_write_lhs_vars(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Vec { @@ -1229,6 +2643,43 @@ fn state_write_lhs_vars(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> Vec, + hir: &hir::Hir<'_>, + expr: &hir::Expr<'_>, + storage_ref_is_dereferenced: bool, +) -> bool { + match &expr.kind { + ExprKind::Ident(reses) => reses.iter().any(|res| { + matches!( + res, + Res::Item(ItemId::Variable(var_id)) + if hir.variable(*var_id).kind.is_state() + || (storage_ref_is_dereferenced + && hir.variable(*var_id).data_location + == Some(DataLocation::Storage)) + ) + }), + ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => { + is_storage_write_lhs(gcx, hir, base, true) + } + ExprKind::Payable(base) | ExprKind::Unary(_, base) | ExprKind::Delete(base) => { + is_storage_write_lhs(gcx, hir, base, storage_ref_is_dereferenced) + } + ExprKind::Call(callee, _, _) => { + is_state_mutating_array_call(gcx, callee) + || (storage_ref_is_dereferenced + && gcx + .type_of_expr(expr.peel_parens().id) + .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage))) + } + ExprKind::Tuple(exprs) => { + exprs.iter().copied().flatten().any(|expr| is_storage_write_lhs(gcx, hir, expr, false)) + } + _ => false, + } +} + fn collect_state_write_lhs_vars( hir: &hir::Hir<'_>, expr: &hir::Expr<'_>, diff --git a/crates/lint/testdata/ReentrancyUnlimitedGas.sol b/crates/lint/testdata/ReentrancyUnlimitedGas.sol new file mode 100644 index 0000000000000..b1a3da4b7e623 --- /dev/null +++ b/crates/lint/testdata/ReentrancyUnlimitedGas.sol @@ -0,0 +1,673 @@ +//@compile-flags: --only-lint reentrancy-unlimited-gas + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +interface ICustomTransfers { + function transfer(uint256 amount) external; + function send(uint256 amount) external returns (bool); +} + +contract SameNamedMembers { + uint256 public counter; + + function transfer(uint256) external { + counter++; + } + + function send(uint256) external returns (bool) { + counter++; + return true; + } +} + +library AttachedTransfers { + function transfer(address payable, string memory) internal pure {} + + function send(address payable, string memory) internal pure returns (bool) { + return true; + } +} + +contract ReentrancyUnlimitedGasBase { + uint256 internal inheritedCounter; + + function inheritedWrite() internal { + inheritedCounter++; + } + + function inheritedTransfer(address payable recipient) internal { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } +} + +contract ReentrancyUnlimitedGas is ReentrancyUnlimitedGasBase { + using AttachedTransfers for address payable; + + struct Record { + uint256 value; + } + + event Paid(address indexed recipient, uint256 amount); + event SendResult(bool success); + + mapping(address => uint256) public balances; + mapping(address => Record) public records; + Record[] public recordValues; + uint256[] public values; + uint256 public counter; + + modifier writeAfter() { + _; + counter += 1; + } + + modifier passthroughOne() { + _; + } + + modifier passthroughTwo() { + _; + } + + function stateWriteAfterTransfer(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + counter = 1; + } + + function stateWriteAfterSend(address payable recipient) external { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + balances[recipient] = success ? 1 : 0; + } + + function eventAfterTransfer(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + emit Paid(recipient, 1 wei); + } + + function eventAfterCheckedSend(address payable recipient) external { + require(recipient.send(1 wei)); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + emit Paid(recipient, 1 wei); + } + + function failedSendBranchDoesNotCallback(address payable recipient) external { + if (!recipient.send(1 wei)) { + counter = 1; + } + } + + function storedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success = recipient.send(1 wei); + if (!success) { + counter = 1; + } + } + + function ternaryStoredFailedSendDoesNotCallback( + address payable recipient, + bool attempt + ) external { + bool success = attempt ? recipient.send(1 wei) : false; + if (!success) counter = 1; + } + + function andStoredFailedSendDoesNotCallback(address payable recipient, bool attempt) external { + bool success = attempt && recipient.send(1 wei); + if (!success) counter = 1; + } + + function orStoredFailedSendDoesNotCallback(address payable recipient, bool skip) external { + bool success = skip || recipient.send(1 wei); + if (!success) counter = 1; + } + + function repeatedAndStoredFailedSendDoesNotCallback(address payable recipient) external { + bool sent = recipient.send(1 wei); + bool success = sent && sent; + if (!success) counter = 1; + } + + function repeatedTernaryStoredFailedSendDoesNotCallback(address payable recipient) external { + bool sent = recipient.send(1 wei); + bool success = sent ? sent : false; + if (!success) counter = 1; + } + + function storedSuccessfulSendBranch(address payable recipient) external { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (success) { + counter = 1; + } + } + + function tupleDeclaredFailedSendBranchDoesNotCallback(address payable recipient) external { + (bool success, uint256 value) = (recipient.send(1 wei), 1); + if (!success) counter = value; + } + + function tupleDeclaredSuccessfulSendBranch(address payable recipient) external { + (bool success, uint256 value) = (recipient.send(1 wei), 1); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (success) counter = value; + } + + function tupleDeclarationDoesNotCorrelateSibling(address payable recipient) external { + (bool success, bool write) = (recipient.send(1 wei), true); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (write) counter = success ? 1 : 0; + } + + function tupleAssignedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success; + uint256 value; + (success, value) = (recipient.send(1 wei), 1); + if (!success) counter = value; + } + + function tupleAssignedSuccessfulSendBranch(address payable recipient) external { + bool success; + uint256 value; + (success, value) = (recipient.send(1 wei), 1); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (success) counter = value; + } + + function tupleAssignedSecondFailedSendBranchDoesNotCallback(address payable recipient) + external + { + bool write; + bool success; + (write, success) = (true, recipient.send(1 wei)); + if (!success) counter = write ? 1 : 0; + } + + function tupleAssignmentDoesNotCorrelateSibling(address payable recipient, bool writeValue) + external + { + bool success; + bool write; + (success, write) = (recipient.send(1 wei), writeValue); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (!write) counter = success ? 1 : 0; + } + + function nestedTupleAssignmentOverwritesSendResult(address payable recipient) external { + bool success; + bool sibling; + ((success, sibling), success) = ((false, true), recipient.send(1 wei)); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (!success) counter = 1; + } + + function overwrittenStoredSendCanSucceed(address payable recipient, bool overwrite) external { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (overwrite) success = false; + if (!success) counter = 1; + } + + function tupleOverwrittenStoredSendCanSucceed(address payable recipient, address target) + external + { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + bytes memory data; + (success, data) = target.call(""); + if (!success) counter = data.length; + } + + function deletedStoredSendCanSucceed(address payable recipient) external { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + delete success; + if (!success) counter = 1; + } + + function repeatedStoredSendCanSucceed(address payable recipient, uint256 count) external { + bool success; + for (uint256 i; i < count; i++) { + success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + if (!success) counter = 1; + } + + function failedSendContinuationDoesNotCallback(address payable recipient) external { + require(!recipient.send(1 wei)); + counter = 1; + } + + function failedSendAssertionDoesNotCallback(address payable recipient) external { + assert(!recipient.send(1 wei)); + counter = 1; + } + + function successfulSendRequireArgument(address payable recipient) external { + require(recipient.send(1 wei), writeMessage()); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function failedSendEqualityDoesNotCallback(address payable recipient) external { + if (recipient.send(1 wei) == false) { + counter = 1; + } + } + + function failedSendTernaryDoesNotCallback(address payable recipient) external { + uint256 result = recipient.send(1 wei) ? counter : counter++; + consume(result, 0); + } + + function failedSendShortCircuitDoesNotCallback(address payable recipient) external { + bool result = !recipient.send(1 wei) && ++counter > 0; + if (result) return; + } + + function rhsFailedSendDoesNotCallback(address payable recipient) external { + if (recipient == address(0) && recipient.send(1 wei)) return; + counter = 1; + } + + function rhsFailedSendOrDoesNotCallback(address payable recipient) external { + if (recipient != address(0) || recipient.send(1 wei)) return; + counter = 1; + } + + function successfulSendShortCircuit(address payable recipient) external { + bool result = recipient.send(1 wei) && ++counter > 0; //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (result) return; + } + + function successfulSendBranchReturns(address payable recipient) external { + if (recipient.send(1 wei)) { + return; + } + counter = 1; + } + + function failedSendBranchReturns(address payable recipient) external { + if (!recipient.send(1 wei)) { //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + return; + } + counter = 1; + } + + function sendInsideEvent(address payable recipient) external { + emit SendResult(recipient.send(1 wei)); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function zeroValueStillCallsRecipient(address payable recipient) external { + recipient.transfer(0); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + counter = 1; + } + + function siblingEvaluationOrderIsUnspecified(address payable recipient) external { + consume(counter++, recipient.send(1 wei) ? 1 : 0); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function nonReturningSiblingDoesNotPersistEffects(address payable recipient) external { + consume(recipient.send(1 wei), writeThenRevert()); + } + + function successfulHaltSiblingPersistsEffects(address payable recipient) external { + consume(recipient.send(1 wei), writeThenStop()); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function helperThenState(address payable recipient) external { + transferInHelper(recipient); + counter = 1; + } + + function returnedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success = sendInHelper(recipient); + if (!success) counter = 1; + } + + function directReturnedFailedSendBranchDoesNotCallback(address payable recipient) external { + if (!sendInHelper(recipient)) counter = 1; + } + + function modifiedReturnedFailedSendBranchDoesNotCallback(address payable recipient) external { + if (!modifiedSendInHelper(recipient)) counter = 1; + } + + function tupleReturnedFailedSendBranchDoesNotCallback(address payable recipient) external { + (bool write, bool success) = tupleSendInHelper(recipient); + if (!success) counter = write ? 1 : 0; + } + + function tupleReturnedSiblingCanFollowSuccessfulSend(address payable recipient) external { + (bool write, bool success) = tupleSendInHelper(recipient); + if (write) counter = success ? 1 : 0; + } + + function namedReturnedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success = namedSendInHelper(recipient); + if (!success) counter = 1; + } + + function returnedSuccessfulSendBranch(address payable recipient) external { + bool success = successfulSendInHelper(recipient); + if (success) counter = 1; + } + + function passedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success = recipient.send(1 wei); + writeOnFailedSend(success); + } + + function passedSuccessfulSendBranch(address payable recipient) external { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + writeOnSuccessfulSend(success); + } + + function namedPassedFailedSendBranchDoesNotCallback(address payable recipient) external { + bool success = recipient.send(1 wei); + writeOnFailedSend({success: success}); + } + + function overriddenHelperResultCanFollowSuccessfulSend( + address payable recipient, + bool overrideResult + ) external { + bool success = overriddenSendInHelper(recipient, overrideResult); + if (!success) counter = 1; + } + + function modifierWritesAfter(address payable recipient) external writeAfter { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function modifierWritesAfterReturn(address payable recipient) external writeAfter { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + return; + } + + function loopCarriedWrite(address payable recipient, uint256 count) external { + for (uint256 i; i < count; i++) { + counter++; + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + } + + function continueRunsForUpdate(address payable recipient, uint256 count) external { + for (; counter < count; counter++) { + if (recipient.send(1 wei)) { //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + continue; + } + return; + } + } + + function doWhileContinueCanExit(address payable recipient, bool again) external { + do { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + continue; + } while (again); + counter = 1; + } + + function doWhileContinueEvaluatesCondition(address payable recipient) external { + do { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + continue; + } while (++counter < 2); + } + + function terminatingLoopDoesNotFallThrough(address payable recipient) external { + for (;;) { + if (recipient == address(0)) break; + recipient.transfer(1 wei); + return; + } + counter = 1; + } + + function effectsBeforeInteraction(address payable recipient) external { + counter = 1; + emit Paid(recipient, 1 wei); + recipient.transfer(1 wei); + } + + function noEffectAfter(address payable recipient) external { + recipient.transfer(1 wei); + } + + function localWriteAfter(address payable recipient) external returns (uint256 local) { + recipient.transfer(1 wei); + local = 1; + } + + function mutuallyExclusiveEffects(address payable recipient, bool transferFirst) external { + if (transferFirst) { + recipient.transfer(1 wei); + } else { + counter = 1; + emit Paid(recipient, 1 wei); + } + } + + function lowLevelGasCapIsOutOfScope(address payable recipient) external { + (bool success,) = recipient.call{value: 1 wei, gas: 2_300}(""); + require(success); + counter = 1; + } + + function uncappedCallIsAnotherDetector(address payable recipient) external { + (bool success,) = recipient.call{value: 1 wei}(""); + require(success); + counter = 1; + } + + function customNamesAreNotBuiltins(ICustomTransfers custom) external { + custom.transfer(1); + custom.send(1); + counter = 1; + emit Paid(address(custom), 1); + } + + function implementedCustomMethodsAreNotBuiltins(SameNamedMembers custom) external { + custom.transfer(1); + custom.send(1); + counter = 1; + } + + function attachedNamesAreNotBuiltins(address payable recipient) external { + recipient.transfer("not ether"); + recipient.send("not ether"); + counter = 1; + } + + function storageAliasWrite(address payable recipient) external { + Record storage record = records[recipient]; + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + record.value++; + } + + function storageParameterWrite(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + writeRecord(records[recipient]); + } + + function arrayPush(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + values.push(1); + } + + function arrayPop(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + values.pop(); + } + + function arrayPushReferenceWrite(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + recordValues.push().value++; + } + + function assemblyStorageWrite(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + assembly { + sstore(counter.slot, 1) + } + } + + function assemblySwitchStorageWrite(address payable recipient, uint256 choice) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + assembly { + switch choice + case 0 { sstore(counter.slot, 1) } + } + } + + function yulForContinueRunsPost(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + assembly { + for { let i := 0 } lt(i, 1) { sstore(counter.slot, 1) } { + i := add(i, 1) + continue + } + } + } + + function storageReturnWrite(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + recordFor(recipient).value++; + } + + function storageReferenceBindingDoesNotWrite(address payable recipient) external { + recipient.transfer(1 wei); + Record storage record = recordFor(recipient); + if (record.value == type(uint256).max) return; + } + + function storageReturnTransfer(address payable recipient) external { + recordAfterTransfer(recipient).value++; + } + + function inheritedHelperWrite(address payable recipient) external { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + super.inheritedWrite(); + } + + function inheritedHelperTransfer(address payable recipient) external { + super.inheritedTransfer(recipient); + counter = 1; + } + + function earlyReturningHelper(address payable recipient, bool stop) external { + transferThenMaybeReturn(recipient, stop); + counter = 1; + } + + function revertingHelperDoesNotContinue(address payable recipient) external { + transferThenRevert(recipient); + counter = 1; + } + + function recursiveHelper(address payable recipient, uint256 depth) external { + recursiveTransfer(recipient, depth); + counter = 1; + } + + constructor(address payable recipient) payable { + recipient.transfer(1 wei); + counter = 1; + emit Paid(recipient, 1 wei); + } + + function transferInHelper(address payable recipient) internal { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function sendInHelper(address payable recipient) internal returns (bool) { + return recipient.send(1 wei); + } + + function modifiedSendInHelper(address payable recipient) + internal + passthroughOne + passthroughTwo + returns (bool) + { + return recipient.send(1 wei); + } + + function tupleSendInHelper(address payable recipient) + internal + returns (bool write, bool success) + { + return (true, recipient.send(1 wei)); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function namedSendInHelper(address payable recipient) internal returns (bool success) { + success = recipient.send(1 wei); + } + + function successfulSendInHelper(address payable recipient) internal returns (bool) { + return recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + } + + function overriddenSendInHelper(address payable recipient, bool overrideResult) + internal + returns (bool) + { + bool success = recipient.send(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + if (overrideResult) return false; + return success; + } + + function writeOnFailedSend(bool success) internal { + if (!success) counter = 1; + } + + function writeOnSuccessfulSend(bool success) internal { + if (success) counter = 1; + } + + function consume(uint256, uint256) internal pure {} + + function consume(bool, uint256) internal pure {} + + function writeThenRevert() internal returns (uint256) { + counter++; + revert(); + } + + function writeThenStop() internal returns (uint256) { + counter++; + assembly { + stop() + } + } + + function writeRecord(Record storage record) internal { + record.value++; + } + + function writeMessage() internal returns (string memory) { + counter++; + return "failed send"; + } + + function recordFor(address recipient) internal view returns (Record storage record) { + record = records[recipient]; + } + + function recordAfterTransfer(address payable recipient) + internal + returns (Record storage record) + { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + record = records[recipient]; + } + + function transferThenMaybeReturn(address payable recipient, bool stop) internal { + if (stop) { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + return; + } + } + + function transferThenRevert(address payable recipient) internal { + recipient.transfer(1 wei); + revert(); + } + + function recursiveTransfer(address payable recipient, uint256 depth) internal { + if (depth == 0) { + recipient.transfer(1 wei); //~NOTE: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + return; + } + recursiveTransfer(recipient, depth - 1); + } +} diff --git a/crates/lint/testdata/ReentrancyUnlimitedGas.stderr b/crates/lint/testdata/ReentrancyUnlimitedGas.stderr new file mode 100644 index 0000000000000..c2ccb147ce914 --- /dev/null +++ b/crates/lint/testdata/ReentrancyUnlimitedGas.stderr @@ -0,0 +1,368 @@ +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … require(recipient.send(1 wei)); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … (bool success, uint256 value) = (recipient.send(1 wei), 1); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … (bool success, bool write) = (recipient.send(1 wei), true); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … (success, value) = (recipient.send(1 wei), 1); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … (success, write) = (recipient.send(1 wei), writeValue); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … ((success, sibling), success) = ((false, true), recipient.send(1 wei)); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … require(recipient.send(1 wei), writeMessage()); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool result = recipient.send(1 wei) && ++counter > 0; + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … if (!recipient.send(1 wei)) { + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … emit SendResult(recipient.send(1 wei)); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ recipient.transfer(0); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … consume(counter++, recipient.send(1 wei) ? 1 : 0); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … consume(recipient.send(1 wei), writeThenStop()); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … return (true, recipient.send(1 wei)); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … return recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … bool success = recipient.send(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … if (recipient.send(1 wei)) { + │ ━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas + +note[reentrancy-unlimited-gas]: state change or event emission follows `transfer`/`send`; gas repricing could enable reentrancy + ╭▸ ROOT/testdata/ReentrancyUnlimitedGas.sol:LL:CC + │ +LL │ … recipient.transfer(1 wei); + │ ━━━━━━━━━━━━━━━━━━━━━━━━━ + │ + ╰ help: https://getfoundry.sh/forge/linting/reentrancy-unlimited-gas +