foundry_evm_fuzz/inspector.rs
1use crate::{invariant::RandomCallGenerator, strategies::EvmFuzzState};
2use foundry_common::mapping_slots::step as mapping_step;
3use foundry_evm_core::constants::CHEATCODE_ADDRESS;
4use revm::{
5 Inspector,
6 context::{ContextTr, JournalTr, Transaction},
7 inspector::JournalExt,
8 interpreter::{CallInput, CallInputs, CallOutcome, CallScheme, CallValue, Interpreter},
9};
10
11/// An inspector that can fuzz and collect data for that effect.
12#[derive(Clone, Debug)]
13pub struct Fuzzer {
14 /// If set, it collects `stack` and `memory` values for fuzzing purposes.
15 pub collect: bool,
16 /// Given a strategy, it generates a random call.
17 pub call_generator: Option<RandomCallGenerator>,
18 /// If `collect` is set, we store the collected values in this fuzz dictionary.
19 pub fuzz_state: EvmFuzzState,
20}
21
22impl<CTX> Inspector<CTX> for Fuzzer
23where
24 CTX: ContextTr<Journal: JournalExt>,
25{
26 #[inline]
27 fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
28 // We only collect `stack` and `memory` data before and after calls.
29 if self.collect {
30 self.collect_data(interp);
31 if let Some(mapping_slots) = &mut self.fuzz_state.mapping_slots {
32 mapping_step(mapping_slots, interp);
33 }
34 }
35 }
36
37 fn call(&mut self, ecx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
38 // We don't want to override the very first call made to the test contract.
39 if self.call_generator.is_some() && ecx.tx().caller() != inputs.caller {
40 self.override_call(ecx, inputs);
41 }
42
43 // We only collect `stack` and `memory` data before and after calls.
44 // this will be turned off on the next `step`
45 self.collect = true;
46
47 None
48 }
49
50 fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) {
51 if let Some(ref mut call_generator) = self.call_generator {
52 // Decrement depth when any call ends while inside an override
53 if call_generator.override_depth > 0 {
54 call_generator.override_depth -= 1;
55 }
56 }
57
58 // We only collect `stack` and `memory` data before and after calls.
59 // this will be turned off on the next `step`
60 self.collect = true;
61 }
62}
63
64impl Fuzzer {
65 /// Collects `stack` and `memory` values into the fuzz dictionary.
66 #[cold]
67 fn collect_data(&mut self, interpreter: &Interpreter) {
68 self.fuzz_state.collect_values(interpreter.stack.data().iter().copied().map(Into::into));
69
70 // TODO: disabled for now since it's flooding the dictionary
71 // for index in 0..interpreter.shared_memory.len() / 32 {
72 // let mut slot = [0u8; 32];
73 // slot.clone_from_slice(interpreter.shared_memory.get_slice(index * 32, 32));
74
75 // state.insert(slot);
76 // }
77
78 self.collect = false;
79 }
80
81 /// Overrides an external call to simulate reentrancy attacks.
82 ///
83 /// This function detects reentrancy vulnerabilities by replacing external calls
84 /// with callbacks that reenter the caller contract.
85 ///
86 /// For calls with value (ETH transfers):
87 /// 1. Performs the ETH transfer via the journal first
88 /// 2. Replaces the call with a reentrant callback (value = 0)
89 ///
90 /// For calls without value:
91 /// - Replaces the call entirely with a reentrant callback
92 ///
93 /// This simulates malicious contracts that immediately reenter when called.
94 fn override_call<CTX>(&mut self, ecx: &mut CTX, call: &mut CallInputs)
95 where
96 CTX: ContextTr<Journal: JournalExt>,
97 {
98 let Some(ref mut call_generator) = self.call_generator else {
99 return;
100 };
101
102 // Skip if:
103 // - Caller is test contract (don't override the initial calls from the test)
104 // - Not a CALL scheme (only override CALLs, not STATICCALLs, DELEGATECALLs, etc.)
105 // - Inside an override (prevent recursive overrides)
106 // - Target is cheatcode address
107 // - Neither caller nor target is a handler contract
108 //
109 // We override calls when either the caller OR target is a handler. This covers:
110 // 1. EtherStore pattern: handler sends ETH out, attacker reenters handler
111 // 2. Rari pattern: external protocol sends ETH to handler, handler reenters protocol
112 let caller_is_handler = call_generator.is_handler(call.caller);
113 let target_is_handler = call_generator.is_handler(call.target_address);
114 if call.caller == call_generator.test_address
115 || call.scheme != CallScheme::Call
116 || call_generator.override_depth > 0
117 || call.target_address == CHEATCODE_ADDRESS
118 || (!caller_is_handler && !target_is_handler)
119 {
120 return;
121 }
122
123 // There's only a ~27% chance that an override happens (90% * 30% from strategy).
124 let Some(tx) = call_generator.next(call.caller, call.target_address) else {
125 return;
126 };
127
128 // For value transfers, perform the ETH transfer before injecting the callback.
129 // This simulates a malicious receive() that gets the ETH and then reenters.
130 let value = call.transfer_value().unwrap_or_default();
131 let has_value = !value.is_zero() && call.gas_limit > 2300;
132 if has_value && ecx.journal_mut().transfer(call.caller, call.target_address, value).is_err()
133 {
134 return;
135 }
136
137 // Replace the call with a reentrant callback
138 call.input = CallInput::Bytes(tx.call_details.calldata.0.into());
139 call.caller = tx.sender;
140 call.target_address = tx.call_details.target;
141 call.bytecode_address = tx.call_details.target;
142 // Clear known_bytecode to force REVM to load bytecode from the new target.
143 // Without this, REVM uses cached bytecode from the original target (e.g., empty
144 // bytecode for EOA), causing the call to short-circuit before executing any code.
145 call.known_bytecode = None;
146 // Clear value since ETH was already transferred above
147 call.value = CallValue::Transfer(alloy_primitives::U256::ZERO);
148
149 // Track that we're inside an overridden call to avoid recursive overrides
150 call_generator.override_depth = 1;
151 }
152}