Repository: lambdaclass/ethrex
Pull request #2508: refactor(levm,l2): hooks
Author: login: ilitteri
Labels: name: tech debt
description: Refactors, cleanups, etc
name: L2
description: Rollup client
Created: 2025-04-21T18:35:09Z | Merged: 2025-04-30T22:49:13Z

**Motivation**

- Remove duplicated code between `DefaultHook` and `L2Hook` implementations.
- Use the `L2Hook` for every tx (regular ETH txs and L2's privilege txs) when running the L2.

**Description**

This PR:
- Generates abstractions to be used in `DefaultHook` and `L2Hook` to remove repeated code.
- Adds the `is_privilege` field to LEVM's `Environment` only compiled under the `l2` feature flag.
- `L2Hook` now supports executing every tx (before only privileged).


Diff:
diff --git a/cmd/ef_tests/state/Cargo.toml b/cmd/ef_tests/state/Cargo.toml
index 79866bb2900..aad7402e81b 100644
--- a/cmd/ef_tests/state/Cargo.toml
+++ b/cmd/ef_tests/state/Cargo.toml
@@ -41,6 +41,7 @@ path = "./lib.rs"
 default = ["c-kzg", "blst"]
 c-kzg = ["ethrex-vm/c-kzg", "ethrex-levm/c-kzg", "ethrex-common/c-kzg"]
 blst = ["ethrex-vm/blst"]
+l2 = ["ethrex-vm/l2", "ethrex-levm/l2"]
 
 [[test]]
 name = "all"
diff --git a/cmd/ef_tests/state/runner/levm_runner.rs b/cmd/ef_tests/state/runner/levm_runner.rs
index 5fa3e7d0f12..2cffbb02dbb 100644
--- a/cmd/ef_tests/state/runner/levm_runner.rs
+++ b/cmd/ef_tests/state/runner/levm_runner.rs
@@ -188,6 +188,7 @@ pub fn prepare_vm_for_tx<'a>(
             tx_nonce: test_tx.nonce,
             block_gas_limit: test.env.current_gas_limit,
             transient_storage: HashMap::new(),
+            is_privileged: false,
         },
         db,
         &tx,
diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs
index d30eba2ded4..9d368266f65 100644
--- a/crates/vm/backends/levm/mod.rs
+++ b/crates/vm/backends/levm/mod.rs
@@ -132,6 +132,7 @@ impl LEVM {
             block_gas_limit: block_header.gas_limit,
             transient_storage: HashMap::new(),
             difficulty: block_header.difficulty,
+            is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
         };
 
         let mut vm = VM::new(env, db, tx)?;
@@ -721,6 +722,7 @@ fn env_from_generic(
         block_gas_limit: header.gas_limit,
         transient_storage: HashMap::new(),
         difficulty: header.difficulty,
+        is_privileged: false,
     })
 }
 
diff --git a/crates/vm/levm/src/environment.rs b/crates/vm/levm/src/environment.rs
index 5e579086761..b8a6711e0cc 100644
--- a/crates/vm/levm/src/environment.rs
+++ b/crates/vm/levm/src/environment.rs
@@ -31,4 +31,5 @@ pub struct Environment {
     pub tx_nonce: u64,
     pub block_gas_limit: u64,
     pub transient_storage: TransientStorage,
+    pub is_privileged: bool,
 }
diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs
index b51220eb4c2..d48f7b9e150 100644
--- a/crates/vm/levm/src/errors.rs
+++ b/crates/vm/levm/src/errors.rs
@@ -194,6 +194,8 @@ pub enum InternalError {
     InvalidSpecId,
     #[error("Account should had been delegated")]
     AccountNotDelegated,
+    #[error("No recipient found for privilege transaction")]
+    RecipientNotFoundForPrivilegeTransaction,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
diff --git a/crates/vm/levm/src/hooks/default_hook.rs b/crates/vm/levm/src/hooks/default_hook.rs
index 014973a261a..880d186161a 100644
--- a/crates/vm/levm/src/hooks/default_hook.rs
+++ b/crates/vm/levm/src/hooks/default_hook.rs
@@ -9,7 +9,7 @@ use crate::{
 
 use ethrex_common::{
     types::{Account, Fork},
-    U256,
+    Address, U256,
 };
 
 use std::cmp::max;
@@ -33,30 +33,7 @@ impl Hook for DefaultHook {
         let sender_account = vm.db.get_account(sender_address)?;
 
         if vm.env.config.fork >= Fork::Prague {
-            // check for gas limit is grater or equal than the minimum required
-            let calldata = vm.current_call_frame()?.calldata.clone();
-            let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
-
-            // calldata_cost = tokens_in_calldata * 4
-            let calldata_cost: u64 =
-                gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
-
-            // same as calculated in gas_used()
-            let tokens_in_calldata: u64 = calldata_cost
-                .checked_div(STANDARD_TOKEN_COST)
-                .ok_or(VMError::Internal(InternalError::DivisionError))?;
-
-            // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
-            let floor_cost_by_tokens = tokens_in_calldata
-                .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
-                .ok_or(VMError::Internal(InternalError::GasOverflow))?
-                .checked_add(TX_BASE_COST)
-                .ok_or(VMError::Internal(InternalError::GasOverflow))?;
-
-            let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
-            if vm.current_call_frame()?.gas_limit < min_gas_limit {
-                return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
-            }
+            validate_min_gas_limit(vm)?;
         }
 
         // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
@@ -68,93 +45,22 @@ impl Hook for DefaultHook {
                 TxValidationError::GasLimitPriceProductOverflow,
             ))?;
 
-        // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice + value + blob_gas_cost
-        let value = vm.current_call_frame()?.msg_value;
-
-        // blob gas cost = max fee per blob gas * blob gas used
-        // https://eips.ethereum.org/EIPS/eip-4844
-        let max_blob_gas_cost =
-            get_max_blob_gas_price(&vm.env.tx_blob_hashes, vm.env.tx_max_fee_per_blob_gas)?;
-
-        // For the transaction to be valid the sender account has to have a balance >= gas_price * gas_limit + value if tx is type 0 and 1
-        // balance >= max_fee_per_gas * gas_limit + value + blob_gas_cost if tx is type 2 or 3
-        let gas_fee_for_valid_tx = vm
-            .env
-            .tx_max_fee_per_gas
-            .unwrap_or(vm.env.gas_price)
-            .checked_mul(vm.env.gas_limit.into())
-            .ok_or(VMError::TxValidation(
-                TxValidationError::GasLimitPriceProductOverflow,
-            ))?;
-
-        let balance_for_valid_tx = gas_fee_for_valid_tx
-            .checked_add(value)
-            .ok_or(VMError::TxValidation(
-                TxValidationError::InsufficientAccountFunds,
-            ))?
-            .checked_add(max_blob_gas_cost)
-            .ok_or(VMError::TxValidation(
-                TxValidationError::InsufficientAccountFunds,
-            ))?;
-        if sender_account.info.balance < balance_for_valid_tx {
-            return Err(VMError::TxValidation(
-                TxValidationError::InsufficientAccountFunds,
-            ));
-        }
-
-        let blob_gas_cost = get_blob_gas_price(
-            &vm.env.tx_blob_hashes,
-            vm.env.block_excess_blob_gas,
-            &vm.env.config,
-        )?;
+        validate_sender_balance(vm, &sender_account)?;
 
         // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
         if let Some(tx_max_fee_per_blob_gas) = vm.env.tx_max_fee_per_blob_gas {
-            if tx_max_fee_per_blob_gas
-                < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::InsufficientMaxFeePerBlobGas,
-                ));
-            }
+            validate_max_fee_per_blob_gas(vm, tx_max_fee_per_blob_gas)?;
         }
 
-        // The real cost to deduct is calculated as effective_gas_price * gas_limit + value + blob_gas_cost
-        let up_front_cost = gaslimit_price_product
-            .checked_add(value)
-            .ok_or(VMError::TxValidation(
-                TxValidationError::InsufficientAccountFunds,
-            ))?
-            .checked_add(blob_gas_cost)
-            .ok_or(VMError::TxValidation(
-                TxValidationError::InsufficientAccountFunds,
-            ))?;
-        // There is no error specified for overflow in up_front_cost
-        // in ef_tests. We went for "InsufficientAccountFunds" simply
-        // because if the upfront cost is bigger than U256, then,
-        // technically, the sender will not be able to pay it.
-
         // (3) INSUFFICIENT_ACCOUNT_FUNDS
-        vm.decrease_account_balance(sender_address, up_front_cost)
-            .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
+        deduct_caller(vm, gaslimit_price_product, sender_address)?;
 
         // (4) INSUFFICIENT_MAX_FEE_PER_GAS
-        if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
-            return Err(VMError::TxValidation(
-                TxValidationError::InsufficientMaxFeePerGas,
-            ));
-        }
+        validate_sufficient_max_fee_per_gas(vm)?;
 
         // (5) INITCODE_SIZE_EXCEEDED
         if vm.is_create() {
-            // [EIP-3860] - INITCODE_SIZE_EXCEEDED
-            if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
-                && vm.env.config.fork >= Fork::Shanghai
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::InitcodeSizeExceeded,
-                ));
-            }
+            validate_init_code_size(vm)?;
         }
 
         // (6) INTRINSIC_GAS_TOO_LOW
@@ -182,102 +88,20 @@ impl Hook for DefaultHook {
         }
 
         // (9) SENDER_NOT_EOA
