foundry_evm/inspectors/
revert_diagnostic.rs
1use 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 InterpreterResult, interpreter::EthInterpreter, interpreter_types::Jumps,
15 },
16};
17use std::fmt;
18
19const IGNORE: [Address; 2] = [HARDHAT_CONSOLE_ADDRESS, CHEATCODE_ADDRESS];
20
21pub fn is_delegatecall(scheme: CallScheme) -> bool {
23 matches!(scheme, CallScheme::DelegateCall | CallScheme::ExtDelegateCall | CallScheme::CallCode)
24}
25
26#[derive(Debug, Clone, Copy)]
27pub enum DetailedRevertReason {
28 CallToNonContract(Address),
29 DelegateCallToNonContract(Address),
30}
31
32impl fmt::Display for DetailedRevertReason {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::CallToNonContract(addr) => {
36 write!(f, "call to non-contract address {addr}")
37 }
38 Self::DelegateCallToNonContract(addr) => write!(
39 f,
40 "delegatecall to non-contract address {addr} (usually an unliked library)"
41 ),
42 }
43 }
44}
45
46#[derive(Clone, Debug, Default)]
63pub struct RevertDiagnostic {
64 pub non_contract_call: Option<(Address, CallScheme, usize)>,
66 pub non_contract_size_check: Option<(Address, usize)>,
68 pub is_extcodesize_step: bool,
70}
71
72impl RevertDiagnostic {
73 fn code_target_address(&self, inputs: &mut CallInputs) -> Address {
76 if is_delegatecall(inputs.scheme) { inputs.bytecode_address } else { inputs.target_address }
77 }
78
79 fn reason(&self) -> Option<DetailedRevertReason> {
81 if let Some((addr, scheme, _)) = self.non_contract_call {
82 let reason = if is_delegatecall(scheme) {
83 DetailedRevertReason::DelegateCallToNonContract(addr)
84 } else {
85 DetailedRevertReason::CallToNonContract(addr)
86 };
87
88 return Some(reason);
89 }
90
91 if let Some((addr, _)) = self.non_contract_size_check {
92 return Some(DetailedRevertReason::CallToNonContract(addr));
94 }
95
96 None
97 }
98
99 fn broadcast_diagnostic(&self, interp: &mut Interpreter) {
101 if let Some(reason) = self.reason() {
102 interp.control.instruction_result = InstructionResult::Revert;
103 interp.control.next_action = InterpreterAction::Return {
104 result: InterpreterResult {
105 output: reason.to_string().abi_encode().into(),
106 gas: interp.control.gas,
107 result: InstructionResult::Revert,
108 },
109 };
110 }
111 }
112
113 #[cold]
119 fn handle_revert<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
120 where
121 D: Database<Error = DatabaseError>,
122 CTX: ContextTr<Db = D>,
123 CTX::Journal: JournalExt,
124 {
125 if let Ok(size) = interp.stack.peek(1)
127 && size.is_zero()
128 {
129 if let Some((_, _, depth)) = self.non_contract_call {
131 if ctx.journal_ref().depth() == depth {
132 self.broadcast_diagnostic(interp);
133 } else {
134 self.non_contract_call = None;
135 }
136 return;
137 }
138
139 if let Some((_, depth)) = self.non_contract_size_check {
141 if depth == ctx.journal_ref().depth() {
142 self.broadcast_diagnostic(interp);
143 } else {
144 self.non_contract_size_check = None;
145 }
146 }
147 }
148 }
149
150 #[cold]
154 fn handle_extcodesize<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
155 where
156 D: Database<Error = DatabaseError>,
157 CTX: ContextTr<Db = D>,
158 CTX::Journal: JournalExt,
159 {
160 if let Ok(word) = interp.stack.peek(0) {
162 let addr = Address::from_word(word.into());
163 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
164 return;
165 }
166
167 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
170 self.is_extcodesize_step = true;
171 }
172 }
173
174 #[cold]
176 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
177 if let Ok(size) = interp.stack.peek(0)
178 && size != U256::ZERO
179 {
180 self.non_contract_size_check = None;
181 }
182
183 self.is_extcodesize_step = false;
184 }
185}
186
187impl<CTX, D> Inspector<CTX, EthInterpreter> for RevertDiagnostic
188where
189 D: Database<Error = DatabaseError>,
190 CTX: ContextTr<Db = D>,
191 CTX::Journal: JournalExt,
192{
193 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
196 let target = self.code_target_address(inputs);
197
198 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
199 return None;
200 }
201
202 if let Ok(state) = ctx.journal().code(target)
203 && state.is_empty()
204 && !inputs.input.is_empty()
205 {
206 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
207 }
208 None
209 }
210
211 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
213 match interp.bytecode.opcode() {
214 opcode::REVERT => self.handle_revert(interp, ctx),
215 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
216 _ => {}
217 }
218 }
219
220 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
221 if self.is_extcodesize_step {
222 self.handle_extcodesize_output(interp);
223 }
224 }
225}