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}