foundry_evm/inspectors/
revert_diagnostic.rs1use alloy_primitives::{Address, U256, map::HashMap};
2use foundry_evm_core::constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS};
3use foundry_evm_traces::RevertDiagnostic as DetailedRevertReason;
4use revm::{
5 Inspector,
6 bytecode::opcode,
7 context::{ContextTr, JournalTr},
8 interpreter::{CallInputs, CallOutcome, CallScheme, Interpreter, interpreter_types::Jumps},
9};
10
11const IGNORE: [Address; 2] = [HARDHAT_CONSOLE_ADDRESS, CHEATCODE_ADDRESS];
12
13pub const fn is_delegatecall(scheme: CallScheme) -> bool {
15 matches!(scheme, CallScheme::DelegateCall | CallScheme::CallCode)
16}
17
18#[derive(Clone, Debug, Default)]
35pub struct RevertDiagnostic {
36 non_contract_call: Option<(Address, CallScheme, usize)>,
38 non_contract_size_check: Option<(Address, usize)>,
40 is_extcodesize_step: bool,
42 pending: Option<DetailedRevertReason>,
44 active_trace_nodes: Vec<Option<usize>>,
46 diagnostics: HashMap<usize, DetailedRevertReason>,
48}
49
50impl RevertDiagnostic {
51 const fn reason(&self) -> Option<DetailedRevertReason> {
53 if let Some((addr, scheme, _)) = self.non_contract_call {
54 let reason = if is_delegatecall(scheme) {
55 DetailedRevertReason::DelegateCallToNonContract(addr)
56 } else {
57 DetailedRevertReason::CallToNonContract(addr)
58 };
59
60 return Some(reason);
61 }
62
63 if let Some((addr, _)) = self.non_contract_size_check {
64 return Some(DetailedRevertReason::CallToNonContract(addr));
66 }
67
68 None
69 }
70
71 pub fn frame_start(&mut self) {
73 self.active_trace_nodes.push(None);
74 }
75
76 pub fn set_trace_node(&mut self, trace_node: usize) {
78 let frame = self.active_trace_nodes.last_mut();
79 debug_assert!(frame.is_some(), "missing active revert diagnostic frame");
80 if let Some(frame) = frame {
81 *frame = Some(trace_node);
82 }
83 }
84
85 pub fn frame_end(&mut self) {
87 let frame = self.active_trace_nodes.pop();
88 debug_assert!(frame.is_some(), "revert diagnostic frame stack underflow");
89 let diagnostic = self.pending.take();
90 if let Some(node_idx) = frame.flatten()
91 && let Some(diagnostic) = diagnostic
92 {
93 self.diagnostics.insert(node_idx, diagnostic);
94 }
95 }
96
97 pub fn into_diagnostics(self) -> HashMap<usize, DetailedRevertReason> {
99 debug_assert!(self.active_trace_nodes.is_empty(), "unclosed revert diagnostic frames");
100 self.diagnostics
101 }
102
103 #[cold]
107 fn handle_revert<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
108 if let Ok(size) = interp.stack.peek(1)
110 && size.is_zero()
111 {
112 if let Some((_, _, depth)) = self.non_contract_call {
114 if ctx.journal_ref().depth() == depth {
115 self.pending = self.reason();
116 } else {
117 self.non_contract_call = None;
118 }
119 return;
120 }
121
122 if let Some((_, depth)) = self.non_contract_size_check {
124 if depth == ctx.journal_ref().depth() {
125 self.pending = self.reason();
126 } else {
127 self.non_contract_size_check = None;
128 }
129 }
130 }
131 }
132
133 #[cold]
137 fn handle_extcodesize<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
138 if let Ok(word) = interp.stack.peek(0) {
140 let addr = Address::from_word(word.into());
141 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
142 return;
143 }
144
145 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
148 self.is_extcodesize_step = true;
149 }
150 }
151
152 #[cold]
154 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
155 if let Ok(size) = interp.stack.peek(0)
156 && size != U256::ZERO
157 {
158 self.non_contract_size_check = None;
159 }
160
161 self.is_extcodesize_step = false;
162 }
163}
164
165impl<CTX: ContextTr> Inspector<CTX> for RevertDiagnostic {
166 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
169 if inputs.input.is_empty() {
170 return None;
171 }
172
173 let target = if is_delegatecall(inputs.scheme) {
175 inputs.bytecode_address
176 } else {
177 inputs.target_address
178 };
179
180 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
181 return None;
182 }
183
184 if let Ok(state) = ctx.journal_mut().code(target)
185 && state.is_empty()
186 {
187 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
188 }
189 None
190 }
191
192 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
194 match interp.bytecode.opcode() {
195 opcode::REVERT => self.handle_revert(interp, ctx),
196 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
197 _ => {}
198 }
199 }
200
201 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
202 if self.is_extcodesize_step {
203 self.handle_extcodesize_output(interp);
204 }
205 }
206}