Skip to main content

foundry_evm_fuzz/
inspector.rs

1use crate::invariant::RandomCallGenerator;
2use alloy_primitives::{Address, B256, Bytes, U256, map::AddressMap};
3use foundry_common::mapping_slots::{
4    MappingSlots, PendingMappingHash, capture_hash as capture_mapping_hash,
5    record_hash as record_mapping_hash, step as mapping_step,
6};
7use foundry_evm_core::constants::CHEATCODE_ADDRESS;
8use revm::{
9    Inspector,
10    context::{ContextTr, JournalTr, Transaction},
11    interpreter::{CallInput, CallInputs, CallOutcome, CallScheme, CallValue, Interpreter},
12};
13
14/// A sub-call observed by the [`Fuzzer`] inspector.
15///
16/// `depth` is 1-indexed relative to the top-level call: depth 1 is a direct call
17/// from the top-level callee, depth 2 is a sub-call of that call, and so on. The
18/// top-level call itself is never recorded.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct ObservedCall {
21    pub depth: u32,
22    pub caller: Address,
23    pub target: Address,
24    pub calldata: Bytes,
25    pub value: Option<U256>,
26}
27
28/// An inspector that can fuzz and collect data for that effect.
29#[derive(Clone, Debug)]
30pub struct Fuzzer {
31    /// If set, it collects `stack` and `memory` values for fuzzing purposes.
32    pub collect: bool,
33    /// Given a strategy, it generates a random call.
34    pub call_generator: Option<RandomCallGenerator>,
35    /// If `collect` is set, we store collected values until the invariant worker drains them.
36    pub collected_values: Vec<B256>,
37    /// Maximum number of stack words staged before the invariant worker drains them.
38    pub max_collected_values: usize,
39    /// Mapping accesses observed during execution, used for storage slot sampling.
40    pub mapping_slots: Option<AddressMap<MappingSlots>>,
41    /// A 64-byte Keccak operation waiting to be recorded after execution.
42    pending_mapping_hash: Option<PendingMappingHash>,
43    /// Whether sub-calls should be buffered for later corpus seeding.
44    record_calls: bool,
45    /// Sub-calls observed since the last drain.
46    observed_calls: Vec<ObservedCall>,
47    /// Current EVM call depth. 0 means no active call, 1 means top-level call.
48    call_depth: u32,
49    /// Additional network-specific cheatcode addresses that must not be overridden.
50    extra_cheatcode_addresses: &'static [Address],
51}
52
53impl<CTX: ContextTr> Inspector<CTX> for Fuzzer {
54    #[inline]
55    fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
56        self.capture_mapping_hash(interp);
57        // We only collect `stack` and `memory` data before and after calls.
58        if self.collect {
59            self.collect_data(interp);
60        }
61    }
62
63    #[inline]
64    fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) {
65        self.record_mapping_hash(interp);
66    }
67
68    fn call(&mut self, ecx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
69        // We don't want to override the very first call made to the test contract.
70        if self.call_generator.is_some() && ecx.tx().caller() != inputs.caller {
71            self.override_call(ecx, inputs);
72        }
73
74        self.call_depth = self.call_depth.saturating_add(1);
75        if self.should_record_observed_call(inputs.scheme) {
76            self.observed_calls.push(ObservedCall {
77                depth: self.call_depth - 1,
78                caller: inputs.caller,
79                target: inputs.target_address,
80                calldata: inputs.input.bytes(ecx),
81                value: inputs.transfer_value().filter(|value| !value.is_zero()),
82            });
83        }
84
85        // We only collect `stack` and `memory` data before and after calls.
86        // this will be turned off on the next `step`
87        self.collect = true;
88
89        None
90    }
91
92    fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) {
93        if let Some(ref mut call_generator) = self.call_generator {
94            // Decrement depth when any call ends while inside an override
95            if call_generator.override_depth > 0 {
96                call_generator.override_depth -= 1;
97            }
98        }
99
100        // We only collect `stack` and `memory` data before and after calls.
101        // this will be turned off on the next `step`
102        self.collect = true;
103
104        self.call_depth = self.call_depth.saturating_sub(1);
105    }
106}
107
108impl Fuzzer {
109    fn capture_mapping_hash(&mut self, interpreter: &Interpreter) {
110        if let Some(mapping_slots) = &mut self.mapping_slots {
111            mapping_step(mapping_slots, interpreter);
112            self.pending_mapping_hash = capture_mapping_hash(interpreter);
113        }
114    }
115
116    fn record_mapping_hash(&mut self, interpreter: &Interpreter) {
117        if let Some(pending) = self.pending_mapping_hash.take()
118            && interpreter.bytecode.action.is_none()
119            && let Some(mapping_slots) = &mut self.mapping_slots
120        {
121            record_mapping_hash(mapping_slots, interpreter, pending);
122        }
123    }
124
125    /// Constructs a new `Fuzzer` inspector.
126    pub const fn new(
127        max_collected_values: usize,
128        mapping_slots: Option<AddressMap<MappingSlots>>,
129    ) -> Self {
130        Self {
131            collect: true,
132            call_generator: None,
133            collected_values: Vec::new(),
134            max_collected_values,
135            mapping_slots,
136            pending_mapping_hash: None,
137            record_calls: false,
138            observed_calls: Vec::new(),
139            call_depth: 0,
140            extra_cheatcode_addresses: &[],
141        }
142    }
143
144    /// Sets additional network-specific cheatcode addresses that must not be overridden.
145    pub const fn with_extra_cheatcode_addresses(mut self, addresses: &'static [Address]) -> Self {
146        self.extra_cheatcode_addresses = addresses;
147        self
148    }
149
150    /// Enables or disables sub-call buffering.
151    pub const fn with_call_recording(mut self, record_calls: bool) -> Self {
152        self.record_calls = record_calls;
153        self
154    }
155
156    /// Enables or disables sub-call buffering on an existing inspector.
157    pub const fn set_call_recording(&mut self, record_calls: bool) {
158        self.record_calls = record_calls;
159    }
160
161    /// Returns the buffered sub-calls observed since the last drain.
162    pub fn take_observed_calls(&mut self) -> Vec<ObservedCall> {
163        std::mem::take(&mut self.observed_calls)
164    }
165
166    #[cfg(test)]
167    fn record_observed_call(
168        &mut self,
169        caller: Address,
170        target: Address,
171        calldata: Bytes,
172        value: Option<U256>,
173        scheme: CallScheme,
174    ) {
175        if self.should_record_observed_call(scheme) {
176            self.observed_calls.push(ObservedCall {
177                depth: self.call_depth - 1,
178                caller,
179                target,
180                calldata,
181                value,
182            });
183        }
184    }
185
186    #[inline]
187    const fn should_record_observed_call(&self, scheme: CallScheme) -> bool {
188        self.record_calls && self.call_depth > 1 && matches!(scheme, CallScheme::Call)
189    }
190
191    #[inline]
192    fn is_cheatcode_address(&self, address: Address) -> bool {
193        address == CHEATCODE_ADDRESS || self.extra_cheatcode_addresses.contains(&address)
194    }
195
196    /// Collects `stack` and `memory` values into the fuzz dictionary.
197    #[cold]
198    fn collect_data(&mut self, interpreter: &Interpreter) {
199        let remaining = self.max_collected_values.saturating_sub(self.collected_values.len());
200        self.collected_values
201            .extend(interpreter.stack.data().iter().take(remaining).copied().map(B256::from));
202
203        // TODO: disabled for now since it's flooding the dictionary
204        // for index in 0..interpreter.shared_memory.len() / 32 {
205        //     let mut slot = [0u8; 32];
206        //     slot.clone_from_slice(interpreter.shared_memory.get_slice(index * 32, 32));
207
208        //     state.insert(slot);
209        // }
210
211        self.collect = false;
212    }
213
214    /// Overrides an external call to simulate reentrancy attacks.
215    ///
216    /// This function detects reentrancy vulnerabilities by replacing external calls
217    /// with callbacks that reenter the caller contract.
218    ///
219    /// For calls with value (ETH transfers):
220    /// 1. Performs the ETH transfer via the journal first
221    /// 2. Replaces the call with a reentrant callback (value = 0)
222    ///
223    /// For calls without value:
224    /// - Replaces the call entirely with a reentrant callback
225    ///
226    /// This simulates malicious contracts that immediately reenter when called.
227    fn override_call<CTX: ContextTr>(&mut self, ecx: &mut CTX, call: &mut CallInputs) {
228        let target_is_cheatcode = self.is_cheatcode_address(call.target_address);
229        let Some(ref mut call_generator) = self.call_generator else {
230            return;
231        };
232
233        // Skip if:
234        // - Caller is test contract (don't override the initial calls from the test)
235        // - Not a CALL scheme (only override CALLs, not STATICCALLs, DELEGATECALLs, etc.)
236        // - Inside an override (prevent recursive overrides)
237        // - Target is cheatcode address
238        // - Neither caller nor target is a handler contract
239        //
240        // We override calls when either the caller OR target is a handler. This covers:
241        // 1. EtherStore pattern: handler sends ETH out, attacker reenters handler
242        // 2. Rari pattern: external protocol sends ETH to handler, handler reenters protocol
243        if call.caller == call_generator.test_address
244            || call.scheme != CallScheme::Call
245            || call_generator.override_depth > 0
246            || target_is_cheatcode
247        {
248            return;
249        }
250        {
251            let handlers = call_generator.handler_addresses.read();
252            if !handlers.contains(&call.caller) && !handlers.contains(&call.target_address) {
253                return;
254            }
255        }
256
257        // There's only a ~27% chance that an override happens (90% * 30% from strategy).
258        let Some(tx) = call_generator.next(call.caller, call.target_address) else {
259            return;
260        };
261
262        // For value transfers, perform the ETH transfer before injecting the callback.
263        // This simulates a malicious receive() that gets the ETH and then reenters.
264        let value = call.transfer_value().unwrap_or_default();
265        let has_value = !value.is_zero() && call.gas_limit > 2300;
266        if has_value && ecx.journal_mut().transfer(call.caller, call.target_address, value).is_err()
267        {
268            return;
269        }
270
271        // Replace the call with a reentrant callback
272        call.input = CallInput::Bytes(tx.call_details.calldata);
273        call.caller = tx.sender;
274        call.target_address = tx.call_details.target;
275        call.bytecode_address = tx.call_details.target;
276        let target = ecx
277            .journal_mut()
278            .load_account_with_code(tx.call_details.target)
279            .expect("failed to load account");
280        // Clear known_bytecode to force REVM to load bytecode from the new target.
281        // Without this, REVM uses cached bytecode from the original target (e.g., empty
282        // bytecode for EOA), causing the call to short-circuit before executing any code.
283        call.known_bytecode = (target.info.code_hash, target.info.code.clone().unwrap_or_default());
284        // Clear value since ETH was already transferred above
285        call.value = CallValue::Transfer(alloy_primitives::U256::ZERO);
286
287        // Track that we're inside an overridden call to avoid recursive overrides
288        call_generator.override_depth = 1;
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use alloy_primitives::keccak256;
296    use foundry_evm_core::constants::MONAD_CHEATCODE_ADDRESS;
297    use revm::bytecode::Bytecode;
298
299    fn fuzzer(record_calls: bool) -> Fuzzer {
300        Fuzzer::new(16, None).with_call_recording(record_calls)
301    }
302
303    #[test]
304    fn network_cheatcode_addresses_are_opt_in() {
305        let ethereum = Fuzzer::new(16, None);
306        assert!(!ethereum.is_cheatcode_address(MONAD_CHEATCODE_ADDRESS));
307
308        let monad =
309            Fuzzer::new(16, None).with_extra_cheatcode_addresses(&[MONAD_CHEATCODE_ADDRESS]);
310        assert!(monad.is_cheatcode_address(MONAD_CHEATCODE_ADDRESS));
311    }
312
313    #[test]
314    fn mapping_hashes_are_recorded_outside_dictionary_collection() {
315        let key = B256::with_last_byte(1);
316        let parent = B256::with_last_byte(2);
317        let preimage = [key.as_slice(), parent.as_slice()].concat();
318        let result = keccak256(&preimage);
319        let mut interpreter =
320            Interpreter::default().with_bytecode(Bytecode::new_raw(Bytes::from_static(&[
321                revm::bytecode::opcode::KECCAK256,
322            ])));
323        interpreter.memory.resize(64);
324        interpreter.memory.set(0, &preimage);
325        assert!(interpreter.stack.push(U256::from(64)));
326        assert!(interpreter.stack.push(U256::ZERO));
327
328        let mut fuzzer = Fuzzer::new(16, Some(AddressMap::default()));
329        fuzzer.collect = false;
330        fuzzer.capture_mapping_hash(&interpreter);
331
332        interpreter.stack.pop().unwrap();
333        interpreter.stack.pop().unwrap();
334        assert!(interpreter.stack.push(result.into()));
335        fuzzer.record_mapping_hash(&interpreter);
336
337        let slots = fuzzer.mapping_slots.unwrap();
338        assert_eq!(slots[&Address::ZERO].seen_sha3.get(&result), Some(&(key, parent)));
339    }
340
341    #[test]
342    fn observed_calls_are_disabled_by_default() {
343        let mut fuzzer = Fuzzer::new(16, None);
344        fuzzer.call_depth = 2;
345
346        fuzzer.record_observed_call(
347            Address::from([0xaa; 20]),
348            Address::from([0x11; 20]),
349            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
350            Some(U256::from(1)),
351            CallScheme::Call,
352        );
353
354        assert!(fuzzer.take_observed_calls().is_empty());
355    }
356
357    #[test]
358    fn observed_calls_skip_top_level_call() {
359        let mut fuzzer = fuzzer(true);
360        fuzzer.call_depth = 1;
361
362        fuzzer.record_observed_call(
363            Address::from([0xaa; 20]),
364            Address::from([0x11; 20]),
365            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
366            None,
367            CallScheme::Call,
368        );
369
370        assert!(fuzzer.take_observed_calls().is_empty());
371    }
372
373    #[test]
374    fn observed_calls_record_subcall_depth_target_calldata_and_value() {
375        let mut fuzzer = fuzzer(true);
376        let caller = Address::from([0x11; 20]);
377        let target = Address::from([0x22; 20]);
378        let calldata = Bytes::from_static(&[0xca, 0xfe, 0xba, 0xbe]);
379        let value = Some(U256::from(7));
380        fuzzer.call_depth = 3;
381
382        fuzzer.record_observed_call(caller, target, calldata.clone(), value, CallScheme::Call);
383
384        assert_eq!(
385            fuzzer.take_observed_calls(),
386            vec![ObservedCall { depth: 2, caller, target, calldata, value }]
387        );
388    }
389
390    #[test]
391    fn observed_calls_skip_non_call_schemes() {
392        let mut fuzzer = fuzzer(true);
393        fuzzer.call_depth = 2;
394
395        fuzzer.record_observed_call(
396            Address::from([0x11; 20]),
397            Address::from([0x22; 20]),
398            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
399            None,
400            CallScheme::DelegateCall,
401        );
402
403        assert!(fuzzer.take_observed_calls().is_empty());
404    }
405
406    #[test]
407    fn take_observed_calls_drains_buffer() {
408        let mut fuzzer = fuzzer(true);
409        fuzzer.call_depth = 2;
410        fuzzer.record_observed_call(
411            Address::from([0xaa; 20]),
412            Address::from([0x33; 20]),
413            Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
414            None,
415            CallScheme::Call,
416        );
417
418        assert_eq!(fuzzer.take_observed_calls().len(), 1);
419        assert!(fuzzer.take_observed_calls().is_empty());
420    }
421}