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