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}