Skip to main content

tempo_precompiles/
error.rs

1//! Unified error handling for Tempo precompiles.
2//!
3//! Provides [`TempoPrecompileError`] — the top-level error enum — along with an
4//! ABI-selector-based decoder registry for mapping raw revert bytes back to
5//! typed error variants.
6
7use std::{
8    collections::HashMap,
9    sync::{Arc, LazyLock},
10};
11
12use crate::tip20::TIP20Error;
13use alloy::{
14    primitives::{Selector, U256},
15    sol_types::{Panic, PanicKind, SolError, SolInterface},
16};
17use revm::{
18    context::journaled_state::JournalLoadErasedError,
19    precompile::{PrecompileError, PrecompileOutput, PrecompileResult},
20};
21use tempo_contracts::precompiles::{
22    AccountKeychainError, FeeManagerError, NonceError, RolesAuthError, StablecoinDEXError,
23    TIP20FactoryError, TIP403RegistryError, TIPFeeAMMError, UnknownFunctionSelector,
24    ValidatorConfigError, ValidatorConfigV2Error,
25};
26
27/// Top-level error type for all Tempo precompile operations
28#[derive(
29    Debug, Clone, PartialEq, Eq, thiserror::Error, derive_more::From, derive_more::TryInto,
30)]
31pub enum TempoPrecompileError {
32    /// Stablecoin DEX error
33    #[error("Stablecoin DEX error: {0:?}")]
34    StablecoinDEX(StablecoinDEXError),
35
36    /// Error from TIP20 token
37    #[error("TIP20 token error: {0:?}")]
38    TIP20(TIP20Error),
39
40    /// Error from TIP20 factory
41    #[error("TIP20 factory error: {0:?}")]
42    TIP20Factory(TIP20FactoryError),
43
44    /// Error from roles auth
45    #[error("Roles auth error: {0:?}")]
46    RolesAuthError(RolesAuthError),
47
48    /// Error from 403 registry
49    #[error("TIP403 registry error: {0:?}")]
50    TIP403RegistryError(TIP403RegistryError),
51
52    /// Error from TIP fee manager
53    #[error("TIP fee manager error: {0:?}")]
54    FeeManagerError(FeeManagerError),
55
56    /// Error from TIP fee AMM
57    #[error("TIP fee AMM error: {0:?}")]
58    TIPFeeAMMError(TIPFeeAMMError),
59
60    /// Error from Tempo Transaction nonce manager
61    #[error("Tempo Transaction nonce error: {0:?}")]
62    NonceError(NonceError),
63
64    /// EVM panic (i.e. arithmetic under/overflow, out-of-bounds access).
65    #[error("Panic({0:?})")]
66    Panic(PanicKind),
67
68    /// Error from validator config
69    #[error("Validator config error: {0:?}")]
70    ValidatorConfigError(ValidatorConfigError),
71
72    /// Error from validator config v2
73    #[error("Validator config v2 error: {0:?}")]
74    ValidatorConfigV2Error(ValidatorConfigV2Error),
75
76    /// Error from account keychain precompile
77    #[error("Account keychain error: {0:?}")]
78    AccountKeychainError(AccountKeychainError),
79
80    /// Gas limit exceeded during precompile execution.
81    #[error("Gas limit exceeded")]
82    OutOfGas,
83
84    /// The calldata's 4-byte selector does not match any known precompile function.
85    #[error("Unknown function selector: {0:?}")]
86    UnknownFunctionSelector([u8; 4]),
87
88    /// Unrecoverable internal error (e.g. database failure).
89    #[error("Fatal precompile error: {0:?}")]
90    #[from(skip)]
91    Fatal(String),
92}
93
94impl From<JournalLoadErasedError> for TempoPrecompileError {
95    fn from(value: JournalLoadErasedError) -> Self {
96        match value {
97            JournalLoadErasedError::DBError(e) => Self::Fatal(e.to_string()),
98            JournalLoadErasedError::ColdLoadSkipped => Self::OutOfGas,
99        }
100    }
101}
102
103/// Result type alias for Tempo precompile operations
104pub type Result<T> = std::result::Result<T, TempoPrecompileError>;
105
106impl TempoPrecompileError {
107    /// Returns true if this error represents a system-level failure that must be propagated
108    /// rather than swallowed, because state may be inconsistent.
109    pub fn is_system_error(&self) -> bool {
110        match self {
111            Self::OutOfGas | Self::Fatal(_) | Self::Panic(_) => true,
112            Self::StablecoinDEX(_)
113            | Self::TIP20(_)
114            | Self::NonceError(_)
115            | Self::TIP20Factory(_)
116            | Self::RolesAuthError(_)
117            | Self::TIPFeeAMMError(_)
118            | Self::FeeManagerError(_)
119            | Self::TIP403RegistryError(_)
120            | Self::ValidatorConfigError(_)
121            | Self::ValidatorConfigV2Error(_)
122            | Self::AccountKeychainError(_)
123            | Self::UnknownFunctionSelector(_) => false,
124        }
125    }
126
127    /// Creates an arithmetic under/overflow panic error.
128    pub fn under_overflow() -> Self {
129        Self::Panic(PanicKind::UnderOverflow)
130    }
131
132    /// Creates an array out-of-bounds panic error.
133    pub fn array_oob() -> Self {
134        Self::Panic(PanicKind::ArrayOutOfBounds)
135    }
136
137    /// ABI-encodes this error and wraps it as a reverted [`PrecompileResult`].
138    ///
139    /// # Errors
140    /// - `PrecompileError::OutOfGas` — if the variant is [`OutOfGas`](Self::OutOfGas)
141    /// - `PrecompileError::Fatal` — if the variant is [`Fatal`](Self::Fatal)
142    pub fn into_precompile_result(self, gas: u64) -> PrecompileResult {
143        let bytes = match self {
144            Self::StablecoinDEX(e) => e.abi_encode().into(),
145            Self::TIP20(e) => e.abi_encode().into(),
146            Self::TIP20Factory(e) => e.abi_encode().into(),
147            Self::RolesAuthError(e) => e.abi_encode().into(),
148            Self::TIP403RegistryError(e) => e.abi_encode().into(),
149            Self::FeeManagerError(e) => e.abi_encode().into(),
150            Self::TIPFeeAMMError(e) => e.abi_encode().into(),
151            Self::NonceError(e) => e.abi_encode().into(),
152            Self::Panic(kind) => {
153                let panic = Panic {
154                    code: U256::from(kind as u32),
155                };
156
157                panic.abi_encode().into()
158            }
159            Self::ValidatorConfigError(e) => e.abi_encode().into(),
160            Self::ValidatorConfigV2Error(e) => e.abi_encode().into(),
161            Self::AccountKeychainError(e) => e.abi_encode().into(),
162            Self::OutOfGas => {
163                return Err(PrecompileError::OutOfGas);
164            }
165            Self::UnknownFunctionSelector(selector) => UnknownFunctionSelector {
166                selector: selector.into(),
167            }
168            .abi_encode()
169            .into(),
170            Self::Fatal(msg) => {
171                return Err(PrecompileError::Fatal(msg));
172            }
173        };
174        Ok(PrecompileOutput::new_reverted(gas, bytes))
175    }
176}
177
178/// Registers all ABI error selectors for a [`SolInterface`] type into the decoder registry.
179pub fn add_errors_to_registry<T: SolInterface>(
180    registry: &mut TempoPrecompileErrorRegistry,
181    converter: impl Fn(T) -> TempoPrecompileError + 'static + Send + Sync,
182) {
183    let converter = Arc::new(converter);
184    for selector in T::selectors() {
185        let converter = Arc::clone(&converter);
186        registry.insert(
187            selector.into(),
188            Box::new(move |data: &[u8]| {
189                T::abi_decode(data)
190                    .ok()
191                    .map(|error| DecodedTempoPrecompileError {
192                        error: converter(error),
193                        revert_bytes: data,
194                    })
195            }),
196        );
197    }
198}
199
200/// A decoded precompile error together with the raw revert bytes.
201pub struct DecodedTempoPrecompileError<'a> {
202    pub error: TempoPrecompileError,
203    pub revert_bytes: &'a [u8],
204}
205
206/// Maps ABI error selectors to their decoder functions.
207pub type TempoPrecompileErrorRegistry = HashMap<
208    Selector,
209    Box<dyn for<'a> Fn(&'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> + Send + Sync>,
210>;
211
212/// Builds a [`TempoPrecompileErrorRegistry`] mapping every known error selector to its decoder.
213pub fn error_decoder_registry() -> TempoPrecompileErrorRegistry {
214    let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
215
216    add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
217    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20);
218    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20Factory);
219    add_errors_to_registry(&mut registry, TempoPrecompileError::RolesAuthError);
220    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP403RegistryError);
221    add_errors_to_registry(&mut registry, TempoPrecompileError::FeeManagerError);
222    add_errors_to_registry(&mut registry, TempoPrecompileError::TIPFeeAMMError);
223    add_errors_to_registry(&mut registry, TempoPrecompileError::NonceError);
224    add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigError);
225    add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigV2Error);
226    add_errors_to_registry(&mut registry, TempoPrecompileError::AccountKeychainError);
227
228    registry
229}
230
231/// Global lazily-initialized registry of all Tempo precompile error decoders.
232pub static ERROR_REGISTRY: LazyLock<TempoPrecompileErrorRegistry> =
233    LazyLock::new(error_decoder_registry);
234
235/// Decodes raw revert bytes into a typed [`DecodedTempoPrecompileError`] using the global
236/// [`ERROR_REGISTRY`], returning `None` if the data is shorter than 4 bytes or the selector
237/// is unrecognized.
238pub fn decode_error<'a>(data: &'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> {
239    if data.len() < 4 {
240        return None;
241    }
242
243    let selector: [u8; 4] = data[0..4].try_into().ok()?;
244    ERROR_REGISTRY
245        .get(&selector)
246        .and_then(|decoder| decoder(data))
247}
248
249/// Extension trait to convert `Result<T, TempoPrecompileError>` into a [`PrecompileResult`].
250pub trait IntoPrecompileResult<T> {
251    /// Converts `self` into a [`PrecompileResult`], using `encode_ok` for the success path.
252    fn into_precompile_result(
253        self,
254        gas: u64,
255        encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
256    ) -> PrecompileResult;
257}
258
259impl<T> IntoPrecompileResult<T> for Result<T> {
260    fn into_precompile_result(
261        self,
262        gas: u64,
263        encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
264    ) -> PrecompileResult {
265        match self {
266            Ok(res) => Ok(PrecompileOutput::new(gas, encode_ok(res))),
267            Err(err) => err.into_precompile_result(gas),
268        }
269    }
270}
271
272impl<T> IntoPrecompileResult<T> for TempoPrecompileError {
273    fn into_precompile_result(
274        self,
275        gas: u64,
276        _encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
277    ) -> PrecompileResult {
278        Self::into_precompile_result(self, gas)
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use tempo_contracts::precompiles::StablecoinDEXError;
286
287    #[test]
288    fn test_add_errors_to_registry_populates_registry() {
289        let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
290
291        assert!(registry.is_empty());
292
293        add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
294
295        assert!(!registry.is_empty());
296
297        let order_not_found_selector = StablecoinDEXError::order_does_not_exist().selector();
298        assert!(
299            registry.contains_key(&order_not_found_selector),
300            "Registry should contain OrderDoesNotExist selector"
301        );
302    }
303
304    #[test]
305    fn test_error_decoder_registry_is_not_empty() {
306        let registry = error_decoder_registry();
307
308        assert!(
309            !registry.is_empty(),
310            "error_decoder_registry should return a populated registry"
311        );
312
313        let dex_selector = StablecoinDEXError::order_does_not_exist().selector();
314        assert!(registry.contains_key(&dex_selector));
315    }
316
317    #[test]
318    fn test_decode_error_returns_some_for_valid_error() {
319        let error = StablecoinDEXError::order_does_not_exist();
320        let encoded = error.abi_encode();
321
322        let result = decode_error(&encoded);
323        assert!(
324            result.is_some(),
325            "decode_error should return Some for valid error"
326        );
327
328        let decoded = result.unwrap();
329        assert!(matches!(
330            decoded.error,
331            TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderDoesNotExist(_))
332        ));
333    }
334
335    #[test]
336    fn test_decode_error_data_length_boundary() {
337        // Empty data (len = 0) should return None (0 < 4)
338        let result = decode_error(&[]);
339        assert!(result.is_none(), "Empty data should return None");
340
341        // 1 byte (len = 1) should return None (1 < 4)
342        let result = decode_error(&[0x01]);
343        assert!(result.is_none(), "1 byte should return None");
344
345        // 2 bytes (len = 2) should return None (2 < 4)
346        let result = decode_error(&[0x01, 0x02]);
347        assert!(result.is_none(), "2 bytes should return None");
348
349        // 3 bytes (len = 3) should return None (3 < 4)
350        let result = decode_error(&[0x01, 0x02, 0x03]);
351        assert!(result.is_none(), "3 bytes should return None");
352
353        // 4 bytes with unknown selector returns None (selector not found)
354        let result = decode_error(&[0x00, 0x00, 0x00, 0x00]);
355        assert!(
356            result.is_none(),
357            "Unknown 4-byte selector should return None"
358        );
359
360        // 4 bytes with valid selector (exactly at boundary) should succeed
361        let error = StablecoinDEXError::order_does_not_exist();
362        let encoded = error.abi_encode();
363        let result = decode_error(&encoded);
364        assert!(
365            result.is_some(),
366            "Valid error at 4+ bytes should return Some"
367        );
368    }
369
370    #[test]
371    fn test_decode_error_with_tip20_error() {
372        // Use insufficient_allowance which has a unique selector (no collision with other errors)
373        let error = TIP20Error::insufficient_allowance();
374        let encoded = error.abi_encode();
375
376        let result = decode_error(&encoded);
377        assert!(result.is_some(), "Should decode TIP20 errors");
378
379        let decoded = result.unwrap();
380        // Verify it's a TIP20 error
381        match decoded.error {
382            TempoPrecompileError::TIP20(_) => {}
383            other => panic!("Expected TIP20 error, got {other:?}"),
384        }
385    }
386}