Skip to main content

chainerrors_evm/
decoder.rs

1//! `EvmErrorDecoder` — the top-level EVM error decoder.
2//!
3//! Decode priority:
4//! 1. Empty data          → `ErrorKind::Empty`
5//! 2. `0x08c379a0` prefix → `ErrorKind::RevertString`   (Error(string))
6//! 3. `0x4e487b71` prefix → `ErrorKind::Panic`           (Panic(uint256))
7//! 4. Known 4-byte selector in registry → `ErrorKind::CustomError`
8//! 5. Fallback            → `ErrorKind::RawRevert`
9
10use std::sync::Arc;
11
12use chainerrors_core::decoder::{DecodeErrorError, ErrorDecoder};
13use chainerrors_core::registry::{ErrorSignature, ErrorSignatureRegistry, MemoryErrorRegistry};
14use chainerrors_core::types::{DecodedError, ErrorContext, ErrorKind};
15
16use crate::custom::decode_custom_error;
17use crate::panic::{decode_panic, PANIC_SELECTOR};
18use crate::revert::{decode_error_string, ERROR_STRING_SELECTOR};
19
20/// EVM error decoder with a bundled signature registry.
21///
22/// # Usage
23/// ```rust,no_run
24/// use chainerrors_evm::EvmErrorDecoder;
25/// use chainerrors_core::ErrorDecoder;
26///
27/// let decoder = EvmErrorDecoder::new();
28/// let result = decoder.decode(&hex::decode("08c379a0...").unwrap(), None).unwrap();
29/// println!("{result}");
30/// ```
31pub struct EvmErrorDecoder {
32    registry: Arc<dyn ErrorSignatureRegistry>,
33}
34
35impl EvmErrorDecoder {
36    /// Create a decoder with the built-in bundled error signature registry.
37    pub fn new() -> Self {
38        let reg = Arc::new(MemoryErrorRegistry::new());
39        Self::with_bundled_signatures(&reg);
40        Self { registry: reg }
41    }
42
43    /// Create a decoder with a custom registry (for testing or extension).
44    pub fn with_registry(registry: Arc<dyn ErrorSignatureRegistry>) -> Self {
45        Self { registry }
46    }
47
48    /// Populate a `MemoryErrorRegistry` with the bundled well-known error signatures.
49    fn with_bundled_signatures(reg: &MemoryErrorRegistry) {
50        use chainerrors_core::registry::{ErrorParam, ErrorSignature};
51        use tiny_keccak::{Hasher, Keccak};
52
53        fn sel(sig: &str) -> [u8; 4] {
54            let mut k = Keccak::v256();
55            k.update(sig.as_bytes());
56            let mut out = [0u8; 32];
57            k.finalize(&mut out);
58            [out[0], out[1], out[2], out[3]]
59        }
60
61        fn p(name: &str, ty: &str) -> ErrorParam {
62            ErrorParam { name: name.to_string(), ty: ty.to_string() }
63        }
64
65        let bundled: &[(&str, &str, &[(&str, &str)], Option<&str>)] = &[
66            // ─── ERC-20 ───────────────────────────────────────────────────────
67            ("ERC20InsufficientBalance", "ERC20InsufficientBalance(address,uint256,uint256)",
68             &[("sender","address"),("balance","uint256"),("needed","uint256")],
69             Some("The sender does not have enough token balance for this transfer.")),
70            ("ERC20InvalidSender", "ERC20InvalidSender(address)",
71             &[("sender","address")], None),
72            ("ERC20InvalidReceiver", "ERC20InvalidReceiver(address)",
73             &[("receiver","address")], None),
74            ("ERC20InsufficientAllowance", "ERC20InsufficientAllowance(address,uint256,uint256)",
75             &[("spender","address"),("allowance","uint256"),("needed","uint256")],
76             Some("Increase the token allowance before calling transferFrom.")),
77            ("ERC20InvalidApprover", "ERC20InvalidApprover(address)",
78             &[("approver","address")], None),
79            ("ERC20InvalidSpender", "ERC20InvalidSpender(address)",
80             &[("spender","address")], None),
81
82            // ─── ERC-721 ──────────────────────────────────────────────────────
83            ("ERC721InvalidOwner", "ERC721InvalidOwner(address)",
84             &[("owner","address")], None),
85            ("ERC721NonexistentToken", "ERC721NonexistentToken(uint256)",
86             &[("tokenId","uint256")], None),
87            ("ERC721IncorrectOwner", "ERC721IncorrectOwner(address,uint256,address)",
88             &[("sender","address"),("tokenId","uint256"),("owner","address")], None),
89            ("ERC721InvalidSender", "ERC721InvalidSender(address)",
90             &[("sender","address")], None),
91            ("ERC721InvalidReceiver", "ERC721InvalidReceiver(address)",
92             &[("receiver","address")], None),
93            ("ERC721InsufficientApproval", "ERC721InsufficientApproval(address,uint256)",
94             &[("operator","address"),("tokenId","uint256")], None),
95            ("ERC721InvalidApprover", "ERC721InvalidApprover(address)",
96             &[("approver","address")], None),
97            ("ERC721InvalidOperator", "ERC721InvalidOperator(address)",
98             &[("operator","address")], None),
99
100            // ─── OpenZeppelin Ownable ─────────────────────────────────────────
101            ("OwnableUnauthorizedAccount", "OwnableUnauthorizedAccount(address)",
102             &[("account","address")],
103             Some("Only the owner can call this function. Ensure you are using the owner address.")),
104            ("OwnableInvalidOwner", "OwnableInvalidOwner(address)",
105             &[("owner","address")], None),
106
107            // ─── OpenZeppelin Access Control ───────────────────────────────────
108            ("AccessControlUnauthorizedAccount", "AccessControlUnauthorizedAccount(address,bytes32)",
109             &[("account","address"),("neededRole","bytes32")],
110             Some("The caller is missing the required role. Grant the role with grantRole().")),
111            ("AccessControlBadConfirmation", "AccessControlBadConfirmation()",
112             &[], None),
113
114            // ─── OpenZeppelin ReentrancyGuard ─────────────────────────────────
115            ("ReentrancyGuardReentrantCall", "ReentrancyGuardReentrantCall()",
116             &[], Some("Reentrancy detected. Do not call this function recursively.")),
117
118            // ─── OpenZeppelin Pausable ────────────────────────────────────────
119            ("EnforcedPause", "EnforcedPause()",
120             &[], Some("The contract is paused. Wait for it to be unpaused.")),
121            ("ExpectedPause", "ExpectedPause()",
122             &[], None),
123
124            // ─── Uniswap V3 custom (terse) errors ────────────────────────────
125            ("T", "T()", &[], Some("Uniswap V3: tick out of range.")),
126            ("LOK", "LOK()", &[], Some("Uniswap V3: pool is locked.")),
127            ("TLU", "TLU()", &[], Some("Uniswap V3: tick lower >= tick upper.")),
128            ("TLM", "TLM()", &[], Some("Uniswap V3: tick lower too low.")),
129            ("TUM", "TUM()", &[], Some("Uniswap V3: tick upper too high.")),
130            ("AS", "AS()", &[], Some("Uniswap V3: amount specified is zero.")),
131            ("M0", "M0()", &[], Some("Uniswap V3: mint amounts are zero.")),
132            ("M1", "M1()", &[], Some("Uniswap V3: mint amount0 exceeds limit.")),
133            ("IIA", "IIA()", &[], Some("Uniswap V3: insufficient input amount.")),
134            ("SPL", "SPL()", &[], Some("Uniswap V3: sqrt price limit is out of range.")),
135            ("F0", "F0()", &[], Some("Uniswap V3: flash amount0 > balance.")),
136            ("F1", "F1()", &[], Some("Uniswap V3: flash amount1 > balance.")),
137            ("L", "L()", &[], Some("Uniswap V3: liquidity is zero.")),
138            ("LS", "LS()", &[], Some("Uniswap V3: liquidity exceeds maximum.")),
139            ("LA", "LA()", &[], Some("Uniswap V3: liquidity amount overflows.")),
140
141            // ─── SafeMath (pre-Solidity 0.8) ─────────────────────────────────
142            // These revert with string, handled by revert decoder.
143            // Custom error versions below if contracts define them.
144
145            // ─── EIP-4626 Vault ───────────────────────────────────────────────
146            ("ERC4626ExceededMaxDeposit", "ERC4626ExceededMaxDeposit(address,uint256,uint256)",
147             &[("receiver","address"),("assets","uint256"),("max","uint256")], None),
148            ("ERC4626ExceededMaxMint", "ERC4626ExceededMaxMint(address,uint256,uint256)",
149             &[("receiver","address"),("shares","uint256"),("max","uint256")], None),
150            ("ERC4626ExceededMaxWithdraw", "ERC4626ExceededMaxWithdraw(address,uint256,uint256)",
151             &[("owner","address"),("assets","uint256"),("max","uint256")], None),
152            ("ERC4626ExceededMaxRedeem", "ERC4626ExceededMaxRedeem(address,uint256,uint256)",
153             &[("owner","address"),("shares","uint256"),("max","uint256")], None),
154
155            // ─── Address utility ──────────────────────────────────────────────
156            ("AddressInsufficientBalance", "AddressInsufficientBalance(address)",
157             &[("account","address")], None),
158            ("AddressEmptyCode", "AddressEmptyCode(address)",
159             &[("target","address")],
160             Some("The target address has no contract code deployed.")),
161            ("FailedInnerCall", "FailedInnerCall()",
162             &[], None),
163
164            // ─── SafeERC20 ────────────────────────────────────────────────────
165            ("SafeERC20FailedOperation", "SafeERC20FailedOperation(address)",
166             &[("token","address")],
167             Some("The ERC-20 token operation failed. Ensure the token is compliant.")),
168            ("SafeERC20FailedDecreaseAllowance", "SafeERC20FailedDecreaseAllowance(address,uint256)",
169             &[("spender","address"),("currentAllowance","uint256")], None),
170        ];
171
172        for (name, sig_str, raw_inputs, hint) in bundled {
173            let inputs = raw_inputs
174                .iter()
175                .map(|(n, t)| p(n, t))
176                .collect::<Vec<_>>();
177            reg.register(ErrorSignature {
178                name: name.to_string(),
179                signature: sig_str.to_string(),
180                selector: sel(sig_str),
181                inputs,
182                source: "bundled".to_string(),
183                suggestion: hint.map(|s| s.to_string()),
184            });
185        }
186    }
187
188    /// Register additional error signatures at runtime (e.g. from a project ABI).
189    pub fn register_signature(&self, _sig: ErrorSignature) {
190        // Only MemoryErrorRegistry supports dynamic registration.
191        // In practice, use with_registry() + manual registration on the registry directly.
192    }
193}
194
195impl Default for EvmErrorDecoder {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl ErrorDecoder for EvmErrorDecoder {
202    fn chain_family(&self) -> &'static str {
203        "evm"
204    }
205
206    fn decode(
207        &self,
208        revert_data: &[u8],
209        ctx: Option<ErrorContext>,
210    ) -> Result<DecodedError, DecodeErrorError> {
211        // ── Case 1: Empty revert data ──────────────────────────────────────────
212        if revert_data.is_empty() {
213            return Ok(DecodedError {
214                kind: ErrorKind::Empty,
215                raw_data: vec![],
216                selector: None,
217                suggestion: Some("Transaction reverted with no error message.".into()),
218                confidence: 0.5,
219                context: ctx,
220            });
221        }
222
223        // Extract 4-byte selector if present
224        let selector: Option<[u8; 4]> = if revert_data.len() >= 4 {
225            Some(revert_data[..4].try_into().unwrap())
226        } else {
227            None
228        };
229
230        // ── Case 2: Error(string) — `0x08c379a0` ──────────────────────────────
231        if let Some(message) = decode_error_string(revert_data) {
232            let suggestion = generate_revert_suggestion(&message);
233            return Ok(DecodedError {
234                kind: ErrorKind::RevertString { message },
235                raw_data: revert_data.to_vec(),
236                selector: Some(ERROR_STRING_SELECTOR),
237                suggestion,
238                confidence: 1.0,
239                context: ctx,
240            });
241        }
242
243        // ── Case 3: Panic(uint256) — `0x4e487b71` ─────────────────────────────
244        if let Some((code, meaning)) = decode_panic(revert_data) {
245            return Ok(DecodedError {
246                kind: ErrorKind::Panic {
247                    code,
248                    meaning: meaning.to_string(),
249                },
250                raw_data: revert_data.to_vec(),
251                selector: Some(PANIC_SELECTOR),
252                suggestion: Some(format!(
253                    "Solidity assert violation (panic code 0x{code:02x}): {meaning}."
254                )),
255                confidence: 1.0,
256                context: ctx,
257            });
258        }
259
260        // ── Case 4: Known custom error from registry ───────────────────────────
261        if let Some(kind) = decode_custom_error(revert_data, self.registry.as_ref()) {
262            let suggestion = if let ErrorKind::CustomError { ref name, .. } = kind {
263                // Look up suggestion from registry
264                self.registry
265                    .get_by_name(name)
266                    .and_then(|s| s.suggestion)
267            } else {
268                None
269            };
270            return Ok(DecodedError {
271                kind,
272                raw_data: revert_data.to_vec(),
273                selector,
274                suggestion,
275                confidence: 0.95,
276                context: ctx,
277            });
278        }
279
280        // ── Case 5: Unknown — return raw revert ────────────────────────────────
281        let selector_hex = selector
282            .map(|s| hex::encode(s))
283            .unwrap_or_else(|| "none".into());
284
285        Ok(DecodedError {
286            kind: ErrorKind::RawRevert {
287                selector: selector_hex,
288                data: revert_data.to_vec(),
289            },
290            raw_data: revert_data.to_vec(),
291            selector,
292            suggestion: Some(
293                "Unknown error selector. Try looking up the selector on https://4byte.directory"
294                    .into(),
295            ),
296            confidence: 0.0,
297            context: ctx,
298        })
299    }
300}
301
302/// Generate a hint based on common revert message patterns.
303fn generate_revert_suggestion(message: &str) -> Option<String> {
304    let msg_lower = message.to_lowercase();
305    if msg_lower.contains("not the owner") || msg_lower.contains("not owner") {
306        Some("Ensure the caller is the contract owner.".into())
307    } else if msg_lower.contains("insufficient") && msg_lower.contains("balance") {
308        Some("The account balance is too low. Check the token balance before calling.".into())
309    } else if msg_lower.contains("allowance") {
310        Some("Increase the token allowance with approve() before calling transferFrom().".into())
311    } else if msg_lower.contains("paused") {
312        Some("The contract is paused. Wait for it to be unpaused.".into())
313    } else if msg_lower.contains("already") && msg_lower.contains("init") {
314        Some("The contract has already been initialized.".into())
315    } else {
316        None
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use chainerrors_core::ErrorDecoder;
324
325    fn decoder() -> EvmErrorDecoder {
326        EvmErrorDecoder::new()
327    }
328
329    #[test]
330    fn decode_empty() {
331        let d = decoder();
332        let result = d.decode(&[], None).unwrap();
333        assert!(matches!(result.kind, ErrorKind::Empty));
334        assert_eq!(result.selector, None);
335    }
336
337    #[test]
338    fn decode_revert_string() {
339        // Error("Not enough tokens")
340        let data = hex::decode(
341            "08c379a0\
342             0000000000000000000000000000000000000000000000000000000000000020\
343             0000000000000000000000000000000000000000000000000000000000000011\
344             4e6f7420656e6f75676820746f6b656e73000000000000000000000000000000",
345        )
346        .unwrap();
347        let result = decoder().decode(&data, None).unwrap();
348        match &result.kind {
349            ErrorKind::RevertString { message } => assert_eq!(message, "Not enough tokens"),
350            _ => panic!("expected RevertString, got {:?}", result.kind),
351        }
352        assert_eq!(result.confidence, 1.0);
353    }
354
355    #[test]
356    fn decode_panic_overflow() {
357        let data = hex::decode(
358            "4e487b710000000000000000000000000000000000000000000000000000000000000011",
359        )
360        .unwrap();
361        let result = decoder().decode(&data, None).unwrap();
362        match &result.kind {
363            ErrorKind::Panic { code, meaning } => {
364                assert_eq!(*code, 0x11);
365                assert!(meaning.contains("overflow"));
366            }
367            _ => panic!("expected Panic, got {:?}", result.kind),
368        }
369        assert_eq!(result.confidence, 1.0);
370    }
371
372    #[test]
373    fn decode_oz_ownable_error() {
374        use tiny_keccak::{Hasher, Keccak};
375        // OwnableUnauthorizedAccount(address)
376        let mut k = Keccak::v256();
377        k.update(b"OwnableUnauthorizedAccount(address)");
378        let mut hash = [0u8; 32];
379        k.finalize(&mut hash);
380        let sel = [hash[0], hash[1], hash[2], hash[3]];
381
382        // Encode address 0xdead...beef
383        let mut data = sel.to_vec();
384        data.extend_from_slice(&[0u8; 12]);
385        data.extend_from_slice(&[0xde, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbe]);
386
387        let result = decoder().decode(&data, None).unwrap();
388        assert!(
389            matches!(&result.kind, ErrorKind::CustomError { name, .. } if name == "OwnableUnauthorizedAccount"),
390            "got: {:?}", result.kind
391        );
392        assert!(result.suggestion.is_some());
393    }
394
395    #[test]
396    fn decode_raw_revert_unknown() {
397        let data = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04];
398        let result = decoder().decode(&data, None).unwrap();
399        assert!(matches!(result.kind, ErrorKind::RawRevert { .. }));
400        assert_eq!(result.confidence, 0.0);
401    }
402
403    #[test]
404    fn decode_hex_helper() {
405        let d = decoder();
406        // Revert string via hex
407        let result = d
408            .decode_hex(
409                "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000548656c6c6f000000000000000000000000000000000000000000000000000000",
410                None,
411            )
412            .unwrap();
413        assert!(matches!(result.kind, ErrorKind::RevertString { ref message } if message == "Hello"));
414    }
415}