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