Skip to main content

foundry_evm_fuzz/
inspector.rs

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