alloy_sol_types/types/
error.rs

1use crate::{
2    Result, SolType, Word,
3    abi::token::{PackedSeqToken, Token, TokenSeq, WordToken},
4    types::interface::RevertReason,
5};
6use alloc::{borrow::Cow, format, string::String, vec::Vec};
7use alloy_primitives::U256;
8use core::{borrow::Borrow, fmt};
9
10/// A Solidity custom error.
11///
12/// # Implementer's Guide
13///
14/// It should not be necessary to implement this trait manually. Instead, use
15/// the [`sol!`](crate::sol!) procedural macro to parse Solidity syntax into
16/// types that implement this trait.
17pub trait SolError: Sized {
18    /// The underlying tuple type which represents the error's members.
19    ///
20    /// If the error has no arguments, this will be the unit type `()`
21    type Parameters<'a>: SolType<Token<'a> = Self::Token<'a>>;
22
23    /// The corresponding [`TokenSeq`] type.
24    type Token<'a>: TokenSeq<'a>;
25
26    /// The error's ABI signature.
27    const SIGNATURE: &'static str;
28
29    /// The error selector: `keccak256(SIGNATURE)[0..4]`
30    const SELECTOR: [u8; 4];
31
32    /// Convert from the tuple type used for ABI encoding and decoding.
33    fn new(tuple: <Self::Parameters<'_> as SolType>::RustType) -> Self;
34
35    /// Convert to the token type used for EIP-712 encoding and decoding.
36    fn tokenize(&self) -> Self::Token<'_>;
37
38    /// The size of the error params when encoded in bytes, **without** the
39    /// selector.
40    #[inline]
41    fn abi_encoded_size(&self) -> usize {
42        if let Some(size) = <Self::Parameters<'_> as SolType>::ENCODED_SIZE {
43            return size;
44        }
45
46        // `total_words` includes the first dynamic offset which we ignore.
47        let offset = <<Self::Parameters<'_> as SolType>::Token<'_> as Token>::DYNAMIC as usize * 32;
48        (self.tokenize().total_words() * Word::len_bytes()).saturating_sub(offset)
49    }
50
51    /// ABI decode this call's arguments from the given slice, **without** its
52    /// selector.
53    #[inline]
54    fn abi_decode_raw(data: &[u8]) -> Result<Self> {
55        <Self::Parameters<'_> as SolType>::abi_decode_sequence(data).map(Self::new)
56    }
57
58    /// ABI decode this error's arguments from the given slice, **without** its
59    /// selector, with validation.
60    ///
61    /// This is the same as [`abi_decode_raw`](Self::abi_decode_raw), but performs
62    /// validation checks on the decoded parameters tuple.
63    #[inline]
64    fn abi_decode_raw_validate(data: &[u8]) -> Result<Self> {
65        <Self::Parameters<'_> as SolType>::abi_decode_sequence_validate(data).map(Self::new)
66    }
67
68    /// ABI decode this error's arguments from the given slice, **with** the
69    /// selector.
70    #[inline]
71    fn abi_decode(data: &[u8]) -> Result<Self> {
72        let data = data
73            .strip_prefix(&Self::SELECTOR)
74            .ok_or_else(|| crate::Error::type_check_fail_sig(data, Self::SIGNATURE))?;
75        Self::abi_decode_raw(data)
76    }
77
78    /// ABI decode this error's arguments from the given slice, **with** the
79    /// selector, with validation.
80    ///
81    /// This is the same as [`abi_decode`](Self::abi_decode), but performs
82    /// validation checks on the decoded parameters tuple.
83    #[inline]
84    fn abi_decode_validate(data: &[u8]) -> Result<Self> {
85        let data = data
86            .strip_prefix(&Self::SELECTOR)
87            .ok_or_else(|| crate::Error::type_check_fail_sig(data, Self::SIGNATURE))?;
88        Self::abi_decode_raw_validate(data)
89    }
90
91    /// ABI encode the error to the given buffer **without** its selector.
92    #[inline]
93    fn abi_encode_raw(&self, out: &mut Vec<u8>) {
94        out.reserve(self.abi_encoded_size());
95        out.extend(crate::abi::encode_sequence(&self.tokenize()));
96    }
97
98    /// ABI encode the error to the given buffer **with** its selector.
99    #[inline]
100    fn abi_encode(&self) -> Vec<u8> {
101        let mut out = Vec::with_capacity(4 + self.abi_encoded_size());
102        out.extend(&Self::SELECTOR);
103        self.abi_encode_raw(&mut out);
104        out
105    }
106}
107
108/// Represents a standard Solidity revert. These are thrown by `revert(reason)`
109/// or `require(condition, reason)` statements in Solidity.
110#[derive(Clone, PartialEq, Eq, Hash)]
111pub struct Revert {
112    /// The reason string, provided by the Solidity contract.
113    pub reason: String,
114}
115
116impl fmt::Debug for Revert {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.debug_tuple("Revert").field(&self.reason).finish()
119    }
120}
121
122impl fmt::Display for Revert {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str("revert: ")?;
125        f.write_str(self.reason())
126    }
127}
128
129impl core::error::Error for Revert {}
130
131impl AsRef<str> for Revert {
132    #[inline]
133    fn as_ref(&self) -> &str {
134        &self.reason
135    }
136}
137
138impl Borrow<str> for Revert {
139    #[inline]
140    fn borrow(&self) -> &str {
141        &self.reason
142    }
143}
144
145impl From<Revert> for String {
146    #[inline]
147    fn from(value: Revert) -> Self {
148        value.reason
149    }
150}
151
152impl From<String> for Revert {
153    #[inline]
154    fn from(reason: String) -> Self {
155        Self { reason }
156    }
157}
158
159impl From<&str> for Revert {
160    #[inline]
161    fn from(value: &str) -> Self {
162        Self { reason: value.into() }
163    }
164}
165
166impl SolError for Revert {
167    type Parameters<'a> = (crate::sol_data::String,);
168    type Token<'a> = (PackedSeqToken<'a>,);
169
170    const SIGNATURE: &'static str = "Error(string)";
171    const SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
172
173    #[inline]
174    fn new(tuple: <Self::Parameters<'_> as SolType>::RustType) -> Self {
175        Self { reason: tuple.0 }
176    }
177
178    #[inline]
179    fn tokenize(&self) -> Self::Token<'_> {
180        (PackedSeqToken::from(self.reason.as_bytes()),)
181    }
182
183    #[inline]
184    fn abi_encoded_size(&self) -> usize {
185        64 + crate::utils::next_multiple_of_32(self.reason.len())
186    }
187
188    #[inline]
189    fn abi_decode_raw_validate(data: &[u8]) -> Result<Self> {
190        Self::abi_decode_raw(data)
191    }
192}
193
194impl Revert {
195    /// Returns the revert reason string, or `"<empty>"` if empty.
196    #[inline]
197    pub fn reason(&self) -> &str {
198        if self.reason.is_empty() { "<empty>" } else { &self.reason }
199    }
200}
201
202/// A [Solidity panic].
203///
204/// These are thrown by `assert(condition)` and by internal Solidity checks,
205/// such as arithmetic overflow or array bounds checks.
206///
207/// The list of all known panic codes can be found in the [PanicKind] enum.
208///
209/// [Solidity panic]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
210#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
211pub struct Panic {
212    /// The [Solidity panic code].
213    ///
214    /// [Solidity panic code]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
215    pub code: U256,
216}
217
218impl AsRef<U256> for Panic {
219    #[inline]
220    fn as_ref(&self) -> &U256 {
221        &self.code
222    }
223}
224
225impl Borrow<U256> for Panic {
226    #[inline]
227    fn borrow(&self) -> &U256 {
228        &self.code
229    }
230}
231
232impl From<PanicKind> for Panic {
233    #[inline]
234    fn from(value: PanicKind) -> Self {
235        Self { code: U256::from(value as u64) }
236    }
237}
238
239impl From<u64> for Panic {
240    #[inline]
241    fn from(value: u64) -> Self {
242        Self { code: U256::from(value) }
243    }
244}
245
246impl From<Panic> for U256 {
247    #[inline]
248    fn from(value: Panic) -> Self {
249        value.code
250    }
251}
252
253impl From<U256> for Panic {
254    #[inline]
255    fn from(value: U256) -> Self {
256        Self { code: value }
257    }
258}
259
260impl fmt::Debug for Panic {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        let mut debug = f.debug_tuple("Panic");
263        if let Some(kind) = self.kind() {
264            debug.field(&kind);
265        } else {
266            debug.field(&self.code);
267        }
268        debug.finish()
269    }
270}
271
272impl fmt::Display for Panic {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.write_str("panic: ")?;
275
276        let kind = self.kind();
277        let msg = kind.map(PanicKind::as_str).unwrap_or("unknown code");
278        f.write_str(msg)?;
279
280        f.write_str(" (0x")?;
281        if let Some(kind) = kind {
282            write!(f, "{:02x}", kind as u32)
283        } else {
284            write!(f, "{:x}", self.code)
285        }?;
286        f.write_str(")")
287    }
288}
289
290impl core::error::Error for Panic {}
291
292impl SolError for Panic {
293    type Parameters<'a> = (crate::sol_data::Uint<256>,);
294    type Token<'a> = (WordToken,);
295
296    const SIGNATURE: &'static str = "Panic(uint256)";
297    const SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
298
299    #[inline]
300    fn new(tuple: <Self::Parameters<'_> as SolType>::RustType) -> Self {
301        Self { code: tuple.0 }
302    }
303
304    #[inline]
305    fn tokenize(&self) -> Self::Token<'_> {
306        (WordToken::from(self.code),)
307    }
308
309    #[inline]
310    fn abi_encoded_size(&self) -> usize {
311        32
312    }
313}
314
315impl Panic {
316    /// Returns the [PanicKind] if this panic code is a known Solidity panic, as
317    /// described [in the Solidity documentation][ref].
318    ///
319    /// [ref]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
320    pub fn kind(&self) -> Option<PanicKind> {
321        // use try_from to avoid copying by using the `&` impl
322        u32::try_from(&self.code).ok().and_then(PanicKind::from_number)
323    }
324
325    /// Returns the geth-compatible panic message string.
326    pub fn as_geth_str(&self) -> Cow<'static, str> {
327        if let Some(kind) = self.kind() {
328            Cow::Borrowed(kind.as_geth_str())
329        } else {
330            Cow::Owned(format!("unknown panic code: {:#x}", self.code))
331        }
332    }
333}
334
335/// Represents a [Solidity panic].
336/// Same as the [Solidity definition].
337///
338/// [Solidity panic]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
339/// [Solidity definition]: https://github.com/ethereum/solidity/blob/9eaa5cebdb1458457135097efdca1a3573af17c8/libsolutil/ErrorCodes.h#L25-L37
340#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
341#[repr(u32)]
342#[non_exhaustive]
343pub enum PanicKind {
344    // Docs extracted from the Solidity definition and documentation, linked above.
345    /// Generic / unspecified error.
346    ///
347    /// Generic compiler inserted panics.
348    #[default]
349    Generic = 0x00,
350    /// Used by the `assert()` builtin.
351    ///
352    /// Thrown when you call `assert` with an argument that evaluates to
353    /// `false`.
354    Assert = 0x01,
355    /// Arithmetic underflow or overflow.
356    ///
357    /// Thrown when an arithmetic operation results in underflow or overflow
358    /// outside of an `unchecked { ... }` block.
359    UnderOverflow = 0x11,
360    /// Division or modulo by zero.
361    ///
362    /// Thrown when you divide or modulo by zero (e.g. `5 / 0` or `23 % 0`).
363    DivisionByZero = 0x12,
364    /// Enum conversion error.
365    ///
366    /// Thrown when you convert a value that is too big or negative into an enum
367    /// type.
368    EnumConversionError = 0x21,
369    /// Invalid encoding in storage.
370    ///
371    /// Thrown when you access a storage byte array that is incorrectly encoded.
372    StorageEncodingError = 0x22,
373    /// Empty array pop.
374    ///
375    /// Thrown when you call `.pop()` on an empty array.
376    EmptyArrayPop = 0x31,
377    /// Array out of bounds access.
378    ///
379    /// Thrown when you access an array, `bytesN` or an array slice at an
380    /// out-of-bounds or negative index (i.e. `x[i]` where `i >= x.length` or
381    /// `i < 0`).
382    ArrayOutOfBounds = 0x32,
383    /// Resource error (too large allocation or too large array).
384    ///
385    /// Thrown when you allocate too much memory or create an array that is too
386    /// large.
387    ResourceError = 0x41,
388    /// Calling invalid internal function.
389    ///
390    /// Thrown when you call a zero-initialized variable of internal function
391    /// type.
392    InvalidInternalFunction = 0x51,
393}
394
395impl fmt::Display for PanicKind {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        f.write_str(self.as_str())
398    }
399}
400
401impl PanicKind {
402    /// Returns the panic code for the given number if it is a known one.
403    pub const fn from_number(value: u32) -> Option<Self> {
404        match value {
405            0x00 => Some(Self::Generic),
406            0x01 => Some(Self::Assert),
407            0x11 => Some(Self::UnderOverflow),
408            0x12 => Some(Self::DivisionByZero),
409            0x21 => Some(Self::EnumConversionError),
410            0x22 => Some(Self::StorageEncodingError),
411            0x31 => Some(Self::EmptyArrayPop),
412            0x32 => Some(Self::ArrayOutOfBounds),
413            0x41 => Some(Self::ResourceError),
414            0x51 => Some(Self::InvalidInternalFunction),
415            _ => None,
416        }
417    }
418
419    /// Returns the panic code's string representation.
420    pub const fn as_str(self) -> &'static str {
421        // modified from the original Solidity comments:
422        // <https://github.com/ethereum/solidity/blob/9eaa5cebdb1458457135097efdca1a3573af17c8/libsolutil/ErrorCodes.h#L25-L37>
423        match self {
424            Self::Generic => "generic/unspecified error",
425            Self::Assert => "assertion failed",
426            Self::UnderOverflow => "arithmetic underflow or overflow",
427            Self::DivisionByZero => "division or modulo by zero",
428            Self::EnumConversionError => "failed to convert value into enum type",
429            Self::StorageEncodingError => "storage byte array incorrectly encoded",
430            Self::EmptyArrayPop => "called `.pop()` on an empty array",
431            Self::ArrayOutOfBounds => "array out-of-bounds access",
432            Self::ResourceError => "memory allocation error",
433            Self::InvalidInternalFunction => "called an invalid internal function",
434        }
435    }
436
437    /// Maps panic codes to their geth-specific string representations.
438    ///
439    /// Copy from go-ethereum's abi package:
440    /// <https://github.com/ethereum/go-ethereum/blob/4414e2833f92f437d0a68b53ed95ac5756a90a16/accounts/abi/abi.go#L261-L272>
441    pub const fn as_geth_str(self) -> &'static str {
442        match self {
443            Self::Generic => "generic panic",
444            Self::Assert => "assert(false)",
445            Self::UnderOverflow => "arithmetic underflow or overflow",
446            Self::DivisionByZero => "division or modulo by zero",
447            Self::EnumConversionError => "enum overflow",
448            Self::StorageEncodingError => "invalid encoded storage byte array accessed",
449            Self::EmptyArrayPop => "out-of-bounds array access; popping on an empty array",
450            Self::ArrayOutOfBounds => "out-of-bounds access of an array or bytesN",
451            Self::ResourceError => "out of memory",
452            Self::InvalidInternalFunction => "uninitialized function",
453        }
454    }
455}
456
457/// Decodes and retrieves the reason for a revert from the provided output data.
458///
459/// This function attempts to decode the provided output data as a generic contract error
460/// or a UTF-8 string (for Vyper reverts) using the `RevertReason::decode` method.
461///
462/// If successful, it returns the decoded revert reason wrapped in an `Option`.
463///
464/// If both attempts fail, it returns `None`.
465pub fn decode_revert_reason(out: &[u8]) -> Option<String> {
466    RevertReason::decode(out).map(|x| x.to_string())
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use crate::{sol, types::interface::SolInterface};
473    use alloc::string::ToString;
474    use alloy_primitives::{address, hex, keccak256};
475
476    #[test]
477    fn revert_encoding() {
478        let revert = Revert::from("test");
479        let encoded = revert.abi_encode();
480        let decoded = Revert::abi_decode(&encoded).unwrap();
481        assert_eq!(encoded.len(), revert.abi_encoded_size() + 4);
482        assert_eq!(encoded.len(), 100);
483        assert_eq!(revert, decoded);
484    }
485
486    #[test]
487    fn panic_encoding() {
488        let panic = Panic { code: U256::ZERO };
489        assert_eq!(panic.kind(), Some(PanicKind::Generic));
490        let encoded = panic.abi_encode();
491        let decoded = Panic::abi_decode(&encoded).unwrap();
492
493        assert_eq!(encoded.len(), panic.abi_encoded_size() + 4);
494        assert_eq!(encoded.len(), 36);
495        assert_eq!(panic, decoded);
496    }
497
498    #[test]
499    fn selectors() {
500        assert_eq!(
501            Revert::SELECTOR,
502            &keccak256(b"Error(string)")[..4],
503            "Revert selector is incorrect"
504        );
505        assert_eq!(
506            Panic::SELECTOR,
507            &keccak256(b"Panic(uint256)")[..4],
508            "Panic selector is incorrect"
509        );
510    }
511
512    #[test]
513    fn decode_solidity_revert_reason() {
514        let revert = Revert::from("test_revert_reason");
515        let encoded = revert.abi_encode();
516        let decoded = decode_revert_reason(&encoded).unwrap();
517        assert_eq!(decoded, revert.to_string());
518    }
519
520    #[test]
521    fn decode_uniswap_revert() {
522        // Solc 0.5.X/0.5.16 adds a random 0x80 byte which makes reserialization check fail.
523        // https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L178
524        // https://github.com/paradigmxyz/evm-inspectors/pull/12
525        let bytes = hex!(
526            "08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024556e697377617056323a20494e53554646494349454e545f494e5055545f414d4f554e5400000000000000000000000000000000000000000000000000000080"
527        );
528
529        let decoded = Revert::abi_decode(&bytes).unwrap();
530        assert_eq!(decoded.reason, "UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
531
532        let decoded = decode_revert_reason(&bytes).unwrap();
533        assert_eq!(decoded, "revert: UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
534    }
535
536    #[test]
537    fn decode_random_revert_reason() {
538        let revert_reason = String::from("test_revert_reason");
539        let decoded = decode_revert_reason(revert_reason.as_bytes()).unwrap();
540        assert_eq!(decoded, "test_revert_reason");
541    }
542
543    #[test]
544    fn decode_non_utf8_revert_reason() {
545        let revert_reason = [0xFF];
546        let decoded = decode_revert_reason(&revert_reason);
547        assert_eq!(decoded, None);
548    }
549
550    // https://github.com/alloy-rs/core/issues/382
551    #[test]
552    fn decode_solidity_no_interface() {
553        sol! {
554            interface C {
555                #[derive(Debug, PartialEq)]
556                error SenderAddressError(address);
557            }
558        }
559
560        let data = hex!("8758782b000000000000000000000000a48388222c7ee7daefde5d0b9c99319995c4a990");
561        assert_eq!(decode_revert_reason(&data), None);
562
563        let C::CErrors::SenderAddressError(decoded) =
564            C::CErrors::abi_decode_validate(&data).unwrap();
565        assert_eq!(
566            decoded,
567            C::SenderAddressError(address!("0xa48388222c7ee7daefde5d0b9c99319995c4a990"))
568        );
569    }
570
571    #[test]
572    fn panic_kind_display() {
573        let panic = Panic::from(PanicKind::Assert);
574        assert_eq!(panic.to_string(), "panic: assertion failed (0x01)");
575
576        let panic = Panic::from(PanicKind::UnderOverflow);
577        assert_eq!(panic.to_string(), "panic: arithmetic underflow or overflow (0x11)");
578
579        let panic = Panic::from(PanicKind::DivisionByZero);
580        assert_eq!(panic.to_string(), "panic: division or modulo by zero (0x12)");
581
582        let panic = Panic { code: U256::from(0x32) };
583        assert_eq!(panic.to_string(), "panic: array out-of-bounds access (0x32)");
584    }
585
586    #[test]
587    fn panic_geth_string() {
588        let panic = Panic::from(PanicKind::Assert);
589        assert_eq!(panic.as_geth_str(), "assert(false)");
590
591        let panic = Panic::from(PanicKind::UnderOverflow);
592        assert_eq!(panic.as_geth_str(), "arithmetic underflow or overflow");
593
594        let panic = Panic::from(PanicKind::DivisionByZero);
595        assert_eq!(panic.as_geth_str(), "division or modulo by zero");
596
597        let panic = Panic { code: U256::from(0x32) };
598        assert_eq!(panic.as_geth_str(), "out-of-bounds access of an array or bytesN");
599    }
600
601    #[test]
602    fn panic_unknown_code_display() {
603        let panic = Panic { code: U256::from(0x99) };
604        assert_eq!(panic.to_string(), "panic: unknown code (0x99)");
605
606        let panic = Panic { code: U256::from(0xFF) };
607        assert_eq!(panic.to_string(), "panic: unknown code (0xff)");
608
609        let panic = Panic { code: U256::from(0x05) };
610        assert_eq!(panic.to_string(), "panic: unknown code (0x5)");
611
612        let panic = Panic { code: U256::from(0x1234) };
613        assert_eq!(panic.to_string(), "panic: unknown code (0x1234)");
614    }
615
616    #[test]
617    fn panic_unknown_code_geth_string() {
618        let panic = Panic { code: U256::from(0x99) };
619        assert_eq!(panic.as_geth_str(), "unknown panic code: 0x99");
620
621        let panic = Panic { code: U256::from(0xFF) };
622        assert_eq!(panic.as_geth_str(), "unknown panic code: 0xff");
623
624        let panic = Panic { code: U256::from(0x05) };
625        assert_eq!(panic.as_geth_str(), "unknown panic code: 0x5");
626
627        let panic = Panic { code: U256::from(0x1234) };
628        assert_eq!(panic.as_geth_str(), "unknown panic code: 0x1234");
629    }
630}