foundry_evm/inspectors/
revert_diagnostic.rs1use alloy_primitives::{Address, U256};
2use alloy_sol_types::SolValue;
3use foundry_evm_core::constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS};
4use revm::{
5 Inspector,
6 bytecode::opcode,
7 context::{ContextTr, JournalTr},
8 interpreter::{
9 CallInputs, CallOutcome, CallScheme, InstructionResult, Interpreter, InterpreterAction,
10 interpreter_types::{Jumps, LoopControl},
11 },
12};
13use std::fmt;
14
15const IGNORE: [Address; 2] = [HARDHAT_CONSOLE_ADDRESS, CHEATCODE_ADDRESS];
16
17pub const fn is_delegatecall(scheme: CallScheme) -> bool {
19 matches!(scheme, CallScheme::DelegateCall | CallScheme::CallCode)
20}
21
22#[derive(Debug, Clone, Copy)]
23pub enum DetailedRevertReason {
24 CallToNonContract(Address),
25 DelegateCallToNonContract(Address),
26}
27
28impl fmt::Display for DetailedRevertReason {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::CallToNonContract(addr) => {
32 write!(f, "call to non-contract address {addr}")
33 }
34 Self::DelegateCallToNonContract(addr) => write!(
35 f,
36 "delegatecall to non-contract address {addr} (usually an unliked library)"
37 ),
38 }
39 }
40}
41
42#[derive(Clone, Debug, Default)]
59pub struct RevertDiagnostic {
60 non_contract_call: Option<(Address, CallScheme, usize)>,
62 non_contract_size_check: Option<(Address, usize)>,
64 is_extcodesize_step: bool,
66}
67
68impl RevertDiagnostic {
69 const fn code_target_address(&self, inputs: &mut CallInputs) -> Address {
72 if is_delegatecall(inputs.scheme) { inputs.bytecode_address } else { inputs.target_address }
73 }
74
75 const fn reason(&self) -> Option<DetailedRevertReason> {
77 if let Some((addr, scheme, _)) = self.non_contract_call {
78 let reason = if is_delegatecall(scheme) {
79 DetailedRevertReason::DelegateCallToNonContract(addr)
80 } else {
81 DetailedRevertReason::CallToNonContract(addr)
82 };
83
84 return Some(reason);
85 }
86
87 if let Some((addr, _)) = self.non_contract_size_check {
88 return Some(DetailedRevertReason::CallToNonContract(addr));
90 }
91
92 None
93 }
94
95 fn broadcast_diagnostic(&self, interpreter: &mut Interpreter) {
97 if let Some(reason) = self.reason() {
98 interpreter.bytecode.set_action(InterpreterAction::new_return(
99 InstructionResult::Revert,
100 reason.to_string().abi_encode().into(),
101 interpreter.gas,
102 ));
103 }
104 }
105
106 #[cold]
112 fn handle_revert<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
113 if let Ok(size) = interp.stack.peek(1)
115 && size.is_zero()
116 {
117 if let Some((_, _, depth)) = self.non_contract_call {
119 if ctx.journal_ref().depth() == depth {
120 self.broadcast_diagnostic(interp);
121 } else {
122 self.non_contract_call = None;
123 }
124 return;
125 }
126
127 if let Some((_, depth)) = self.non_contract_size_check {
129 if depth == ctx.journal_ref().depth() {
130 self.broadcast_diagnostic(interp);
131 } else {
132 self.non_contract_size_check = None;
133 }
134 }
135 }
136 }
137
138 #[cold]
142 fn handle_extcodesize<CTX: ContextTr>(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
143 if let Ok(word) = interp.stack.peek(0) {
145 let addr = Address::from_word(word.into());
146 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
147 return;
148 }
149
150 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
153 self.is_extcodesize_step = true;
154 }
155 }
156
157 #[cold]
159 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
160 if let Ok(size) = interp.stack.peek(0)
161 && size != U256::ZERO
162 {
163 self.non_contract_size_check = None;
164 }
165
166 self.is_extcodesize_step = false;
167 }
168}
169
170impl<CTX: ContextTr> Inspector<CTX> for RevertDiagnostic {
171 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
174 let target = self.code_target_address(inputs);
175
176 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
177 return None;
178 }
179
180 if let Ok(state) = ctx.journal_mut().code(target)
181 && state.is_empty()
182 && !inputs.input.is_empty()
183 {
184 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
185 }
186 None
187 }
188
189 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
191 match interp.bytecode.opcode() {
192 opcode::REVERT => self.handle_revert(interp, ctx),
193 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
194 _ => {}
195 }
196 }
197
198 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
199 if self.is_extcodesize_step {
200 self.handle_extcodesize_output(interp);
201 }
202 }
203}