foundry_evm_core/
decode.rs1use crate::abi::{Vm, console};
4use alloy_dyn_abi::JsonAbiExt;
5use alloy_json_abi::{Error, JsonAbi};
6use alloy_primitives::{Log, Selector, hex, map::HashMap};
7use alloy_sol_types::{
8 ContractError::Revert, RevertReason, RevertReason::ContractError, SolEventInterface,
9 SolInterface, SolValue,
10};
11use foundry_common::SELECTOR_LEN;
12use itertools::Itertools;
13use revm::interpreter::InstructionResult;
14use std::{fmt, sync::OnceLock};
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct SkipReason(pub Option<String>);
19
20impl SkipReason {
21 pub fn decode(raw_result: &[u8]) -> Option<Self> {
23 raw_result.strip_prefix(crate::constants::MAGIC_SKIP).map(|reason| {
24 let reason = String::from_utf8_lossy(reason).into_owned();
25 Self((!reason.is_empty()).then_some(reason))
26 })
27 }
28
29 pub fn decode_self(s: &str) -> Option<Self> {
33 s.strip_prefix("skipped").map(|rest| Self(rest.strip_prefix(": ").map(ToString::to_string)))
34 }
35}
36
37impl fmt::Display for SkipReason {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str("skipped")?;
40 if let Some(reason) = &self.0 {
41 f.write_str(": ")?;
42 f.write_str(reason)?;
43 }
44 Ok(())
45 }
46}
47
48pub fn decode_console_logs(logs: &[Log]) -> Vec<String> {
50 logs.iter().filter_map(decode_console_log).collect()
51}
52
53fn decode_console_log(log: &Log) -> Option<String> {
58 console::ds::ConsoleEvents::decode_log(log).ok().map(|decoded| decoded.to_string())
59}
60
61#[derive(Clone, Debug, Default)]
63pub struct RevertDecoder {
64 errors: HashMap<Selector, Vec<Error>>,
66}
67
68impl Default for &RevertDecoder {
69 fn default() -> Self {
70 static EMPTY: OnceLock<RevertDecoder> = OnceLock::new();
71 EMPTY.get_or_init(RevertDecoder::new)
72 }
73}
74
75impl RevertDecoder {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn with_abis<'a>(mut self, abi: impl IntoIterator<Item = &'a JsonAbi>) -> Self {
85 self.extend_from_abis(abi);
86 self
87 }
88
89 pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
93 self.extend_from_abi(abi);
94 self
95 }
96
97 fn extend_from_abis<'a>(&mut self, abi: impl IntoIterator<Item = &'a JsonAbi>) {
99 for abi in abi {
100 self.extend_from_abi(abi);
101 }
102 }
103
104 fn extend_from_abi(&mut self, abi: &JsonAbi) {
106 for error in abi.errors() {
107 self.push_error(error.clone());
108 }
109 }
110
111 pub fn push_error(&mut self, error: Error) {
113 self.errors.entry(error.selector()).or_default().push(error);
114 }
115
116 pub fn decode(&self, err: &[u8], status: Option<InstructionResult>) -> String {
121 self.maybe_decode(err, status).unwrap_or_else(|| {
122 if err.is_empty() { "<empty revert data>".to_string() } else { trimmed_hex(err) }
123 })
124 }
125
126 pub fn maybe_decode(&self, err: &[u8], status: Option<InstructionResult>) -> Option<String> {
130 if let Some(reason) = SkipReason::decode(err) {
131 return Some(reason.to_string());
132 }
133
134 if let Some(ContractError(Revert(revert))) = RevertReason::decode(err) {
136 return Some(revert.reason);
137 }
138
139 if let Ok(e) = alloy_sol_types::ContractError::<Vm::VmErrors>::abi_decode(err) {
141 return Some(e.to_string());
142 }
143
144 let string_decoded = decode_as_non_empty_string(err);
145
146 if let Some((selector, data)) = err.split_first_chunk::<SELECTOR_LEN>() {
147 if let Some(errors) = self.errors.get(selector) {
149 for error in errors {
150 if let Ok(decoded) = error.abi_decode_input(data) {
153 return Some(format!(
154 "{}({})",
155 error.name,
156 decoded.iter().map(foundry_common::fmt::format_token).format(", ")
157 ));
158 }
159 }
160 }
161
162 if string_decoded.is_some() {
163 return string_decoded;
164 }
165
166 return Some({
168 let mut s = format!("custom error {}", hex::encode_prefixed(selector));
169 if !data.is_empty() {
170 s.push_str(": ");
171 match std::str::from_utf8(data) {
172 Ok(data) => s.push_str(data),
173 Err(_) => s.push_str(&hex::encode(data)),
174 }
175 }
176 s
177 });
178 }
179
180 if string_decoded.is_some() {
181 return string_decoded;
182 }
183
184 if let Some(status) = status
185 && !status.is_ok()
186 {
187 return Some(format!("EvmError: {status:?}"));
188 }
189 if err.is_empty() {
190 None
191 } else {
192 Some(format!("custom error bytes {}", hex::encode_prefixed(err)))
193 }
194 }
195}
196
197fn decode_as_non_empty_string(err: &[u8]) -> Option<String> {
199 if let Ok(s) = String::abi_decode(err)
201 && !s.is_empty()
202 {
203 return Some(s);
204 }
205
206 if err.is_ascii() {
208 let msg = std::str::from_utf8(err).unwrap().to_string();
209 if !msg.is_empty() {
210 return Some(msg);
211 }
212 }
213
214 None
215}
216
217fn trimmed_hex(s: &[u8]) -> String {
218 let n = 32;
219 if s.len() <= n {
220 hex::encode(s)
221 } else {
222 format!(
223 "{}…{} ({} bytes)",
224 &hex::encode(&s[..n / 2]),
225 &hex::encode(&s[s.len() - n / 2..]),
226 s.len(),
227 )
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_trimmed_hex() {
237 assert_eq!(trimmed_hex(&hex::decode("1234567890").unwrap()), "1234567890");
238 assert_eq!(
239 trimmed_hex(&hex::decode("492077697368207275737420737570706F72746564206869676865722D6B696E646564207479706573").unwrap()),
240 "49207769736820727573742073757070…6865722d6b696e646564207479706573 (41 bytes)"
241 );
242 }
243
244 #[test]
246 fn partial_decode() {
247 let mut decoder = RevertDecoder::default();
252 decoder.push_error("ValidationFailed(bytes)".parse().unwrap());
253
254 let data = &hex!(
258 "0xe17594de"
259 "756688fe00000000000000000000000000000000000000000000000000000000"
260 );
261 assert_eq!(
262 decoder.decode(data, None),
263 "custom error 0xe17594de: 756688fe00000000000000000000000000000000000000000000000000000000"
264 );
265
266 let data = &hex!(
270 "0xe17594de"
271 "0000000000000000000000000000000000000000000000000000000000000020"
272 "0000000000000000000000000000000000000000000000000000000000000004"
273 "756688fe00000000000000000000000000000000000000000000000000000000"
274 );
275 assert_eq!(decoder.decode(data, None), "ValidationFailed(0x756688fe)");
276 }
277}