-        if sender_account.has_code() && !has_delegation(&sender_account)? {
-            return Err(VMError::TxValidation(TxValidationError::SenderNotEOA));
-        }
+        validate_sender(&sender_account)?;
 
         // (10) GAS_ALLOWANCE_EXCEEDED
-        if vm.env.gas_limit > vm.env.block_gas_limit {
-            return Err(VMError::TxValidation(
-                TxValidationError::GasAllowanceExceeded,
-            ));
-        }
+        validate_gas_allowance(vm)?;
 
         // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
         if vm.env.tx_max_fee_per_blob_gas.is_some() {
-            // (11) TYPE_3_TX_PRE_FORK
-            if vm.env.config.fork < Fork::Cancun {
-                return Err(VMError::TxValidation(TxValidationError::Type3TxPreFork));
-            }
-
-            let blob_hashes = &vm.env.tx_blob_hashes;
-
-            // (12) TYPE_3_TX_ZERO_BLOBS
-            if blob_hashes.is_empty() {
-                return Err(VMError::TxValidation(TxValidationError::Type3TxZeroBlobs));
-            }
-
-            // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
-            for blob_hash in blob_hashes {
-                let blob_hash = blob_hash.as_bytes();
-                if let Some(first_byte) = blob_hash.first() {
-                    if !VALID_BLOB_PREFIXES.contains(first_byte) {
-                        return Err(VMError::TxValidation(
-                            TxValidationError::Type3TxInvalidBlobVersionedHash,
-                        ));
-                    }
-                }
-            }
-
-            // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
-            if blob_hashes.len()
-                > vm.env
-                    .config
-                    .blob_schedule
-                    .max
-                    .try_into()
-                    .map_err(|_| VMError::Internal(InternalError::ConversionError))?
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type3TxBlobCountExceeded,
-                ));
-            }
-
-            // (15) TYPE_3_TX_CONTRACT_CREATION
-            // NOTE: This will never happen, since the EIP-4844 tx (type 3) does not have a TxKind field
-            // only supports an Address which must be non-empty.
-            // If a type 3 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
-            // it won't reach this point.
-            // For more information, please check the following thread:
-            // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
-            if vm.is_create() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type3TxContractCreation,
-                ));
-            }
+            validate_4844_tx(vm)?;
         }
 
         // [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
         // Transaction is type 4 if authorization_list is Some
-        if let Some(auth_list) = &vm.authorization_list {
-            // (16) TYPE_4_TX_PRE_FORK
-            if vm.env.config.fork < Fork::Prague {
-                return Err(VMError::TxValidation(TxValidationError::Type4TxPreFork));
-            }
-
-            // (17) TYPE_4_TX_CONTRACT_CREATION
-            // From the EIP docs: a null destination is not valid.
-            // NOTE: This will never happen, since the EIP-7702 tx (type 4) does not have a TxKind field
-            // only supports an Address which must be non-empty.
-            // If a type 4 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
-            // it won't reach this point.
-            // For more information, please check the following thread:
-            // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
-            if vm.is_create() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type4TxContractCreation,
-                ));
-            }
-
-            // (18) TYPE_4_TX_LIST_EMPTY
-            // From the EIP docs: The transaction is considered invalid if the length of authorization_list is zero.
-            if auth_list.is_empty() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type4TxAuthorizationListIsEmpty,
-                ));
-            }
-
-            vm.eip7702_set_access_code()?;
+        if vm.authorization_list.is_some() {
+            validate_type_4_tx(vm)?;
         }
 
         if vm.is_create() {
@@ -307,92 +131,357 @@ impl Hook for DefaultHook {
         vm: &mut VM<'_>,
         report: &mut ExecutionReport,
     ) -> Result<(), VMError> {
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
-            }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            undo_value_transfer(vm)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
+        let gas_refunded: u64 = compute_gas_refunded(vm, report)?;
+        let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+        refund_sender(vm, report, gas_refunded, actual_gas_used)?;
 
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
+        pay_coinbase(vm, actual_gas_used)?;
 
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
-        } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
-
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
-
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
+        delete_self_destruct_accounts(vm)?;
 
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // d. Finally, return unspent gas to the sender.
-        let gas_to_return = vm
-            .env
-            .gas_limit
-            .checked_sub(actual_gas_used)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // In a create if Tx was reverted the account won't even exist by this point.
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
 
-        let wei_return_amount = vm
-            .env
-            .gas_price
-            .checked_mul(U256::from(gas_to_return))
-            .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+    vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)
+}
 
-        vm.increase_account_balance(sender_address, wei_return_amount)?;
+pub fn refund_sender(
+    vm: &mut VM<'_>,
+    report: &mut ExecutionReport,
+    refunded_gas: u64,
+    actual_gas_used: u64,
+) -> Result<(), VMError> {
+    // c. Update gas used and refunded in the Execution Report.
+    report.gas_used = actual_gas_used;
+    report.gas_refunded = refunded_gas;
+
+    // d. Finally, return unspent gas to the sender.
+    let gas_to_return = vm
+        .env
+        .gas_limit
+        .checked_sub(actual_gas_used)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+
+    let wei_return_amount = vm
+        .env
+        .gas_price
+        .checked_mul(U256::from(gas_to_return))
+        .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+
+    vm.increase_account_balance(vm.env.origin, wei_return_amount)
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
+pub fn compute_gas_refunded(vm: &mut VM<'_>, report: &ExecutionReport) -> Result<u64, VMError> {
+    let refund_quotient = if vm.env.config.fork < Fork::London {
+        MAX_REFUND_QUOTIENT_PRE_LONDON
+    } else {
+        MAX_REFUND_QUOTIENT
+    };
+    Ok(report.gas_refunded.min(
+        report
+            .gas_used
+            .checked_div(refund_quotient)
+            .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
+    ))
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
-
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
-
-        // 4. Destruct addresses in vm.selfdestruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
+pub fn compute_actual_gas_used(
+    vm: &mut VM<'_>,
+    refunded_gas: u64,
+    gas_used_without_refunds: u64,
+) -> Result<u64, VMError> {
+    let exec_gas_consumed = gas_used_without_refunds
+        .checked_sub(refunded_gas)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+
+    if vm.env.config.fork >= Fork::Prague {
+        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
+    } else {
+        Ok(exec_gas_consumed)
+    }
+}
+
+pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
+    let priority_fee_per_gas = vm
+        .env
+        .gas_price
+        .checked_sub(vm.env.base_fee_per_gas)
+        .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
+
+    let coinbase_fee = U256::from(gas_to_pay)
+        .checked_mul(priority_fee_per_gas)
+        .ok_or(VMError::BalanceOverflow)?;
+
+    vm.increase_account_balance(vm.env.coinbase, coinbase_fee)
+}
+
+// In Cancun the only addresses destroyed are contracts created in this transaction
+pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
+    for address in selfdestruct_set {
+        let account_to_remove = vm.get_account_mut(address)?;
+        *account_to_remove = Account::default();
+    }
+    Ok(())
+}
+
+pub fn validate_min_gas_limit(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // check for gas limit is grater or equal than the minimum required
+    let calldata = vm.current_call_frame()?.calldata.clone();
+    let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+
+    // calldata_cost = tokens_in_calldata * 4
+    let calldata_cost: u64 =
+        gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
+
+    // same as calculated in gas_used()
+    let tokens_in_calldata: u64 = calldata_cost
+        .checked_div(STANDARD_TOKEN_COST)
+        .ok_or(VMError::Internal(InternalError::DivisionError))?;
+
+    // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
+    let floor_cost_by_tokens = tokens_in_calldata
+        .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?
+        .checked_add(TX_BASE_COST)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+
+    let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+    if vm.current_call_frame()?.gas_limit < min_gas_limit {
+        return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+    }
+
+    Ok(())
+}
+
+pub fn validate_max_fee_per_blob_gas(
+    vm: &mut VM<'_>,
+    tx_max_fee_per_blob_gas: U256,
+) -> Result<(), VMError> {
+    if tx_max_fee_per_blob_gas
+        < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerBlobGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
+    if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
+        && vm.env.config.fork >= Fork::Shanghai
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InitcodeSizeExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_4844_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // (11) TYPE_3_TX_PRE_FORK
+    if vm.env.config.fork < Fork::Cancun {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxPreFork));
+    }
+
+    let blob_hashes = &vm.env.tx_blob_hashes;
+
+    // (12) TYPE_3_TX_ZERO_BLOBS
+    if blob_hashes.is_empty() {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxZeroBlobs));
+    }
+
+    // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
+    for blob_hash in blob_hashes {
+        let blob_hash = blob_hash.as_bytes();
+        if let Some(first_byte) = blob_hash.first() {
+            if !VALID_BLOB_PREFIXES.contains(first_byte) {
+                return Err(VMError::TxValidation(
+                    TxValidationError::Type3TxInvalidBlobVersionedHash,
+                ));
+            }
         }
+    }
 
