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