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 bytecode::opcode,
9 context::{ContextTr, JournalTr},
10 inspector::JournalExt,
11 interpreter::{
12 interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, CallScheme,
13 InstructionResult, Interpreter, InterpreterAction, InterpreterResult,
14 },
15 Database, Inspector,
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) {
77 inputs.bytecode_address
78 } else {
79 inputs.target_address
80 }
81 }
82
83 fn reason(&self) -> Option<DetailedRevertReason> {
85 if let Some((addr, scheme, _)) = self.non_contract_call {
86 let reason = if is_delegatecall(scheme) {
87 DetailedRevertReason::DelegateCallToNonContract(addr)
88 } else {
89 DetailedRevertReason::CallToNonContract(addr)
90 };
91
92 return Some(reason);
93 }
94
95 if let Some((addr, _)) = self.non_contract_size_check {
96 return Some(DetailedRevertReason::CallToNonContract(addr));
98 }
99
100 None
101 }
102
103 fn broadcast_diagnostic(&self, interp: &mut Interpreter) {
105 if let Some(reason) = self.reason() {
106 interp.control.instruction_result = InstructionResult::Revert;
107 interp.control.next_action = InterpreterAction::Return {
108 result: InterpreterResult {
109 output: reason.to_string().abi_encode().into(),
110 gas: interp.control.gas,
111 result: InstructionResult::Revert,
112 },
113 };
114 }
115 }
116
117 #[cold]
123 fn handle_revert<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
124 where
125 D: Database<Error = DatabaseError>,
126 CTX: ContextTr<Db = D>,
127 CTX::Journal: JournalExt,
128 {
129 if let Ok(size) = interp.stack.peek(1) {
131 if size.is_zero() {
132 if let Some((_, _, depth)) = self.non_contract_call {
134 if ctx.journal_ref().depth() == depth {
135 self.broadcast_diagnostic(interp);
136 } else {
137 self.non_contract_call = None;
138 }
139 return;
140 }
141
142 if let Some((_, depth)) = self.non_contract_size_check {
144 if depth == ctx.journal_ref().depth() {
145 self.broadcast_diagnostic(interp);
146 } else {
147 self.non_contract_size_check = None;
148 }
149 }
150 }
151 }
152 }
153
154 #[cold]
158 fn handle_extcodesize<CTX, D>(&mut self, interp: &mut Interpreter, ctx: &mut CTX)
159 where
160 D: Database<Error = DatabaseError>,
161 CTX: ContextTr<Db = D>,
162 CTX::Journal: JournalExt,
163 {
164 if let Ok(word) = interp.stack.peek(0) {
166 let addr = Address::from_word(word.into());
167 if IGNORE.contains(&addr) || ctx.journal_ref().precompile_addresses().contains(&addr) {
168 return;
169 }
170
171 self.non_contract_size_check = Some((addr, ctx.journal_ref().depth()));
174 self.is_extcodesize_step = true;
175 }
176 }
177
178 #[cold]
180 fn handle_extcodesize_output(&mut self, interp: &mut Interpreter) {
181 if let Ok(size) = interp.stack.peek(0) {
182 if size != U256::ZERO {
183 self.non_contract_size_check = None;
184 }
185 }
186
187 self.is_extcodesize_step = false;
188 }
189}
190
191impl<CTX, D> Inspector<CTX, EthInterpreter> for RevertDiagnostic
192where
193 D: Database<Error = DatabaseError>,
194 CTX: ContextTr<Db = D>,
195 CTX::Journal: JournalExt,
196{
197 fn call(&mut self, ctx: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
200 let target = self.code_target_address(inputs);
201
202 if IGNORE.contains(&target) || ctx.journal_ref().precompile_addresses().contains(&target) {
203 return None;
204 }
205
206 if let Ok(state) = ctx.journal().code(target) {
207 if state.is_empty() && !inputs.input.is_empty() {
208 self.non_contract_call = Some((target, inputs.scheme, ctx.journal_ref().depth()));
209 }
210 }
211 None
212 }
213
214 fn step(&mut self, interp: &mut Interpreter, ctx: &mut CTX) {
216 match interp.bytecode.opcode() {
217 opcode::REVERT => self.handle_revert(interp, ctx),
218 opcode::EXTCODESIZE => self.handle_extcodesize(interp, ctx),
219 _ => {}
220 }
221 }
222
223 fn step_end(&mut self, interp: &mut Interpreter, _ctx: &mut CTX) {
224 if self.is_extcodesize_step {
225 self.handle_extcodesize_output(interp);
226 }
227 }
228}