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
13#[derive(Clone, Debug, Default)]
30pub struct RevertDiagnostic {
31 non_contract_call: Option<(Address, CallScheme, usize)>,
33 non_contract_size_check: Option<(Address, usize)>,
35 is_extcodesize_step: bool,
37 pending: Option<DetailedRevertReason>,
39 active_trace_nodes: Vec<Option<usize>>,
41 diagnostics: HashMap<usize, DetailedRevertReason>,
43}
44
45impl RevertDiagnostic {
46 const fn reason(&self) -> Option<DetailedRevertReason> {
48 if let Some((addr, scheme, _)) = self.non_contract_call {
49 let reason = if is_delegatecall(scheme) {
50 DetailedRevertReason::DelegateCallToNonContract(addr)
51 } else {
52 DetailedRevertReason::CallToNonContract(addr)
53 };
54
55 return Some(reason);
56 }
57
58 if let Some((addr, _)) = self.non_contract_size_check {
59 return Some(DetailedRevertReason::CallToNonContract(addr));
61 }
62
63 None
64 }
65
66 pub fn frame_start(&mut self) {
68 self.active_trace_nodes.push(None);
69 }
70
71 pub fn set_trace_node(&mut self, trace_node: usize) {
73 let frame = self.active_trace_nodes.last_mut();
74 debug_assert!(frame.is_some(), "missing active revert diagnostic frame");
75 if let Some(frame) = frame {
76 *frame = Some(trace_node);
77 }
78 }
79
80 pub fn frame_end(&mut self) {
82 let frame = self.active_trace_nodes.pop();
83 debug_assert!(frame.is_some(), "revert diagnostic frame stack underflow");
84 let diagnostic = self.pending.take();
85 if let Some(node_idx) = frame.flatten()
86 && let Some(diagnostic) = diagnostic
87 {
88 self.diagnostics.insert(node_idx, diagnostic);
89 }
90 }
91
92 pub fn into_diagnostics(self) -> HashMap<usize, DetailedRevertReason> {
94 debug_assert!(self.active_trace_nodes.is_empty(), "unclosed revert diagnostic frames");
95 self.diagnostics
96 }
97
98 #[cold]
102 fn handle_revert<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
103 if let Ok(size) = interp.stack.peek(1)
105 && size.is_zero()
106 {
107 if let Some((_, _, depth)) = self.non_contract_call {
109 if ctx.journal_ref().depth() == depth {
110 self.pending = self.reason();
111 } else {
112 self.non_contract_call = None;
113 }
114 return;
115 }
116
117 if let Some((_, depth)) = self.non_contract_size_check {
119 if depth == ctx.journal_ref().depth() {
120 self.pending = self.reason();
121 } else {
122 self.non_contract_size_check = None;
123 }
124 }
125 }
126 }
127
128 #[cold]
132 fn handle_extcodesize<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
133 if let Ok(word) = interp.stack.peek(0) {
135 let addr = Address::from_word(word.into());
136 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
137 return;
138 }
139
140 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
143 self.is_extcodesize_step = true;
144 }
145 }
146
147 #[cold]
149 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
150 if let Ok(size) = interp.stack.peek(0)
151 && size != U256::ZERO
152 {
153 self.non_contract_size_check = None;
154 }
155
156 self.is_extcodesize_step = false;
157 }
158}
159
160impl<CTX: ContextTr> Inspector<CTX> for RevertDiagnostic {
161 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
164 if inputs.input.is_empty() {
165 return None;
166 }
167
168 let target = if is_delegatecall(inputs.scheme) {
170 inputs.bytecode_address
171 } else {
172 inputs.target_address
173 };
174
175 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
176 return None;
177 }
178
179 if let Ok(state) = ctx.journal_mut().code(target)
180 && state.is_empty()
181 {
182 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
183 }
184 None
185 }
186
187 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
189 match interp.bytecode.opcode() {
190 opcode::REVERT => self.handle_revert(interp, ctx),
191 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
192 _ => {}
193 }
194 }
195
196 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
197 if self.is_extcodesize_step {
198 self.handle_extcodesize_output(interp);
199 }
200 }
201}
202
203pub const fn is_delegatecall(scheme: CallScheme) -> bool {
205 matches!(scheme, CallScheme::DelegateCall | CallScheme::CallCode)
206}