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