-        Ok(())
+    // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
+    if blob_hashes.len()
+        > vm.env
+            .config
+            .blob_schedule
+            .max
+            .try_into()
+            .map_err(|_| VMError::Internal(InternalError::ConversionError))?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxBlobCountExceeded,
+        ));
+    }
+
+    // (15) TYPE_3_TX_CONTRACT_CREATION
+    // NOTE: This will never happen, since the EIP-4844 tx (type 3) does not have a TxKind field
+    // only supports an Address which must be non-empty.
+    // If a type 3 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
+    // it won't reach this point.
+    // For more information, please check the following thread:
+    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
+    if vm.is_create() {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxContractCreation,
+        ));
+    }
+
+    Ok(())
+}
+
+pub fn validate_type_4_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let Some(auth_list) = &vm.authorization_list else {
+        // vm.authorization_list should be Some at this point.
+        return Err(VMError::Internal(InternalError::UndefinedState(-1)));
+    };
+
+    // (16) TYPE_4_TX_PRE_FORK
+    if vm.env.config.fork < Fork::Prague {
+        return Err(VMError::TxValidation(TxValidationError::Type4TxPreFork));
+    }
+
+    // (17) TYPE_4_TX_CONTRACT_CREATION
+    // From the EIP docs: a null destination is not valid.
+    // NOTE: This will never happen, since the EIP-7702 tx (type 4) does not have a TxKind field
+    // only supports an Address which must be non-empty.
+    // If a type 4 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
+    // it won't reach this point.
+    // For more information, please check the following thread:
+    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
+    if vm.is_create() {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type4TxContractCreation,
+        ));
     }
+
+    // (18) TYPE_4_TX_LIST_EMPTY
+    // From the EIP docs: The transaction is considered invalid if the length of authorization_list is zero.
+    if auth_list.is_empty() {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type4TxAuthorizationListIsEmpty,
+        ));
+    }
+
+    vm.eip7702_set_access_code()
+}
+
+pub fn validate_sender(sender_account: &Account) -> Result<(), VMError> {
+    if sender_account.has_code() && !has_delegation(sender_account)? {
+        return Err(VMError::TxValidation(TxValidationError::SenderNotEOA));
+    }
+    Ok(())
+}
+
+pub fn validate_gas_allowance(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.gas_limit > vm.env.block_gas_limit {
+        return Err(VMError::TxValidation(
+            TxValidationError::GasAllowanceExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sender_balance(vm: &mut VM<'_>, sender_account: &Account) -> Result<(), VMError> {
+    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice + value + blob_gas_cost
+    let value = vm.current_call_frame()?.msg_value;
+
+    // blob gas cost = max fee per blob gas * blob gas used
+    // https://eips.ethereum.org/EIPS/eip-4844
+    let max_blob_gas_cost =
+        get_max_blob_gas_price(&vm.env.tx_blob_hashes, vm.env.tx_max_fee_per_blob_gas)?;
+
+    // For the transaction to be valid the sender account has to have a balance >= gas_price * gas_limit + value if tx is type 0 and 1
+    // balance >= max_fee_per_gas * gas_limit + value + blob_gas_cost if tx is type 2 or 3
+    let gas_fee_for_valid_tx = vm
+        .env
+        .tx_max_fee_per_gas
+        .unwrap_or(vm.env.gas_price)
+        .checked_mul(vm.env.gas_limit.into())
+        .ok_or(VMError::TxValidation(
+            TxValidationError::GasLimitPriceProductOverflow,
+        ))?;
+
+    let balance_for_valid_tx = gas_fee_for_valid_tx
+        .checked_add(value)
+        .ok_or(VMError::TxValidation(
+            TxValidationError::InsufficientAccountFunds,
+        ))?
+        .checked_add(max_blob_gas_cost)
+        .ok_or(VMError::TxValidation(
+            TxValidationError::InsufficientAccountFunds,
+        ))?;
+
+    if sender_account.info.balance < balance_for_valid_tx {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientAccountFunds,
+        ));
+    }
+
+    Ok(())
+}
+
+pub fn deduct_caller(
+    vm: &mut VM<'_>,
+    gas_limit_price_product: U256,
+    sender_address: Address,
+) -> Result<(), VMError> {
+    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice + value + blob_gas_cost
+    let value = vm.current_call_frame()?.msg_value;
+
+    let blob_gas_cost = get_blob_gas_price(
+        &vm.env.tx_blob_hashes,
+        vm.env.block_excess_blob_gas,
+        &vm.env.config,
+    )?;
+
+    // The real cost to deduct is calculated as effective_gas_price * gas_limit + value + blob_gas_cost
+    let up_front_cost = gas_limit_price_product
+        .checked_add(value)
+        .ok_or(VMError::TxValidation(
+            TxValidationError::InsufficientAccountFunds,
+        ))?
+        .checked_add(blob_gas_cost)
+        .ok_or(VMError::TxValidation(
+            TxValidationError::InsufficientAccountFunds,
+        ))?;
+    // There is no error specified for overflow in up_front_cost
+    // in ef_tests. We went for "InsufficientAccountFunds" simply
+    // because if the upfront cost is bigger than U256, then,
+    // technically, the sender will not be able to pay it.
+
+    vm.decrease_account_balance(sender_address, up_front_cost)
+        .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
+
+    Ok(())
 }
diff --git a/crates/vm/levm/src/hooks/l2_hook.rs b/crates/vm/levm/src/hooks/l2_hook.rs
index 24d5f93b80d..5f9107a616c 100644
--- a/crates/vm/levm/src/hooks/l2_hook.rs
+++ b/crates/vm/levm/src/hooks/l2_hook.rs
@@ -1,88 +1,74 @@
-use std::cmp::max;
-
-use ethrex_common::{
-    types::{Account, Fork},
-    Address, U256,
-};
-
 use crate::{
-    constants::{INIT_CODE_MAX_SIZE, TX_BASE_COST, VALID_BLOB_PREFIXES},
-    errors::{InternalError, TxValidationError, VMError},
-    gas_cost::{self, STANDARD_TOKEN_COST, TOTAL_COST_FLOOR_PER_TOKEN},
-    utils::{get_base_fee_per_blob_gas, get_valid_jump_destinations},
+    errors::{ExecutionReport, InternalError, TxValidationError, VMError},
+    hooks::{default_hook, hook::Hook},
+    utils::get_valid_jump_destinations,
+    vm::VM,
 };
 
-use super::{
-    default_hook::{MAX_REFUND_QUOTIENT, MAX_REFUND_QUOTIENT_PRE_LONDON},
-    hook::Hook,
-};
+use ethrex_common::{types::Fork, Address, U256};
 
 pub struct L2Hook {
-    pub recipient: Address,
+    pub recipient: Option<Address>,
 }
 
 impl Hook for L2Hook {
     fn prepare_execution(&self, vm: &mut crate::vm::VM<'_>) -> Result<(), crate::errors::VMError> {
-        vm.increase_account_balance(self.recipient, vm.current_call_frame()?.msg_value)?;
+        if vm.env.is_privileged {
+            let Some(recipient) = self.recipient else {
+                return Err(VMError::Internal(
+                    InternalError::RecipientNotFoundForPrivilegeTransaction,
+                ));
+            };
+            vm.increase_account_balance(recipient, vm.current_call_frame()?.msg_value)?;
+            vm.current_call_frame_mut()?.msg_value = U256::from(0);
+        }
 
-        vm.current_call_frame_mut()?.msg_value = U256::from(0);
+        let sender_address = vm.env.origin;
+        let sender_account = vm.db.get_account(sender_address)?;
 
         if vm.env.config.fork >= Fork::Prague {
-            // check for gas limit is grater or equal than the minimum required
-            let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+            default_hook::validate_min_gas_limit(vm)?;
+        }
 
-            // calldata_cost = tokens_in_calldata * 4
-            let calldata_cost: u64 =
-                gas_cost::tx_calldata(&vm.current_call_frame()?.calldata, vm.env.config.fork)
-                    .map_err(VMError::OutOfGas)?;
+        if !vm.env.is_privileged {
+            // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
+            let gaslimit_price_product = vm
+                .env
+                .gas_price
+                .checked_mul(vm.env.gas_limit.into())
+                .ok_or(VMError::TxValidation(
+                    TxValidationError::GasLimitPriceProductOverflow,
+                ))?;
 
-            // same as calculated in gas_used()
-            let tokens_in_calldata: u64 = calldata_cost
-                .checked_div(STANDARD_TOKEN_COST)
-                .ok_or(VMError::Internal(InternalError::DivisionError))?;
+            default_hook::validate_sender_balance(vm, &sender_account)?;
 
-            // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
-            let floor_cost_by_tokens = tokens_in_calldata
-                .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
-                .ok_or(VMError::Internal(InternalError::GasOverflow))?
-                .checked_add(TX_BASE_COST)
-                .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+            // (3) INSUFFICIENT_ACCOUNT_FUNDS
+            default_hook::deduct_caller(vm, gaslimit_price_product, sender_address)?;
 
-            let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+            // (7) NONCE_IS_MAX
+            vm.increment_account_nonce(sender_address)
+                .map_err(|_| VMError::TxValidation(TxValidationError::NonceIsMax))?;
 
-            if vm.current_call_frame()?.gas_limit < min_gas_limit {
-                return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+            // check for nonce mismatch
+            if sender_account.info.nonce != vm.env.tx_nonce {
+                return Err(VMError::TxValidation(TxValidationError::NonceMismatch));
             }
+
+            // (9) SENDER_NOT_EOA
+            default_hook::validate_sender(&sender_account)?;
         }
 
         // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
         if let Some(tx_max_fee_per_blob_gas) = vm.env.tx_max_fee_per_blob_gas {
-            if tx_max_fee_per_blob_gas
-                < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::InsufficientMaxFeePerBlobGas,
-                ));
-            }
+            default_hook::validate_max_fee_per_blob_gas(vm, tx_max_fee_per_blob_gas)?;
         }
 
         // (4) INSUFFICIENT_MAX_FEE_PER_GAS
-        if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
-            return Err(VMError::TxValidation(
-                TxValidationError::InsufficientMaxFeePerGas,
-            ));
-        }
+        default_hook::validate_sufficient_max_fee_per_gas(vm)?;
 
         // (5) INITCODE_SIZE_EXCEEDED
         if vm.is_create() {
-            // [EIP-3860] - INITCODE_SIZE_EXCEEDED
-            if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
-                && vm.env.config.fork >= Fork::Shanghai
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::InitcodeSizeExceeded,
-                ));
-            }
+            default_hook::validate_init_code_size(vm)?;
         }
 
         // (6) INTRINSIC_GAS_TOO_LOW
