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}