Skip to main content

revm_trace/utils/
error_utils.rs

1//! Error handling utilities for Ethereum transactions
2//!
3//! This module provides utilities for parsing and handling various types of errors:
4//! - Custom Solidity errors (Error(string))
5//! - Solidity panic codes (Panic(uint256))
6//! - Custom error selectors
7//!
8//! Common error scenarios that this module handles:
9//! - Revert with string message
10//! - Assertion failures
11//! - Arithmetic operations
12//! - Array bounds checks
13
14use alloy::dyn_abi::{DynSolType, DynSolValue};
15
16/// Parse custom error output from a failed transaction
17///
18/// Handles two main types of errors:
19/// 1. Error(string) - Standard revert with message (selector: 0x08c379a0)
20/// 2. Panic(uint256) - Solidity panic with error code (selector: 0x4e487b71)
21///
22/// # Arguments
23/// * `output` - Raw error output bytes from the failed transaction
24///
25/// # Returns
26/// * `Some(String)` - Decoded error message or panic reason
27/// * `None` - If the error format is not recognized or cannot be decoded
28///
29/// # Example
30/// ```no_run
31/// use revm_trace::utils::error_utils::parse_custom_error;
32/// use alloy::primitives::hex;
33/// let error_output = hex::decode("08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014496e73756666696369656e742062616c616e636500000000000000000000000000").unwrap();
34/// let error_message = parse_custom_error(&error_output);
35/// assert_eq!(error_message, Some("Insufficient balance".to_string()));
36/// ```
37pub fn parse_custom_error(output: &[u8]) -> Option<String> {
38    if output.len() < 4 {
39        return None;
40    }
41
42    let selector = &output[0..4];
43    match selector {
44        // Error(string) - 0x08c379a0
45        [0x08, 0xc3, 0x79, 0xa0] => {
46            if let Ok(DynSolValue::String(reason)) = DynSolType::String.abi_decode(&output[4..]) {
47                Some(reason)
48            } else {
49                None
50            }
51        }
52        // Panic(uint256) - 0x4e487b71
53        [0x4e, 0x48, 0x7b, 0x71] => {
54            if let Ok(DynSolValue::Uint(code, _)) = DynSolType::Uint(256).abi_decode(&output[4..]) {
55                return Some(match code.to::<u64>() {
56                    0x01 => "Panic: Assertion failed".to_string(),
57                    0x11 => "Panic: Arithmetic overflow".to_string(),
58                    0x12 => "Panic: Division by zero".to_string(),
59                    0x21 => "Panic: Invalid array access".to_string(),
60                    0x22 => "Panic: Array access out of bounds".to_string(),
61                    0x31 => "Panic: Invalid enum value".to_string(),
62                    0x32 => "Panic: Invalid storage access".to_string(),
63                    0x41 => "Panic: Zero initialization".to_string(),
64                    0x51 => "Panic: Invalid calldata access".to_string(),
65                    code => format!("Panic: Unknown error code (0x{code:x})"),
66                });
67            }
68            None
69        }
70        _ => None,
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use alloy::primitives::hex::decode;
78
79    #[test]
80    fn test_parse_error_string() {
81        // "Insufficient balance" encoded as Error(string)
82        let error_bytes = decode(
83            "08c379a0\
84             0000000000000000000000000000000000000000000000000000000000000020\
85             0000000000000000000000000000000000000000000000000000000000000014\
86             496e73756666696369656e742062616c616e636500000000000000000000000000",
87        )
88        .unwrap();
89
90        let result = parse_custom_error(&error_bytes);
91        assert_eq!(result, Some("Insufficient balance".to_string()));
92
93        // Test invalid error string format
94        let invalid_bytes = decode("08c379a0").unwrap();
95        assert_eq!(parse_custom_error(&invalid_bytes), None);
96    }
97
98    #[test]
99    fn test_parse_panic() {
100        // Test various panic codes
101        let panic_codes = [
102            (0x01, "Panic: Assertion failed"),
103            (0x11, "Panic: Arithmetic overflow"),
104            (0x12, "Panic: Division by zero"),
105            (0x21, "Panic: Invalid array access"),
106            (0x22, "Panic: Array access out of bounds"),
107            (0x31, "Panic: Invalid enum value"),
108            (0x32, "Panic: Invalid storage access"),
109            (0x41, "Panic: Zero initialization"),
110            (0x51, "Panic: Invalid calldata access"),
111            (0xFF, "Panic: Unknown error code (0xff)"),
112        ];
113
114        for (code, expected_message) in panic_codes {
115            // Encode Panic(uint256)
116            let panic_bytes = [
117                // Selector for Panic(uint256)
118                0x4e, 0x48, 0x7b, 0x71, // Encoded uint256 value (32 bytes, left-padded)
119                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
120                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
121                0x00, 0x00, 0x00, code as u8,
122            ];
123
124            let result = parse_custom_error(&panic_bytes);
125            assert_eq!(result, Some(expected_message.to_string()));
126        }
127    }
128
129    #[test]
130    fn test_invalid_inputs() {
131        // Test empty input
132        assert_eq!(parse_custom_error(&[]), None);
133
134        // Test input shorter than selector
135        assert_eq!(parse_custom_error(&[0x08, 0xc3, 0x79]), None);
136
137        // Test unknown selector
138        assert_eq!(parse_custom_error(&[0x00, 0x00, 0x00, 0x00]), None);
139
140        // Test invalid panic code encoding
141        let invalid_panic = [
142            0x4e, 0x48, 0x7b, 0x71, // Panic selector
143            0x00, // Invalid uint256 encoding
144        ];
145        assert_eq!(parse_custom_error(&invalid_panic), None);
146    }
147}