mega-evm 1.7.0

The evm tailored for the MegaETH
use alloy_primitives::Bytes;
use alloy_sol_types::SolError;

mod compute_gas;
mod data_size;
mod frame_limit;
mod kv_update;
#[allow(clippy::module_inception)]
mod limit;
mod state_growth;
mod storage_call_stipend;

pub use data_size::*;
pub(crate) use frame_limit::{FrameLimitTracker, TxRuntimeLimit};
pub use limit::*;

use crate::MegaHaltReason;

alloy_sol_types::sol! {
    /// ABI-encoded error emitted as revert data when a frame-local resource limit is exceeded.
    #[derive(Debug, PartialEq, Eq)]
    error MegaLimitExceeded(uint8 kind, uint64 limit);
}

/// Identifies which resource limit was exceeded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LimitKind {
    /// Data size limit (bytes of data transmitted and stored).
    DataSize,
    /// Key-value update limit (number of state-modifying operations).
    KVUpdate,
    /// Compute gas limit (cumulative EVM instruction gas).
    ComputeGas,
    /// State growth limit (net new accounts and storage slots).
    StateGrowth,
}

impl LimitKind {
    /// Returns the discriminant value used in ABI-encoded revert data.
    pub const fn as_u8(&self) -> u8 {
        match self {
            Self::DataSize => 0,
            Self::KVUpdate => 1,
            Self::ComputeGas => 2,
            Self::StateGrowth => 3,
        }
    }

    /// Converts a discriminant value back to a `LimitKind`.
    pub const fn from_u8(kind: u8) -> Option<Self> {
        match kind {
            0 => Some(Self::DataSize),
            1 => Some(Self::KVUpdate),
            2 => Some(Self::ComputeGas),
            3 => Some(Self::StateGrowth),
            _ => None,
        }
    }
}

/// Result of a limit check.
///
/// Carries three semantically distinct states: limits passed; a limit was exceeded (with
/// metadata for the halt path); or per-tx metering is exempt (REX6+ system-originated tx —
/// see [`crate::is_system_originated`]). The `Exempt` state is **sticky**: once `AdditionalLimit`
/// stores it in `has_exceeded_limit`, `check_limit` short-circuits and the sub-tracker checks
/// are skipped, so no later overflow can overwrite it.
#[derive(Debug, Default, Clone, Copy)]
pub enum LimitCheck {
    /// All limits are within their configured thresholds.
    #[default]
    WithinLimit,
    /// A limit has been exceeded.
    ExceedsLimit {
        /// Which resource limit was exceeded.
        kind: LimitKind,
        /// The configured limit.
        limit: u64,
        /// The current usage.
        used: u64,
        /// Whether this exceed is from a frame-local budget (absorbable at frame boundary)
        /// vs a TX-level limit (must propagate to halt the transaction).
        frame_local: bool,
    },
    /// Per-tx metering is exempt: REX6+ system-originated tx (see
    /// [`crate::is_system_originated`]). Behaves as not-exceeded for halt decisions; sticky as
    /// described on the enum. Sub-tracker `check_limit` impls never produce this variant —
    /// only `AdditionalLimit::has_exceeded_limit` carries it, set via
    /// [`AdditionalLimit::mark_exempt`](super::AdditionalLimit::mark_exempt).
    Exempt,
}

impl LimitCheck {
    /// Returns `true` if a resource limit has been exceeded.
    ///
    /// `Exempt` returns `false`: per-tx metering is suppressed, so the halt path must not fire.
    #[inline]
    pub const fn exceeded_limit(&self) -> bool {
        matches!(self, Self::ExceedsLimit { .. })
    }

    /// Returns `true` strictly when no limit check has been performed yet or the last check passed.
    ///
    /// `Exempt` returns `false`: it is a distinct sticky state, not a "passed" result. Callers
    /// that just want to gate the halt path should use [`exceeded_limit`](Self::exceeded_limit)
    /// (its negation), not this predicate.
    #[inline]
    pub const fn within_limit(&self) -> bool {
        matches!(self, Self::WithinLimit)
    }