@@ -101,85 +87,17 @@ impl Hook for L2Hook {
         }
 
         // (10) GAS_ALLOWANCE_EXCEEDED
-        if vm.env.gas_limit > vm.env.block_gas_limit {
-            return Err(VMError::TxValidation(
-                TxValidationError::GasAllowanceExceeded,
-            ));
-        }
+        default_hook::validate_gas_allowance(vm)?;
 
         // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
         if vm.env.tx_max_fee_per_blob_gas.is_some() {
-            // (11) TYPE_3_TX_PRE_FORK
-            if vm.env.config.fork < Fork::Cancun {
-                return Err(VMError::TxValidation(TxValidationError::Type3TxPreFork));
-            }
-
-            let blob_hashes = &vm.env.tx_blob_hashes;
-
-            // (12) TYPE_3_TX_ZERO_BLOBS
-            if blob_hashes.is_empty() {
-                return Err(VMError::TxValidation(TxValidationError::Type3TxZeroBlobs));
-            }
-
-            // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
-            for blob_hash in blob_hashes {
-                let blob_hash = blob_hash.as_bytes();
-                if let Some(first_byte) = blob_hash.first() {
-                    if !VALID_BLOB_PREFIXES.contains(first_byte) {
-                        return Err(VMError::TxValidation(
-                            TxValidationError::Type3TxInvalidBlobVersionedHash,
-                        ));
-                    }
-                }
-            }
-
-            // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
-            if blob_hashes.len()
-                > vm.env
-                    .config
-                    .blob_schedule
-                    .max
-                    .try_into()
-                    .map_err(|_| VMError::Internal(InternalError::ConversionError))?
-            {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type3TxBlobCountExceeded,
-                ));
-            }
-
-            // (15) TYPE_3_TX_CONTRACT_CREATION
-            if vm.is_create() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type3TxContractCreation,
-                ));
-            }
+            default_hook::validate_4844_tx(vm)?;
         }
 
         // [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
         // Transaction is type 4 if authorization_list is Some
-        if let Some(auth_list) = &vm.authorization_list {
-            // (16) TYPE_4_TX_PRE_FORK
-            if vm.env.config.fork < Fork::Prague {
-                return Err(VMError::TxValidation(TxValidationError::Type4TxPreFork));
-            }
-
-            // (17) TYPE_4_TX_CONTRACT_CREATION
-            // From the EIP docs: a null destination is not valid.
-            if vm.is_create() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type4TxContractCreation,
-                ));
-            }
-
-            // (18) TYPE_4_TX_LIST_EMPTY
-            // From the EIP docs: The transaction is considered invalid if the length of authorization_list is zero.
-            if auth_list.is_empty() {
-                return Err(VMError::TxValidation(
-                    TxValidationError::Type4TxAuthorizationListIsEmpty,
-                ));
-            }
-
-            vm.eip7702_set_access_code()?;
+        if vm.authorization_list.is_some() {
+            default_hook::validate_type_4_tx(vm)?;
         }
 
         if vm.is_create() {
@@ -188,6 +106,13 @@ impl Hook for L2Hook {
                 std::mem::take(&mut vm.current_call_frame_mut()?.calldata);
             vm.current_call_frame_mut()?.valid_jump_destinations =
                 get_valid_jump_destinations(&vm.current_call_frame()?.bytecode).unwrap_or_default();
+        } else if !vm.env.is_privileged {
+            // Transfer value to receiver
+            // It's here to avoid storing the "to" address in the cache before eip7702_set_access_code() step 7).
+            vm.increase_account_balance(
+                vm.current_call_frame()?.to,
+                vm.current_call_frame()?.msg_value,
+            )?;
         }
         Ok(())
     }
@@ -197,78 +122,58 @@ impl Hook for L2Hook {
         vm: &mut crate::vm::VM<'_>,
         report: &mut crate::errors::ExecutionReport,
     ) -> Result<(), crate::errors::VMError> {
-        // POST-EXECUTION Changes
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
+            if vm.env.is_privileged {
+                undo_value_transfer(vm)?;
+            } else {
+                default_hook::undo_value_transfer(vm)?;
             }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
-
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
-
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
+        if vm.env.is_privileged {
+            let gas_to_pay_coinbase = compute_coinbase_fee(vm, report)?;
+            default_hook::pay_coinbase(vm, gas_to_pay_coinbase)?;
         } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
+            let gas_refunded = default_hook::compute_gas_refunded(vm, report)?;
+            let actual_gas_used =
+                default_hook::compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+            default_hook::refund_sender(vm, report, gas_refunded, actual_gas_used)?;
+            default_hook::pay_coinbase(vm, actual_gas_used)?;
+        }
 
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+        default_hook::delete_self_destruct_accounts(vm)?;
 
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
-
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
+    Ok(())
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
+pub fn compute_coinbase_fee(vm: &mut VM<'_>, report: &mut ExecutionReport) -> Result<u64, VMError> {
+    let mut gas_refunded = default_hook::compute_gas_refunded(vm, report)?;
+    let mut gas_consumed = report.gas_used;
 
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
+    report.gas_refunded = gas_refunded;
 
-        // 4. Destruct addresses in vm.estruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+    if vm.env.config.fork >= Fork::Prague {
+        let floor_gas_price = vm.get_min_gas_used()?;
+        let execution_gas_used = gas_consumed.saturating_sub(gas_refunded);
+        if floor_gas_price > execution_gas_used {
+            gas_consumed = floor_gas_price;
+            gas_refunded = 0;
         }
-
-        Ok(())
     }
+
+    gas_consumed
+        .checked_sub(gas_refunded)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(2)))
 }
diff --git a/crates/vm/levm/src/hooks/mod.rs b/crates/vm/levm/src/hooks/mod.rs
index d62610d25d5..b27d17e8e99 100644
--- a/crates/vm/levm/src/hooks/mod.rs
+++ b/crates/vm/levm/src/hooks/mod.rs
@@ -1,3 +1,8 @@
 pub mod default_hook;
 pub mod hook;
+#[cfg(feature = "l2")]
 pub mod l2_hook;
