Skip to main content

ethrex_levm/hooks/
default_hook.rs

1use crate::{
2    account::LevmAccount,
3    constants::*,
4    errors::{ContextResult, ExceptionalHalt, InternalError, TxValidationError, VMError},
5    gas_cost::{STANDARD_TOKEN_COST, floor_tokens_in_access_list, total_cost_floor_per_token},
6    hooks::hook::Hook,
7    utils::*,
8    vm::VM,
9};
10
11use ethrex_common::{
12    Address, H256, U256,
13    types::{Code, Fork},
14};
15
16pub const MAX_REFUND_QUOTIENT: u64 = 5;
17
18pub struct DefaultHook;
19
20impl Hook for DefaultHook {
21    /// ## Description
22    /// This method performs validations and returns an error if any of these fail.
23    /// It also makes pre-execution changes:
24    /// - It increases sender nonce
25    /// - It substracts up-front-cost from sender balance.
26    /// - It adds value to receiver balance.
27    /// - It calculates and adds intrinsic gas to the 'gas used' of callframe and environment.
28    ///   See 'docs' for more information about validations.
29    fn prepare_execution(&mut self, vm: &mut VM<'_>) -> Result<(), VMError> {
30        // System calls (EELS `process_unchecked_system_transaction`) have no
31        // sender semantics: no validation, no nonce bump, no fee deduction.
32        // EELS never reads the SYSTEM_ADDRESS account, so skip the whole
33        // sender path to keep the read out of execution witnesses (EIP-8025).
34        if vm.env.is_system_call {
35            let intrinsic = vm.get_intrinsic_gas()?;
36            vm.add_intrinsic_gas(&intrinsic)?;
37            transfer_value(vm)?;
38            set_bytecode_and_code_address(vm)?;
39            return Ok(());
40        }
41
42        let sender_address = vm.env.origin;
43        let sender_info = vm.db.get_account(sender_address)?.info.clone();
44
45        // Compute intrinsic gas once and reuse it for both the min-gas-limit
46        // validation and `add_intrinsic_gas` below (nothing in between mutates the
47        // calldata / access-list / auth-list it depends on).
48        let intrinsic = vm.get_intrinsic_gas()?;
49
50        if vm.env.config.fork >= Fork::Prague {
51            validate_min_gas_limit(vm, &intrinsic)?;
52            // EIP-7825 (Osaka to pre-Amsterdam): reject tx if gas_limit > POST_OSAKA_GAS_LIMIT_CAP.
53            // Amsterdam removes this restriction (EIP-8037 reservoir model).
54            if vm.env.config.fork >= Fork::Osaka
55                && vm.env.config.fork < Fork::Amsterdam
56                && vm.tx.gas_limit() > POST_OSAKA_GAS_LIMIT_CAP
57            {
58                return Err(VMError::TxValidation(
59                    TxValidationError::TxMaxGasLimitExceeded {
60                        tx_hash: vm.tx.hash(),
61                        tx_gas_limit: vm.tx.gas_limit(),
62                    },
63                ));
64            }
65        }
66
67        // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
68        let gaslimit_price_product = vm
69            .env
70            .gas_price
71            .checked_mul(vm.env.gas_limit.into())
72            .ok_or(TxValidationError::GasLimitPriceProductOverflow)?;
73
74        validate_sender_balance(vm, sender_info.balance)?;
75
76        // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
77        if let Some(tx_max_fee_per_blob_gas) = vm.env.tx_max_fee_per_blob_gas {
78            validate_max_fee_per_blob_gas(vm, tx_max_fee_per_blob_gas)?;
79        }
80
81        // (3) INSUFFICIENT_ACCOUNT_FUNDS
82        deduct_caller(vm, gaslimit_price_product, sender_address)?;
83
84        // (4) INSUFFICIENT_MAX_FEE_PER_GAS
85        validate_sufficient_max_fee_per_gas(vm)?;
86
87        // (5) INITCODE_SIZE_EXCEEDED
88        if vm.is_create()? {
89            validate_init_code_size(vm)?;
90        }
91
92        // (6) INTRINSIC_GAS_TOO_LOW
93        vm.add_intrinsic_gas(&intrinsic)?;
94
95        // (7) NONCE_IS_MAX
96        vm.increment_account_nonce(sender_address)
97            .map_err(|_| TxValidationError::NonceIsMax)?;
98
99        // check for nonce mismatch
100        if sender_info.nonce != vm.env.tx_nonce {
101            return Err(TxValidationError::NonceMismatch {
102                expected: sender_info.nonce,
103                actual: vm.env.tx_nonce,
104            }
105            .into());
106        }
107
108        // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS
109        if let (Some(tx_max_priority_fee), Some(tx_max_fee_per_gas)) = (
110            vm.env.tx_max_priority_fee_per_gas,
111            vm.env.tx_max_fee_per_gas,
112        ) && tx_max_priority_fee > tx_max_fee_per_gas
113        {
114            return Err(TxValidationError::PriorityGreaterThanMaxFeePerGas {
115                priority_fee: tx_max_priority_fee,
116                max_fee_per_gas: tx_max_fee_per_gas,
117            }
118            .into());
119        }
120
121        // (9) SENDER_NOT_EOA
122        let code = vm.db.get_code(sender_info.code_hash)?;
123        validate_sender(sender_address, code.code())?;
124
125        // (10) GAS_ALLOWANCE_EXCEEDED
126        validate_gas_allowance(vm)?;
127
128        // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
129        if vm.env.tx_max_fee_per_blob_gas.is_some() {
130            validate_4844_tx(vm)?;
131        }
132
133        // [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
134        // Transaction is type 4 if authorization_list is Some
135        if vm.tx.authorization_list().is_some() {
136            validate_type_4_tx(vm)?;
137        }
138
139        transfer_value(vm)?;
140
141        set_bytecode_and_code_address(vm)?;
142
143        Ok(())
144    }
145
146    /// ## Changes post execution
147    /// 1. Undo value transfer if the transaction was reverted
148    /// 2. Return unused gas + gas refunds to the sender.
149    /// 3. Pay coinbase fee
150    /// 4. Destruct addresses in selfdestruct set.
151    fn finalize_execution(
152        &mut self,
153        vm: &mut VM<'_>,
154        ctx_result: &mut ContextResult,
155    ) -> Result<(), VMError> {
156        // System calls (EELS `process_unchecked_system_transaction`) have no
157        // sender or fee semantics: there is nothing to undo, refund, or pay
158        // (the value and gas price are zero), so skip straight to the
159        // self-destruct cleanup. Callers ignore the gas accounting fields for
160        // system calls.
161        if vm.env.is_system_call {
162            delete_self_destruct_accounts(vm)?;
163            return Ok(());
164        }
165
166        if !ctx_result.is_success() {
167            undo_value_transfer(vm)?;
168        }
169
170        // EIP-8037 (Amsterdam+): CREATE-tx address collision.
171        // Per EELS process_message_call (interpreter.py:120-145) the collision
172        // returns `state_gas_left = message.state_gas_reservoir` (reservoir is
173        // PRESERVED, not burned). The failure block in fork.py:1086-1094 then
174        // adds `new_account_refund` to both `state_gas_left` and `state_refund`,
175        // so the user gets back reservoir + new_account_refund. tx_state_gas
176        // collapses to 0, tx_regular_gas = max(intrinsic_regular + message.gas,
177        // calldata_floor). The user does NOT lose the whole gas_limit.
178        if vm.env.config.fork >= Fork::Amsterdam && ctx_result.is_collision() {
179            let gas_limit = vm.env.gas_limit;
180            // state_gas_used is already net (signed, inline refunds applied).
181            // state_refund carries the EIP-7702 auth refund and CREATE-failure intrinsic
182            // (added by vm.finalize_execution). Clamp at zero.
183            let state_refund_signed =
184                i64::try_from(vm.state_refund).map_err(|_| InternalError::Overflow)?;
185            let state_gas: u64 =
186                u64::try_from(vm.state_gas_used.saturating_sub(state_refund_signed).max(0))
187                    .map_err(|_| InternalError::Overflow)?;
188            let floor = vm.get_min_gas_used()?;
189            // Regular gas = gas_limit - state_gas_left, where state_gas_left =
190            // reservoir (PRESERVED across collision in EELS, with new_account_refund
191            // already folded in by vm.finalize_execution above). Mirrors EELS
192            // tx_gas_used_before_refund = tx.gas - gas_left(=0) - state_gas_left.
193            let regular_gas = gas_limit.saturating_sub(vm.state_gas_reservoir);
194            let effective_regular = regular_gas.max(floor);
195            ctx_result.gas_used = effective_regular
196                .checked_add(state_gas)
197                .ok_or(InternalError::Overflow)?;
198            // User pays only the effective regular (post-floor); coinbase gets the
199            // same; remainder returns to sender.
200            ctx_result.gas_spent = effective_regular;
201            pay_coinbase(vm, effective_regular)?;
202            let gas_to_return = gas_limit
203                .checked_sub(effective_regular)
204                .ok_or(InternalError::Underflow)?;
205            let wei_return_amount = vm
206                .env
207                .gas_price
208                .checked_mul(U256::from(gas_to_return))
209                .ok_or(InternalError::Overflow)?;
210            vm.increase_account_balance(vm.env.origin, wei_return_amount)?;
211            return Ok(());
212        }
213
214        // EIP-8037 (Amsterdam+): unused reservoir is always returned to sender.
215        // Per EELS, state_gas_left is preserved even on exceptional halt — only
216        // regular gas_left is burned.  The user does NOT pay for unspent reservoir.
217        if vm.env.config.fork >= Fork::Amsterdam {
218            ctx_result.gas_used = ctx_result.gas_used.saturating_sub(vm.state_gas_reservoir);
219        }
220
221        // Save pre-refund gas for EIP-7778 block accounting
222        let gas_used_pre_refund = ctx_result.gas_used;
223
224        // Note: compute_gas_refunded caps at gas_used / MAX_REFUND_QUOTIENT, where
225        // gas_used already has the reservoir subtracted (line above). This matches
226        // EELS, which applies the refund cap after reservoir removal but before the
227        // regular/state gas split.
228        let gas_refunded: u64 = compute_gas_refunded(vm, ctx_result)?;
229        let gas_spent = compute_actual_gas_used(vm, gas_refunded, gas_used_pre_refund)?;
230
231        refund_sender(vm, ctx_result, gas_refunded, gas_spent)?;
232
233        pay_coinbase(vm, gas_spent)?;
234
235        delete_self_destruct_accounts(vm)?;
236
237        Ok(())
238    }
239}
240
241pub fn undo_value_transfer(vm: &mut VM<'_>) -> Result<(), VMError> {
242    // In a create if Tx was reverted the account won't even exist by this point.
243    if !vm.is_create()? {
244        vm.decrease_account_balance(vm.current_call_frame.to, vm.current_call_frame.msg_value)?;
245    }
246
247    vm.increase_account_balance(vm.env.origin, vm.current_call_frame.msg_value)?;
248
249    Ok(())
250}
251
252/// Refunds unused gas to the sender. The user pays `gas_spent` (post-refund);
253/// for Amsterdam+, block-level accounting is recomputed dimensionally from VM
254/// fields, not from a pre-refund total.
255pub fn refund_sender(
256    vm: &mut VM<'_>,
257    ctx_result: &mut ContextResult,
258    refunded_gas: u64,
259    gas_spent: u64,
260) -> Result<(), VMError> {
261    vm.substate.refunded_gas = refunded_gas;
262
263    // EIP-7778: Separate block vs user gas accounting for Amsterdam+
264    // Block header gas_used = max(regular_dimension, state_dimension) per EIP-7778.
265    // Receipt cumulative_gas_used = post-refund total (what user pays).
266    if vm.env.config.fork >= Fork::Amsterdam {
267        // EIP-7623 floor applies to the regular (non-state) gas component only.
268        let floor = vm.get_min_gas_used()?;
269        // EIP-8037: state_gas_used is already net (signed, credits applied inline).
270        // Subtract state_refund (EIP-7702 tx-level channel) and clamp at zero.
271        let state_refund_signed =
272            i64::try_from(vm.state_refund).map_err(|_| InternalError::Overflow)?;
273        let state_gas: u64 =
274            u64::try_from(vm.state_gas_used.saturating_sub(state_refund_signed).max(0))
275                .map_err(|_| InternalError::Overflow)?;
276        // Compute raw consumption from scratch (gas_limit minus gas_remaining)
277        // to avoid interference from any reservoir-current subtraction baked
278        // into the caller's pre-refund number.
279        #[expect(clippy::as_conversions, reason = "gas_remaining is >= 0 here")]
280        let gas_remaining = vm.current_call_frame.gas_remaining.max(0) as u64;
281        let raw_consumed = vm.env.gas_limit.saturating_sub(gas_remaining);
282        // Subtract intrinsic_state (pre-consumed from gas_remaining as part of total intrinsic),
283        // the initial reservoir (pre-consumed from gas_remaining), and state-gas spills
284        // (EELS charge_state_gas spills don't count as regular_gas_used).
285        let regular_gas = raw_consumed
286            .saturating_sub(vm.intrinsic_state_gas)
287            .saturating_sub(vm.state_gas_reservoir_initial)
288            .saturating_sub(vm.state_gas_spill);
289        let effective_regular = regular_gas.max(floor);
290        ctx_result.gas_used = effective_regular
291            .checked_add(state_gas)
292            .ok_or(InternalError::Overflow)?;
293        // User pays post-refund gas (with floor)
294        ctx_result.gas_spent = gas_spent;
295    } else {
296        // Pre-Amsterdam: both use post-refund value
297        ctx_result.gas_used = gas_spent;
298        ctx_result.gas_spent = gas_spent;
299    }
300
301    // Return unspent gas to the sender (based on what user pays)
302    let gas_to_return = vm
303        .env
304        .gas_limit
305        .checked_sub(gas_spent)
306        .ok_or(InternalError::Underflow)?;
307
308    let wei_return_amount = vm
309        .env
310        .gas_price
311        .checked_mul(U256::from(gas_to_return))
312        .ok_or(InternalError::Overflow)?;
313
314    vm.increase_account_balance(vm.env.origin, wei_return_amount)?;
315
316    Ok(())
317}
318
319// [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529)
320pub fn compute_gas_refunded(vm: &VM<'_>, ctx_result: &ContextResult) -> Result<u64, VMError> {
321    Ok(vm
322        .substate
323        .refunded_gas
324        .min(ctx_result.gas_used / MAX_REFUND_QUOTIENT))
325}
326
327// Calculate actual gas used in the whole transaction. Since Prague there is a base minimum to be consumed.
328pub fn compute_actual_gas_used(
329    vm: &mut VM<'_>,
330    refunded_gas: u64,
331    gas_used_without_refunds: u64,
332) -> Result<u64, VMError> {
333    let exec_gas_consumed = gas_used_without_refunds
334        .checked_sub(refunded_gas)
335        .ok_or(InternalError::Underflow)?;
336
337    if vm.env.config.fork >= Fork::Prague {
338        Ok(exec_gas_consumed.max(vm.get_min_gas_used()?))
339    } else {
340        Ok(exec_gas_consumed)
341    }
342}
343
344pub fn pay_coinbase(vm: &mut VM<'_>, gas_to_pay: u64) -> Result<(), VMError> {
345    let priority_fee_per_gas = vm
346        .env
347        .gas_price
348        .checked_sub(vm.env.base_fee_per_gas)
349        .ok_or(InternalError::Underflow)?;
350
351    let coinbase_fee = U256::from(gas_to_pay)
352        .checked_mul(priority_fee_per_gas)
353        .ok_or(InternalError::Overflow)?;
354
355    // Per EIP-7928: Coinbase must appear in BAL when there's a user transaction,
356    // even if the priority fee is zero. System contract calls have gas_price = 0,
357    // so we use this to distinguish them from user transactions.
358    if !vm.env.gas_price.is_zero()
359        && let Some(recorder) = vm.db.bal_recorder.as_mut()
360    {
361        recorder.record_touched_address(vm.env.coinbase);
362    }
363
364    // Only pay coinbase if there's actually a fee to pay.
365    if !coinbase_fee.is_zero() {
366        vm.increase_account_balance(vm.env.coinbase, coinbase_fee)?;
367    } else if !vm.env.is_system_call {
368        // The spec reads the coinbase account unconditionally during user-tx
369        // fee transfer (EELS `process_transaction` calls `get_account` before
370        // deciding whether to credit), but system contract calls never touch
371        // the coinbase. Keep the read observable for zero-fee user txs
372        // (including gas-price-zero txs on zero-base-fee chains) so execution
373        // witnesses (EIP-8025) record the coinbase trie path, including its
374        // exclusion proof when it doesn't exist.
375        vm.db.get_account(vm.env.coinbase)?;
376    }
377
378    Ok(())
379}
380
381// In Cancun the only addresses destroyed are contracts created in this transaction
382pub fn delete_self_destruct_accounts(vm: &mut VM<'_>) -> Result<(), VMError> {
383    // EIP-7708: Emit Burn logs for accounts with non-zero balance marked for deletion
384    // Must emit in lexicographical order of address
385    if vm.env.config.fork >= Fork::Amsterdam {
386        let mut addresses_with_balance: Vec<(Address, U256)> = vm
387            .substate
388            .iter_selfdestruct()
389            .filter_map(|addr| {
390                let balance = vm.db.get_account(*addr).ok()?.info.balance;
391                if !balance.is_zero() {
392                    Some((*addr, balance))
393                } else {
394                    None
395                }
396            })
397            .collect();
398
399        // Sort by address (lexicographical order per EIP-7708)
400        addresses_with_balance.sort_by_key(|(addr, _)| *addr);
401
402        for (addr, balance) in addresses_with_balance {
403            let log = create_burn_log(addr, balance);
404            vm.substate.add_log(log);
405        }
406    }
407
408    // Delete the accounts
409    for address in vm.substate.iter_selfdestruct() {
410        // Backup must be taken before mark_modified flips `exists` to true.
411        let account_to_remove = vm.db.get_account(*address)?;
412        vm.current_call_frame
413            .call_frame_backup
414            .backup_account_info(*address, account_to_remove)?;
415
416        let account_to_remove = vm.db.get_account_mut(*address)?;
417        *account_to_remove = LevmAccount::default();
418        account_to_remove.mark_destroyed();
419
420        // EIP-7928: Clean up BAL for selfdestructed account
421        if let Some(recorder) = vm.db.bal_recorder.as_mut() {
422            recorder.track_selfdestruct(*address);
423        }
424    }
425
426    Ok(())
427}
428
429pub fn validate_min_gas_limit(vm: &mut VM<'_>, intrinsic: &IntrinsicGas) -> Result<(), VMError> {
430    // check for gas limit is grater or equal than the minimum required
431    let regular_gas = intrinsic.regular;
432    let state_gas = intrinsic.state;
433    let intrinsic_gas: u64 = regular_gas
434        .checked_add(state_gas)
435        .ok_or(ExceptionalHalt::OutOfGas)?;
436
437    if vm.current_call_frame.gas_limit < intrinsic_gas {
438        return Err(TxValidationError::IntrinsicGasTooLow.into());
439    }
440
441    let fork = vm.env.config.fork;
442
443    // EIP-7976 floor tokens: for the floor arm, all calldata bytes count unweighted.
444    // floor_tokens_in_calldata = (zero_bytes + nonzero_bytes) * STANDARD_TOKEN_COST
445    // Pre-Amsterdam uses the weighted EIP-7623 formula: (nonzero * 16 + zero * 4) / 4
446    let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
447        // EIP-7976: floor tokens = total_bytes * STANDARD_TOKEN_COST (unweighted).
448        let total_bytes: u64 = vm
449            .current_call_frame
450            .calldata
451            .len()
452            .try_into()
453            .map_err(|_| InternalError::TypeConversion)?;
454        total_bytes
455            .checked_mul(STANDARD_TOKEN_COST)
456            .ok_or(InternalError::Overflow)?
457    } else {
458        // Pre-Amsterdam: weighted EIP-7623 token count. Reuse the calldata cost already
459        // computed in `intrinsic` (same byte string) instead of re-walking the calldata.
460        intrinsic.calldata_cost / STANDARD_TOKEN_COST
461    };
462
463    // EIP-7981 (Amsterdam+): access-list data bytes fold into the floor-token count.
464    // floor_tokens_in_access_list = access_list_bytes * STANDARD_TOKEN_COST
465    // where access_list_bytes = 20 * address_count + 32 * storage_key_count.
466    if fork >= Fork::Amsterdam {
467        let al_floor_tokens = floor_tokens_in_access_list(vm.tx.access_list());
468        tokens_in_calldata = tokens_in_calldata
469            .checked_add(al_floor_tokens)
470            .ok_or(InternalError::Overflow)?;
471    }
472
473    // floor_cost_by_tokens = TX_BASE_COST + total_cost_floor_per_token(fork) * tokens
474    // EIP-7976 (Amsterdam+) raises the floor multiplier from 10 to 16.
475    let floor_cost_by_tokens = tokens_in_calldata
476        .checked_mul(total_cost_floor_per_token(fork))
477        .ok_or(InternalError::Overflow)?
478        .checked_add(TX_BASE_COST)
479        .ok_or(InternalError::Overflow)?;
480
481    // EIP-8037 (Amsterdam+): Regular gas is capped at TX_MAX_GAS_LIMIT — reject if
482    // intrinsic regular gas or calldata floor exceeds the cap (no amount of gas_limit
483    // can make the TX valid since excess gas_limit becomes state gas reservoir).
484    // Must be checked before the floor check so the correct error is returned.
485    // NOTE: We use IntrinsicGasTooLow (not TxMaxGasLimitExceeded) intentionally —
486    // this matches the EELS exception mapping for this specific case.
487    if vm.env.config.fork >= Fork::Amsterdam
488        && regular_gas.max(floor_cost_by_tokens) > TX_MAX_GAS_LIMIT_AMSTERDAM
489    {
490        return Err(TxValidationError::IntrinsicGasTooLow.into());
491    }
492
493    if vm.current_call_frame.gas_limit < floor_cost_by_tokens {
494        return Err(TxValidationError::IntrinsicGasBelowFloorGasCost.into());
495    }
496
497    Ok(())
498}
499
500pub fn validate_max_fee_per_blob_gas(
501    vm: &mut VM<'_>,
502    tx_max_fee_per_blob_gas: U256,
503) -> Result<(), VMError> {
504    let base_fee_per_blob_gas = vm.env.base_blob_fee_per_gas;
505    if tx_max_fee_per_blob_gas < base_fee_per_blob_gas {
506        return Err(TxValidationError::InsufficientMaxFeePerBlobGas {
507            base_fee_per_blob_gas,
508            tx_max_fee_per_blob_gas,
509        }
510        .into());
511    }
512
513    Ok(())
514}
515
516pub fn validate_init_code_size(vm: &mut VM<'_>) -> Result<(), VMError> {
517    // [EIP-3860] - INITCODE_SIZE_EXCEEDED
518    // [EIP-7954] - Amsterdam increases the limit
519    let code_size = vm.current_call_frame.calldata.len();
520    let max_size = if vm.env.config.fork >= Fork::Amsterdam {
521        AMSTERDAM_INIT_CODE_MAX_SIZE
522    } else {
523        INIT_CODE_MAX_SIZE
524    };
525    if code_size > max_size && vm.env.config.fork >= Fork::Shanghai {
526        return Err(TxValidationError::InitcodeSizeExceeded {
527            max_size,
528            actual_size: code_size,
529        }
530        .into());
531    }
532    Ok(())
533}
534
535pub fn validate_sufficient_max_fee_per_gas(vm: &mut VM<'_>) -> Result<(), TxValidationError> {
536    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < vm.env.base_fee_per_gas {
537        return Err(TxValidationError::InsufficientMaxFeePerGas);
538    }
539    Ok(())
540}
541
542pub fn validate_4844_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
543    // (11) TYPE_3_TX_PRE_FORK
544    if vm.env.config.fork < Fork::Cancun {
545        return Err(TxValidationError::Type3TxPreFork.into());
546    }
547
548    let blob_hashes = &vm.env.tx_blob_hashes;
549
550    // (12) TYPE_3_TX_ZERO_BLOBS
551    if blob_hashes.is_empty() {
552        return Err(TxValidationError::Type3TxZeroBlobs.into());
553    }
554
555    // (13) TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH
556    for blob_hash in blob_hashes {
557        let blob_hash = blob_hash.as_bytes();
558        if blob_hash
559            .first()
560            .is_some_and(|first_byte| !VALID_BLOB_PREFIXES.contains(first_byte))
561        {
562            return Err(TxValidationError::Type3TxInvalidBlobVersionedHash.into());
563        }
564    }
565
566    // (14) TYPE_3_TX_BLOB_COUNT_EXCEEDED
567    let max_blob_count = vm
568        .env
569        .config
570        .blob_schedule
571        .max
572        .try_into()
573        .map_err(|_| InternalError::TypeConversion)?;
574    let blob_count = blob_hashes.len();
575    if blob_count > max_blob_count {
576        return Err(TxValidationError::Type3TxBlobCountExceeded {
577            max_blob_count,
578            actual_blob_count: blob_count,
579        }
580        .into());
581    }
582    if vm.env.config.fork >= Fork::Osaka && blob_count > MAX_BLOB_COUNT_TX {
583        return Err(TxValidationError::Type3TxBlobCountExceeded {
584            max_blob_count: MAX_BLOB_COUNT_TX,
585            actual_blob_count: blob_count,
586        }
587        .into());
588    }
589
590    // (15) TYPE_3_TX_CONTRACT_CREATION
591    // NOTE: This will never happen, since the EIP-4844 tx (type 3) does not have a TxKind field
592    // only supports an Address which must be non-empty.
593    // If a type 3 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
594    // it won't reach this point.
595    // For more information, please check the following thread:
596    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
597    if vm.is_create()? {
598        return Err(TxValidationError::Type3TxContractCreation.into());
599    }
600
601    Ok(())
602}
603
604pub fn validate_type_4_tx(vm: &mut VM<'_>) -> Result<(), VMError> {
605    let Some(auth_list) = vm.tx.authorization_list() else {
606        // vm.authorization_list should be Some at this point.
607        return Err(InternalError::Custom("Auth list not found".to_string()).into());
608    };
609
610    // (16) TYPE_4_TX_PRE_FORK
611    if vm.env.config.fork < Fork::Prague {
612        return Err(TxValidationError::Type4TxPreFork.into());
613    }
614
615    // (17) TYPE_4_TX_CONTRACT_CREATION
616    // From the EIP docs: a null destination is not valid.
617    // NOTE: This will never happen, since the EIP-7702 tx (type 4) does not have a TxKind field
618    // only supports an Address which must be non-empty.
619    // If a type 4 tx has the field `to` as null (signaling create), it will raise an exception on RLP decoding,
620    // it won't reach this point.
621    // For more information, please check the following thread:
622    // - https://github.com/lambdaclass/ethrex/pull/2425/files/819825516dc633275df56b2886b921061c4d7681#r2035611105
623    if vm.is_create()? {
624        return Err(TxValidationError::Type4TxContractCreation.into());
625    }
626
627    // (18) TYPE_4_TX_LIST_EMPTY
628    // From the EIP docs: The transaction is considered invalid if the length of authorization_list is zero.
629    if auth_list.is_empty() {
630        return Err(TxValidationError::Type4TxAuthorizationListIsEmpty.into());
631    }
632
633    vm.eip7702_set_access_code()
634}
635
636pub fn validate_sender(sender_address: Address, code: &[u8]) -> Result<(), VMError> {
637    if !code.is_empty() && !code_has_delegation(code)? {
638        return Err(TxValidationError::SenderNotEOA(sender_address).into());
639    }
640    Ok(())
641}
642
643pub fn validate_gas_allowance(vm: &mut VM<'_>) -> Result<(), TxValidationError> {
644    // System contract calls (EIP-2935, EIP-4788, EIP-7002, EIP-7251) bypass the
645    // block-level gas-allowance check — their 30M gas budget is a protocol rule
646    // independent of `block_gas_limit`.
647    if vm.env.is_system_call {
648        return Ok(());
649    }
650    if vm.env.gas_limit > vm.env.block_gas_limit {
651        return Err(TxValidationError::GasAllowanceExceeded {
652            block_gas_limit: vm.env.block_gas_limit,
653            tx_gas_limit: vm.env.gas_limit,
654        });
655    }
656    Ok(())
657}
658
659pub fn validate_sender_balance(vm: &mut VM<'_>, sender_balance: U256) -> Result<(), VMError> {
660    if vm.env.disable_balance_check {
661        return Ok(());
662    }
663
664    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice + value + blob_gas_cost
665    let value = vm.current_call_frame.msg_value;
666
667    // blob gas cost = max fee per blob gas * blob gas used
668    // https://eips.ethereum.org/EIPS/eip-4844
669    let max_blob_gas_cost =
670        get_max_blob_gas_price(&vm.env.tx_blob_hashes, vm.env.tx_max_fee_per_blob_gas)?;
671
672    // 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
673    // balance >= max_fee_per_gas * gas_limit + value + blob_gas_cost if tx is type 2 or 3
674    let gas_fee_for_valid_tx = vm
675        .env
676        .tx_max_fee_per_gas
677        .unwrap_or(vm.env.gas_price)
678        .checked_mul(vm.env.gas_limit.into())
679        .ok_or(TxValidationError::GasLimitPriceProductOverflow)?;
680
681    let balance_for_valid_tx = gas_fee_for_valid_tx
682        .checked_add(value)
683        .ok_or(TxValidationError::InsufficientAccountFunds)?
684        .checked_add(max_blob_gas_cost)
685        .ok_or(TxValidationError::InsufficientAccountFunds)?;
686
687    if sender_balance < balance_for_valid_tx {
688        return Err(TxValidationError::InsufficientAccountFunds.into());
689    }
690
691    Ok(())
692}
693
694pub fn deduct_caller(
695    vm: &mut VM<'_>,
696    gas_limit_price_product: U256,
697    sender_address: Address,
698) -> Result<(), VMError> {
699    if vm.env.disable_balance_check {
700        return Ok(());
701    }
702
703    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice + value + blob_gas_cost
704    let value = vm.current_call_frame.msg_value;
705
706    let blob_gas_cost =
707        calculate_blob_gas_cost(&vm.env.tx_blob_hashes, vm.env.base_blob_fee_per_gas)?;
708
709    // The real cost to deduct is calculated as effective_gas_price * gas_limit + value + blob_gas_cost
710    let up_front_cost = gas_limit_price_product
711        .checked_add(value)
712        .ok_or(TxValidationError::InsufficientAccountFunds)?
713        .checked_add(blob_gas_cost)
714        .ok_or(TxValidationError::InsufficientAccountFunds)?;
715    // There is no error specified for overflow in up_front_cost
716    // in ef_tests. We went for "InsufficientAccountFunds" simply
717    // because if the upfront cost is bigger than U256, then,
718    // technically, the sender will not be able to pay it.
719
720    vm.decrease_account_balance(sender_address, up_front_cost)
721        .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
722
723    Ok(())
724}
725
726/// Transfer msg_value to transaction recipient
727pub fn transfer_value(vm: &mut VM<'_>) -> Result<(), VMError> {
728    if !vm.is_create()? {
729        let value = vm.current_call_frame.msg_value;
730        let to = vm.current_call_frame.to;
731
732        vm.increase_account_balance(to, value)?;
733
734        // EIP-7708: Emit transfer log for nonzero-value transactions to DIFFERENT accounts
735        // Self-transfers (origin == to) should NOT emit a log per the EIP spec
736        let from = vm.env.origin;
737        if vm.env.config.fork >= Fork::Amsterdam && !value.is_zero() && from != to {
738            let log = create_eth_transfer_log(from, to, value);
739            vm.substate.add_log(log);
740        }
741    }
742    Ok(())
743}
744
745/// Sets bytecode and code_address to CallFrame
746pub fn set_bytecode_and_code_address(vm: &mut VM<'_>) -> Result<(), VMError> {
747    // Get bytecode and code_address for assigning those values to the callframe.
748    let (bytecode, code_address) = if vm.is_create()? {
749        // Here bytecode is the calldata and the code_address is just the created contract address.
750        let calldata = std::mem::take(&mut vm.current_call_frame.calldata);
751        (
752            // SAFETY: we don't need the hash for the initcode
753            Code::from_bytecode_unchecked(calldata, H256::zero()),
754            vm.current_call_frame.to,
755        )
756    } else {
757        // Here bytecode and code_address could be either from the account or from the delegated account.
758        let to = vm.current_call_frame.to;
759
760        // Record tx.to as touched in BAL (the target of message call transaction)
761        if let Some(recorder) = vm.db.bal_recorder.as_mut() {
762            recorder.record_touched_address(to);
763        }
764
765        let (is_delegation, _eip7702_gas_consumed, code_address, bytecode) =
766            eip7702_get_code(vm.db, &mut vm.substate, to)?;
767
768        // If EIP-7702 delegation, also record the delegation target (code source) in BAL
769        if is_delegation && let Some(recorder) = vm.db.bal_recorder.as_mut() {
770            recorder.record_touched_address(code_address);
771        }
772
773        (bytecode, code_address)
774    };
775
776    // Assign code and code_address to callframe
777    vm.current_call_frame.code_address = code_address;
778    vm.current_call_frame.set_code(bytecode)?;
779
780    Ok(())
781}