    /// Returns `true` when per-tx metering is suppressed for the current transaction.
    #[inline]
    pub const fn is_exempt(&self) -> bool {
        matches!(self, Self::Exempt)
    }

    /// Returns whether this is a frame-local exceed.
    #[inline]
    pub const fn is_frame_local(&self) -> bool {
        matches!(self, Self::ExceedsLimit { frame_local: true, .. })
    }

    /// Returns ABI-encoded revert data for a frame-local limit exceed.
    ///
    /// Encodes as `MegaLimitExceeded(uint8 kind, uint64 limit)`. Returns empty bytes for
    /// `WithinLimit` and `Exempt` (neither produces a frame-local revert).
    pub fn revert_data(&self) -> Bytes {
        match self {
            Self::ExceedsLimit { kind, limit, .. } => {
                MegaLimitExceeded { kind: kind.as_u8(), limit: *limit }.abi_encode().into()
            }
            Self::WithinLimit | Self::Exempt => Bytes::new(),
        }
    }

    /// Returns the [`MegaHaltReason`] if a limit has been exceeded.
    ///
    /// `WithinLimit` and `Exempt` both return `None`: neither halts the transaction.
    pub fn maybe_halt_reason(&self) -> Option<MegaHaltReason> {
        match self {
            Self::ExceedsLimit { kind: LimitKind::DataSize, limit, used, .. } => {
                Some(MegaHaltReason::DataLimitExceeded { limit: *limit, actual: *used })
            }
            Self::ExceedsLimit { kind: LimitKind::KVUpdate, limit, used, .. } => {
                Some(MegaHaltReason::KVUpdateLimitExceeded { limit: *limit, actual: *used })
            }
            Self::ExceedsLimit { kind: LimitKind::ComputeGas, limit, used, .. } => {
                Some(MegaHaltReason::ComputeGasLimitExceeded { limit: *limit, actual: *used })
            }
            Self::ExceedsLimit { kind: LimitKind::StateGrowth, limit, used, .. } => {
                Some(MegaHaltReason::StateGrowthLimitExceeded { limit: *limit, actual: *used })
            }
            Self::WithinLimit | Self::Exempt => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Pins the predicate truth-table for the `Exempt` variant so a future change that flips one
    /// predicate (e.g., reverting `exceeded_limit` to `!matches!(WithinLimit)`) is caught here
    /// rather than silently re-enabling halts for exempt txs.
    #[test]
    fn test_limit_check_exempt_predicate_truth_table() {
        let exempt = LimitCheck::Exempt;
        assert!(!exempt.exceeded_limit());
        assert!(!exempt.within_limit());
        assert!(exempt.is_exempt());
        assert!(!exempt.is_frame_local());
        assert!(exempt.revert_data().is_empty());
        assert!(exempt.maybe_halt_reason().is_none());
    }

    /// `within_limit` must mirror the enum variant exactly, not return a constant.
    #[test]
    fn test_within_limit_reflects_variant() {
        assert!(LimitCheck::WithinLimit.within_limit());
        let exceeded = LimitCheck::ExceedsLimit {
            kind: LimitKind::DataSize,
            limit: 100,
            used: 150,
            frame_local: false,
        };
        assert!(!exceeded.within_limit());
    }

    /// Every `LimitKind` discriminant must survive an `as_u8` -> `from_u8` round-trip,
    /// and unknown discriminants must map to `None`.
    #[test]
    fn test_limit_kind_u8_roundtrip() {
        for kind in [
            LimitKind::DataSize,
            LimitKind::KVUpdate,
            LimitKind::ComputeGas,
            LimitKind::StateGrowth,
        ] {
            assert_eq!(
                LimitKind::from_u8(kind.as_u8()),
                Some(kind),
                "round-trip failed for {kind:?}"
            );
        }
        assert_eq!(LimitKind::from_u8(4), None);
    }
}