+
+pub use default_hook::DefaultHook;
+#[cfg(feature = "l2")]
+pub use l2_hook::L2Hook;
diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs
index 4ff55f68958..c62eaa2f9d3 100644
--- a/crates/vm/levm/src/vm.rs
+++ b/crates/vm/levm/src/vm.rs
@@ -4,7 +4,7 @@ use crate::{
     db::{cache, gen_db::GeneralizedDatabase},
     environment::Environment,
     errors::{ExecutionReport, InternalError, OpcodeResult, TxResult, VMError},
-    hooks::{default_hook::DefaultHook, hook::Hook, l2_hook::L2Hook},
+    hooks::hook::Hook,
     precompiles::{
         execute_precompile, is_precompile, SIZE_PRECOMPILES_CANCUN, SIZE_PRECOMPILES_PRAGUE,
         SIZE_PRECOMPILES_PRE_CANCUN,
@@ -16,7 +16,8 @@ use bytes::Bytes;
 use ethrex_common::{
     types::{
         tx_fields::{AccessList, AuthorizationList},
-        BlockHeader, ChainConfig, Fork, ForkBlobSchedule, Transaction, TxKind,
+        BlockHeader, ChainConfig, Fork, ForkBlobSchedule, PrivilegedL2Transaction, Transaction,
+        TxKind,
     },
     Address, H256, U256,
 };
@@ -25,6 +26,12 @@ use std::{
     fmt::Debug,
     sync::Arc,
 };
+
+#[cfg(not(feature = "l2"))]
+use crate::hooks::DefaultHook;
+#[cfg(feature = "l2")]
+use crate::hooks::L2Hook;
+
 pub type Storage = HashMap<U256, H256>;
 
 #[derive(Debug, Clone, Default)]
@@ -218,11 +225,20 @@ impl<'a> VM<'a> {
             default_touched_accounts.insert(Address::from_low_u64_be(i));
         }
 
-        let hooks: Vec<Arc<dyn Hook>> = match tx {
-            Transaction::PrivilegedL2Transaction(privileged_tx) => vec![Arc::new(L2Hook {
-                recipient: privileged_tx.recipient,
-            })],
-            _ => vec![Arc::new(DefaultHook)],
+        #[cfg(not(feature = "l2"))]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(DefaultHook)];
+        #[cfg(feature = "l2")]
+        let hooks: Vec<Arc<dyn Hook>> = {
+            let recipient = if let Transaction::PrivilegedL2Transaction(PrivilegedL2Transaction {
+                recipient,
+                ..
+            }) = tx
+            {
+                Some(*recipient)
+            } else {
+                None
+            };
+            vec![Arc::new(L2Hook { recipient })]
         };
 
         let mut substate = Substate {


Reviews:
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:18:30Z
user: login: ilitteri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T16:37:04Z
user: login: ilitteri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T16:37:35Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T17:05:57Z
user: login: tomip01
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T17:06:10Z
user: login: ManuelBilbao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T18:22:31Z
user: login: ilitteri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T20:22:17Z
user: login: ilitteri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T20:22:29Z
user: login: ilitteri
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T20:22:32Z
user: login: jrchatruc
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T22:06:41Z

Review comments:
diff_hunk: @@ -246,12 +252,12 @@ impl<'a> VM<'a> {
             }
         }
 
-        let hooks: Vec<Arc<dyn Hook>> = match tx {
-            Transaction::PrivilegedL2Transaction(privileged_tx) => vec![Arc::new(L2Hook {
-                recipient: privileged_tx.recipient,
-            })],
-            _ => vec![Arc::new(DefaultHook)],
-        };
+        #[cfg(not(feature = "l2"))]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(DefaultHook)];
+        #[cfg(feature = "l2")]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(L2Hook {
+            recipient: env.origin,
path: crates/vm/levm/src/vm.rs
user: login: tomip01
body: The `env.origin` is the tx sender (see [here](https://github.com/lambdaclass/ethrex/blob/main/crates/vm/backends/levm/mod.rs#L113)) and it may not be the same as the recipient, I think we should keep the `tx.recipient`
created_at: 2025-04-24T13:17:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,12 +252,12 @@ impl<'a> VM<'a> {
             }
         }
 
-        let hooks: Vec<Arc<dyn Hook>> = match tx {
-            Transaction::PrivilegedL2Transaction(privileged_tx) => vec![Arc::new(L2Hook {
-                recipient: privileged_tx.recipient,
-            })],
-            _ => vec![Arc::new(DefaultHook)],
-        };
+        #[cfg(not(feature = "l2"))]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(DefaultHook)];
+        #[cfg(feature = "l2")]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(L2Hook {
+            recipient: env.origin,
path: crates/vm/levm/src/vm.rs
user: login: ilitteri
body: You are right, I'll file an issue to add this behaviour to the integration test.
created_at: 2025-04-24T16:37:04Z
author_association: COLLABORATOR
diff_hunk: @@ -246,12 +252,12 @@ impl<'a> VM<'a> {
             }
         }
 
-        let hooks: Vec<Arc<dyn Hook>> = match tx {
-            Transaction::PrivilegedL2Transaction(privileged_tx) => vec![Arc::new(L2Hook {
-                recipient: privileged_tx.recipient,
-            })],
-            _ => vec![Arc::new(DefaultHook)],
-        };
+        #[cfg(not(feature = "l2"))]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(DefaultHook)];
+        #[cfg(feature = "l2")]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(L2Hook {
+            recipient: env.origin,
path: crates/vm/levm/src/vm.rs
user: login: ilitteri
body: Fixed in [fe211c6](https://github.com/lambdaclass/ethrex/pull/2508/commits/fe211c6c3a1431ab63969a23bbb650e33d7e5ce9).
created_at: 2025-04-24T16:37:35Z
author_association: COLLABORATOR
diff_hunk: @@ -246,12 +252,12 @@ impl<'a> VM<'a> {
             }
         }
 
-        let hooks: Vec<Arc<dyn Hook>> = match tx {
-            Transaction::PrivilegedL2Transaction(privileged_tx) => vec![Arc::new(L2Hook {
-                recipient: privileged_tx.recipient,
-            })],
-            _ => vec![Arc::new(DefaultHook)],
-        };
+        #[cfg(not(feature = "l2"))]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(DefaultHook)];
+        #[cfg(feature = "l2")]
+        let hooks: Vec<Arc<dyn Hook>> = vec![Arc::new(L2Hook {
+            recipient: env.origin,
path: crates/vm/levm/src/vm.rs
user: login: tomip01
body: Nice
created_at: 2025-04-24T17:05:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -188,6 +188,8 @@ pub fn prepare_vm_for_tx<'a>(
             tx_nonce: test_tx.nonce,
             block_gas_limit: test.env.current_gas_limit,
             transient_storage: HashMap::new(),
+            #[cfg(feature = "l2")]
+            is_privilege: false,
path: cmd/ef_tests/state/runner/levm_runner.rs
user: login: ManuelBilbao
body: Should this be `is_privileged`?
created_at: 2025-04-30T17:54:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,92 +131,357 @@ impl Hook for DefaultHook {
         vm: &mut VM<'_>,
         report: &mut ExecutionReport,
     ) -> Result<(), VMError> {
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
-            }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            undo_value_transfer(vm)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
+        let gas_refunded: u64 = compute_gas_refunded(vm, report)?;
+        let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+        refund_sender(vm, report, gas_refunded, actual_gas_used)?;
 
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
+        pay_coinbase(vm, actual_gas_used)?;
 
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
-        } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
-
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
-
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
+        delete_self_destruct_accounts(vm)?;
 
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // d. Finally, return unspent gas to the sender.
-        let gas_to_return = vm
-            .env
-            .gas_limit
-            .checked_sub(actual_gas_used)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // In a create if Tx was reverted the account won't even exist by this point.
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
 
-        let wei_return_amount = vm
-            .env
-            .gas_price
-            .checked_mul(U256::from(gas_to_return))
-            .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+    vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)
+}
 
-        vm.increase_account_balance(sender_address, wei_return_amount)?;
+pub fn refund_sender(
+    vm: &mut VM<'_>,
+    report: &mut ExecutionReport,
+    refunded_gas: u64,
+    actual_gas_used: u64,
+) -> Result<(), VMError> {
+    // c. Update gas used and refunded in the Execution Report.
+    report.gas_used = actual_gas_used;
+    report.gas_refunded = refunded_gas;
+
+    // d. Finally, return unspent gas to the sender.
+    let gas_to_return = vm
+        .env
+        .gas_limit
+        .checked_sub(actual_gas_used)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+
+    let wei_return_amount = vm
+        .env
+        .gas_price
+        .checked_mul(U256::from(gas_to_return))
+        .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+
+    vm.increase_account_balance(vm.env.origin, wei_return_amount)
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
+pub fn compute_gas_refunded(vm: &mut VM<'_>, report: &ExecutionReport) -> Result<u64, VMError> {
+    let refund_quotient = if vm.env.config.fork < Fork::London {
+        MAX_REFUND_QUOTIENT_PRE_LONDON
+    } else {
+        MAX_REFUND_QUOTIENT
+    };
+    Ok(report.gas_refunded.min(
+        report
+            .gas_used
+            .checked_div(refund_quotient)
+            .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
+    ))
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
-
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
-
-        // 4. Destruct addresses in vm.selfdestruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
+pub fn compute_actual_gas_used(
+    vm: &mut VM<'_>,
+    refunded_gas: u64,
+    gas_used_without_refunds: u64,
+) -> Result<u64, VMError> {
+    let exec_gas_consumed = gas_used_without_refunds
+        .checked_sub(refunded_gas)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+
+    if vm.env.config.fork >= Fork::Prague {
+        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
+    } else {
+        Ok(exec_gas_consumed)
+    }
+}
+
+pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
+    let priority_fee_per_gas = vm
+        .env
+        .gas_price
+        .checked_sub(vm.env.base_fee_per_gas)
+        .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
+
+    let coinbase_fee = U256::from(gas_to_pay)
+        .checked_mul(priority_fee_per_gas)
+        .ok_or(VMError::BalanceOverflow)?;
+
+    vm.increase_account_balance(vm.env.coinbase, coinbase_fee)
+}
+
+// In Cancun the only addresses destroyed are contracts created in this transaction
+pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
+    for address in selfdestruct_set {
+        let account_to_remove = vm.get_account_mut(address)?;
+        *account_to_remove = Account::default();
+    }
+    Ok(())
+}
+
+pub fn validate_min_gas_limit(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // check for gas limit is grater or equal than the minimum required
+    let calldata = vm.current_call_frame()?.calldata.clone();
+    let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+
+    // calldata_cost = tokens_in_calldata * 4
+    let calldata_cost: u64 =
+        gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
+
+    // same as calculated in gas_used()
+    let tokens_in_calldata: u64 = calldata_cost
+        .checked_div(STANDARD_TOKEN_COST)
+        .ok_or(VMError::Internal(InternalError::DivisionError))?;
+
+    // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
+    let floor_cost_by_tokens = tokens_in_calldata
+        .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?
+        .checked_add(TX_BASE_COST)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+
+    let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+    if vm.current_call_frame()?.gas_limit < min_gas_limit {
+        return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+    }
+
+    Ok(())
+}
+
+pub fn validate_max_fee_per_blob_gas(
+    vm: &mut VM<'_>,
+    tx_max_fee_per_blob_gas: U256,
+) -> Result<(), VMError> {
+    if tx_max_fee_per_blob_gas
+        < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerBlobGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
+    if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
+        && vm.env.config.fork >= Fork::Shanghai
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InitcodeSizeExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_type_3_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
path: crates/vm/levm/src/hooks/default_hook.rs
user: login: ManuelBilbao
body: Something like `validate_4844_tx` might be easier to read
created_at: 2025-04-30T18:00:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,92 +131,357 @@ impl Hook for DefaultHook {
         vm: &mut VM<'_>,
         report: &mut ExecutionReport,
     ) -> Result<(), VMError> {
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
-            }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            undo_value_transfer(vm)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
+        let gas_refunded: u64 = compute_gas_refunded(vm, report)?;
+        let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+        refund_sender(vm, report, gas_refunded, actual_gas_used)?;
 
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
+        pay_coinbase(vm, actual_gas_used)?;
 
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
-        } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
-
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
-
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
+        delete_self_destruct_accounts(vm)?;
 
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // d. Finally, return unspent gas to the sender.
-        let gas_to_return = vm
-            .env
-            .gas_limit
-            .checked_sub(actual_gas_used)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // In a create if Tx was reverted the account won't even exist by this point.
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
 
-        let wei_return_amount = vm
-            .env
-            .gas_price
-            .checked_mul(U256::from(gas_to_return))
-            .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+    vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)
+}
 
-        vm.increase_account_balance(sender_address, wei_return_amount)?;
+pub fn refund_sender(
+    vm: &mut VM<'_>,
+    report: &mut ExecutionReport,
+    refunded_gas: u64,
+    actual_gas_used: u64,
+) -> Result<(), VMError> {
+    // c. Update gas used and refunded in the Execution Report.
+    report.gas_used = actual_gas_used;
+    report.gas_refunded = refunded_gas;
+
+    // d. Finally, return unspent gas to the sender.
+    let gas_to_return = vm
+        .env
+        .gas_limit
+        .checked_sub(actual_gas_used)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+
+    let wei_return_amount = vm
+        .env
+        .gas_price
+        .checked_mul(U256::from(gas_to_return))
+        .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+
+    vm.increase_account_balance(vm.env.origin, wei_return_amount)
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
+pub fn compute_gas_refunded(vm: &mut VM<'_>, report: &ExecutionReport) -> Result<u64, VMError> {
+    let refund_quotient = if vm.env.config.fork < Fork::London {
+        MAX_REFUND_QUOTIENT_PRE_LONDON
+    } else {
+        MAX_REFUND_QUOTIENT
+    };
+    Ok(report.gas_refunded.min(
+        report
+            .gas_used
+            .checked_div(refund_quotient)
+            .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
+    ))
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
-
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
-
-        // 4. Destruct addresses in vm.selfdestruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
+pub fn compute_actual_gas_used(
+    vm: &mut VM<'_>,
+    refunded_gas: u64,
+    gas_used_without_refunds: u64,
+) -> Result<u64, VMError> {
+    let exec_gas_consumed = gas_used_without_refunds
+        .checked_sub(refunded_gas)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+
+    if vm.env.config.fork >= Fork::Prague {
+        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
+    } else {
+        Ok(exec_gas_consumed)
+    }
+}
+
+pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
+    let priority_fee_per_gas = vm
+        .env
+        .gas_price
+        .checked_sub(vm.env.base_fee_per_gas)
+        .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
+
+    let coinbase_fee = U256::from(gas_to_pay)
+        .checked_mul(priority_fee_per_gas)
+        .ok_or(VMError::BalanceOverflow)?;
+
+    vm.increase_account_balance(vm.env.coinbase, coinbase_fee)
+}
+
+// In Cancun the only addresses destroyed are contracts created in this transaction
+pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
+    for address in selfdestruct_set {
+        let account_to_remove = vm.get_account_mut(address)?;
+        *account_to_remove = Account::default();
+    }
+    Ok(())
+}
+
+pub fn validate_min_gas_limit(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // check for gas limit is grater or equal than the minimum required
+    let calldata = vm.current_call_frame()?.calldata.clone();
+    let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+
+    // calldata_cost = tokens_in_calldata * 4
+    let calldata_cost: u64 =
+        gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
+
+    // same as calculated in gas_used()
+    let tokens_in_calldata: u64 = calldata_cost
+        .checked_div(STANDARD_TOKEN_COST)
+        .ok_or(VMError::Internal(InternalError::DivisionError))?;
+
+    // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
+    let floor_cost_by_tokens = tokens_in_calldata
+        .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?
+        .checked_add(TX_BASE_COST)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+
+    let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+    if vm.current_call_frame()?.gas_limit < min_gas_limit {
+        return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+    }
+
+    Ok(())
+}
+
+pub fn validate_max_fee_per_blob_gas(
+    vm: &mut VM<'_>,
+    tx_max_fee_per_blob_gas: U256,
+) -> Result<(), VMError> {
+    if tx_max_fee_per_blob_gas
+        < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerBlobGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
+    if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
+        && vm.env.config.fork >= Fork::Shanghai
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InitcodeSizeExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_type_3_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // (11) TYPE_3_TX_PRE_FORK
+    if vm.env.config.fork < Fork::Cancun {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxPreFork));
+    }
+
+    let blob_hashes = &vm.env.tx_blob_hashes;
+
+    // (12) TYPE_3_TX_ZERO_BLOBS
+    if blob_hashes.is_empty() {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxZeroBlobs));
+    }
+
+    // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
+    for blob_hash in blob_hashes {
+        let blob_hash = blob_hash.as_bytes();
+        if let Some(first_byte) = blob_hash.first() {
+            if !VALID_BLOB_PREFIXES.contains(first_byte) {
+                return Err(VMError::TxValidation(
+                    TxValidationError::Type3TxInvalidBlobVersionedHash,
+                ));
+            }
         }
+    }
 
