Skip to main content

foundry_evm_core/
decode.rs

1//! Various utilities to decode test results.
2
3use 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/// Stable user-facing fallback for empty revert payloads.
17pub const EMPTY_REVERT_DATA: &str = "<empty revert data>";
18
19/// Prefix used by Foundry assertion helpers in user-visible failure messages.
20pub const ASSERTION_FAILED_PREFIX: &str = "assertion failed";
21
22/// A skip reason.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct SkipReason(pub Option<String>);
25
26impl SkipReason {
27    /// Decodes a skip reason, if any.
28    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    /// Decodes a skip reason from a string that was obtained by formatting `Self`.
36    ///
37    /// This is a hack to support re-decoding a skip reason in proptest.
38    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
54/// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log`
55pub fn decode_console_logs(logs: &[Log]) -> Vec<String> {
56    logs.iter().filter_map(decode_console_log).collect()
57}
58
59/// Decode a single log.
60///
61/// This function returns [None] if it is not a DSTest log or the result of a Hardhat
62/// `console.log`.
63pub fn decode_console_log(log: &Log) -> Option<String> {
64    console::ds::ConsoleEvents::decode_log(log).ok().map(|decoded| decoded.to_string())
65}
66
67/// Decodes revert data.
68#[derive(Clone, Debug, Default)]
69pub struct RevertDecoder {
70    /// The custom errors to use for decoding.
71    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    /// Creates a new, empty revert decoder.
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Sets the ABIs to use for error decoding.
88    ///
89    /// Note that this is decently expensive as it will hash all errors for faster indexing.
90    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    /// Sets the ABI to use for error decoding.
96    ///
97    /// Note that this is decently expensive as it will hash all errors for faster indexing.
98    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
99        self.extend_from_abi(abi);
100        self
101    }
102
103    /// Extends the decoder with the given ABI's custom errors.
104    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    /// Extends the decoder with the given ABI's custom errors.
111    fn extend_from_abi(&mut self, abi: &JsonAbi) {
112        for error in abi.errors() {
113            self.push_error(error.clone());
114        }
115    }
116
117    /// Adds a custom error to use for decoding.
118    pub fn push_error(&mut self, error: Error) {
119        self.errors.entry(error.selector()).or_default().push(error);
120    }
121
122    /// Tries to decode an error message from the given revert bytes.
123    ///
124    /// Note that this is just a best-effort guess, and should not be relied upon for anything other
125    /// than user output.
126    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    /// Tries to decode an error message from the given revert bytes.
133    ///
134    /// See [`decode`](Self::decode) for more information.
135    pub fn maybe_decode(&self, err: &[u8], status: Option<InstructionResult>) -> Option<String> {
136        // Solidity's `Error(string)` (handled separately in order to strip revert: prefix)
137        if let Some(ContractError(Revert(revert))) = RevertReason::decode(err) {
138            return Some(revert.reason);
139        }
140
141        // Solidity's `Panic(uint256)` and `Vm`'s custom errors.
142        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            // Custom errors.
150            if let Some(errors) = self.errors.get(selector) {
151                for error in errors {
152                    // If we don't decode, don't return an error, try to decode as a string
153                    // later.
154                    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            // Generic custom error.
169            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
199/// Helper function that decodes provided error as an ABI encoded or an ASCII string (if not empty).
200fn decode_as_non_empty_string(err: &[u8]) -> Option<String> {
201    // ABI-encoded `string`.
202    if let Ok(s) = String::abi_decode(err)
203        && !s.is_empty()
204    {
205        return Some(s);
206    }
207
208    // ASCII string.
209    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    // https://github.com/foundry-rs/foundry/issues/10162
247    #[test]
248    fn partial_decode() {
249        /*
250        error ValidationFailed(bytes);
251        error InvalidNonce();
252        */
253        let mut decoder = RevertDecoder::default();
254        decoder.push_error("ValidationFailed(bytes)".parse().unwrap());
255
256        /*
257        abi.encodeWithSelector(ValidationFailed.selector, InvalidNonce.selector)
258        */
259        let data = &hex!(
260            "0xe17594de"
261            "756688fe00000000000000000000000000000000000000000000000000000000"
262        );
263        assert_eq!(
264            decoder.decode(data, None),
265            "custom error 0xe17594de: 756688fe00000000000000000000000000000000000000000000000000000000"
266        );
267
268        /*
269        abi.encodeWithSelector(ValidationFailed.selector, abi.encodeWithSelector(InvalidNonce.selector))
270        */
271        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}