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