-        Ok(())
+    // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
+    if blob_hashes.len()
+        > vm.env
+            .config
+            .blob_schedule
+            .max
+            .try_into()
+            .map_err(|_| VMError::Internal(InternalError::ConversionError))?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxBlobCountExceeded,
+        ));
+    }
+
+    // (15) TYPE_3_TX_CONTRACT_CREATION
+    // NOTE: This will never happen, since the EIP-4844 tx (type 3) does not have a TxKind field
+    // only supports an Address which must be non-empty.
+    // If a type 3 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
+    // it won't reach this point.
+    // For more information, please check the following thread:
+    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
+    if vm.is_create() {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxContractCreation,
+        ));
+    }
+
+    Ok(())
+}
+
+pub fn validate_type_4_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
path: crates/vm/levm/src/hooks/default_hook.rs
user: login: ManuelBilbao
body: Same here
created_at: 2025-04-30T18:00:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -188,6 +188,8 @@ pub fn prepare_vm_for_tx<'a>(
             tx_nonce: test_tx.nonce,
             block_gas_limit: test.env.current_gas_limit,
             transient_storage: HashMap::new(),
+            #[cfg(feature = "l2")]
+            is_privilege: false,
path: cmd/ef_tests/state/runner/levm_runner.rs
user: login: ilitteri
body: Done in [e60b7ee](https://github.com/lambdaclass/ethrex/pull/2508/commits/e60b7eec9932dda89308158a563ef2fd30a4e089).
created_at: 2025-04-30T20:22:17Z
author_association: COLLABORATOR
diff_hunk: @@ -307,92 +131,357 @@ impl Hook for DefaultHook {
         vm: &mut VM<'_>,
         report: &mut ExecutionReport,
     ) -> Result<(), VMError> {
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
-            }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            undo_value_transfer(vm)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
+        let gas_refunded: u64 = compute_gas_refunded(vm, report)?;
+        let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+        refund_sender(vm, report, gas_refunded, actual_gas_used)?;
 
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
+        pay_coinbase(vm, actual_gas_used)?;
 
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
-        } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
-
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
-
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
+        delete_self_destruct_accounts(vm)?;
 
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // d. Finally, return unspent gas to the sender.
-        let gas_to_return = vm
-            .env
-            .gas_limit
-            .checked_sub(actual_gas_used)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // In a create if Tx was reverted the account won't even exist by this point.
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
 
-        let wei_return_amount = vm
-            .env
-            .gas_price
-            .checked_mul(U256::from(gas_to_return))
-            .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+    vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)
+}
 
