Skip to main content

ethrex_levm/
utils.rs

1use crate::{
2    EVMConfig, Environment,
3    account::{AccountStatus, LevmAccount},
4    call_frame::CallFrameBackup,
5    constants::*,
6    db::gen_db::GeneralizedDatabase,
7    errors::{ExceptionalHalt, InternalError, TxValidationError, VMError},
8    gas_cost::{
9        self, ACCOUNT_WRITE_AMSTERDAM, BLOB_GAS_PER_BLOB, CREATE_ACCESS_AMSTERDAM,
10        CREATE_BASE_COST, PER_AUTH_BASE_COST_AMSTERDAM, STANDARD_TOKEN_COST,
11        STATE_BYTES_PER_AUTH_TOTAL, STATE_BYTES_PER_NEW_ACCOUNT, TRANSFER_LOG_COST_AMSTERDAM,
12        TX_VALUE_COST_AMSTERDAM, WARM_ADDRESS_ACCESS_COST, cold_account_access_cost,
13        cost_per_state_byte, floor_tokens_in_access_list, total_cost_floor_per_token, tx_base_cost,
14    },
15    vm::{Substate, VM},
16};
17use ExceptionalHalt::OutOfGas;
18use bytes::Bytes;
19use ethrex_common::constants::SYSTEM_ADDRESS;
20use ethrex_common::types::Log;
21use ethrex_common::{
22    Address, H256, U256,
23    evm::calculate_create_address,
24    types::{Account, Code, Fork, Transaction, fake_exponential, tx_fields::*},
25    utils::{keccak, u256_to_big_endian},
26};
27use ethrex_common::{types::TxKind, utils::u256_from_big_endian_const};
28use ethrex_rlp;
29use rustc_hash::FxHashMap;
30pub type Storage = FxHashMap<U256, H256>;
31
32// ================== Address related functions ======================
33/// Converts address (H160) to word (U256)
34pub fn address_to_word(address: Address) -> U256 {
35    let mut word = [0u8; 32];
36
37    for (word_byte, address_byte) in word.iter_mut().skip(12).zip(address.as_bytes().iter()) {
38        *word_byte = *address_byte;
39    }
40
41    u256_from_big_endian_const(word)
42}
43
44/// Calculates the address of a new contract using the CREATE2 opcode as follows
45///
46/// initialization_code = memory[offset:offset+size]
47///
48/// address = keccak256(0xff || sender_address || salt || keccak256(initialization_code))[12:]
49pub fn calculate_create2_address(
50    sender_address: Address,
51    initialization_code: &Bytes,
52    salt: U256,
53) -> Result<Address, InternalError> {
54    let init_code_hash = keccak(initialization_code);
55
56    let generated_address = Address::from_slice(
57        keccak(
58            [
59                &[0xff],
60                sender_address.as_bytes(),
61                &salt.to_big_endian(),
62                init_code_hash.as_bytes(),
63            ]
64            .concat(),
65        )
66        .as_bytes()
67        .get(12..)
68        .ok_or(InternalError::Slicing)?,
69    );
70    Ok(generated_address)
71}
72
73// ================== Backup related functions =======================
74
75/// Restore the state of the cache to the state it in the callframe backup.
76/// Also restores BAL recorder state changes (but not touched_addresses) per EIP-7928.
77pub fn restore_cache_state(
78    db: &mut GeneralizedDatabase,
79    callframe_backup: CallFrameBackup,
80) -> Result<(), VMError> {
81    for (address, account) in callframe_backup.original_accounts_info {
82        if let Some(current_account) = db.current_accounts_state.get_mut(&address) {
83            current_account.info = account.info;
84            current_account.status = account.status;
85            current_account.has_storage = account.has_storage;
86            current_account.exists = account.exists;
87        }
88    }
89
90    for (address, storage) in callframe_backup.original_account_storage_slots {
91        // This call to `get_account_mut` should never return None, because we are looking up accounts
92        // that had their storage modified, which means they should be in the cache. That's why
93        // we return an internal error in case we haven't found it.
94        let account = db
95            .current_accounts_state
96            .get_mut(&address)
97            .ok_or(InternalError::AccountNotFound)?;
98
99        for (key, value) in storage {
100            account.storage.insert(key, value);
101        }
102    }
103
104    // Evict codes the reverted frame(s) deployed: a stale by-hash cache entry
105    // would serve a later read of the same hash (from a pre-existing account)
106    // without hitting the store, hiding the read from execution-witness
107    // recording (EIP-8025). Only hashes that were NOT cached before the frame
108    // are tracked, so committed or store-loaded codes are never evicted.
109    for code_hash in callframe_backup.inserted_code_hashes {
110        db.codes.remove(&code_hash);
111    }
112
113    // Restore BAL recorder to checkpoint (but keep touched_addresses per EIP-7928)
114    if let Some(checkpoint) = callframe_backup.bal_checkpoint
115        && let Some(recorder) = db.bal_recorder.as_mut()
116    {
117        recorder.restore(checkpoint);
118    }
119
120    Ok(())
121}
122
123// ================= Blob hash related functions =====================
124pub fn get_base_fee_per_blob_gas(
125    block_excess_blob_gas: Option<u64>,
126    evm_config: &EVMConfig,
127) -> Result<U256, VMError> {
128    let base_fee_update_fraction = evm_config.blob_schedule.base_fee_update_fraction;
129    let excess_blob_gas = block_excess_blob_gas.unwrap_or_default();
130
131    fake_exponential(
132        MIN_BASE_FEE_PER_BLOB_GAS.into(),
133        excess_blob_gas.into(),
134        base_fee_update_fraction,
135    )
136    .map_err(|err| VMError::Internal(InternalError::FakeExponentialError(err)))
137}
138
139/// Gets the max blob gas cost for a transaction that a user is
140/// willing to pay.
141pub fn get_max_blob_gas_price(
142    tx_blob_hashes: &[H256],
143    tx_max_fee_per_blob_gas: Option<U256>,
144) -> Result<U256, VMError> {
145    let blobhash_amount: u64 = tx_blob_hashes
146        .len()
147        .try_into()
148        .map_err(|_| InternalError::TypeConversion)?;
149
150    let blob_gas_used: u64 = blobhash_amount
151        .checked_mul(BLOB_GAS_PER_BLOB)
152        .unwrap_or_default();
153
154    let max_blob_gas_cost = tx_max_fee_per_blob_gas
155        .unwrap_or_default()
156        .checked_mul(blob_gas_used.into())
157        .ok_or(InternalError::Overflow)?;
158
159    Ok(max_blob_gas_cost)
160}
161/// Calculate the actual blob gas cost.
162pub fn calculate_blob_gas_cost(
163    tx_blob_hashes: &[H256],
164    base_blob_fee_per_gas: U256,
165) -> Result<U256, VMError> {
166    let blobhash_amount: u64 = tx_blob_hashes
167        .len()
168        .try_into()
169        .map_err(|_| InternalError::TypeConversion)?;
170
171    let blob_gas_used: u64 = blobhash_amount
172        .checked_mul(BLOB_GAS_PER_BLOB)
173        .unwrap_or_default();
174
175    let blob_gas_used: U256 = blob_gas_used.into();
176    let blob_fee: U256 = blob_gas_used
177        .checked_mul(base_blob_fee_per_gas)
178        .ok_or(InternalError::Overflow)?;
179
180    Ok(blob_fee)
181}
182
183// ==================== Word related functions =======================
184pub fn word_to_address(word: U256) -> Address {
185    Address::from_slice(&u256_to_big_endian(word)[12..])
186}
187
188// ================== EIP-7702 related functions =====================
189
190pub fn code_has_delegation(code: &[u8]) -> Result<bool, VMError> {
191    // Delegate to the canonical predicate in `ethrex-common` so the
192    // EIP-7702 designation check (`0xef0100 || address`, exactly 23 bytes)
193    // has a single source of truth. Signature kept as `&[u8]` for callers.
194    Ok(ethrex_common::types::is_eip7702_delegation(code))
195}
196
197/// Gets the address inside the bytecode if it has been
198/// delegated as the EIP7702 determines.
199pub fn get_authorized_address_from_code(code: &[u8]) -> Result<Address, VMError> {
200    if code_has_delegation(code)? {
201        let address_bytes = &code
202            .get(SET_CODE_DELEGATION_BYTES.len()..)
203            .ok_or(InternalError::Slicing)?;
204        // It shouldn't panic when doing Address::from_slice()
205        // because the length is checked inside the code_has_delegation() function
206        let address = Address::from_slice(address_bytes);
207        Ok(address)
208    } else {
209        // if we end up here, it means that the address wasn't previously delegated.
210        Err(InternalError::AccountNotDelegated.into())
211    }
212}
213
214pub fn eip7702_recover_address(
215    auth_tuple: &AuthorizationTuple,
216    crypto: &dyn ethrex_crypto::Crypto,
217) -> Result<Option<Address>, VMError> {
218    use ethrex_rlp::encode::RLPEncode;
219
220    if auth_tuple.s_signature > *SECP256K1_ORDER_OVER2 || U256::zero() >= auth_tuple.s_signature {
221        return Ok(None);
222    }
223    if auth_tuple.r_signature > *SECP256K1_ORDER || U256::zero() >= auth_tuple.r_signature {
224        return Ok(None);
225    }
226    if auth_tuple.y_parity != U256::one() && auth_tuple.y_parity != U256::zero() {
227        return Ok(None);
228    }
229
230    let mut rlp_buf = Vec::with_capacity(128);
231    rlp_buf.push(MAGIC);
232    (auth_tuple.chain_id, auth_tuple.address, auth_tuple.nonce).encode(&mut rlp_buf);
233    let msg = crypto.keccak256(&rlp_buf);
234
235    let y_parity: u8 =
236        TryInto::<u8>::try_into(auth_tuple.y_parity).map_err(|_| InternalError::TypeConversion)?;
237
238    let mut sig = [0u8; 65];
239    sig[..32].copy_from_slice(&auth_tuple.r_signature.to_big_endian());
240    sig[32..64].copy_from_slice(&auth_tuple.s_signature.to_big_endian());
241    sig[64] = y_parity;
242
243    match crypto.recover_signer(&sig, &msg) {
244        Ok(address) => Ok(Some(address)),
245        Err(_) => Ok(None),
246    }
247}
248
249/// Gets code of an account, returning early if it's not a delegated account, otherwise
250/// Returns tuple (is_delegated, eip7702_cost, code_address, code).
251/// Notice that it also inserts the delegated account to the "accessed accounts" set.
252///
253/// Where:
254/// - `is_delegated`: True if account is a delegated account.
255/// - `eip7702_cost`: Cost of accessing the delegated account (if any)
256/// - `code_address`: Code address (if delegated, returns the delegated address)
257/// - `code`: Bytecode of the code_address, what the EVM will execute.
258pub fn eip7702_get_code(
259    db: &mut GeneralizedDatabase,
260    accrued_substate: &mut Substate,
261    address: Address,
262    fork: Fork,
263) -> Result<(bool, u64, Address, Code), VMError> {
264    let (bytecode, delegation) = eip7702_peek_delegation(db, accrued_substate, address, fork)?;
265    let Some((auth_address, access_cost)) = delegation else {
266        return Ok((false, 0, address, bytecode));
267    };
268
269    accrued_substate.add_accessed_address(auth_address);
270    let authorized_bytecode = db.get_account_code(auth_address)?.clone();
271
272    Ok((true, access_cost, auth_address, authorized_bytecode))
273}
274
275/// First half of [`eip7702_get_code`]: read `address`'s code and detect a
276/// delegation designation WITHOUT touching the delegate account.
277///
278/// Returns `address`'s code and, when delegated, the delegate address with
279/// its warm/cold access cost (computed from the current substate, not
280/// recorded). CALL-family opcodes use this to gas-check the delegation
281/// access cost before reading the delegate (EELS order); reading it earlier
282/// would leak the delegate account into execution witnesses on OOG.
283pub fn eip7702_peek_delegation(
284    db: &mut GeneralizedDatabase,
285    substate: &Substate,
286    address: Address,
287    fork: Fork,
288) -> Result<(Code, Option<(Address, u64)>), VMError> {
289    let bytecode = db.get_account_code(address)?.clone();
290    if !code_has_delegation(bytecode.code())? {
291        return Ok((bytecode, None));
292    }
293    let auth_address = get_authorized_address_from_code(bytecode.code())?;
294    let access_cost = if substate.is_address_accessed(&auth_address) {
295        WARM_ADDRESS_ACCESS_COST
296    } else {
297        // EIP-8038 (glamsterdam-devnet-6): cold account access is repriced
298        // 2600 -> 3000 at Amsterdam, so the 7702 delegate-access cost must be
299        // fork-dependent rather than the flat COLD_ADDRESS_ACCESS_COST.
300        cold_account_access_cost(fork)
301    };
302    Ok((bytecode, Some((auth_address, access_cost))))
303}
304
305/// Precomputed intrinsic-gas components for a transaction.
306///
307/// Computed once per tx in the prepare-execution hook and reused by
308/// [`VM::validate_min_gas_limit`](crate::hooks::default_hook::validate_min_gas_limit)
309/// and [`VM::add_intrinsic_gas`]. Previously the full calldata / access-list /
310/// auth-list walk ran 2-3x per tx (once in each function, plus the pre-Amsterdam
311/// floor's own `tx_calldata`).
312#[derive(Clone, Copy, Debug)]
313pub struct IntrinsicGas {
314    /// Regular (EIP-8037) intrinsic-gas arm.
315    pub regular: u64,
316    /// State (EIP-8037, Amsterdam+) intrinsic-gas arm; always 0 pre-Amsterdam.
317    pub state: u64,
318    /// `gas_cost::tx_calldata` over `current_call_frame.calldata`. Reused by the
319    /// pre-Amsterdam floor check (same byte string, same point in execution).
320    pub calldata_cost: u64,
321}
322
323impl<'a> VM<'a> {
324    /// Sets the account code as the EIP7702 determines.
325    /// EIP-8037 Rule 1 (Amsterdam+): an INVALID EIP-7702 authorization is skipped
326    /// without per-auth processing, so the worst-case intrinsic gas charged for it
327    /// from the auth-list length must be credited back. Its entire state-gas portion
328    /// `(STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) × CPSB`
329    /// (`state_gas_auth_total`) is refilled to the reservoir and the tx-level state
330    /// refund channel, and the `ACCOUNT_WRITE` regular surcharge is refunded — so the
331    /// net per-tx state gas of a skipped authorization is zero (the report subtracts
332    /// `state_refund`). Pre-Amsterdam has no state-gas dimension and uses the legacy
333    /// auth-cost model, so this is a no-op there.
334    fn refund_skipped_authorization(&mut self, refunded_gas: &mut u64) -> Result<(), VMError> {
335        if self.env.config.fork < Fork::Amsterdam {
336            return Ok(());
337        }
338        self.state_gas_reservoir = self
339            .state_gas_reservoir
340            .checked_add(self.state_gas_auth_total)
341            .ok_or(InternalError::Overflow)?;
342        self.state_refund = self
343            .state_refund
344            .checked_add(self.state_gas_auth_total)
345            .ok_or(InternalError::Overflow)?;
346        *refunded_gas = refunded_gas
347            .checked_add(ACCOUNT_WRITE_AMSTERDAM)
348            .ok_or(InternalError::Overflow)?;
349        Ok(())
350    }
351
352    pub fn eip7702_set_access_code(&mut self) -> Result<(), VMError> {
353        let mut refunded_gas: u64 = 0;
354        // EELS `delegated_before_tx`: whether each authority was already delegated at
355        // tx start. The first time an authority is seen its code is still the pre-tx
356        // code (auth processing runs before execution), so we cache it then.
357        let mut delegated_before_tx: FxHashMap<Address, bool> = FxHashMap::default();
358
359        // IMPORTANT:
360        // If any of the below steps fail, immediately stop processing that tuple and continue to the next tuple in the list. It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence.
361        // If transaction execution results in failure (any exceptional condition or code reverting), setting delegation designations is not rolled back.
362        for auth_tuple in self.tx.authorization_list().cloned().unwrap_or_default() {
363            let chain_id_not_equals_this_chain_id = auth_tuple.chain_id != self.env.chain_id;
364            let chain_id_not_zero = !auth_tuple.chain_id.is_zero();
365
366            // 1. Verify the chain id is either 0 or the chain’s current ID.
367            if chain_id_not_zero && chain_id_not_equals_this_chain_id {
368                self.refund_skipped_authorization(&mut refunded_gas)?;
369                continue;
370            }
371
372            // 2. Verify the nonce is less than 2**64 - 1.
373            // NOTE: nonce is a u64, it's always less than or equal to u64::MAX
374            if auth_tuple.nonce == u64::MAX {
375                self.refund_skipped_authorization(&mut refunded_gas)?;
376                continue;
377            }
378
379            // 3. authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s)
380            //      s value must be less than or equal to secp256k1n/2, as specified in EIP-2.
381            let Some(authority_address) = eip7702_recover_address(&auth_tuple, self.crypto)? else {
382                self.refund_skipped_authorization(&mut refunded_gas)?;
383                continue;
384            };
385
386            // 4. Add authority to accessed_addresses (as defined in EIP-2929).
387            let authority_account = self.db.get_account(authority_address)?;
388            let authority_exists = authority_account.exists;
389            let authority_info = authority_account.info.clone();
390            let authority_code = self.db.get_code(authority_info.code_hash)?;
391            self.substate.add_accessed_address(authority_address);
392
393            // 5. Verify the code of authority is either empty or already delegated.
394            // Check this BEFORE recording to BAL so we can release the borrow on authority_code.
395            let authority_code_is_empty = authority_code.is_empty();
396            let delegated_now = code_has_delegation(authority_code.code())?;
397            let empty_or_delegated = authority_code_is_empty || delegated_now;
398            // First sighting of an authority captures its pre-tx delegation state.
399            let pre_delegated = *delegated_before_tx
400                .entry(authority_address)
401                .or_insert(delegated_now);
402
403            // Record authority as touched for BAL per EIP-7928, even if validation fails later.
404            // This ensures authority appears in BAL with empty change set when:
405            // - Authority was loaded (above)
406            // - But validation fails (checks below)
407            if let Some(recorder) = self.db.bal_recorder.as_mut() {
408                recorder.record_touched_address(authority_address);
409            }
410
411            if !empty_or_delegated {
412                self.refund_skipped_authorization(&mut refunded_gas)?;
413                continue;
414            }
415
416            // 6. Verify the nonce of authority is equal to nonce. In case authority does not exist in the trie, verify that nonce is equal to 0.
417            // If it doesn't exist, it means the nonce is zero. The get_account() function will return Account::default()
418            // If it has nonce, the account.info.nonce should equal auth_tuple.nonce
419            if authority_info.nonce != auth_tuple.nonce {
420                self.refund_skipped_authorization(&mut refunded_gas)?;
421                continue;
422            }
423
424            // 7. Refund if authority exists in the trie.
425            // EIP-8037 (Amsterdam+): return STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte
426            // to the state gas reservoir (the new-account portion of the auth state charge).
427            // Pre-Amsterdam: add REFUND_AUTH_PER_EXISTING_ACCOUNT (12500) to global refund counter.
428            // NOTE: Uses `exists` (account_exists in EELS / Exist in geth), NOT `!is_empty()`.
429            // An account can exist in the trie but be empty (e.g., has non-empty storage root).
430            if authority_exists {
431                if self.env.config.fork >= Fork::Amsterdam {
432                    // EIP-7702: refund
433                    // `STATE_BYTES_PER_NEW_ACCOUNT * cpsb` for each existing authority via
434                    // two independent channels:
435                    //   1. `state_gas_reservoir += refund` — sender gets the gas back via
436                    //      receipt refund at tx finalize.
437                    //   2. `state_refund += refund` — block-level state-gas accounting
438                    //      subtracts this at refund_sender (mirrors EELS
439                    //      `MessageCallOutput.state_refund`).
440                    // `state_gas_used` is NOT decremented here: the refund goes through
441                    // `state_refund` (tx-level channel) so block-level accounting subtracts it.
442                    let refund = self.state_gas_new_account;
443                    self.state_gas_reservoir = self
444                        .state_gas_reservoir
445                        .checked_add(refund)
446                        .ok_or(InternalError::Overflow)?;
447                    self.state_refund = self
448                        .state_refund
449                        .checked_add(refund)
450                        .ok_or(InternalError::Overflow)?;
451                    // EIP-8038: the ACCOUNT_WRITE charged per auth at intrinsic time is
452                    // not needed for an existing authority; refund it (regular counter).
453                    refunded_gas = refunded_gas
454                        .checked_add(ACCOUNT_WRITE_AMSTERDAM)
455                        .ok_or(InternalError::Overflow)?;
456                } else {
457                    refunded_gas = refunded_gas
458                        .checked_add(REFUND_AUTH_PER_EXISTING_ACCOUNT)
459                        .ok_or(InternalError::Overflow)?;
460                }
461            }
462
463            // EIP-7702 AUTH_BASE state-gas refill, mirroring EELS `set_delegation`:
464            //   clear (auth.address == 0): refund AUTH_BASE; +AUTH_BASE more if the
465            //     slot was delegated this tx but not at tx start (delegated_now &&
466            //     !delegated_before_tx), undoing the prior auth's AUTH_BASE charge.
467            //   set (auth.address != 0): refund AUTH_BASE if already delegated now or
468            //     at tx start (no new indicator bytes are created).
469            if self.env.config.fork >= Fork::Amsterdam {
470                let auth_base_refills: u64 = if auth_tuple.address == Address::zero() {
471                    if delegated_now && !pre_delegated {
472                        2
473                    } else {
474                        1
475                    }
476                } else {
477                    u64::from(delegated_now || pre_delegated)
478                };
479                if auth_base_refills > 0 {
480                    let refund = self
481                        .state_gas_auth_base
482                        .checked_mul(auth_base_refills)
483                        .ok_or(InternalError::Overflow)?;
484                    self.state_gas_reservoir = self
485                        .state_gas_reservoir
486                        .checked_add(refund)
487                        .ok_or(InternalError::Overflow)?;
488                    self.state_refund = self
489                        .state_refund
490                        .checked_add(refund)
491                        .ok_or(InternalError::Overflow)?;
492                }
493            }
494
495            // 8. Set the code of authority to be 0xef0100 || address. This is a delegation designation.
496            let delegation_bytes = [
497                &SET_CODE_DELEGATION_BYTES[..],
498                auth_tuple.address.as_bytes(),
499            ]
500            .concat();
501
502            // As a special case, if address is 0x0000000000000000000000000000000000000000 do not write the designation.
503            // Clear the account’s code and reset the account’s code hash to the empty hash.
504            let code = if auth_tuple.address != Address::zero() {
505                delegation_bytes.into()
506            } else {
507                Bytes::new()
508            };
509            self.update_account_bytecode(
510                authority_address,
511                Code::from_bytecode(code, self.crypto),
512            )?;
513
514            // 9. Increase the nonce of authority by one.
515            self.increment_account_nonce(authority_address)
516                .map_err(|_| TxValidationError::NonceIsMax)?;
517        }
518
519        self.substate.refunded_gas = self
520            .substate
521            .refunded_gas
522            .checked_add(refunded_gas)
523            .ok_or(InternalError::Overflow)?;
524
525        Ok(())
526    }
527
528    pub fn add_intrinsic_gas(&mut self, intrinsic: &IntrinsicGas) -> Result<(), VMError> {
529        // Intrinsic gas is the gas consumed by the transaction before the execution of the opcodes. Section 6.2 in the Yellow Paper.
530
531        let regular_gas = intrinsic.regular;
532        let state_gas = intrinsic.state;
533
534        let total_gas = regular_gas.checked_add(state_gas).ok_or(OutOfGas)?;
535
536        self.current_call_frame
537            .increase_consumed_gas(total_gas)
538            .map_err(|_| TxValidationError::IntrinsicGasTooLow)?;
539
540        // state_gas_used is i64; intrinsic state gas is bounded by tx gas limit (< i64::MAX).
541        self.state_gas_used = self
542            .state_gas_used
543            .checked_add(i64::try_from(state_gas).map_err(|_| InternalError::Overflow)?)
544            .ok_or(InternalError::Overflow)?;
545        // Remember the intrinsic split so we can leave it in state_gas_used on top-level
546        // error (matches EELS `tx_env.intrinsic_state_gas`, which is kept separate from
547        // `tx_output.state_gas_used` and never refunded).
548        debug_assert_eq!(self.intrinsic_state_gas, 0, "intrinsic_state_gas set twice");
549        self.intrinsic_state_gas = state_gas;
550
551        // EIP-8037 (Amsterdam+): compute state gas reservoir from excess gas_limit.
552        // execution_gas = what remains after all intrinsic gas; regular_gas_budget = how much
553        // regular execution gas is allowed (capped at TX_MAX_GAS_LIMIT_AMSTERDAM); the difference becomes
554        // the reservoir for drawing state gas without consuming regular gas_remaining.
555        if self.env.config.fork >= Fork::Amsterdam {
556            if self.env.is_system_call {
557                // EIP-8037: system
558                // transactions get a dedicated state-gas reservoir of
559                // `state_gas_storage_set * SYSTEM_MAX_SSTORES_PER_CALL` ON TOP of
560                // the full SYS_CALL_GAS_LIMIT regular budget — so SSTORE-heavy
561                // system contracts (EIP-2935, EIP-4788) cannot OOG on state-gas
562                // growth alone. Skip the regular reservoir computation so we don't
563                // pre-consume `gas_remaining`; EELS sets `intrinsic_regular_gas=0`
564                // and `gas=SYSTEM_TRANSACTION_GAS` for the message
565                // (amsterdam/fork.py::process_unchecked_system_transaction).
566                let sys_reservoir = self
567                    .state_gas_storage_set
568                    .saturating_mul(SYSTEM_MAX_SSTORES_PER_CALL);
569                self.state_gas_reservoir = sys_reservoir;
570                self.state_gas_reservoir_initial = sys_reservoir;
571            } else {
572                let gas_limit = self.tx.gas_limit();
573                let execution_gas = gas_limit.saturating_sub(total_gas);
574                let regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM.saturating_sub(regular_gas);
575                let gas_left = regular_gas_budget.min(execution_gas);
576                let reservoir = execution_gas.saturating_sub(gas_left);
577                if reservoir > 0 {
578                    // Pre-consume reservoir from gas_remaining so GAS opcode returns <= TX_MAX_GAS_LIMIT_AMSTERDAM
579                    let reservoir_i64 =
580                        i64::try_from(reservoir).map_err(|_| InternalError::Overflow)?;
581                    self.current_call_frame.gas_remaining = self
582                        .current_call_frame
583                        .gas_remaining
584                        .checked_sub(reservoir_i64)
585                        .ok_or(InternalError::Overflow)?;
586                    self.state_gas_reservoir = reservoir;
587                }
588                // Capture initial reservoir for block-dimensional regular gas computation.
589                self.state_gas_reservoir_initial = reservoir;
590            }
591
592            // EIP-8037: seed the top frame's state-gas entry baseline to the post-intrinsic
593            // value of `state_gas_used` (the intrinsic state gas was already added above).
594            // A top-level revert/halt refill (`refill_frame_state_gas`) then rolls back only
595            // the execution portion and preserves the intrinsic state gas (which EELS keeps
596            // separate as `tx_env.intrinsic_state_gas` and never refunds).
597            self.current_call_frame.state_gas_used_at_entry = self.state_gas_used;
598        }
599
600        Ok(())
601    }
602
603    // ==================== Gas related functions =======================
604    /// Returns `(regular_gas, state_gas)` intrinsic gas for the transaction.
605    /// For Amsterdam+, state_gas is the EIP-8037 state portion.
606    /// For pre-Amsterdam, state_gas is always 0.
607    pub fn get_intrinsic_gas(&self) -> Result<IntrinsicGas, VMError> {
608        // Intrinsic Gas = Calldata cost + Create cost + Base cost + Access list cost
609        let mut regular_gas: u64 = 0;
610        let mut state_gas: u64 = 0;
611        let fork = self.env.config.fork;
612
613        // Calldata Cost
614        // 4 gas for each zero byte in the transaction data 16 gas for each non-zero byte in the transaction.
615        let calldata_cost = gas_cost::tx_calldata(&self.current_call_frame.calldata)?;
616
617        regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;
618
619        let is_create = self.is_create()?;
620
621        if fork >= Fork::Amsterdam {
622            // EIP-2780 (merged EIPs#11645): resource-based intrinsic gas.
623            // The flat 21000 base is decomposed into a sender base + recipient
624            // access charge + value-transfer charge. These tx-level sender/to
625            // charges are ALWAYS the cold rate; access lists do NOT warm
626            // tx-level accounts. Because the intrinsic uses fixed constants
627            // (not substate warmth), the cold rate is applied automatically.
628            let sender = self.env.origin;
629            let to = self.tx.to();
630            let is_self_transfer = matches!(to, TxKind::Call(addr) if addr == sender);
631
632            // Sender base: always TX_BASE_COST_AMSTERDAM (12000).
633            regular_gas = regular_gas
634                .checked_add(tx_base_cost(fork))
635                .ok_or(OutOfGas)?;
636
637            // tx.to charge.
638            if is_self_transfer {
639                // sender == to: no recipient access charge.
640            } else if is_create {
641                // Contract-creation: CREATE_ACCESS_AMSTERDAM regular + state gas
642                // for the new account (EIP-8037).
643                regular_gas = regular_gas
644                    .checked_add(CREATE_ACCESS_AMSTERDAM)
645                    .ok_or(OutOfGas)?;
646                state_gas = state_gas
647                    .checked_add(self.state_gas_new_account)
648                    .ok_or(OutOfGas)?;
649            } else {
650                // Regular call to a distinct account: cold account access.
651                regular_gas = regular_gas
652                    .checked_add(cold_account_access_cost(fork))
653                    .ok_or(OutOfGas)?;
654            }
655
656            // tx.value charge.
657            let value_is_zero = self.tx.value().is_zero();
658            if value_is_zero || is_self_transfer {
659                // zero value or self-transfer: no value-transfer charge.
660            } else if is_create {
661                // non-zero value contract-creation: transfer-log cost only.
662                regular_gas = regular_gas
663                    .checked_add(TRANSFER_LOG_COST_AMSTERDAM)
664                    .ok_or(OutOfGas)?;
665            } else {
666                // non-zero value to a distinct account: transfer-log + value cost.
667                regular_gas = regular_gas
668                    .checked_add(TRANSFER_LOG_COST_AMSTERDAM)
669                    .ok_or(OutOfGas)?
670                    .checked_add(TX_VALUE_COST_AMSTERDAM)
671                    .ok_or(OutOfGas)?;
672            }
673        } else {
674            // Base Cost
675            regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;
676
677            // Create Cost
678            if is_create {
679                // https://eips.ethereum.org/EIPS/eip-2#specification
680                regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
681            }
682        }
683
684        // EIP-3860 init code words (Shanghai+), unchanged by EIP-2780.
685        if is_create && fork >= Fork::Shanghai {
686            let number_of_words = &self.current_call_frame.calldata.len().div_ceil(WORD_SIZE);
687            let double_number_of_words: u64 = number_of_words
688                .checked_mul(2)
689                .ok_or(OutOfGas)?
690                .try_into()
691                .map_err(|_| InternalError::TypeConversion)?;
692
693            regular_gas = regular_gas
694                .checked_add(double_number_of_words)
695                .ok_or(OutOfGas)?;
696        }
697
698        // Access List Cost
699        let mut access_lists_cost: u64 = 0;
700        for (_, keys) in self.tx.access_list() {
701            access_lists_cost = access_lists_cost
702                .checked_add(gas_cost::access_list_address_cost(fork))
703                .ok_or(OutOfGas)?;
704            for _ in keys {
705                access_lists_cost = access_lists_cost
706                    .checked_add(gas_cost::access_list_storage_key_cost(fork))
707                    .ok_or(OutOfGas)?;
708            }
709        }
710
711        // EIP-7981 (Amsterdam+): access-list data bytes also contribute to the regular arm.
712        // access_list_cost += floor_tokens_in_access_list * total_cost_floor_per_token
713        // = access_list_bytes * STANDARD_TOKEN_COST * total_cost_floor_per_token
714        // Effective: +1280 per address, +2048 per storage key.
715        if fork >= Fork::Amsterdam {
716            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
717            let al_data_cost = al_floor_tokens
718                .checked_mul(total_cost_floor_per_token(fork))
719                .ok_or(InternalError::Overflow)?;
720            access_lists_cost = access_lists_cost
721                .checked_add(al_data_cost)
722                .ok_or(InternalError::Overflow)?;
723        }
724
725        regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;
726
727        // Authorization List Cost
728        // `unwrap_or_default` will return an empty vec when the `authorization_list` field is None.
729        // If the vec is empty, the len will be 0, thus the authorization_list_cost is 0.
730        let amount_of_auth_tuples: u64 = match self.tx.authorization_list() {
731            None => 0,
732            Some(list) => list
733                .len()
734                .try_into()
735                .map_err(|_| InternalError::TypeConversion)?,
736        };
737
738        if fork >= Fork::Amsterdam {
739            // EIP-8038: per-auth regular = ACCOUNT_WRITE + PER_AUTH_BASE_COST_AMSTERDAM;
740            // ACCOUNT_WRITE is refunded for existing authorities (process_authorization_list).
741            // State is STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte.
742            let regular_auth_cost = ACCOUNT_WRITE_AMSTERDAM
743                .checked_add(PER_AUTH_BASE_COST_AMSTERDAM)
744                .ok_or(InternalError::Overflow)?
745                .checked_mul(amount_of_auth_tuples)
746                .ok_or(InternalError::Overflow)?;
747            regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
748            let state_auth_cost = self
749                .state_gas_auth_total
750                .checked_mul(amount_of_auth_tuples)
751                .ok_or(InternalError::Overflow)?;
752            state_gas = state_gas.checked_add(state_auth_cost).ok_or(OutOfGas)?;
753        } else {
754            let authorization_list_cost = PER_EMPTY_ACCOUNT_COST
755                .checked_mul(amount_of_auth_tuples)
756                .ok_or(InternalError::Overflow)?;
757            regular_gas = regular_gas
758                .checked_add(authorization_list_cost)
759                .ok_or(OutOfGas)?;
760        }
761
762        Ok(IntrinsicGas {
763            regular: regular_gas,
764            state: state_gas,
765            calldata_cost,
766        })
767    }
768
769    /// Calculates the minimum gas to be consumed in the transaction.
770    pub fn get_min_gas_used(&self) -> Result<u64, VMError> {
771        let fork = self.env.config.fork;
772
773        // If the transaction is a CREATE transaction, the calldata is emptied and the bytecode is assigned.
774        let calldata = if self.is_create()? {
775            self.current_call_frame.bytecode.code()
776        } else {
777            self.current_call_frame.calldata.as_ref()
778        };
779
780        // EIP-7976 floor tokens: for the floor arm, all calldata bytes count unweighted.
781        // floor_tokens_in_calldata = (zero_bytes + nonzero_bytes) * STANDARD_TOKEN_COST
782        // Pre-Amsterdam uses the weighted EIP-7623 formula: (nonzero * 16 + zero * 4) / 4
783        let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
784            // EIP-7976: floor tokens = total_bytes * STANDARD_TOKEN_COST (unweighted).
785            let total_bytes: u64 = calldata
786                .len()
787                .try_into()
788                .map_err(|_| InternalError::TypeConversion)?;
789            total_bytes
790                .checked_mul(STANDARD_TOKEN_COST)
791                .ok_or(InternalError::Overflow)?
792        } else {
793            // Pre-Amsterdam: weighted EIP-7623 token count.
794            gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
795        };
796
797        // EIP-7981 (Amsterdam+): access-list data bytes fold into the floor-token count.
798        // floor_tokens_in_access_list = access_list_bytes * STANDARD_TOKEN_COST
799        // where access_list_bytes = 20 * address_count + 32 * storage_key_count.
800        if fork >= Fork::Amsterdam {
801            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
802            tokens_in_calldata = tokens_in_calldata
803                .checked_add(al_floor_tokens)
804                .ok_or(InternalError::Overflow)?;
805        }
806
807        // EELS `data_floor_gas_cost = total_floor_tokens * TX_DATA_TOKEN_FLOOR + TX_BASE`.
808        // Floor base is `tx_base_cost(fork)`: 21000 pre-Amsterdam, 12000 at Amsterdam
809        // (EIP-2780). EIP-7976 raises the per-token rate from 10 to 16.
810        let mut min_gas_used: u64 = tokens_in_calldata
811            .checked_mul(total_cost_floor_per_token(fork))
812            .ok_or(InternalError::Overflow)?;
813
814        min_gas_used = min_gas_used
815            .checked_add(tx_base_cost(fork))
816            .ok_or(InternalError::Overflow)?;
817
818        Ok(min_gas_used)
819    }
820
821    /// Gets transaction callee, calculating create address if it's a "Create" transaction.
822    /// Bool indicates whether it is a `create` transaction or not.
823    pub fn get_tx_callee(
824        tx: &Transaction,
825        db: &mut GeneralizedDatabase,
826        env: &Environment,
827        substate: &mut Substate,
828    ) -> Result<(Address, bool), VMError> {
829        match tx.to() {
830            TxKind::Call(address_to) => {
831                substate.add_accessed_address(address_to);
832
833                Ok((address_to, false))
834            }
835
836            TxKind::Create => {
837                let sender_nonce = db.get_account(env.origin)?.info.nonce;
838
839                let created_address = calculate_create_address(env.origin, sender_nonce);
840
841                substate.add_accessed_address(created_address);
842                substate.add_created_account(created_address);
843
844                Ok((created_address, true))
845            }
846        }
847    }
848}
849
850/// Compute `(regular, state)` intrinsic gas for a transaction without needing
851/// a full VM instance. Mirrors `VM::get_intrinsic_gas` but operates on the raw
852/// transaction, fork, and block gas limit (for cpsb derivation). Pre-Amsterdam
853/// returns `(regular, 0)`.
854///
855/// Used by the block executor to perform the EIP-8037 (PR #2703) per-tx 2D
856/// inclusion check before the tx runs.
857///
858/// `sender` is the transaction's recovered sender; it is required for the
859/// EIP-2780 (Amsterdam+) self-transfer rule, which zeroes the recipient and
860/// value charges when `sender == tx.to`. The parameter is taken
861/// unconditionally so this function stays byte-identical with
862/// `VM::get_intrinsic_gas` across every tx shape (guarded by
863/// `test_intrinsic_parity_*`). A type-3/type-4 tx can never carry `to == sender`
864/// in practice, but the parameter is still threaded through for parity.
865pub fn intrinsic_gas_dimensions(
866    tx: &Transaction,
867    sender: Address,
868    fork: Fork,
869    block_gas_limit: u64,
870) -> Result<(u64, u64), VMError> {
871    let mut regular_gas: u64 = 0;
872    let mut state_gas: u64 = 0;
873
874    let (state_gas_new_account, state_gas_auth_total) = if fork >= Fork::Amsterdam {
875        let cpsb = cost_per_state_byte(block_gas_limit);
876        (
877            STATE_BYTES_PER_NEW_ACCOUNT
878                .checked_mul(cpsb)
879                .ok_or(InternalError::Overflow)?,
880            STATE_BYTES_PER_AUTH_TOTAL
881                .checked_mul(cpsb)
882                .ok_or(InternalError::Overflow)?,
883        )
884    } else {
885        (0, 0)
886    };
887
888    // Calldata cost (EIP-2028 weighted)
889    let calldata_cost = gas_cost::tx_calldata(tx.data())?;
890    regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;
891
892    let to = tx.to();
893    let is_create = matches!(to, TxKind::Create);
894
895    if fork >= Fork::Amsterdam {
896        // EIP-2780 (merged EIPs#11645): resource-based intrinsic gas.
897        // Mirror of `VM::get_intrinsic_gas`. These tx-level sender/to charges
898        // are ALWAYS the cold rate; access lists do NOT warm tx-level accounts.
899        let is_self_transfer = matches!(to, TxKind::Call(addr) if addr == sender);
900
901        // Sender base: always TX_BASE_COST_AMSTERDAM (12000).
902        regular_gas = regular_gas
903            .checked_add(tx_base_cost(fork))
904            .ok_or(OutOfGas)?;
905
906        // tx.to charge.
907        if is_self_transfer {
908            // sender == to: no recipient access charge.
909        } else if is_create {
910            regular_gas = regular_gas
911                .checked_add(CREATE_ACCESS_AMSTERDAM)
912                .ok_or(OutOfGas)?;
913            state_gas = state_gas
914                .checked_add(state_gas_new_account)
915                .ok_or(OutOfGas)?;
916        } else {
917            regular_gas = regular_gas
918                .checked_add(cold_account_access_cost(fork))
919                .ok_or(OutOfGas)?;
920        }
921
922        // tx.value charge.
923        let value_is_zero = tx.value().is_zero();
924        if value_is_zero || is_self_transfer {
925            // zero value or self-transfer: no value-transfer charge.
926        } else if is_create {
927            regular_gas = regular_gas
928                .checked_add(TRANSFER_LOG_COST_AMSTERDAM)
929                .ok_or(OutOfGas)?;
930        } else {
931            regular_gas = regular_gas
932                .checked_add(TRANSFER_LOG_COST_AMSTERDAM)
933                .ok_or(OutOfGas)?
934                .checked_add(TX_VALUE_COST_AMSTERDAM)
935                .ok_or(OutOfGas)?;
936        }
937    } else {
938        // Base cost
939        regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;
940
941        if is_create {
942            regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
943        }
944    }
945
946    // EIP-3860 init code words (Shanghai+), unchanged by EIP-2780.
947    if is_create && fork >= Fork::Shanghai {
948        let words = tx.data().len().div_ceil(WORD_SIZE);
949        let double_words: u64 = words
950            .checked_mul(2)
951            .ok_or(OutOfGas)?
952            .try_into()
953            .map_err(|_| InternalError::TypeConversion)?;
954        regular_gas = regular_gas.checked_add(double_words).ok_or(OutOfGas)?;
955    }
956
957    // Access list cost
958    let mut access_lists_cost: u64 = 0;
959    for (_, keys) in tx.access_list() {
960        access_lists_cost = access_lists_cost
961            .checked_add(gas_cost::access_list_address_cost(fork))
962            .ok_or(OutOfGas)?;
963        for _ in keys {
964            access_lists_cost = access_lists_cost
965                .checked_add(gas_cost::access_list_storage_key_cost(fork))
966                .ok_or(OutOfGas)?;
967        }
968    }
969
970    // EIP-7981 (Amsterdam+): access-list data bytes fold into regular gas
971    if fork >= Fork::Amsterdam {
972        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
973        let al_data_cost = al_floor_tokens
974            .checked_mul(total_cost_floor_per_token(fork))
975            .ok_or(InternalError::Overflow)?;
976        access_lists_cost = access_lists_cost
977            .checked_add(al_data_cost)
978            .ok_or(InternalError::Overflow)?;
979    }
980    regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;
981
982    // Authorization list cost
983    let amount_of_auth_tuples: u64 = match tx.authorization_list() {
984        None => 0,
985        Some(list) => list
986            .len()
987            .try_into()
988            .map_err(|_| InternalError::TypeConversion)?,
989    };
990
991    if fork >= Fork::Amsterdam {
992        // EIP-8038: ACCOUNT_WRITE + PER_AUTH_BASE_COST_AMSTERDAM per auth (charged amount;
993        // ACCOUNT_WRITE is refunded for existing authorities at execution). Mirrors
994        // `VM::get_intrinsic_gas`.
995        let regular_auth_cost = ACCOUNT_WRITE_AMSTERDAM
996            .checked_add(PER_AUTH_BASE_COST_AMSTERDAM)
997            .ok_or(InternalError::Overflow)?
998            .checked_mul(amount_of_auth_tuples)
999            .ok_or(InternalError::Overflow)?;
1000        regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
1001        let state_auth_cost = state_gas_auth_total
1002            .checked_mul(amount_of_auth_tuples)
1003            .ok_or(InternalError::Overflow)?;
1004        state_gas = state_gas.checked_add(state_auth_cost).ok_or(OutOfGas)?;
1005    } else {
1006        let auth_cost = PER_EMPTY_ACCOUNT_COST
1007            .checked_mul(amount_of_auth_tuples)
1008            .ok_or(InternalError::Overflow)?;
1009        regular_gas = regular_gas.checked_add(auth_cost).ok_or(OutOfGas)?;
1010    }
1011
1012    Ok((regular_gas, state_gas))
1013}
1014
1015/// Standalone EIP-7623/7976/7981 floor gas for a transaction. Mirrors
1016/// [`VM::get_min_gas_used`] but operates on the raw transaction + fork, so it
1017/// can be called by mempool admission / the payload builder without needing a
1018/// VM instance. Returns `TX_BASE_COST + floor_rate * total_floor_tokens`.
1019///
1020/// Amsterdam+ uses the unweighted EIP-7976 floor (16 gas/token = 64 gas/byte)
1021/// and folds EIP-7981 access-list data bytes into the token count. Pre-
1022/// Amsterdam uses the weighted EIP-7623 formula.
1023///
1024/// A mismatch between this and `VM::get_min_gas_used` would cause mempool
1025/// admission to drift from VM rejection; keep the two in sync. The
1026/// `test_intrinsic_parity_*` suite also guards this.
1027pub fn intrinsic_gas_floor(tx: &Transaction, fork: Fork) -> Result<u64, VMError> {
1028    // EIP-7976: floor tokens count ALL calldata bytes unweighted. For CREATE
1029    // txs the calldata is the init code. Mirrors `get_min_gas_used`.
1030    let calldata = tx.data();
1031
1032    let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
1033        let total_bytes: u64 = calldata
1034            .len()
1035            .try_into()
1036            .map_err(|_| InternalError::TypeConversion)?;
1037        total_bytes
1038            .checked_mul(STANDARD_TOKEN_COST)
1039            .ok_or(InternalError::Overflow)?
1040    } else {
1041        gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
1042    };
1043
1044    if fork >= Fork::Amsterdam {
1045        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
1046        tokens_in_calldata = tokens_in_calldata
1047            .checked_add(al_floor_tokens)
1048            .ok_or(InternalError::Overflow)?;
1049    }
1050
1051    // Floor base `tx_base_cost(fork)` (12000 at Amsterdam); mirrors `get_min_gas_used`.
1052    tokens_in_calldata
1053        .checked_mul(total_cost_floor_per_token(fork))
1054        .ok_or(InternalError::Overflow)?
1055        .checked_add(tx_base_cost(fork))
1056        .ok_or(InternalError::Overflow.into())
1057}
1058
1059/// Converts Account to LevmAccount
1060/// The problem with this is that we don't have the storage root.
1061pub fn account_to_levm_account(account: Account) -> (LevmAccount, Code) {
1062    (
1063        LevmAccount {
1064            info: account.info,
1065            has_storage: !account.storage.is_empty(), // This is used in scenarios in which the storage is already all in the account. For the Levm Runner
1066            storage: account.storage,
1067            status: AccountStatus::Unmodified,
1068            exists: true,
1069        },
1070        account.code,
1071    )
1072}
1073
1074/// Converts a U256 value into usize, returning an error if the value is over 32 bits
1075/// This is generally used for memory offsets and sizes, 32 bits is more than enough for this purpose.
1076#[expect(clippy::as_conversions)]
1077pub fn u256_to_usize(val: U256) -> Result<usize, VMError> {
1078    if val.0[0] > u32::MAX as u64 || val.0[1] != 0 || val.0[2] != 0 || val.0[3] != 0 {
1079        return Err(VMError::ExceptionalHalt(ExceptionalHalt::VeryLargeNumber));
1080    }
1081    Ok(val.0[0] as usize)
1082}
1083
1084/// Converts U256 size and offset to usize.
1085/// If the size is zero, the offset will be zero regardless of its original value as it is not relevant
1086pub fn size_offset_to_usize(size: U256, offset: U256) -> Result<(usize, usize), VMError> {
1087    if size.is_zero() {
1088        // Offset is irrelevant
1089        Ok((0, 0))
1090    } else {
1091        Ok((u256_to_usize(size)?, u256_to_usize(offset)?))
1092    }
1093}
1094
1095// ==================== EIP-7708 Helper Functions ====================
1096
1097/// Creates EIP-7708 Transfer log (LOG3) for ETH transfers.
1098/// Emitted from SYSTEM_ADDRESS when ETH is transferred.
1099#[inline]
1100pub fn create_eth_transfer_log(from: Address, to: Address, value: U256) -> Log {
1101    let mut from_topic = [0u8; 32];
1102    from_topic[12..].copy_from_slice(from.as_bytes());
1103
1104    let mut to_topic = [0u8; 32];
1105    to_topic[12..].copy_from_slice(to.as_bytes());
1106
1107    let data = value.to_big_endian();
1108
1109    Log {
1110        address: SYSTEM_ADDRESS,
1111        topics: vec![
1112            TRANSFER_EVENT_TOPIC,
1113            H256::from(from_topic),
1114            H256::from(to_topic),
1115        ],
1116        data: Bytes::from(data.to_vec()),
1117    }
1118}