foundry_evm/inspectors/
revert_diagnostic.rs1use alloy_primitives::{Address, U256};
2use alloy_sol_types::SolValue;
3use foundry_evm_core::{
4 backend::DatabaseError,
5 constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS},
6};
7use revm::{
8 Database, Inspector,
9 bytecode::opcode,
10 context::{ContextTr, JournalTr},
11 inspector::JournalExt,
12 interpreter::{
13 CallInputs, CallOutcome, CallScheme, InstructionResult, Interpreter, InterpreterAction,
14 interpreter::EthInterpreter,
15 interpreter_types::{Jumps, LoopControl},
16 },
17};
18use std::fmt;
19
20const IGNORE: [Address; 2] = [HARDHAT_CONSOLE_ADDRESS, CHEATCODE_ADDRESS];
21
22pub fn is_delegatecall(scheme: CallScheme) -> bool {
24 matches!(scheme, CallScheme::DelegateCall | CallScheme::CallCode)
25}
26
27#[derive(Debug, Clone, Copy)]
28pub enum DetailedRevertReason {
29 CallToNonContract(Address),
30 DelegateCallToNonContract(Address),
31}
32
33impl fmt::Display for DetailedRevertReason {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::CallToNonContract(addr) => {
37 write!(f, "call to non-contract address {addr}")
38 }
39 Self::DelegateCallToNonContract(addr) => write!(
40 f,
41 "delegatecall to non-contract address {addr} (usually an unliked library)"
42 ),
43 }
44 }
45}
46
47#[derive(Clone, Debug, Default)]
64pub struct RevertDiagnostic {
65 pub non_contract_call: Option<(Address, CallScheme, usize)>,
67 pub non_contract_size_check: Option<(Address, usize)>,
69 pub is_extcodesize_step: bool,
71}
72
73impl RevertDiagnostic {
74 fn code_target_address(&self, inputs: &mut CallInputs) -> Address {
77 if is_delegatecall(inputs.scheme) { inputs.bytecode_address } else { inputs.target_address }
78 }
79
80 fn reason(&self) -> Option<DetailedRevertReason> {
82 if let Some((addr, scheme, _)) = self.non_contract_call {
83 let reason = if is_delegatecall(scheme) {
84 DetailedRevertReason::DelegateCallToNonContract(addr)
85 } else {
86 DetailedRevertReason::CallToNonContract(addr)
87 };
88
89 return Some(reason);
90 }
91
92 if let Some((addr, _)) = self.non_contract_size_check {
93 return Some(DetailedRevertReason::CallToNonContract(addr));
95 }
96
97 None
98 }
99
100 fn broadcast_diagnostic(&self, interpreter: &mut Interpreter) {
102 if let Some(reason) = self.reason() {
103 interpreter.bytecode.set_action(InterpreterAction::new_return(
104 InstructionResult::Revert,
105 reason.to_string().abi_encode().into(),
106 interpreter.gas,
107 ));
108 }
109 }
110
111 #[cold]
117 fn handle_revert<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
118 where
119 D: Database<Error = DatabaseError>,
120 CTX: ContextTr<Db = D>,
121 CTX::Journal: JournalExt,
122 {
123 if let Ok(size) = interp.stack.peek(1)
125 && size.is_zero()
126 {
127 if let Some((_, _, depth)) = self.non_contract_call {
129 if ctx.journal_ref().depth() == depth {
130 self.broadcast_diagnostic(interp);
131 } else {
132 self.non_contract_call = None;
133 }
134 return;
135 }
136
137 if let Some((_, depth)) = self.non_contract_size_check {
139 if depth == ctx.journal_ref().depth() {
140 self.broadcast_diagnostic(interp);
141 } else {
142 self.non_contract_size_check = None;
143 }
144 }
145 }
146 }
147
148 #[cold]
152 fn handle_extcodesize<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
153 where
154 D: Database<Error = DatabaseError>,
155 CTX: ContextTr<Db = D>,
156 CTX::Journal: JournalExt,
157 {
158 if let Ok(word) = interp.stack.peek(0) {
160 let addr = Address::from_word(word.into());
161 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
162 return;
163 }
164
165 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
168 self.is_extcodesize_step = true;
169 }
170 }
171
172 #[cold]
174 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
175 if let Ok(size) = interp.stack.peek(0)
176 && size != U256::ZERO
177 {
178 self.non_contract_size_check = None;
179 }
180
181 self.is_extcodesize_step = false;
182 }
183}
184
185impl<CTX, D> Inspector<CTX, EthInterpreter> for RevertDiagnostic
186where
187 D: Database<Error = DatabaseError>,
188 CTX: ContextTr<Db = D>,
189 CTX::Journal: JournalExt,
190{
191 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
194 let target = self.code_target_address(inputs);
195
196 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
197 return None;
198 }
199
200 if let Ok(state) = ctx.journal_mut().code(target)
201 && state.is_empty()
202 && !inputs.input.is_empty()
203 {
204 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
205 }
206 None
207 }
208
209 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
211 match interp.bytecode.opcode() {
212 opcode::REVERT => self.handle_revert(interp, ctx),
213 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
214 _ => {}
215 }
216 }
217
218 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
219 if self.is_extcodesize_step {
220 self.handle_extcodesize_output(interp);
221 }
222 }
223}