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