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.transfer_from() {
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.transfer_from(),
79                target: inputs.transfer_to(),
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 && scheme.is_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.transfer_to());
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.transfer_from() == call_generator.test_address
244            || !call.scheme.is_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.transfer_from()) && !handlers.contains(&call.transfer_to())
253            {
254                return;
255            }
256        }
257
258        // There's only a ~27% chance that an override happens (90% * 30% from strategy).
259        let Some(tx) = call_generator.next(call.transfer_from(), call.transfer_to()) else {
260            return;
261        };
262
263        // For value transfers, perform the ETH transfer before injecting the callback.
264        // This simulates a malicious receive() that gets the ETH and then reenters.
265        let value = call.transfer_value().unwrap_or_default();
266        let has_value = !value.is_zero() && call.gas_limit > 2300;
267        if has_value
268            && ecx.journal_mut().transfer(call.transfer_from(), call.transfer_to(), value).is_err()
269        {
270            return;
271        }
272
273        // Replace the call with a reentrant callback
274        call.input = CallInput::Bytes(tx.call_details.calldata);
275        call.caller = tx.sender;
276        call.target_address = tx.call_details.target;
277        call.bytecode_address = tx.call_details.target;
278        let target = ecx
279            .journal_mut()
280            .load_account_with_code(tx.call_details.target)
281            .expect("failed to load account");
282        // Clear known_bytecode to force REVM to load bytecode from the new target.
283        // Without this, REVM uses cached bytecode from the original target (e.g., empty
284        // bytecode for EOA), causing the call to short-circuit before executing any code.
285        call.known_bytecode =
286            (target.info.code_hash(), target.info.code.clone().unwrap_or_default());
287        // Clear value since ETH was already transferred above
288        call.value = CallValue::Transfer(alloy_primitives::U256::ZERO);
289
290        // Track that we're inside an overridden call to avoid recursive overrides
291        call_generator.override_depth = 1;
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use alloy_primitives::keccak256;
299    use foundry_evm_core::constants::MONAD_CHEATCODE_ADDRESS;
300    use revm::bytecode::Bytecode;
301
302    fn fuzzer(record_calls: bool) -> Fuzzer {
303        Fuzzer::new(16, None).with_call_recording(record_calls)
304    }
305
306    #[test]
307    fn network_cheatcode_addresses_are_opt_in() {
308        let ethereum = Fuzzer::new(16, None);
309        assert!(!ethereum.is_cheatcode_address(MONAD_CHEATCODE_ADDRESS));
310
311        let monad =
312            Fuzzer::new(16, None).with_extra_cheatcode_addresses(&[MONAD_CHEATCODE_ADDRESS]);
313        assert!(monad.is_cheatcode_address(MONAD_CHEATCODE_ADDRESS));
314    }
315
316    #[test]
317    fn mapping_hashes_are_recorded_outside_dictionary_collection() {
318        let key = B256::with_last_byte(1);
319        let parent = B256::with_last_byte(2);
320        let preimage = [key.as_slice(), parent.as_slice()].concat();
321        let result = keccak256(&preimage);
322        let mut interpreter =
323            Interpreter::default().with_bytecode(Bytecode::new_raw(Bytes::from_static(&[
324                revm::bytecode::opcode::KECCAK256,
325            ])));
326        interpreter.memory.resize(64);
327        interpreter.memory.set(0, &preimage);
328        assert!(interpreter.stack.push(U256::from(64)));
329        assert!(interpreter.stack.push(U256::ZERO));
330
331        let mut fuzzer = Fuzzer::new(16, Some(AddressMap::default()));
332        fuzzer.collect = false;
333        fuzzer.capture_mapping_hash(&interpreter);
334
335        interpreter.stack.pop().unwrap();
336        interpreter.stack.pop().unwrap();
337        assert!(interpreter.stack.push(result.into()));
338        fuzzer.record_mapping_hash(&interpreter);
339
340        let slots = fuzzer.mapping_slots.unwrap();
341        assert_eq!(slots[&Address::ZERO].seen_sha3.get(&result), Some(&(key, parent)));
342    }
343
344    #[test]
345    fn observed_calls_are_disabled_by_default() {
346        let mut fuzzer = Fuzzer::new(16, None);
347        fuzzer.call_depth = 2;
348
349        fuzzer.record_observed_call(
350            Address::repeat_byte(0xaa),
351            Address::repeat_byte(0x11),
352            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
353            Some(U256::ONE),
354            CallScheme::Call,
355        );
356
357        assert!(fuzzer.take_observed_calls().is_empty());
358    }
359
360    #[test]
361    fn observed_calls_skip_top_level_call() {
362        let mut fuzzer = fuzzer(true);
363        fuzzer.call_depth = 1;
364
365        fuzzer.record_observed_call(
366            Address::repeat_byte(0xaa),
367            Address::repeat_byte(0x11),
368            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
369            None,
370            CallScheme::Call,
371        );
372
373        assert!(fuzzer.take_observed_calls().is_empty());
374    }
375
376    #[test]
377    fn observed_calls_record_subcall_depth_target_calldata_and_value() {
378        let mut fuzzer = fuzzer(true);
379        let caller = Address::repeat_byte(0x11);
380        let target = Address::repeat_byte(0x22);
381        let calldata = Bytes::from_static(&[0xca, 0xfe, 0xba, 0xbe]);
382        let value = Some(U256::from(7));
383        fuzzer.call_depth = 3;
384
385        fuzzer.record_observed_call(caller, target, calldata.clone(), value, CallScheme::Call);
386
387        assert_eq!(
388            fuzzer.take_observed_calls(),
389            vec![ObservedCall { depth: 2, caller, target, calldata, value }]
390        );
391    }
392
393    #[test]
394    fn observed_calls_skip_non_call_schemes() {
395        let mut fuzzer = fuzzer(true);
396        fuzzer.call_depth = 2;
397
398        fuzzer.record_observed_call(
399            Address::repeat_byte(0x11),
400            Address::repeat_byte(0x22),
401            Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
402            None,
403            CallScheme::DelegateCall,
404        );
405
406        assert!(fuzzer.take_observed_calls().is_empty());
407    }
408
409    #[test]
410    fn take_observed_calls_drains_buffer() {
411        let mut fuzzer = fuzzer(true);
412        fuzzer.call_depth = 2;
413        fuzzer.record_observed_call(
414            Address::repeat_byte(0xaa),
415            Address::repeat_byte(0x33),
416            Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
417            None,
418            CallScheme::Call,
419        );
420
421        assert_eq!(fuzzer.take_observed_calls().len(), 1);
422        assert!(fuzzer.take_observed_calls().is_empty());
423    }
424}