-        vm.increase_account_balance(sender_address, wei_return_amount)?;
+pub fn refund_sender(
+    vm: &mut VM<'_>,
+    report: &mut ExecutionReport,
+    refunded_gas: u64,
+    actual_gas_used: u64,
+) -> Result<(), VMError> {
+    // c. Update gas used and refunded in the Execution Report.
+    report.gas_used = actual_gas_used;
+    report.gas_refunded = refunded_gas;
+
+    // d. Finally, return unspent gas to the sender.
+    let gas_to_return = vm
+        .env
+        .gas_limit
+        .checked_sub(actual_gas_used)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+
+    let wei_return_amount = vm
+        .env
+        .gas_price
+        .checked_mul(U256::from(gas_to_return))
+        .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+
+    vm.increase_account_balance(vm.env.origin, wei_return_amount)
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
+pub fn compute_gas_refunded(vm: &mut VM<'_>, report: &ExecutionReport) -> Result<u64, VMError> {
+    let refund_quotient = if vm.env.config.fork < Fork::London {
+        MAX_REFUND_QUOTIENT_PRE_LONDON
+    } else {
+        MAX_REFUND_QUOTIENT
+    };
+    Ok(report.gas_refunded.min(
+        report
+            .gas_used
+            .checked_div(refund_quotient)
+            .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
+    ))
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
-
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
-
-        // 4. Destruct addresses in vm.selfdestruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
+pub fn compute_actual_gas_used(
+    vm: &mut VM<'_>,
+    refunded_gas: u64,
+    gas_used_without_refunds: u64,
+) -> Result<u64, VMError> {
+    let exec_gas_consumed = gas_used_without_refunds
+        .checked_sub(refunded_gas)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+
+    if vm.env.config.fork >= Fork::Prague {
+        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
+    } else {
+        Ok(exec_gas_consumed)
+    }
+}
+
+pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
+    let priority_fee_per_gas = vm
+        .env
+        .gas_price
+        .checked_sub(vm.env.base_fee_per_gas)
+        .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
+
+    let coinbase_fee = U256::from(gas_to_pay)
+        .checked_mul(priority_fee_per_gas)
+        .ok_or(VMError::BalanceOverflow)?;
+
+    vm.increase_account_balance(vm.env.coinbase, coinbase_fee)
+}
+
+// In Cancun the only addresses destroyed are contracts created in this transaction
+pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
+    for address in selfdestruct_set {
+        let account_to_remove = vm.get_account_mut(address)?;
+        *account_to_remove = Account::default();
+    }
+    Ok(())
+}
+
+pub fn validate_min_gas_limit(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // check for gas limit is grater or equal than the minimum required
+    let calldata = vm.current_call_frame()?.calldata.clone();
+    let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+
+    // calldata_cost = tokens_in_calldata * 4
+    let calldata_cost: u64 =
+        gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
+
+    // same as calculated in gas_used()
+    let tokens_in_calldata: u64 = calldata_cost
+        .checked_div(STANDARD_TOKEN_COST)
+        .ok_or(VMError::Internal(InternalError::DivisionError))?;
+
+    // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
+    let floor_cost_by_tokens = tokens_in_calldata
+        .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?
+        .checked_add(TX_BASE_COST)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+
+    let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+    if vm.current_call_frame()?.gas_limit < min_gas_limit {
+        return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+    }
+
+    Ok(())
+}
+
+pub fn validate_max_fee_per_blob_gas(
+    vm: &mut VM<'_>,
+    tx_max_fee_per_blob_gas: U256,
+) -> Result<(), VMError> {
+    if tx_max_fee_per_blob_gas
+        < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerBlobGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
+    if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
+        && vm.env.config.fork >= Fork::Shanghai
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InitcodeSizeExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_type_3_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
path: crates/vm/levm/src/hooks/default_hook.rs
user: login: ilitteri
body: Done in [8941381](https://github.com/lambdaclass/ethrex/pull/2508/commits/89413816760026a9b2e76800fd7e73150a47dfe2).
created_at: 2025-04-30T20:22:29Z
author_association: COLLABORATOR
diff_hunk: @@ -307,92 +131,357 @@ impl Hook for DefaultHook {
         vm: &mut VM<'_>,
         report: &mut ExecutionReport,
     ) -> Result<(), VMError> {
-        let sender_address = vm.current_call_frame()?.msg_sender;
-
-        // 1. Undo value transfer if Tx reverted
         if !report.is_success() {
-            // In a create if Tx was reverted the account won't even exist by this point.
-            if !vm.is_create() {
-                vm.decrease_account_balance(
-                    vm.current_call_frame()?.to,
-                    vm.current_call_frame()?.msg_value,
-                )?;
-            }
-
-            vm.increase_account_balance(sender_address, vm.current_call_frame()?.msg_value)?;
+            undo_value_transfer(vm)?;
         }
 
-        // 2. Return unused gas + gas refunds to the sender.
+        let gas_refunded: u64 = compute_gas_refunded(vm, report)?;
+        let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, report.gas_used)?;
+        refund_sender(vm, report, gas_refunded, actual_gas_used)?;
 
-        // a. Calculate refunded gas
-        let gas_used_without_refunds = report.gas_used;
+        pay_coinbase(vm, actual_gas_used)?;
 
-        // [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
-        // "The max refundable proportion of gas was reduced from one half to one fifth by EIP-3529 by Buterin and Swende [2021] in the London release"
-        let refund_quotient = if vm.env.config.fork < Fork::London {
-            MAX_REFUND_QUOTIENT_PRE_LONDON
-        } else {
-            MAX_REFUND_QUOTIENT
-        };
-        let refunded_gas = report.gas_refunded.min(
-            gas_used_without_refunds
-                .checked_div(refund_quotient)
-                .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
-        );
-
-        // b. Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
-        let exec_gas_consumed = gas_used_without_refunds
-            .checked_sub(refunded_gas)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
-
-        let actual_gas_used = if vm.env.config.fork >= Fork::Prague {
-            let minimum_gas_consumed = vm.get_min_gas_used()?;
-            exec_gas_consumed.max(minimum_gas_consumed)
-        } else {
-            exec_gas_consumed
-        };
+        delete_self_destruct_accounts(vm)?;
 
-        // c. Update gas used and refunded in the Execution Report.
-        report.gas_used = actual_gas_used;
-        report.gas_refunded = refunded_gas;
+        Ok(())
+    }
+}
 
-        // d. Finally, return unspent gas to the sender.
-        let gas_to_return = vm
-            .env
-            .gas_limit
-            .checked_sub(actual_gas_used)
-            .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // In a create if Tx was reverted the account won't even exist by this point.
+    if !vm.is_create() {
+        vm.decrease_account_balance(
+            vm.current_call_frame()?.to,
+            vm.current_call_frame()?.msg_value,
+        )?;
+    }
 
-        let wei_return_amount = vm
-            .env
-            .gas_price
-            .checked_mul(U256::from(gas_to_return))
-            .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+    vm.increase_account_balance(vm.env.origin, vm.current_call_frame()?.msg_value)
+}
 
-        vm.increase_account_balance(sender_address, wei_return_amount)?;
+pub fn refund_sender(
+    vm: &mut VM<'_>,
+    report: &mut ExecutionReport,
+    refunded_gas: u64,
+    actual_gas_used: u64,
+) -> Result<(), VMError> {
+    // c. Update gas used and refunded in the Execution Report.
+    report.gas_used = actual_gas_used;
+    report.gas_refunded = refunded_gas;
+
+    // d. Finally, return unspent gas to the sender.
+    let gas_to_return = vm
+        .env
+        .gas_limit
+        .checked_sub(actual_gas_used)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(0)))?;
+
+    let wei_return_amount = vm
+        .env
+        .gas_price
+        .checked_mul(U256::from(gas_to_return))
+        .ok_or(VMError::Internal(InternalError::UndefinedState(1)))?;
+
+    vm.increase_account_balance(vm.env.origin, wei_return_amount)
+}
 
-        // 3. Pay coinbase fee
-        let coinbase_address = vm.env.coinbase;
+// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
+pub fn compute_gas_refunded(vm: &mut VM<'_>, report: &ExecutionReport) -> Result<u64, VMError> {
+    let refund_quotient = if vm.env.config.fork < Fork::London {
+        MAX_REFUND_QUOTIENT_PRE_LONDON
+    } else {
+        MAX_REFUND_QUOTIENT
+    };
+    Ok(report.gas_refunded.min(
+        report
+            .gas_used
+            .checked_div(refund_quotient)
+            .ok_or(VMError::Internal(InternalError::UndefinedState(-1)))?,
+    ))
+}
 
