Skip to main content

revm_context_interface/
result.rs

1//! Result of the EVM execution. Containing both execution result, state and errors.
2//!
3//! [`ExecutionResult`] is the result of the EVM execution.
4//!
5//! [`InvalidTransaction`] is the error that is returned when the transaction is invalid.
6//!
7//! [`InvalidHeader`] is the error that is returned when the header is invalid.
8//!
9//! [`SuccessReason`] is the reason that the transaction successfully completed.
10use crate::{context::ContextError, transaction::TransactionError};
11use core::fmt::{self, Debug};
12use database_interface::DBErrorMarker;
13use primitives::{Address, Bytes, Log, U256};
14use state::EvmState;
15use std::{borrow::Cow, boxed::Box, string::String, sync::Arc, vec::Vec};
16
17/// Trait for the halt reason.
18pub trait HaltReasonTr: Clone + Debug + PartialEq + Eq + From<HaltReason> {}
19
20impl<T> HaltReasonTr for T where T: Clone + Debug + PartialEq + Eq + From<HaltReason> {}
21
22/// Tuple containing evm execution result and state.s
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct ExecResultAndState<R, S = EvmState> {
26    /// Execution result
27    pub result: R,
28    /// Output State.
29    pub state: S,
30}
31
32/// Type alias for backwards compatibility.
33pub type ResultAndState<H = HaltReason, S = EvmState> = ExecResultAndState<ExecutionResult<H>, S>;
34
35/// Tuple containing multiple execution results and state.
36pub type ResultVecAndState<R, S> = ExecResultAndState<Vec<R>, S>;
37
38impl<R, S> ExecResultAndState<R, S> {
39    /// Creates new ResultAndState.
40    pub fn new(result: R, state: S) -> Self {
41        Self { result, state }
42    }
43}
44
45/// Gas accounting result from transaction execution.
46///
47/// Self-contained gas snapshot with all values needed for downstream consumers.
48///
49/// ## Stored values
50///
51/// | Getter                 | Source                             | Description                                    |
52/// |------------------------|------------------------------------|------------------------------------------------|
53/// | [`total_gas_spent()`]  | `Gas::spent()` = limit − remaining | Total gas consumed before refund               |
54/// | [`inner_refunded()`]   | `Gas::refunded()` as u64           | Gas refunded (capped per EIP-3529)             |
55/// | [`floor_gas()`]        | `InitialAndFloorGas::floor_gas`    | EIP-7623 floor gas (0 if not applicable)       |
56/// | [`state_gas_spent()`]  | `Gas::state_gas_spent`             | State gas consumed during execution (EIP-8037) |
57///
58/// [`total_gas_spent()`]: ResultGas::total_gas_spent
59/// [`inner_refunded()`]: ResultGas::inner_refunded
60/// [`floor_gas()`]: ResultGas::floor_gas
61/// [`state_gas_spent()`]: ResultGas::state_gas_spent
62///
63/// ## Derived values
64///
65/// - [`tx_gas_used()`](ResultGas::tx_gas_used) = `max(total_gas_spent − refunded, floor_gas)` (the value that goes into receipts)
66/// - [`block_regular_gas_used()`](ResultGas::block_regular_gas_used) = `max(total_gas_spent − state_gas_spent, floor_gas)`
67/// - [`block_state_gas_used()`](ResultGas::block_state_gas_used) = `state_gas_spent`
68/// - [`spent_sub_refunded()`](ResultGas::spent_sub_refunded) = `total_gas_spent − refunded` (before floor gas check)
69/// - [`final_refunded()`](ResultGas::final_refunded) = `refunded` when floor gas is inactive, `0` when floor gas kicks in
70#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct ResultGas {
73    /// Total gas spent consisting of regular and state gas.
74    /// For actual gas used, use [`used()`](ResultGas::used).
75    #[cfg_attr(feature = "serde", serde(rename = "gas_spent"))]
76    total_gas_spent: u64,
77    /// State gas consumed during execution (EIP-8037).
78    /// Tracks gas for storage creation, account creation, and code deposit.
79    /// Zero when state gas is not enabled.
80    #[cfg_attr(feature = "serde", serde(default))]
81    state_gas_spent: u64,
82    /// Gas refund amount (capped per EIP-3529).
83    ///
84    /// Note: This is the raw refund before EIP-7623 floor gas adjustment.
85    /// Use [`final_refunded()`](ResultGas::final_refunded) for the effective refund.
86    #[cfg_attr(feature = "serde", serde(rename = "gas_refunded"))]
87    refunded: u64,
88    /// EIP-7623 floor gas. Zero when not applicable.
89    floor_gas: u64,
90}
91
92impl ResultGas {
93    /****** Constructor functions *****/
94
95    /// Creates a new `ResultGas`.
96    #[inline]
97    #[deprecated(
98        since = "32.0.0",
99        note = "It can be a footgun as gas limit is removed, use ResultGas::with_* functions instead"
100    )]
101    pub const fn new(total_gas_spent: u64, refunded: u64, floor_gas: u64) -> Self {
102        Self {
103            total_gas_spent,
104            refunded,
105            floor_gas,
106            state_gas_spent: 0,
107        }
108    }
109
110    /// Creates a new `ResultGas` with state gas tracking.
111    #[inline]
112    pub const fn new_with_state_gas(
113        total_gas_spent: u64,
114        refunded: u64,
115        floor_gas: u64,
116        state_gas_spent: u64,
117    ) -> Self {
118        Self {
119            total_gas_spent,
120            refunded,
121            floor_gas,
122            state_gas_spent,
123        }
124    }
125
126    /****** Simple getters *****/
127
128    /// Returns the total gas spent inside execution before any refund.
129    ///
130    /// If you want final gas used, use [`used()`](ResultGas::used).
131    #[inline]
132    pub const fn total_gas_spent(&self) -> u64 {
133        self.total_gas_spent
134    }
135
136    /// Returns the state gas spent during execution (EIP-8037).
137    ///
138    /// This is same as [`ResultGas::block_state_gas_used`] for the transaction.
139    #[inline]
140    pub const fn state_gas_spent(&self) -> u64 {
141        self.state_gas_spent
142    }
143
144    /// Returns the EIP-7623 floor gas.
145    #[inline]
146    pub const fn floor_gas(&self) -> u64 {
147        self.floor_gas
148    }
149
150    /// Returns the raw refund from EVM execution, before EIP-7623 floor gas adjustment.
151    ///
152    /// This is the `refunded` field value (capped per EIP-3529 but not adjusted for floor gas).
153    /// See [`final_refunded()`](ResultGas::final_refunded) for the effective refund.
154    #[inline]
155    pub const fn inner_refunded(&self) -> u64 {
156        self.refunded
157    }
158
159    /// Returns the total gas spent.
160    #[inline]
161    #[deprecated(
162        since = "32.0.0",
163        note = "After EIP-8037 gas is split on
164    regular and state gas, this method is no longer valid.
165    Use [`ResultGas::total_gas_spent`] instead"
166    )]
167    pub const fn spent(&self) -> u64 {
168        self.total_gas_spent()
169    }
170
171    /****** Simple setters *****/
172
173    /// Sets the `total_gas_spent` field by mutable reference.
174    #[inline]
175    pub fn set_total_gas_spent(&mut self, total_gas_spent: u64) {
176        self.total_gas_spent = total_gas_spent;
177    }
178
179    /// Sets the `refunded` field by mutable reference.
180    #[inline]
181    pub fn set_refunded(&mut self, refunded: u64) {
182        self.refunded = refunded;
183    }
184
185    /// Sets the `floor_gas` field by mutable reference.
186    #[inline]
187    pub fn set_floor_gas(&mut self, floor_gas: u64) {
188        self.floor_gas = floor_gas;
189    }
190
191    /// Sets the `state_gas_spent` field by mutable reference.
192    #[inline]
193    pub fn set_state_gas_spent(&mut self, state_gas_spent: u64) {
194        self.state_gas_spent = state_gas_spent;
195    }
196
197    /// Sets the `spent` field by mutable reference.
198    #[inline]
199    #[deprecated(
200        since = "32.0.0",
201        note = "After EIP-8037 gas is split on
202            regular and state gas, this method is no longer valid.
203            Use [`ResultGas::set_total_gas_spent`] instead"
204    )]
205    pub fn set_spent(&mut self, spent: u64) {
206        self.total_gas_spent = spent;
207    }
208
209    /****** Builder with_* methods *****/
210
211    /// Sets the `total_gas_spent` field.
212    #[inline]
213    pub const fn with_total_gas_spent(mut self, total_gas_spent: u64) -> Self {
214        self.total_gas_spent = total_gas_spent;
215        self
216    }
217
218    /// Sets the `refunded` field.
219    #[inline]
220    pub const fn with_refunded(mut self, refunded: u64) -> Self {
221        self.refunded = refunded;
222        self
223    }
224
225    /// Sets the `floor_gas` field.
226    #[inline]
227    pub const fn with_floor_gas(mut self, floor_gas: u64) -> Self {
228        self.floor_gas = floor_gas;
229        self
230    }
231
232    /// Sets the `state_gas_spent` field.
233    #[inline]
234    pub const fn with_state_gas_spent(mut self, state_gas_spent: u64) -> Self {
235        self.state_gas_spent = state_gas_spent;
236        self
237    }
238
239    /// Sets the `spent` field.
240    #[inline]
241    #[deprecated(
242        since = "32.0.0",
243        note = "After EIP-8037 gas is split on
244    regular and state gas, this method is no longer valid.
245    Use [`ResultGas::with_total_gas_spent`] instead"
246    )]
247    pub const fn with_spent(mut self, spent: u64) -> Self {
248        self.total_gas_spent = spent;
249        self
250    }
251
252    /* Aggregated getters */
253
254    /// Returns the total gas used by the transaction.
255    ///
256    /// This value is set inside Receipt.
257    #[inline]
258    pub const fn tx_gas_used(&self) -> u64 {
259        // consiste of regular and state gas.
260        let total_gas_spent = self.total_gas_spent();
261        // from total gas subtract the refunded gas. Refunded is capped by 20% of total gas spent.
262        let tx_gas_refunded = total_gas_spent.saturating_sub(self.inner_refunded());
263        max(tx_gas_refunded, self.floor_gas())
264    }
265
266    /// Returns the regular gas used by the block.
267    #[inline]
268    pub const fn block_regular_gas_used(&self) -> u64 {
269        let execution_gas_spent = self
270            .total_gas_spent()
271            .saturating_sub(self.state_gas_spent());
272        max(execution_gas_spent, self.floor_gas())
273    }
274
275    /// Returns the state gas used by the block.
276    ///
277    /// This is same as [`ResultGas::state_gas_spent`] for the block.
278    #[inline]
279    pub const fn block_state_gas_used(&self) -> u64 {
280        self.state_gas_spent()
281    }
282
283    /// Returns the final gas used: `max(spent - refunded, floor_gas)`.
284    ///
285    /// This is the value used for receipt `cumulative_gas_used` accumulation
286    /// and the per-transaction gas charge.
287    #[inline]
288    #[deprecated(
289        since = "32.0.0",
290        note = "Used is not descriptive enough, use [`ResultGas::tx_gas_used`] instead"
291    )]
292    pub const fn used(&self) -> u64 {
293        // EIP-7623: Increase calldata cost
294        // spend at least a gas_floor amount of gas.
295        let spent_sub_refunded = self.spent_sub_refunded();
296        if spent_sub_refunded < self.floor_gas {
297            return self.floor_gas;
298        }
299        spent_sub_refunded
300    }
301
302    /// Returns the gas spent minus the refunded gas.
303    ///
304    /// This does not take into account EIP-7623 floor gas. If you want to get the gas used in
305    /// receipt, use [`used()`](ResultGas::used) instead.
306    #[inline]
307    pub const fn spent_sub_refunded(&self) -> u64 {
308        self.total_gas_spent().saturating_sub(self.refunded)
309    }
310
311    /// Returns the effective refund after EIP-7623 floor gas adjustment.
312    ///
313    /// When floor gas kicks in (`spent - refunded < floor_gas`), the refund is zero
314    /// because the floor gas charge absorbs it entirely. Otherwise returns the raw refund.
315    #[inline]
316    pub const fn final_refunded(&self) -> u64 {
317        if self.spent_sub_refunded() < self.floor_gas {
318            0
319        } else {
320            self.refunded
321        }
322    }
323}
324
325/// Const function that returns the maximum of two u64 values.
326#[inline(always)]
327pub const fn max(a: u64, b: u64) -> u64 {
328    if a > b {
329        a
330    } else {
331        b
332    }
333}
334
335/// Const function that returns the minimum of two u64 values.
336#[inline(always)]
337pub const fn min(a: u64, b: u64) -> u64 {
338    if a < b {
339        a
340    } else {
341        b
342    }
343}
344
345impl fmt::Display for ResultGas {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        write!(
348            f,
349            "Gas used: {}, total spent: {}",
350            self.tx_gas_used(),
351            self.total_gas_spent()
352        )?;
353        if self.refunded > 0 {
354            write!(f, ", refunded: {}", self.refunded)?;
355        }
356        if self.floor_gas > 0 {
357            write!(f, ", floor: {}", self.floor_gas)?;
358        }
359        if self.state_gas_spent > 0 {
360            write!(f, ", state_gas: {}", self.state_gas_spent)?;
361        }
362        Ok(())
363    }
364}
365
366/// Result of a transaction execution
367#[derive(Clone, Debug, PartialEq, Eq, Hash)]
368#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
369pub enum ExecutionResult<HaltReasonTy = HaltReason> {
370    /// Returned successfully
371    Success {
372        /// Reason for the success.
373        reason: SuccessReason,
374        /// Gas accounting for the transaction.
375        gas: ResultGas,
376        /// Logs emitted by the transaction.
377        logs: Vec<Log>,
378        /// Output of the transaction.
379        output: Output,
380    },
381    /// Reverted by `REVERT` opcode that doesn't spend all gas
382    Revert {
383        /// Gas accounting for the transaction.
384        gas: ResultGas,
385        /// Logs emitted before the revert.
386        logs: Vec<Log>,
387        /// Output of the transaction.
388        output: Bytes,
389    },
390    /// Reverted for various reasons and spend all gas
391    Halt {
392        /// Reason for the halt.
393        reason: HaltReasonTy,
394        /// Gas accounting for the transaction.
395        ///
396        /// For standard EVM halts, gas used typically equals the gas limit.
397        /// Some system- or L2-specific halts may intentionally report less gas used.
398        gas: ResultGas,
399        /// Logs emitted before the halt.
400        logs: Vec<Log>,
401    },
402}
403
404impl<HaltReasonTy> ExecutionResult<HaltReasonTy> {
405    /// Returns if transaction execution is successful.
406    ///
407    /// 1 indicates success, 0 indicates revert.
408    ///
409    /// <https://eips.ethereum.org/EIPS/eip-658>
410    pub fn is_success(&self) -> bool {
411        matches!(self, Self::Success { .. })
412    }
413
414    /// Maps a `DBError` to a new error type using the provided closure, leaving other variants unchanged.
415    pub fn map_haltreason<F, OHR>(self, op: F) -> ExecutionResult<OHR>
416    where
417        F: FnOnce(HaltReasonTy) -> OHR,
418    {
419        match self {
420            Self::Success {
421                reason,
422                gas,
423                logs,
424                output,
425            } => ExecutionResult::Success {
426                reason,
427                gas,
428                logs,
429                output,
430            },
431            Self::Revert { gas, logs, output } => ExecutionResult::Revert { gas, logs, output },
432            Self::Halt { reason, gas, logs } => ExecutionResult::Halt {
433                reason: op(reason),
434                gas,
435                logs,
436            },
437        }
438    }
439
440    /// Returns created address if execution is Create transaction
441    /// and Contract was created.
442    pub fn created_address(&self) -> Option<Address> {
443        match self {
444            Self::Success { output, .. } => output.address().cloned(),
445            _ => None,
446        }
447    }
448
449    /// Returns true if execution result is a Halt.
450    pub fn is_halt(&self) -> bool {
451        matches!(self, Self::Halt { .. })
452    }
453
454    /// Returns the output data of the execution.
455    ///
456    /// Returns [`None`] if the execution was halted.
457    pub fn output(&self) -> Option<&Bytes> {
458        match self {
459            Self::Success { output, .. } => Some(output.data()),
460            Self::Revert { output, .. } => Some(output),
461            _ => None,
462        }
463    }
464
465    /// Consumes the type and returns the output data of the execution.
466    ///
467    /// Returns [`None`] if the execution was halted.
468    pub fn into_output(self) -> Option<Bytes> {
469        match self {
470            Self::Success { output, .. } => Some(output.into_data()),
471            Self::Revert { output, .. } => Some(output),
472            _ => None,
473        }
474    }
475
476    /// Returns the logs emitted during execution.
477    pub fn logs(&self) -> &[Log] {
478        match self {
479            Self::Success { logs, .. } | Self::Revert { logs, .. } | Self::Halt { logs, .. } => {
480                logs.as_slice()
481            }
482        }
483    }
484
485    /// Consumes [`self`] and returns the logs emitted during execution.
486    pub fn into_logs(self) -> Vec<Log> {
487        match self {
488            Self::Success { logs, .. } | Self::Revert { logs, .. } | Self::Halt { logs, .. } => {
489                logs
490            }
491        }
492    }
493
494    /// Returns the gas accounting information.
495    pub fn gas(&self) -> &ResultGas {
496        match self {
497            Self::Success { gas, .. } | Self::Revert { gas, .. } | Self::Halt { gas, .. } => gas,
498        }
499    }
500
501    /// Returns the gas used needed for the transaction receipt.
502    pub fn tx_gas_used(&self) -> u64 {
503        self.gas().tx_gas_used()
504    }
505
506    /// Returns the gas used.
507    #[inline]
508    #[deprecated(
509        since = "32.0.0",
510        note = "Use `tx_gas_used()` instead, `gas_used` is ambiguous after EIP-8037 state gas split"
511    )]
512    pub fn gas_used(&self) -> u64 {
513        self.tx_gas_used()
514    }
515}
516
517impl<HaltReasonTy: fmt::Display> fmt::Display for ExecutionResult<HaltReasonTy> {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        match self {
520            Self::Success {
521                reason,
522                gas,
523                logs,
524                output,
525            } => {
526                write!(f, "Success ({reason}): {gas}")?;
527                if !logs.is_empty() {
528                    write!(
529                        f,
530                        ", {} log{}",
531                        logs.len(),
532                        if logs.len() == 1 { "" } else { "s" }
533                    )?;
534                }
535                write!(f, ", {output}")
536            }
537            Self::Revert { gas, logs, output } => {
538                write!(f, "Revert: {gas}")?;
539                if !logs.is_empty() {
540                    write!(
541                        f,
542                        ", {} log{}",
543                        logs.len(),
544                        if logs.len() == 1 { "" } else { "s" }
545                    )?;
546                }
547                if !output.is_empty() {
548                    write!(f, ", {} bytes output", output.len())?;
549                }
550                Ok(())
551            }
552            Self::Halt { reason, gas, logs } => {
553                write!(f, "Halted: {reason} ({gas})")?;
554                if !logs.is_empty() {
555                    write!(
556                        f,
557                        ", {} log{}",
558                        logs.len(),
559                        if logs.len() == 1 { "" } else { "s" }
560                    )?;
561                }
562                Ok(())
563            }
564        }
565    }
566}
567
568/// Output of a transaction execution
569#[derive(Debug, Clone, PartialEq, Eq, Hash)]
570#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
571pub enum Output {
572    /// Output of a call.
573    Call(Bytes),
574    /// Output of a create.
575    Create(Bytes, Option<Address>),
576}
577
578impl Output {
579    /// Returns the output data of the execution output.
580    pub fn into_data(self) -> Bytes {
581        match self {
582            Output::Call(data) => data,
583            Output::Create(data, _) => data,
584        }
585    }
586
587    /// Returns the output data of the execution output.
588    pub fn data(&self) -> &Bytes {
589        match self {
590            Output::Call(data) => data,
591            Output::Create(data, _) => data,
592        }
593    }
594
595    /// Returns the created address, if any.
596    pub fn address(&self) -> Option<&Address> {
597        match self {
598            Output::Call(_) => None,
599            Output::Create(_, address) => address.as_ref(),
600        }
601    }
602}
603
604impl fmt::Display for Output {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        match self {
607            Output::Call(data) => {
608                if data.is_empty() {
609                    write!(f, "no output")
610                } else {
611                    write!(f, "{} bytes output", data.len())
612                }
613            }
614            Output::Create(data, Some(addr)) => {
615                if data.is_empty() {
616                    write!(f, "contract created at {}", addr)
617                } else {
618                    write!(f, "contract created at {} ({} bytes)", addr, data.len())
619                }
620            }
621            Output::Create(data, None) => {
622                if data.is_empty() {
623                    write!(f, "contract creation (no address)")
624                } else {
625                    write!(f, "contract creation (no address, {} bytes)", data.len())
626                }
627            }
628        }
629    }
630}
631
632/// Type-erased error type.
633#[derive(Debug, Clone)]
634pub struct AnyError(Arc<dyn core::error::Error + Send + Sync>);
635impl AnyError {
636    /// Creates a new [`AnyError`] from any error type.
637    pub fn new(err: impl core::error::Error + Send + Sync + 'static) -> Self {
638        Self(Arc::new(err))
639    }
640}
641
642impl PartialEq for AnyError {
643    fn eq(&self, other: &Self) -> bool {
644        Arc::ptr_eq(&self.0, &other.0)
645    }
646}
647impl Eq for AnyError {}
648impl core::hash::Hash for AnyError {
649    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
650        (Arc::as_ptr(&self.0) as *const ()).hash(state);
651    }
652}
653impl fmt::Display for AnyError {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        fmt::Display::fmt(&self.0, f)
656    }
657}
658impl core::error::Error for AnyError {
659    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
660        self.0.source()
661    }
662}
663
664#[cfg(feature = "serde")]
665impl serde::Serialize for AnyError {
666    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
667        serializer.collect_str(self)
668    }
669}
670
671#[derive(Debug)]
672struct StringError(String);
673impl fmt::Display for StringError {
674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675        f.write_str(&self.0)
676    }
677}
678impl core::error::Error for StringError {}
679
680impl From<String> for AnyError {
681    fn from(value: String) -> Self {
682        Self::new(StringError(value))
683    }
684}
685impl From<&'static str> for AnyError {
686    fn from(s: &'static str) -> Self {
687        Self::new(StringError(s.into()))
688    }
689}
690
691#[cfg(feature = "serde")]
692impl<'de> serde::Deserialize<'de> for AnyError {
693    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
694        let s = String::deserialize(deserializer)?;
695        Ok(s.into())
696    }
697}
698
699/// Main EVM error
700#[derive(Debug, Clone, PartialEq, Eq)]
701#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
702pub enum EVMError<DBError, TransactionError = InvalidTransaction> {
703    /// Transaction validation error
704    Transaction(TransactionError),
705    /// Header validation error
706    Header(InvalidHeader),
707    /// Database error
708    Database(DBError),
709    /// Custom error for non-standard EVM failures.
710    ///
711    /// This includes fatal precompile errors (`PrecompileError::Fatal` and `PrecompileError::FatalAny`)
712    /// errors as well as any custom errors returned by handler registers.
713    Custom(String),
714    /// Custom error for non-standard EVM failures.
715    ///
716    /// This includes fatal precompile errors (`PrecompileError::Fatal` and `PrecompileError::FatalAny`)
717    /// errors as well as any custom errors returned by handler registers.
718    CustomAny(AnyError),
719}
720
721impl<DBError, TransactionValidationErrorT> From<ContextError<DBError>>
722    for EVMError<DBError, TransactionValidationErrorT>
723{
724    fn from(value: ContextError<DBError>) -> Self {
725        match value {
726            ContextError::Db(e) => Self::Database(e),
727            ContextError::Custom(e) => Self::Custom(e),
728        }
729    }
730}
731
732impl<DBError: DBErrorMarker, TX> From<DBError> for EVMError<DBError, TX> {
733    fn from(value: DBError) -> Self {
734        Self::Database(value)
735    }
736}
737
738/// Trait for converting a string to an [`EVMError::Custom`] error.
739pub trait FromStringError {
740    /// Converts a string to an [`EVMError::Custom`] error.
741    fn from_string(value: String) -> Self;
742}
743
744impl<DB, TX> FromStringError for EVMError<DB, TX> {
745    fn from_string(value: String) -> Self {
746        Self::Custom(value)
747    }
748}
749
750impl<DB, TXE: From<InvalidTransaction>> From<InvalidTransaction> for EVMError<DB, TXE> {
751    fn from(value: InvalidTransaction) -> Self {
752        Self::Transaction(TXE::from(value))
753    }
754}
755
756impl<DBError, TransactionValidationErrorT> EVMError<DBError, TransactionValidationErrorT> {
757    /// Maps a `DBError` to a new error type using the provided closure, leaving other variants unchanged.
758    pub fn map_db_err<F, E>(self, op: F) -> EVMError<E, TransactionValidationErrorT>
759    where
760        F: FnOnce(DBError) -> E,
761    {
762        match self {
763            Self::Transaction(e) => EVMError::Transaction(e),
764            Self::Header(e) => EVMError::Header(e),
765            Self::Database(e) => EVMError::Database(op(e)),
766            Self::Custom(e) => EVMError::Custom(e),
767            Self::CustomAny(e) => EVMError::CustomAny(e),
768        }
769    }
770}
771
772impl<DBError, TransactionValidationErrorT> core::error::Error
773    for EVMError<DBError, TransactionValidationErrorT>
774where
775    DBError: core::error::Error + 'static,
776    TransactionValidationErrorT: core::error::Error + 'static,
777{
778    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
779        match self {
780            Self::Transaction(e) => Some(e),
781            Self::Header(e) => Some(e),
782            Self::Database(e) => Some(e),
783            Self::Custom(_) => None,
784            Self::CustomAny(e) => Some(e.0.as_ref()),
785        }
786    }
787}
788
789impl<DBError, TransactionValidationErrorT> fmt::Display
790    for EVMError<DBError, TransactionValidationErrorT>
791where
792    DBError: fmt::Display,
793    TransactionValidationErrorT: fmt::Display,
794{
795    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796        match self {
797            Self::Transaction(e) => write!(f, "transaction validation error: {e}"),
798            Self::Header(e) => write!(f, "header validation error: {e}"),
799            Self::Database(e) => write!(f, "database error: {e}"),
800            Self::Custom(e) => f.write_str(e),
801            Self::CustomAny(e) => write!(f, "{e}"),
802        }
803    }
804}
805
806impl<DBError, TransactionValidationErrorT> From<InvalidHeader>
807    for EVMError<DBError, TransactionValidationErrorT>
808{
809    fn from(value: InvalidHeader) -> Self {
810        Self::Header(value)
811    }
812}
813
814/// Transaction validation error.
815#[derive(Debug, Clone, PartialEq, Eq, Hash)]
816#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
817pub enum InvalidTransaction {
818    /// When using the EIP-1559 fee model introduced in the London upgrade, transactions specify two primary fee fields:
819    /// - `gas_max_fee`: The maximum total fee a user is willing to pay, inclusive of both base fee and priority fee.
820    /// - `gas_priority_fee`: The extra amount a user is willing to give directly to the miner, often referred to as the "tip".
821    ///
822    /// Provided `gas_priority_fee` exceeds the total `gas_max_fee`.
823    PriorityFeeGreaterThanMaxFee,
824    /// EIP-1559: `gas_price` is less than `basefee`.
825    GasPriceLessThanBasefee,
826    /// `gas_limit` in the tx is bigger than `block_gas_limit`.
827    CallerGasLimitMoreThanBlock,
828    /// Initial gas for a Call is bigger than `gas_limit`.
829    ///
830    /// Initial gas for a Call contains:
831    /// - initial stipend gas
832    /// - gas for access list and input data
833    CallGasCostMoreThanGasLimit {
834        /// Initial gas for a Call.
835        initial_gas: u64,
836        /// Gas limit for the transaction.
837        gas_limit: u64,
838    },
839    /// Gas floor calculated from EIP-7623 Increase calldata cost
840    /// is more than the gas limit.
841    ///
842    /// Tx data is too large to be executed.
843    GasFloorMoreThanGasLimit {
844        /// Gas floor calculated from EIP-7623 Increase calldata cost.
845        gas_floor: u64,
846        /// Gas limit for the transaction.
847        gas_limit: u64,
848    },
849    /// EIP-3607 Reject transactions from senders with deployed code
850    RejectCallerWithCode,
851    /// Transaction account does not have enough amount of ether to cover transferred value and gas_limit*gas_price.
852    LackOfFundForMaxFee {
853        /// Fee for the transaction.
854        fee: Box<U256>,
855        /// Balance of the sender.
856        balance: Box<U256>,
857    },
858    /// Overflow payment in transaction.
859    OverflowPaymentInTransaction,
860    /// Nonce overflows in transaction.
861    NonceOverflowInTransaction,
862    /// Nonce is too high.
863    NonceTooHigh {
864        /// Nonce of the transaction.
865        tx: u64,
866        /// Nonce of the state.
867        state: u64,
868    },
869    /// Nonce is too low.
870    NonceTooLow {
871        /// Nonce of the transaction.
872        tx: u64,
873        /// Nonce of the state.
874        state: u64,
875    },
876    /// EIP-3860: Limit and meter initcode
877    CreateInitCodeSizeLimit,
878    /// Transaction chain id does not match the config chain id.
879    InvalidChainId,
880    /// Missing chain id.
881    MissingChainId,
882    /// Transaction gas limit is greater than the cap.
883    TxGasLimitGreaterThanCap {
884        /// Transaction gas limit.
885        gas_limit: u64,
886        /// Gas limit cap.
887        cap: u64,
888    },
889    /// Access list is not supported for blocks before the Berlin hardfork.
890    AccessListNotSupported,
891    /// `max_fee_per_blob_gas` is not supported for blocks before the Cancun hardfork.
892    MaxFeePerBlobGasNotSupported,
893    /// `blob_hashes`/`blob_versioned_hashes` is not supported for blocks before the Cancun hardfork.
894    BlobVersionedHashesNotSupported,
895    /// Block `blob_gas_price` is greater than tx-specified `max_fee_per_blob_gas` after Cancun.
896    BlobGasPriceGreaterThanMax {
897        /// Block `blob_gas_price`.
898        block_blob_gas_price: u128,
899        /// Tx-specified `max_fee_per_blob_gas`.
900        tx_max_fee_per_blob_gas: u128,
901    },
902    /// There should be at least one blob in Blob transaction.
903    EmptyBlobs,
904    /// Blob transaction can't be a create transaction.
905    ///
906    /// `to` must be present
907    BlobCreateTransaction,
908    /// Transaction has more then `max` blobs
909    TooManyBlobs {
910        /// Maximum number of blobs allowed.
911        max: usize,
912        /// Number of blobs in the transaction.
913        have: usize,
914    },
915    /// Blob transaction contains a versioned hash with an incorrect version
916    BlobVersionNotSupported,
917    /// EIP-7702 is not enabled.
918    AuthorizationListNotSupported,
919    /// EIP-7702 transaction has invalid fields set.
920    AuthorizationListInvalidFields,
921    /// Empty Authorization List is not allowed.
922    EmptyAuthorizationList,
923    /// EIP-2930 is not supported.
924    Eip2930NotSupported,
925    /// EIP-1559 is not supported.
926    Eip1559NotSupported,
927    /// EIP-4844 is not supported.
928    Eip4844NotSupported,
929    /// EIP-7702 is not supported.
930    Eip7702NotSupported,
931    /// EIP-7873 is not supported.
932    Eip7873NotSupported,
933    /// EIP-7873 initcode transaction should have `to` address.
934    Eip7873MissingTarget,
935    /// Custom string error for flexible error handling.
936    Str(Cow<'static, str>),
937}
938
939impl TransactionError for InvalidTransaction {}
940
941impl core::error::Error for InvalidTransaction {}
942
943impl fmt::Display for InvalidTransaction {
944    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
945        match self {
946            Self::PriorityFeeGreaterThanMaxFee => {
947                write!(f, "priority fee is greater than max fee")
948            }
949            Self::GasPriceLessThanBasefee => {
950                write!(f, "gas price is less than basefee")
951            }
952            Self::CallerGasLimitMoreThanBlock => {
953                write!(f, "caller gas limit exceeds the block gas limit")
954            }
955            Self::TxGasLimitGreaterThanCap { gas_limit, cap } => {
956                write!(
957                    f,
958                    "transaction gas limit ({gas_limit}) is greater than the cap ({cap})"
959                )
960            }
961            Self::CallGasCostMoreThanGasLimit {
962                initial_gas,
963                gas_limit,
964            } => {
965                write!(
966                    f,
967                    "call gas cost ({initial_gas}) exceeds the gas limit ({gas_limit})"
968                )
969            }
970            Self::GasFloorMoreThanGasLimit {
971                gas_floor,
972                gas_limit,
973            } => {
974                write!(
975                    f,
976                    "gas floor ({gas_floor}) exceeds the gas limit ({gas_limit})"
977                )
978            }
979            Self::RejectCallerWithCode => {
980                write!(f, "reject transactions from senders with deployed code")
981            }
982            Self::LackOfFundForMaxFee { fee, balance } => {
983                write!(f, "lack of funds ({balance}) for max fee ({fee})")
984            }
985            Self::OverflowPaymentInTransaction => {
986                write!(f, "overflow payment in transaction")
987            }
988            Self::NonceOverflowInTransaction => {
989                write!(f, "nonce overflow in transaction")
990            }
991            Self::NonceTooHigh { tx, state } => {
992                write!(f, "nonce {tx} too high, expected {state}")
993            }
994            Self::NonceTooLow { tx, state } => {
995                write!(f, "nonce {tx} too low, expected {state}")
996            }
997            Self::CreateInitCodeSizeLimit => {
998                write!(f, "create initcode size limit")
999            }
1000            Self::InvalidChainId => write!(f, "invalid chain ID"),
1001            Self::MissingChainId => write!(f, "missing chain ID"),
1002            Self::AccessListNotSupported => write!(f, "access list not supported"),
1003            Self::MaxFeePerBlobGasNotSupported => {
1004                write!(f, "max fee per blob gas not supported")
1005            }
1006            Self::BlobVersionedHashesNotSupported => {
1007                write!(f, "blob versioned hashes not supported")
1008            }
1009            Self::BlobGasPriceGreaterThanMax {
1010                block_blob_gas_price,
1011                tx_max_fee_per_blob_gas,
1012            } => {
1013                write!(
1014                    f,
1015                    "blob gas price ({block_blob_gas_price}) is greater than max fee per blob gas ({tx_max_fee_per_blob_gas})"
1016                )
1017            }
1018            Self::EmptyBlobs => write!(f, "empty blobs"),
1019            Self::BlobCreateTransaction => write!(f, "blob create transaction"),
1020            Self::TooManyBlobs { max, have } => {
1021                write!(f, "too many blobs, have {have}, max {max}")
1022            }
1023            Self::BlobVersionNotSupported => write!(f, "blob version not supported"),
1024            Self::AuthorizationListNotSupported => write!(f, "authorization list not supported"),
1025            Self::AuthorizationListInvalidFields => {
1026                write!(f, "authorization list tx has invalid fields")
1027            }
1028            Self::EmptyAuthorizationList => write!(f, "empty authorization list"),
1029            Self::Eip2930NotSupported => write!(f, "Eip2930 is not supported"),
1030            Self::Eip1559NotSupported => write!(f, "Eip1559 is not supported"),
1031            Self::Eip4844NotSupported => write!(f, "Eip4844 is not supported"),
1032            Self::Eip7702NotSupported => write!(f, "Eip7702 is not supported"),
1033            Self::Eip7873NotSupported => write!(f, "Eip7873 is not supported"),
1034            Self::Eip7873MissingTarget => {
1035                write!(f, "Eip7873 initcode transaction should have `to` address")
1036            }
1037            Self::Str(msg) => f.write_str(msg),
1038        }
1039    }
1040}
1041
1042/// Errors related to misconfiguration of a [`crate::Block`].
1043#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1044#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1045pub enum InvalidHeader {
1046    /// `prevrandao` is not set for Merge and above.
1047    PrevrandaoNotSet,
1048    /// `excess_blob_gas` is not set for Cancun and above.
1049    ExcessBlobGasNotSet,
1050}
1051
1052impl core::error::Error for InvalidHeader {}
1053
1054impl fmt::Display for InvalidHeader {
1055    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1056        match self {
1057            Self::PrevrandaoNotSet => write!(f, "`prevrandao` not set"),
1058            Self::ExcessBlobGasNotSet => write!(f, "`excess_blob_gas` not set"),
1059        }
1060    }
1061}
1062
1063/// Reason a transaction successfully completed.
1064#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1065#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1066pub enum SuccessReason {
1067    /// Stop [`state::bytecode::opcode::STOP`] opcode.
1068    Stop,
1069    /// Return [`state::bytecode::opcode::RETURN`] opcode.
1070    Return,
1071    /// Self destruct opcode.
1072    SelfDestruct,
1073}
1074
1075impl fmt::Display for SuccessReason {
1076    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1077        match self {
1078            Self::Stop => write!(f, "Stop"),
1079            Self::Return => write!(f, "Return"),
1080            Self::SelfDestruct => write!(f, "SelfDestruct"),
1081        }
1082    }
1083}
1084
1085/// Indicates that the EVM has experienced an exceptional halt.
1086///
1087/// This causes execution to immediately end with all gas being consumed.
1088#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1089#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1090pub enum HaltReason {
1091    /// Out of gas error.
1092    OutOfGas(OutOfGasError),
1093    /// Opcode not found error.
1094    OpcodeNotFound,
1095    /// Invalid FE opcode error.
1096    InvalidFEOpcode,
1097    /// Invalid jump destination.
1098    InvalidJump,
1099    /// The feature or opcode is not activated in hardfork.
1100    NotActivated,
1101    /// Attempting to pop a value from an empty stack.
1102    StackUnderflow,
1103    /// Attempting to push a value onto a full stack.
1104    StackOverflow,
1105    /// Invalid memory or storage offset for [`state::bytecode::opcode::RETURNDATACOPY`].
1106    OutOfOffset,
1107    /// Address collision during contract creation.
1108    CreateCollision,
1109    /// Precompile error.
1110    PrecompileError,
1111    /// Precompile error with message from context.
1112    PrecompileErrorWithContext(String),
1113    /// Nonce overflow.
1114    NonceOverflow,
1115    /// Create init code size exceeds limit (runtime).
1116    CreateContractSizeLimit,
1117    /// Error on created contract that begins with EF
1118    CreateContractStartingWithEF,
1119    /// EIP-3860: Limit and meter initcode. Initcode size limit exceeded.
1120    CreateInitCodeSizeLimit,
1121
1122    /* Internal Halts that can be only found inside Inspector */
1123    /// Overflow payment. Not possible to happen on mainnet.
1124    OverflowPayment,
1125    /// State change during static call.
1126    StateChangeDuringStaticCall,
1127    /// Call not allowed inside static call.
1128    CallNotAllowedInsideStatic,
1129    /// Out of funds to pay for the call.
1130    OutOfFunds,
1131    /// Call is too deep.
1132    CallTooDeep,
1133}
1134
1135impl core::error::Error for HaltReason {}
1136
1137impl fmt::Display for HaltReason {
1138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1139        match self {
1140            Self::OutOfGas(err) => write!(f, "{err}"),
1141            Self::OpcodeNotFound => write!(f, "opcode not found"),
1142            Self::InvalidFEOpcode => write!(f, "invalid 0xFE opcode"),
1143            Self::InvalidJump => write!(f, "invalid jump destination"),
1144            Self::NotActivated => write!(f, "feature or opcode not activated"),
1145            Self::StackUnderflow => write!(f, "stack underflow"),
1146            Self::StackOverflow => write!(f, "stack overflow"),
1147            Self::OutOfOffset => write!(f, "out of offset"),
1148            Self::CreateCollision => write!(f, "create collision"),
1149            Self::PrecompileError => write!(f, "precompile error"),
1150            Self::PrecompileErrorWithContext(msg) => write!(f, "precompile error: {msg}"),
1151            Self::NonceOverflow => write!(f, "nonce overflow"),
1152            Self::CreateContractSizeLimit => write!(f, "create contract size limit"),
1153            Self::CreateContractStartingWithEF => {
1154                write!(f, "create contract starting with 0xEF")
1155            }
1156            Self::CreateInitCodeSizeLimit => write!(f, "create initcode size limit"),
1157            Self::OverflowPayment => write!(f, "overflow payment"),
1158            Self::StateChangeDuringStaticCall => write!(f, "state change during static call"),
1159            Self::CallNotAllowedInsideStatic => write!(f, "call not allowed inside static call"),
1160            Self::OutOfFunds => write!(f, "out of funds"),
1161            Self::CallTooDeep => write!(f, "call too deep"),
1162        }
1163    }
1164}
1165
1166/// Out of gas errors.
1167#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1169pub enum OutOfGasError {
1170    /// Basic OOG error. Not enough gas to execute the opcode.
1171    Basic,
1172    /// Tried to expand past memory limit.
1173    MemoryLimit,
1174    /// Basic OOG error from memory expansion
1175    Memory,
1176    /// Precompile threw OOG error
1177    Precompile,
1178    /// When performing something that takes a U256 and casts down to a u64, if its too large this would fire
1179    /// i.e. in `as_usize_or_fail`
1180    InvalidOperand,
1181    /// When performing SSTORE the gasleft is less than or equal to 2300
1182    ReentrancySentry,
1183}
1184
1185impl core::error::Error for OutOfGasError {}
1186
1187impl fmt::Display for OutOfGasError {
1188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1189        match self {
1190            Self::Basic => write!(f, "out of gas"),
1191            Self::MemoryLimit => write!(f, "out of gas: memory limit exceeded"),
1192            Self::Memory => write!(f, "out of gas: memory expansion"),
1193            Self::Precompile => write!(f, "out of gas: precompile"),
1194            Self::InvalidOperand => write!(f, "out of gas: invalid operand"),
1195            Self::ReentrancySentry => write!(f, "out of gas: reentrancy sentry"),
1196        }
1197    }
1198}
1199
1200/// Error that includes transaction index for batch transaction processing.
1201#[derive(Debug, Clone, PartialEq, Eq)]
1202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1203pub struct TransactionIndexedError<Error> {
1204    /// The original error that occurred.
1205    pub error: Error,
1206    /// The index of the transaction that failed.
1207    pub transaction_index: usize,
1208}
1209
1210impl<Error> TransactionIndexedError<Error> {
1211    /// Create a new `TransactionIndexedError` with the given error and transaction index.
1212    #[must_use]
1213    pub fn new(error: Error, transaction_index: usize) -> Self {
1214        Self {
1215            error,
1216            transaction_index,
1217        }
1218    }
1219
1220    /// Get a reference to the underlying error.
1221    pub fn error(&self) -> &Error {
1222        &self.error
1223    }
1224
1225    /// Convert into the underlying error.
1226    #[must_use]
1227    pub fn into_error(self) -> Error {
1228        self.error
1229    }
1230}
1231
1232impl<Error: fmt::Display> fmt::Display for TransactionIndexedError<Error> {
1233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1234        write!(
1235            f,
1236            "transaction {} failed: {}",
1237            self.transaction_index, self.error
1238        )
1239    }
1240}
1241
1242impl<Error: core::error::Error + 'static> core::error::Error for TransactionIndexedError<Error> {
1243    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1244        Some(&self.error)
1245    }
1246}
1247
1248impl From<&'static str> for InvalidTransaction {
1249    fn from(s: &'static str) -> Self {
1250        Self::Str(Cow::Borrowed(s))
1251    }
1252}
1253
1254impl From<String> for InvalidTransaction {
1255    fn from(s: String) -> Self {
1256        Self::Str(Cow::Owned(s))
1257    }
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262    use super::*;
1263
1264    #[test]
1265    fn test_execution_result_display() {
1266        let result: ExecutionResult<HaltReason> = ExecutionResult::Success {
1267            reason: SuccessReason::Return,
1268            gas: ResultGas::default()
1269                .with_total_gas_spent(100000)
1270                .with_refunded(26000)
1271                .with_floor_gas(5000),
1272            logs: vec![Log::default(), Log::default()],
1273            output: Output::Call(Bytes::from(vec![1, 2, 3])),
1274        };
1275        assert_eq!(
1276            result.to_string(),
1277            "Success (Return): Gas used: 74000, total spent: 100000, refunded: 26000, floor: 5000, 2 logs, 3 bytes output"
1278        );
1279
1280        let result: ExecutionResult<HaltReason> = ExecutionResult::Revert {
1281            gas: ResultGas::default()
1282                .with_total_gas_spent(100000)
1283                .with_refunded(100000),
1284            logs: vec![],
1285            output: Bytes::from(vec![1, 2, 3, 4]),
1286        };
1287        assert_eq!(
1288            result.to_string(),
1289            "Revert: Gas used: 0, total spent: 100000, refunded: 100000, 4 bytes output"
1290        );
1291
1292        let result: ExecutionResult<HaltReason> = ExecutionResult::Halt {
1293            reason: HaltReason::OutOfGas(OutOfGasError::Basic),
1294            gas: ResultGas::default()
1295                .with_total_gas_spent(1000000)
1296                .with_refunded(1000000),
1297            logs: vec![],
1298        };
1299        assert_eq!(
1300            result.to_string(),
1301            "Halted: out of gas (Gas used: 0, total spent: 1000000, refunded: 1000000)"
1302        );
1303    }
1304
1305    #[test]
1306    fn test_result_gas_display() {
1307        // No refund, no floor
1308        assert_eq!(
1309            ResultGas::default().with_total_gas_spent(21000).to_string(),
1310            "Gas used: 21000, total spent: 21000"
1311        );
1312        // With refund
1313        assert_eq!(
1314            ResultGas::default()
1315                .with_total_gas_spent(50000)
1316                .with_refunded(10000)
1317                .to_string(),
1318            "Gas used: 40000, total spent: 50000, refunded: 10000"
1319        );
1320        // With refund and floor
1321        assert_eq!(
1322            ResultGas::default()
1323                .with_total_gas_spent(50000)
1324                .with_refunded(10000)
1325                .with_floor_gas(30000)
1326                .to_string(),
1327            "Gas used: 40000, total spent: 50000, refunded: 10000, floor: 30000"
1328        );
1329    }
1330
1331    #[test]
1332    fn test_result_gas_used_and_remaining() {
1333        let gas = ResultGas::default()
1334            .with_total_gas_spent(100)
1335            .with_refunded(30);
1336        assert_eq!(gas.total_gas_spent(), 100);
1337        assert_eq!(gas.inner_refunded(), 30);
1338        assert_eq!(gas.spent_sub_refunded(), 70);
1339
1340        // Saturating: refunded > spent
1341        let gas = ResultGas::default()
1342            .with_total_gas_spent(10)
1343            .with_refunded(50);
1344        assert_eq!(gas.spent_sub_refunded(), 0);
1345    }
1346
1347    #[test]
1348    fn test_final_refunded_with_floor_gas() {
1349        // No floor gas: final_refunded == refunded
1350        let gas = ResultGas::default()
1351            .with_total_gas_spent(50000)
1352            .with_refunded(10000);
1353        assert_eq!(gas.tx_gas_used(), 40000);
1354        assert_eq!(gas.final_refunded(), 10000);
1355
1356        // Floor gas active (spent_sub_refunded < floor_gas): final_refunded == 0
1357        // spent=50000, refunded=10000, spent_sub_refunded=40000 < floor_gas=45000
1358        let gas = ResultGas::default()
1359            .with_total_gas_spent(50000)
1360            .with_refunded(10000)
1361            .with_floor_gas(45000);
1362        assert_eq!(gas.tx_gas_used(), 45000);
1363        assert_eq!(gas.final_refunded(), 0);
1364
1365        // Floor gas inactive (spent_sub_refunded >= floor_gas): final_refunded == refunded
1366        // spent=50000, refunded=10000, spent_sub_refunded=40000 >= floor_gas=30000
1367        let gas = ResultGas::default()
1368            .with_total_gas_spent(50000)
1369            .with_refunded(10000)
1370            .with_floor_gas(30000);
1371        assert_eq!(gas.tx_gas_used(), 40000);
1372        assert_eq!(gas.final_refunded(), 10000);
1373
1374        // Edge case: spent_sub_refunded == floor_gas exactly
1375        // spent=50000, refunded=10000, spent_sub_refunded=40000 == floor_gas=40000
1376        let gas = ResultGas::default()
1377            .with_total_gas_spent(50000)
1378            .with_refunded(10000)
1379            .with_floor_gas(40000);
1380        assert_eq!(gas.tx_gas_used(), 40000);
1381        assert_eq!(gas.final_refunded(), 10000);
1382    }
1383}