alloy_sol_types/types/
error.rs

1use crate::{
2    abi::token::{PackedSeqToken, Token, TokenSeq, WordToken},
3    types::interface::RevertReason,
4    Result, SolType, Word,
5};
6use alloc::{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], validate: bool) -> Result<Self> {
55        <Self::Parameters<'_> as SolType>::abi_decode_sequence(data, validate).map(Self::new)
56    }
57
58    /// ABI decode this error's arguments from the given slice, **with** the
59    /// selector.
60    #[inline]
61    fn abi_decode(data: &[u8], validate: bool) -> Result<Self> {
62        let data = data
63            .strip_prefix(&Self::SELECTOR)
64            .ok_or_else(|| crate::Error::type_check_fail_sig(data, Self::SIGNATURE))?;
65        Self::abi_decode_raw(data, validate)
66    }
67
68    /// ABI encode the error to the given buffer **without** its selector.
69    #[inline]
70    fn abi_encode_raw(&self, out: &mut Vec<u8>) {
71        out.reserve(self.abi_encoded_size());
72        out.extend(crate::abi::encode_sequence(&self.tokenize()));
73    }
74
75    /// ABI encode the error to the given buffer **with** its selector.
76    #[inline]
77    fn abi_encode(&self) -> Vec<u8> {
78        let mut out = Vec::with_capacity(4 + self.abi_encoded_size());
79        out.extend(&Self::SELECTOR);
80        self.abi_encode_raw(&mut out);
81        out
82    }
83}
84
85/// Represents a standard Solidity revert. These are thrown by `revert(reason)`
86/// or `require(condition, reason)` statements in Solidity.
87#[derive(Clone, PartialEq, Eq, Hash)]
88pub struct Revert {
89    /// The reason string, provided by the Solidity contract.
90    pub reason: String,
91}
92
93impl fmt::Debug for Revert {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_tuple("Revert").field(&self.reason).finish()
96    }
97}
98
99impl fmt::Display for Revert {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str("revert: ")?;
102        f.write_str(self.reason())
103    }
104}
105
106impl core::error::Error for Revert {}
107
108impl AsRef<str> for Revert {
109    #[inline]
110    fn as_ref(&self) -> &str {
111        &self.reason
112    }
113}
114
115impl Borrow<str> for Revert {
116    #[inline]
117    fn borrow(&self) -> &str {
118        &self.reason
119    }
120}
121
122impl From<Revert> for String {
123    #[inline]
124    fn from(value: Revert) -> Self {
125        value.reason
126    }
127}
128
129impl From<String> for Revert {
130    #[inline]
131    fn from(reason: String) -> Self {
132        Self { reason }
133    }
134}
135
136impl From<&str> for Revert {
137    #[inline]
138    fn from(value: &str) -> Self {
139        Self { reason: value.into() }
140    }
141}
142
143impl SolError for Revert {
144    type Parameters<'a> = (crate::sol_data::String,);
145    type Token<'a> = (PackedSeqToken<'a>,);
146
147    const SIGNATURE: &'static str = "Error(string)";
148    const SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
149
150    #[inline]
151    fn new(tuple: <Self::Parameters<'_> as SolType>::RustType) -> Self {
152        Self { reason: tuple.0 }
153    }
154
155    #[inline]
156    fn tokenize(&self) -> Self::Token<'_> {
157        (PackedSeqToken::from(self.reason.as_bytes()),)
158    }
159
160    #[inline]
161    fn abi_encoded_size(&self) -> usize {
162        64 + crate::utils::next_multiple_of_32(self.reason.len())
163    }
164}
165
166impl Revert {
167    /// Returns the revert reason string, or `"<empty>"` if empty.
168    #[inline]
169    pub fn reason(&self) -> &str {
170        if self.reason.is_empty() {
171            "<empty>"
172        } else {
173            &self.reason
174        }
175    }
176}
177
178/// A [Solidity panic].
179///
180/// These are thrown by `assert(condition)` and by internal Solidity checks,
181/// such as arithmetic overflow or array bounds checks.
182///
183/// The list of all known panic codes can be found in the [PanicKind] enum.
184///
185/// [Solidity panic]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
186#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
187pub struct Panic {
188    /// The [Solidity panic code].
189    ///
190    /// [Solidity panic code]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
191    pub code: U256,
192}
193
194impl AsRef<U256> for Panic {
195    #[inline]
196    fn as_ref(&self) -> &U256 {
197        &self.code
198    }
199}
200
201impl Borrow<U256> for Panic {
202    #[inline]
203    fn borrow(&self) -> &U256 {
204        &self.code
205    }
206}
207
208impl From<PanicKind> for Panic {
209    #[inline]
210    fn from(value: PanicKind) -> Self {
211        Self { code: U256::from(value as u64) }
212    }
213}
214
215impl From<u64> for Panic {
216    #[inline]
217    fn from(value: u64) -> Self {
218        Self { code: U256::from(value) }
219    }
220}
221
222impl From<Panic> for U256 {
223    #[inline]
224    fn from(value: Panic) -> Self {
225        value.code
226    }
227}
228
229impl From<U256> for Panic {
230    #[inline]
231    fn from(value: U256) -> Self {
232        Self { code: value }
233    }
234}
235
236impl fmt::Debug for Panic {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        let mut debug = f.debug_tuple("Panic");
239        if let Some(kind) = self.kind() {
240            debug.field(&kind);
241        } else {
242            debug.field(&self.code);
243        }
244        debug.finish()
245    }
246}
247
248impl fmt::Display for Panic {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.write_str("panic: ")?;
251
252        let kind = self.kind();
253        let msg = kind.map(PanicKind::as_str).unwrap_or("unknown code");
254        f.write_str(msg)?;
255
256        f.write_str(" (0x")?;
257        if let Some(kind) = kind {
258            write!(f, "{:02x}", kind as u32)
259        } else {
260            write!(f, "{:x}", self.code)
261        }?;
262        f.write_str(")")
263    }
264}
265
266impl core::error::Error for Panic {}
267
268impl SolError for Panic {
269    type Parameters<'a> = (crate::sol_data::Uint<256>,);
270    type Token<'a> = (WordToken,);
271
272    const SIGNATURE: &'static str = "Panic(uint256)";
273    const SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
274
275    #[inline]
276    fn new(tuple: <Self::Parameters<'_> as SolType>::RustType) -> Self {
277        Self { code: tuple.0 }
278    }
279
280    #[inline]
281    fn tokenize(&self) -> Self::Token<'_> {
282        (WordToken::from(self.code),)
283    }
284
285    #[inline]
286    fn abi_encoded_size(&self) -> usize {
287        32
288    }
289}
290
291impl Panic {
292    /// Returns the [PanicKind] if this panic code is a known Solidity panic, as
293    /// described [in the Solidity documentation][ref].
294    ///
295    /// [ref]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
296    pub fn kind(&self) -> Option<PanicKind> {
297        // use try_from to avoid copying by using the `&` impl
298        u32::try_from(&self.code).ok().and_then(PanicKind::from_number)
299    }
300}
301
302/// Represents a [Solidity panic].
303/// Same as the [Solidity definition].
304///
305/// [Solidity panic]: https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
306/// [Solidity definition]: https://github.com/ethereum/solidity/blob/9eaa5cebdb1458457135097efdca1a3573af17c8/libsolutil/ErrorCodes.h#L25-L37
307#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
308#[repr(u32)]
309#[non_exhaustive]
310pub enum PanicKind {
311    // Docs extracted from the Solidity definition and documentation, linked above.
312    /// Generic / unspecified error.
313    ///
314    /// Generic compiler inserted panics.
315    #[default]
316    Generic = 0x00,
317    /// Used by the `assert()` builtin.
318    ///
319    /// Thrown when you call `assert` with an argument that evaluates to
320    /// `false`.
321    Assert = 0x01,
322    /// Arithmetic underflow or overflow.
323    ///
324    /// Thrown when an arithmetic operation results in underflow or overflow
325    /// outside of an `unchecked { ... }` block.
326    UnderOverflow = 0x11,
327    /// Division or modulo by zero.
328    ///
329    /// Thrown when you divide or modulo by zero (e.g. `5 / 0` or `23 % 0`).
330    DivisionByZero = 0x12,
331    /// Enum conversion error.
332    ///
333    /// Thrown when you convert a value that is too big or negative into an enum
334    /// type.
335    EnumConversionError = 0x21,
336    /// Invalid encoding in storage.
337    ///
338    /// Thrown when you access a storage byte array that is incorrectly encoded.
339    StorageEncodingError = 0x22,
340    /// Empty array pop.
341    ///
342    /// Thrown when you call `.pop()` on an empty array.
343    EmptyArrayPop = 0x31,
344    /// Array out of bounds access.
345    ///
346    /// Thrown when you access an array, `bytesN` or an array slice at an
347    /// out-of-bounds or negative index (i.e. `x[i]` where `i >= x.length` or
348    /// `i < 0`).
349    ArrayOutOfBounds = 0x32,
350    /// Resource error (too large allocation or too large array).
351    ///
352    /// Thrown when you allocate too much memory or create an array that is too
353    /// large.
354    ResourceError = 0x41,
355    /// Calling invalid internal function.
356    ///
357    /// Thrown when you call a zero-initialized variable of internal function
358    /// type.
359    InvalidInternalFunction = 0x51,
360}
361
362impl fmt::Display for PanicKind {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.write_str(self.as_str())
365    }
366}
367
368impl PanicKind {
369    /// Returns the panic code for the given number if it is a known one.
370    pub const fn from_number(value: u32) -> Option<Self> {
371        match value {
372            0x00 => Some(Self::Generic),
373            0x01 => Some(Self::Assert),
374            0x11 => Some(Self::UnderOverflow),
375            0x12 => Some(Self::DivisionByZero),
376            0x21 => Some(Self::EnumConversionError),
377            0x22 => Some(Self::StorageEncodingError),
378            0x31 => Some(Self::EmptyArrayPop),
379            0x32 => Some(Self::ArrayOutOfBounds),
380            0x41 => Some(Self::ResourceError),
381            0x51 => Some(Self::InvalidInternalFunction),
382            _ => None,
383        }
384    }
385
386    /// Returns the panic code's string representation.
387    pub const fn as_str(self) -> &'static str {
388        // modified from the original Solidity comments:
389        // https://github.com/ethereum/solidity/blob/9eaa5cebdb1458457135097efdca1a3573af17c8/libsolutil/ErrorCodes.h#L25-L37
390        match self {
391            Self::Generic => "generic/unspecified error",
392            Self::Assert => "assertion failed",
393            Self::UnderOverflow => "arithmetic underflow or overflow",
394            Self::DivisionByZero => "division or modulo by zero",
395            Self::EnumConversionError => "failed to convert value into enum type",
396            Self::StorageEncodingError => "storage byte array incorrectly encoded",
397            Self::EmptyArrayPop => "called `.pop()` on an empty array",
398            Self::ArrayOutOfBounds => "array out-of-bounds access",
399            Self::ResourceError => "memory allocation error",
400            Self::InvalidInternalFunction => "called an invalid internal function",
401        }
402    }
403}
404
405/// Decodes and retrieves the reason for a revert from the provided output data.
406///
407/// This function attempts to decode the provided output data as a generic contract error
408/// or a UTF-8 string (for Vyper reverts) using the `RevertReason::decode` method.
409///
410/// If successful, it returns the decoded revert reason wrapped in an `Option`.
411///
412/// If both attempts fail, it returns `None`.
413pub fn decode_revert_reason(out: &[u8]) -> Option<String> {
414    RevertReason::decode(out).map(|x| x.to_string())
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use crate::{sol, types::interface::SolInterface};
421    use alloc::string::ToString;
422    use alloy_primitives::{address, hex, keccak256};
423
424    #[test]
425    fn revert_encoding() {
426        let revert = Revert::from("test");
427        let encoded = revert.abi_encode();
428        let decoded = Revert::abi_decode(&encoded, true).unwrap();
429        assert_eq!(encoded.len(), revert.abi_encoded_size() + 4);
430        assert_eq!(encoded.len(), 100);
431        assert_eq!(revert, decoded);
432    }
433
434    #[test]
435    fn panic_encoding() {
436        let panic = Panic { code: U256::ZERO };
437        assert_eq!(panic.kind(), Some(PanicKind::Generic));
438        let encoded = panic.abi_encode();
439        let decoded = Panic::abi_decode(&encoded, true).unwrap();
440
441        assert_eq!(encoded.len(), panic.abi_encoded_size() + 4);
442        assert_eq!(encoded.len(), 36);
443        assert_eq!(panic, decoded);
444    }
445
446    #[test]
447    fn selectors() {
448        assert_eq!(
449            Revert::SELECTOR,
450            &keccak256(b"Error(string)")[..4],
451            "Revert selector is incorrect"
452        );
453        assert_eq!(
454            Panic::SELECTOR,
455            &keccak256(b"Panic(uint256)")[..4],
456            "Panic selector is incorrect"
457        );
458    }
459
460    #[test]
461    fn decode_solidity_revert_reason() {
462        let revert = Revert::from("test_revert_reason");
463        let encoded = revert.abi_encode();
464        let decoded = decode_revert_reason(&encoded).unwrap();
465        assert_eq!(decoded, revert.to_string());
466    }
467
468    #[test]
469    fn decode_uniswap_revert() {
470        // Solc 0.5.X/0.5.16 adds a random 0x80 byte which makes reserialization check fail.
471        // https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L178
472        // https://github.com/paradigmxyz/evm-inspectors/pull/12
473        let bytes = hex!("08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024556e697377617056323a20494e53554646494349454e545f494e5055545f414d4f554e5400000000000000000000000000000000000000000000000000000080");
474
475        Revert::abi_decode(&bytes, true).unwrap_err();
476
477        let decoded = Revert::abi_decode(&bytes, false).unwrap();
478        assert_eq!(decoded.reason, "UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
479
480        let decoded = decode_revert_reason(&bytes).unwrap();
481        assert_eq!(decoded, "revert: UniswapV2: INSUFFICIENT_INPUT_AMOUNT");
482    }
483
484    #[test]
485    fn decode_random_revert_reason() {
486        let revert_reason = String::from("test_revert_reason");
487        let decoded = decode_revert_reason(revert_reason.as_bytes()).unwrap();
488        assert_eq!(decoded, "test_revert_reason");
489    }
490
491    #[test]
492    fn decode_non_utf8_revert_reason() {
493        let revert_reason = [0xFF];
494        let decoded = decode_revert_reason(&revert_reason);
495        assert_eq!(decoded, None);
496    }
497
498    // https://github.com/alloy-rs/core/issues/382
499    #[test]
500    fn decode_solidity_no_interface() {
501        sol! {
502            interface C {
503                #[derive(Debug, PartialEq)]
504                error SenderAddressError(address);
505            }
506        }
507
508        let data = hex!("8758782b000000000000000000000000a48388222c7ee7daefde5d0b9c99319995c4a990");
509        assert_eq!(decode_revert_reason(&data), None);
510
511        let C::CErrors::SenderAddressError(decoded) = C::CErrors::abi_decode(&data, true).unwrap();
512        assert_eq!(
513            decoded,
514            C::SenderAddressError { _0: address!("0xa48388222c7ee7daefde5d0b9c99319995c4a990") }
515        );
516    }
517}