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
16pub const EMPTY_REVERT_DATA: &str = "<empty revert data>";
18
19pub const ASSERTION_FAILED_PREFIX: &str = "assertion failed";
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct SkipReason(pub Option<String>);
25
26impl SkipReason {
27 pub fn decode(raw_result: &[u8]) -> Option<Self> {
29 raw_result.strip_prefix(crate::constants::MAGIC_SKIP).map(|reason| {
30 let reason = String::from_utf8_lossy(reason).into_owned();
31 Self((!reason.is_empty()).then_some(reason))
32 })
33 }
34
35 pub fn decode_self(s: &str) -> Option<Self> {
39 s.strip_prefix("skipped").map(|rest| Self(rest.strip_prefix(": ").map(ToString::to_string)))
40 }
41}
42
43impl fmt::Display for SkipReason {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str("skipped")?;
46 if let Some(reason) = &self.0 {
47 f.write_str(": ")?;
48 f.write_str(reason)?;
49 }
50 Ok(())
51 }
52}
53
54pub fn decode_console_logs(logs: &[Log]) -> Vec<String> {
56 logs.iter().filter_map(decode_console_log).collect()
57}
58
59pub fn decode_console_log(log: &Log) -> Option<String> {
64 console::ds::ConsoleEvents::decode_log(log).ok().map(|decoded| decoded.to_string())
65}
66
67#[derive(Clone, Debug, Default)]
69pub struct RevertDecoder {
70 errors: HashMap<Selector, Vec<Error>>,
72}
73
74impl Default for &RevertDecoder {
75 fn default() -> Self {
76 static EMPTY: OnceLock<RevertDecoder> = OnceLock::new();
77 EMPTY.get_or_init(RevertDecoder::new)
78 }
79}
80
81impl RevertDecoder {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn with_abis<'a>(mut self, abi: impl IntoIterator<Item = &'a JsonAbi>) -> Self {
91 self.extend_from_abis(abi);
92 self
93 }
94
95 pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
99 self.extend_from_abi(abi);
100 self
101 }
102
103 fn extend_from_abis<'a>(&mut self, abi: impl IntoIterator<Item = &'a JsonAbi>) {
105 for abi in abi {
106 self.extend_from_abi(abi);
107 }
108 }
109
110 fn extend_from_abi(&mut self, abi: &JsonAbi) {
112 for error in abi.errors() {
113 self.push_error(error.clone());
114 }
115 }
116
117 pub fn push_error(&mut self, error: Error) {
119 self.errors.entry(error.selector()).or_default().push(error);
120 }
121
122 pub fn decode(&self, err: &[u8], status: Option<InstructionResult>) -> String {
127 self.maybe_decode(err, status).unwrap_or_else(|| {
128 if err.is_empty() { EMPTY_REVERT_DATA.to_string() } else { trimmed_hex(err) }
129 })
130 }
131
132 pub fn maybe_decode(&self, err: &[u8], status: Option<InstructionResult>) -> Option<String> {
136 if let Some(ContractError(Revert(revert))) = RevertReason::decode(err) {
138 return Some(revert.reason);
139 }
140
141 if let Ok(e) = alloy_sol_types::ContractError::<Vm::VmErrors>::abi_decode(err) {
143 return Some(e.to_string());
144 }
145
146 let string_decoded = decode_as_non_empty_string(err);
147
148 if let Some((selector, data)) = err.split_first_chunk::<SELECTOR_LEN>() {
149 if let Some(errors) = self.errors.get(selector) {
151 for error in errors {
152 if let Ok(decoded) = error.abi_decode_input(data) {
155 return Some(format!(
156 "{}({})",
157 error.name,
158 decoded.iter().map(foundry_common::fmt::format_token).format(", ")
159 ));
160 }
161 }
162 }
163
164 if string_decoded.is_some() {
165 return string_decoded;
166 }
167
168 return Some({
170 let mut s = format!("custom error {}", hex::encode_prefixed(selector));
171 if !data.is_empty() {
172 s.push_str(": ");
173 match std::str::from_utf8(data) {
174 Ok(data) => s.push_str(data),
175 Err(_) => s.push_str(&hex::encode(data)),
176 }
177 }
178 s
179 });
180 }
181
182 if string_decoded.is_some() {
183 return string_decoded;
184 }
185
186 if let Some(status) = status
187 && !status.is_ok()
188 {
189 return Some(format!("EvmError: {status:?}"));
190 }
191 if err.is_empty() {
192 None
193 } else {
194 Some(format!("custom error bytes {}", hex::encode_prefixed(err)))
195 }
196 }
197}
198
199fn decode_as_non_empty_string(err: &[u8]) -> Option<String> {
201 if let Ok(s) = String::abi_decode(err)
203 && !s.is_empty()
204 {
205 return Some(s);
206 }
207
208 if err.is_ascii() {
210 let msg = std::str::from_utf8(err).unwrap().to_string();
211 if !msg.is_empty() {
212 return Some(msg);
213 }
214 }
215
216 None
217}
218
219fn trimmed_hex(s: &[u8]) -> String {
220 let n = 32;
221 if s.len() <= n {
222 hex::encode(s)
223 } else {
224 format!(
225 "{}…{} ({} bytes)",
226 hex::encode(&s[..n / 2]),
227 hex::encode(&s[s.len() - n / 2..]),
228 s.len(),
229 )
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_trimmed_hex() {
239 assert_eq!(trimmed_hex(&hex::decode("1234567890").unwrap()), "1234567890");
240 assert_eq!(
241 trimmed_hex(&hex::decode("492077697368207275737420737570706F72746564206869676865722D6B696E646564207479706573").unwrap()),
242 "49207769736820727573742073757070…6865722d6b696e646564207479706573 (41 bytes)"
243 );
244 }
245
246 #[test]
248 fn partial_decode() {
249 let mut decoder = RevertDecoder::default();
254 decoder.push_error("ValidationFailed(bytes)".parse().unwrap());
255
256 let data = &hex!(
260 "0xe17594de"
261 "756688fe00000000000000000000000000000000000000000000000000000000"
262 );
263 assert_eq!(
264 decoder.decode(data, None),
265 "custom error 0xe17594de: 756688fe00000000000000000000000000000000000000000000000000000000"
266 );
267
268 let data = &hex!(
272 "0xe17594de"
273 "0000000000000000000000000000000000000000000000000000000000000020"
274 "0000000000000000000000000000000000000000000000000000000000000004"
275 "756688fe00000000000000000000000000000000000000000000000000000000"
276 );
277 assert_eq!(decoder.decode(data, None), "ValidationFailed(0x756688fe)");
278 }
279
280 #[test]
281 fn maybe_decode_magic_skip_is_not_skip_marker() {
282 let decoder = RevertDecoder::new();
283 let reason = decoder.maybe_decode(crate::constants::MAGIC_SKIP, None).unwrap();
284
285 assert_eq!(reason, "FOUNDRY::SKIP");
286 assert!(SkipReason::decode_self(&reason).is_none());
287 }
288}