-        let priority_fee_per_gas = vm
-            .env
-            .gas_price
-            .checked_sub(vm.env.base_fee_per_gas)
-            .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
-        let coinbase_fee = U256::from(actual_gas_used)
-            .checked_mul(priority_fee_per_gas)
-            .ok_or(VMError::BalanceOverflow)?;
-
-        vm.increase_account_balance(coinbase_address, coinbase_fee)?;
-
-        // 4. Destruct addresses in vm.selfdestruct set.
-        // In Cancun the only addresses destroyed are contracts created in this transaction
-        let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
-        for address in selfdestruct_set {
-            let account_to_remove = vm.get_account_mut(address)?;
-            *account_to_remove = Account::default();
+// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
+pub fn compute_actual_gas_used(
+    vm: &mut VM<'_>,
+    refunded_gas: u64,
+    gas_used_without_refunds: u64,
+) -> Result<u64, VMError> {
+    let exec_gas_consumed = gas_used_without_refunds
+        .checked_sub(refunded_gas)
+        .ok_or(VMError::Internal(InternalError::UndefinedState(-2)))?;
+
+    if vm.env.config.fork >= Fork::Prague {
+        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
+    } else {
+        Ok(exec_gas_consumed)
+    }
+}
+
+pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
+    let priority_fee_per_gas = vm
+        .env
+        .gas_price
+        .checked_sub(vm.env.base_fee_per_gas)
+        .ok_or(VMError::GasPriceIsLowerThanBaseFee)?;
+
+    let coinbase_fee = U256::from(gas_to_pay)
+        .checked_mul(priority_fee_per_gas)
+        .ok_or(VMError::BalanceOverflow)?;
+
+    vm.increase_account_balance(vm.env.coinbase, coinbase_fee)
+}
+
+// In Cancun the only addresses destroyed are contracts created in this transaction
+pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
+    let selfdestruct_set = vm.accrued_substate.selfdestruct_set.clone();
+    for address in selfdestruct_set {
+        let account_to_remove = vm.get_account_mut(address)?;
+        *account_to_remove = Account::default();
+    }
+    Ok(())
+}
+
+pub fn validate_min_gas_limit(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // check for gas limit is grater or equal than the minimum required
+    let calldata = vm.current_call_frame()?.calldata.clone();
+    let intrinsic_gas: u64 = vm.get_intrinsic_gas()?;
+
+    // calldata_cost = tokens_in_calldata * 4
+    let calldata_cost: u64 =
+        gas_cost::tx_calldata(&calldata, vm.env.config.fork).map_err(VMError::OutOfGas)?;
+
+    // same as calculated in gas_used()
+    let tokens_in_calldata: u64 = calldata_cost
+        .checked_div(STANDARD_TOKEN_COST)
+        .ok_or(VMError::Internal(InternalError::DivisionError))?;
+
+    // floor_cost_by_tokens = TX_BASE_COST + TOTAL_COST_FLOOR_PER_TOKEN * tokens_in_calldata
+    let floor_cost_by_tokens = tokens_in_calldata
+        .checked_mul(TOTAL_COST_FLOOR_PER_TOKEN)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?
+        .checked_add(TX_BASE_COST)
+        .ok_or(VMError::Internal(InternalError::GasOverflow))?;
+
+    let min_gas_limit = max(intrinsic_gas, floor_cost_by_tokens);
+    if vm.current_call_frame()?.gas_limit < min_gas_limit {
+        return Err(VMError::TxValidation(TxValidationError::IntrinsicGasTooLow));
+    }
+
+    Ok(())
+}
+
+pub fn validate_max_fee_per_blob_gas(
+    vm: &mut VM<'_>,
+    tx_max_fee_per_blob_gas: U256,
+) -> Result<(), VMError> {
+    if tx_max_fee_per_blob_gas
+        < get_base_fee_per_blob_gas(vm.env.block_excess_blob_gas, &vm.env.config)?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerBlobGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
+    if vm.current_call_frame()?.calldata.len() > INIT_CODE_MAX_SIZE
+        && vm.env.config.fork >= Fork::Shanghai
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::InitcodeSizeExceeded,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), VMError> {
+    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
+        return Err(VMError::TxValidation(
+            TxValidationError::InsufficientMaxFeePerGas,
+        ));
+    }
+    Ok(())
+}
+
+pub fn validate_type_3_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
+    // (11) TYPE_3_TX_PRE_FORK
+    if vm.env.config.fork < Fork::Cancun {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxPreFork));
+    }
+
+    let blob_hashes = &vm.env.tx_blob_hashes;
+
+    // (12) TYPE_3_TX_ZERO_BLOBS
+    if blob_hashes.is_empty() {
+        return Err(VMError::TxValidation(TxValidationError::Type3TxZeroBlobs));
+    }
+
+    // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
+    for blob_hash in blob_hashes {
+        let blob_hash = blob_hash.as_bytes();
+        if let Some(first_byte) = blob_hash.first() {
+            if !VALID_BLOB_PREFIXES.contains(first_byte) {
+                return Err(VMError::TxValidation(
+                    TxValidationError::Type3TxInvalidBlobVersionedHash,
+                ));
+            }
         }
+    }
 
-        Ok(())
+    // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
+    if blob_hashes.len()
+        > vm.env
+            .config
+            .blob_schedule
+            .max
+            .try_into()
+            .map_err(|_| VMError::Internal(InternalError::ConversionError))?
+    {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxBlobCountExceeded,
+        ));
+    }
+
+    // (15) TYPE_3_TX_CONTRACT_CREATION
+    // NOTE: This will never happen, since the EIP-4844 tx (type 3) does not have a TxKind field
+    // only supports an Address which must be non-empty.
+    // If a type 3 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
+    // it won't reach this point.
+    // For more information, please check the following thread:
+    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
+    if vm.is_create() {
+        return Err(VMError::TxValidation(
+            TxValidationError::Type3TxContractCreation,
+        ));
+    }
+
+    Ok(())
+}
+
+pub fn validate_type_4_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
path: crates/vm/levm/src/hooks/default_hook.rs
user: login: ilitteri
body: Done in [8941381](https://github.com/lambdaclass/ethrex/pull/2508/commits/89413816760026a9b2e76800fd7e73150a47dfe2).
created_at: 2025-04-30T20:22:32Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-21T18:36:12Z
body: <h2>Lines of code report</h2>

Total lines added: `99`
Total lines removed: `61`
Total lines changed: `160`

<details>
<summary>Detailed view</summary>

```
+-------------------------------------------------+-------+------+
| File                                            | Lines | Diff |
+-------------------------------------------------+-------+------+
| ethrex/cmd/ef_tests/state/runner/levm_runner.rs | 383   | +1   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs           | 640   | +2   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/environment.rs        | 29    | +1   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/errors.rs             | 230   | +2   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/default_hook.rs | 339   | +75  |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/l2_hook.rs      | 133   | -61  |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/mod.rs          | 7     | +4   |
+-------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/vm.rs                 | 386   | +14  |
+-------------------------------------------------+-------+------+

```
</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-21T18:47:02Z
body: # Benchmark Results Comparison
## PR Results
#### Benchmark Results: Factorial
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Factorial` | 239.3 ± 1.2 | 238.0 | 242.4 | 1.00 |
| `levm_Factorial` | 879.1 ± 4.0 | 873.0 | 886.7 | 3.67 ± 0.02 |
#### Benchmark Results: Factorial - Recursive
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_FactorialRecursive` | 1.437 ± 0.073 | 1.352 | 1.540 | 1.00 |
| `levm_FactorialRecursive` | 13.302 ± 0.134 | 13.066 | 13.451 | 9.26 ± 0.48 |
#### Benchmark Results: Fibonacci
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Fibonacci` | 209.3 ± 0.6 | 207.9 | 210.2 | 1.00 |
| `levm_Fibonacci` | 879.0 ± 8.3 | 870.2 | 897.0 | 4.20 ± 0.04 |
#### Benchmark Results: ManyHashes
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ManyHashes` | 8.8 ± 0.1 | 8.7 | 9.0 | 1.00 |
| `levm_ManyHashes` | 17.6 ± 0.3 | 17.3 | 18.3 | 2.01 ± 0.04 |
#### Benchmark Results: BubbleSort
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_BubbleSort` | 3.243 ± 0.033 | 3.210 | 3.306 | 1.00 |
| `levm_BubbleSort` | 5.734 ± 0.028 | 5.702 | 5.780 | 1.77 ± 0.02 |
#### Benchmark Results: ERC20 - Transfer
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Transfer` | 252.0 ± 3.9 | 247.7 | 257.9 | 1.00 |
| `levm_ERC20Transfer` | 507.9 ± 7.1 | 499.9 | 522.9 | 2.02 ± 0.04 |
#### Benchmark Results: ERC20 - Mint
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Mint` | 142.6 ± 0.6 | 141.7 | 143.6 | 1.00 |
| `levm_ERC20Mint` | 322.2 ± 4.5 | 316.1 | 329.8 | 2.26 ± 0.03 |
#### Benchmark Results: ERC20 - Approval
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Approval` | 1.040 ± 0.007 | 1.031 | 1.051 | 1.00 |
| `levm_ERC20Approval` | 1.922 ± 0.040 | 1.894 | 2.032 | 1.85 ± 0.04 |
## Main Results
#### Benchmark Results: Factorial
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Factorial` | 237.9 ± 1.9 | 233.0 | 240.4 | 1.00 |
| `levm_Factorial` | 885.5 ± 16.1 | 874.0 | 916.8 | 3.72 ± 0.07 |
#### Benchmark Results: Factorial - Recursive
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_FactorialRecursive` | 1.577 ± 0.086 | 1.412 | 1.675 | 1.00 |
| `levm_FactorialRecursive` | 13.203 ± 0.201 | 12.986 | 13.430 | 8.37 ± 0.47 |
#### Benchmark Results: Fibonacci
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Fibonacci` | 207.5 ± 2.1 | 205.9 | 213.3 | 1.00 |
| `levm_Fibonacci` | 874.9 ± 3.3 | 871.7 | 881.5 | 4.22 ± 0.05 |
#### Benchmark Results: ManyHashes
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ManyHashes` | 8.7 ± 0.1 | 8.6 | 8.8 | 1.00 |
| `levm_ManyHashes` | 17.6 ± 0.3 | 17.4 | 18.4 | 2.01 ± 0.04 |
#### Benchmark Results: BubbleSort
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_BubbleSort` | 3.259 ± 0.013 | 3.245 | 3.283 | 1.00 |
| `levm_BubbleSort` | 5.795 ± 0.065 | 5.724 | 5.889 | 1.78 ± 0.02 |
#### Benchmark Results: ERC20 - Transfer
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Transfer` | 252.4 ± 0.5 | 251.2 | 252.9 | 1.00 |
| `levm_ERC20Transfer` | 511.4 ± 8.9 | 501.8 | 533.5 | 2.03 ± 0.04 |
#### Benchmark Results: ERC20 - Mint
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Mint` | 145.2 ± 3.5 | 143.3 | 155.0 | 1.00 |
| `levm_ERC20Mint` | 318.8 ± 2.0 | 316.1 | 322.0 | 2.20 ± 0.05 |
#### Benchmark Results: ERC20 - Approval
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Approval` | 1.039 ± 0.002 | 1.037 | 1.042 | 1.00 |
| `levm_ERC20Approval` | 1.925 ± 0.015 | 1.901 | 1.943 | 1.85 ± 0.01 |

author_association: NONE