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/// A skip reason.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct SkipReason(pub Option<String>);
19
20impl SkipReason {
21    /// Decodes a skip reason, if any.
22    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    /// Decodes a skip reason from a string that was obtained by formatting `Self`.
30    ///
31    /// This is a hack to support re-decoding a skip reason in proptest.
32    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
48/// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log`
49pub fn decode_console_logs(logs: &[Log]) -> Vec<String> {
50    logs.iter().filter_map(decode_console_log).collect()
51}
52
53/// Decode a single log.
54///
55/// This function returns [None] if it is not a DSTest log or the result of a Hardhat
56/// `console.log`.
57fn decode_console_log(log: &Log) -> Option<String> {
58    console::ds::ConsoleEvents::decode_log(log).ok().map(|decoded| decoded.to_string())
59}
60
61/// Decodes revert data.
62#[derive(Clone, Debug, Default)]
63pub struct RevertDecoder {
64    /// The custom errors to use for decoding.
65    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    /// Creates a new, empty revert decoder.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Sets the ABIs to use for error decoding.
82    ///
83    /// Note that this is decently expensive as it will hash all errors for faster indexing.
84    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    /// Sets the ABI to use for error decoding.
90    ///
91    /// Note that this is decently expensive as it will hash all errors for faster indexing.
92    pub fn with_abi(mut self, abi: &JsonAbi) -> Self {
93        self.extend_from_abi(abi);
94        self
95    }
96
97    /// Extends the decoder with the given ABI's custom errors.
98    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    /// Extends the decoder with the given ABI's custom errors.
105    fn extend_from_abi(&mut self, abi: &JsonAbi) {
106        for error in abi.errors() {
107            self.push_error(error.clone());
108        }
109    }
110
111    /// Adds a custom error to use for decoding.
112    pub fn push_error(&mut self, error: Error) {
113        self.errors.entry(error.selector()).or_default().push(error);
114    }
115
116    /// Tries to decode an error message from the given revert bytes.
117    ///
118    /// Note that this is just a best-effort guess, and should not be relied upon for anything other
119    /// than user output.
120    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    /// Tries to decode an error message from the given revert bytes.
127    ///
128    /// See [`decode`](Self::decode) for more information.
129    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        // Solidity's `Error(string)` (handled separately in order to strip revert: prefix)
135        if let Some(ContractError(Revert(revert))) = RevertReason::decode(err) {
136            return Some(revert.reason);
137        }
138
139        // Solidity's `Panic(uint256)` and `Vm`'s custom errors.
140        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            // Custom errors.
148            if let Some(errors) = self.errors.get(selector) {
149                for error in errors {
150                    // If we don't decode, don't return an error, try to decode as a string
151                    // later.
152                    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            // Generic custom error.
167            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
197/// Helper function that decodes provided error as an ABI encoded or an ASCII string (if not empty).
198fn decode_as_non_empty_string(err: &[u8]) -> Option<String> {
199    // ABI-encoded `string`.
200    if let Ok(s) = String::abi_decode(err)
201        && !s.is_empty()
202    {
203        return Some(s);
204    }
205
206    // ASCII string.
207    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    // https://github.com/foundry-rs/foundry/issues/10162
245    #[test]
246    fn partial_decode() {
247        /*
248        error ValidationFailed(bytes);
249        error InvalidNonce();
250        */
251        let mut decoder = RevertDecoder::default();
252        decoder.push_error("ValidationFailed(bytes)".parse().unwrap());
253
254        /*
255        abi.encodeWithSelector(ValidationFailed.selector, InvalidNonce.selector)
256        */
257        let data = &hex!(
258            "0xe17594de"
259            "756688fe00000000000000000000000000000000000000000000000000000000"
260        );
261        assert_eq!(
262            decoder.decode(data, None),
263            "custom error 0xe17594de: 756688fe00000000000000000000000000000000000000000000000000000000"
264        );
265
266        /*
267        abi.encodeWithSelector(ValidationFailed.selector, abi.encodeWithSelector(InvalidNonce.selector))
268        */
269        let data = &hex!(
270            "0xe17594de"
271            "0000000000000000000000000000000000000000000000000000000000000020"
272            "0000000000000000000000000000000000000000000000000000000000000004"
273            "756688fe00000000000000000000000000000000000000000000000000000000"
274        );
275        assert_eq!(decoder.decode(data, None), "ValidationFailed(0x756688fe)");
276    }
277}