Skip to main content

ethrex_levm/hooks/
l2_hook.rs

1use crate::{
2    constants::{POST_OSAKA_GAS_LIMIT_CAP, TX_MAX_GAS_LIMIT_AMSTERDAM},
3    db::gen_db::GeneralizedDatabase,
4    errors::{ContextResult, ExecutionReport, InternalError, TxValidationError, VMError},
5    hooks::{DefaultHook, default_hook, hook::Hook},
6    opcodes::Opcode,
7    tracing::LevmCallTracer,
8    vm::{VM, VMType},
9};
10
11use bytes::Bytes;
12use ethrex_common::{
13    Address, H160, H256, U256,
14    constants::GAS_PER_BLOB,
15    types::{
16        Code, EIP1559Transaction, Fork, Transaction, TxKind,
17        {
18            SAFE_BYTES_PER_BLOB,
19            fee_config::{FeeConfig, L1FeeConfig, OperatorFeeConfig},
20        },
21    },
22};
23use ethrex_rlp::encode::RLPEncode;
24
25pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([
26    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
27    0x00, 0x00, 0xff, 0xff,
28]);
29pub const FEE_TOKEN_REGISTRY_ADDRESS: Address = H160([
30    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
31    0x00, 0x00, 0xff, 0xfc,
32]);
33pub const FEE_TOKEN_RATIO_ADDRESS: Address = H160([
34    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
35    0x00, 0x00, 0xff, 0xfb,
36]);
37
38// lockFee(address payer, uint256 amount) public onlyBridge
39const LOCK_FEE_SELECTOR: [u8; 4] = [0x89, 0x9c, 0x86, 0xe2];
40// payFee(address receiver, uint256 amount) public onlyBridge
41const PAY_FEE_SELECTOR: [u8; 4] = [0x72, 0x74, 0x6e, 0xaf];
42// isFeeToken(address token) external view override returns (bool)
43const IS_FEE_TOKEN_SELECTOR: [u8; 4] = [0x16, 0xad, 0x82, 0xd7];
44// getFeeTokenRatio(address token) external view returns (uint256)
45const FEE_TOKEN_RATIO_SELECTOR: [u8; 4] = [0xc6, 0xab, 0x85, 0xd8];
46const SIMULATION_GAS_LIMIT: u64 = 21000 * 100;
47const SIMULATION_MAX_FEE: u64 = 100;
48
49pub struct L2Hook {
50    pub fee_config: FeeConfig,
51    /// Cached fee-token ratio from `prepare_execution_fee_token`.
52    /// Reused in `finalize_non_privileged_execution` to ensure both phases
53    /// use the same ratio, even if the tx modifies the ratio contract mid-execution.
54    cached_fee_token_ratio: Option<U256>,
55}
56
57impl L2Hook {
58    pub fn new(fee_config: FeeConfig) -> Self {
59        Self {
60            fee_config,
61            cached_fee_token_ratio: None,
62        }
63    }
64}
65
66impl Hook for L2Hook {
67    fn prepare_execution(&mut self, vm: &mut VM<'_>) -> Result<(), crate::errors::VMError> {
68        if vm.env.is_privileged {
69            return prepare_execution_privileged(vm);
70        } else if vm.env.fee_token.is_some() {
71            let ratio = prepare_execution_fee_token(vm)?;
72            self.cached_fee_token_ratio = Some(ratio);
73        } else {
74            DefaultHook.prepare_execution(vm)?;
75        }
76        // Different from L1:
77        // Max fee per gas must be sufficient to cover base fee + operator fee
78        validate_sufficient_max_fee_per_gas_l2(vm, &self.fee_config.operator_fee_config)?;
79        // Reserve L1 gas from the execution budget so execution can't consume it.
80        // If gas_limit < intrinsic_gas + l1_gas, this returns L1GasReservationTooLow.
81        reserve_l1_gas(vm, &self.fee_config.l1_fee_config)?;
82        Ok(())
83    }
84
85    fn finalize_execution(
86        &mut self,
87        vm: &mut VM<'_>,
88        ctx_result: &mut ContextResult,
89    ) -> Result<(), crate::errors::VMError> {
90        if vm.env.is_privileged {
91            if !ctx_result.is_success() && vm.env.origin != COMMON_BRIDGE_L2_ADDRESS {
92                default_hook::undo_value_transfer(vm)?;
93            }
94            // Even if privileged transactions themselves can't create
95            // They can call contracts that use CREATE/CREATE2
96            default_hook::delete_self_destruct_accounts(vm)?;
97        } else {
98            finalize_non_privileged_execution(
99                vm,
100                ctx_result,
101                &self.fee_config,
102                vm.env.fee_token.is_some(),
103                self.cached_fee_token_ratio,
104            )?;
105        }
106
107        Ok(())
108    }
109}
110
111/// Finalizes the execution of a non-privileged L2 transaction.
112/// This will execute the standard checks and requirements as the one defined in the specs or add standard L2 configs applied to general txs.
113/// We can set to pay the fees with an ERC20 token instead of ETH.
114fn finalize_non_privileged_execution(
115    vm: &mut VM<'_>,
116    ctx_result: &mut ContextResult,
117    fee_config: &FeeConfig,
118    use_fee_token: bool,
119    cached_fee_token_ratio: Option<U256>,
120) -> Result<(), crate::errors::VMError> {
121    if !ctx_result.is_success() {
122        default_hook::undo_value_transfer(vm)?;
123    }
124
125    let l1_gas = calculate_l1_fee_gas(vm, &fee_config.l1_fee_config)?;
126
127    // ctx_result.gas_used includes l1_gas (reserved in prepare_execution).
128    // Separate execution gas for refund calculation — l1_gas is not refundable.
129    let execution_gas_pre_refund = ctx_result
130        .gas_used
131        .checked_sub(l1_gas)
132        .ok_or(InternalError::Underflow)?;
133
134    // Refund cap based on execution gas only (EIP-3529)
135    let gas_refunded: u64 = vm
136        .substate
137        .refunded_gas
138        .min(execution_gas_pre_refund / default_hook::MAX_REFUND_QUOTIENT);
139    let execution_gas =
140        default_hook::compute_actual_gas_used(vm, gas_refunded, execution_gas_pre_refund)?;
141
142    let actual_gas_used = execution_gas
143        .checked_add(l1_gas)
144        .ok_or(InternalError::Overflow)?;
145
146    // EIP-7778: pre-refund gas for block accounting
147    let total_gas_pre_refund = ctx_result.gas_used;
148
149    // Save the execution backup (contains storage slot backups from SSTORE, etc.)
150    // before clearing, so BackupHook can include it in the tx-level undo snapshot.
151    let execution_backup = std::mem::take(&mut vm.current_call_frame.call_frame_backup);
152
153    // === Phase 1: Fallible computations (no state mutations) ===
154    // Perform contract calls and conversions that can fail BEFORE any
155    // mutations, so an error here leaves the DB state unchanged.
156    let fee_token_ratio: u64 = match (cached_fee_token_ratio, use_fee_token) {
157        (Some(cached), _) => {
158            // Use the ratio cached during prepare to ensure consistency.
159            // Re-fetching here would read post-execution state, which the tx
160            // may have modified — leading to lock/settlement mismatches.
161            cached.try_into().map_err(|_| {
162                VMError::Internal(InternalError::Custom(
163                    "Failed to convert fee token ratio".to_owned(),
164                ))
165            })?
166        }
167        (None, true) => {
168            return Err(VMError::Internal(InternalError::Custom(
169                "use_fee_token is true but fee_token_ratio was not cached".to_owned(),
170            )));
171        }
172        (None, false) => 1u64,
173    };
174
175    // === Phase 2: State mutations (with rollback on error) ===
176    // Mutations record original values in call_frame_backup via
177    // backup_account_info / backup_storage_slot. If any step fails,
178    // we restore the cache to undo all partial mutations.
179    // The call_frame_backup is empty here so rollback only undoes Phase 2.
180    let result = apply_finalize_mutations(
181        vm,
182        ctx_result,
183        fee_config,
184        use_fee_token,
185        fee_token_ratio,
186        l1_gas,
187        gas_refunded,
188        execution_gas,
189        actual_gas_used,
190        total_gas_pre_refund,
191    );
192
193    if let Err(e) = result {
194        // Rollback DB cache to undo partial Phase 2 mutations.
195        // Note: substate (logs, selfdestruct) is NOT rolled back here because
196        // the substate parent was already consumed by handle_state_backup()
197        // during run_execution(). This is safe because the Err propagates to
198        // the caller, which discards the entire VM context.
199        vm.restore_cache_state()?;
200        return Err(e);
201    }
202
203    // Merge the saved execution backup back so BackupHook can capture
204    // both execution-time and finalize-time state for tx-level undo.
205    vm.current_call_frame
206        .call_frame_backup
207        .extend(execution_backup);
208
209    Ok(())
210}
211
212/// Applies all finalize mutations atomically: if any step fails, the caller
213/// reverts the DB cache using `restore_cache_state`.
214#[allow(clippy::too_many_arguments)]
215fn apply_finalize_mutations(
216    vm: &mut VM<'_>,
217    ctx_result: &mut ContextResult,
218    fee_config: &FeeConfig,
219    use_fee_token: bool,
220    fee_token_ratio: u64,
221    l1_gas: u64,
222    gas_refunded: u64,
223    execution_gas: u64,
224    actual_gas_used: u64,
225    total_gas_pre_refund: u64,
226) -> Result<(), crate::errors::VMError> {
227    default_hook::delete_self_destruct_accounts(vm)?;
228
229    if let Some(l1_fee_config) = fee_config.l1_fee_config {
230        pay_to_l1_fee_vault(
231            vm,
232            l1_gas.saturating_mul(fee_token_ratio),
233            l1_fee_config,
234            use_fee_token,
235        )?;
236    }
237
238    if use_fee_token {
239        refund_sender_fee_token(
240            vm,
241            ctx_result,
242            gas_refunded,
243            actual_gas_used,
244            total_gas_pre_refund,
245            fee_token_ratio,
246        )?;
247    } else {
248        default_hook::refund_sender(vm, ctx_result, gas_refunded, actual_gas_used)?;
249    }
250
251    pay_coinbase_l2(
252        vm,
253        execution_gas.saturating_mul(fee_token_ratio),
254        &fee_config.operator_fee_config,
255        use_fee_token,
256    )?;
257
258    // We want to pay the base fee vault if it is set.
259    // If not set and it is a fee token transaction we want to burn the fee by sending it
260    // to the zero address because it is an ERC20.
261    // If not an ERC20 the fees are burned not by a transaction.
262    if let Some(base_fee_vault) = fee_config.base_fee_vault {
263        pay_base_fee_vault(
264            vm,
265            execution_gas.saturating_mul(fee_token_ratio),
266            base_fee_vault,
267            use_fee_token,
268        )?;
269    } else if use_fee_token {
270        pay_base_fee_vault(
271            vm,
272            execution_gas.saturating_mul(fee_token_ratio),
273            Address::zero(),
274            use_fee_token,
275        )?;
276    }
277
278    if let Some(operator_fee_config) = fee_config.operator_fee_config {
279        pay_operator_fee(
280            vm,
281            execution_gas.saturating_mul(fee_token_ratio),
282            operator_fee_config,
283            use_fee_token,
284        )?;
285    }
286
287    // Note: ctx_result.gas_used is already correctly set by refund_sender/refund_sender_fee_token
288    // based on EIP-7778 (pre-refund for Amsterdam+, post-refund for earlier forks)
289
290    Ok(())
291}
292
293fn validate_sufficient_max_fee_per_gas_l2(
294    vm: &VM<'_>,
295    operator_fee_config: &Option<OperatorFeeConfig>,
296) -> Result<(), TxValidationError> {
297    let Some(fee_config) = operator_fee_config else {
298        // No operator fee configured, this check was done in default hook
299        return Ok(());
300    };
301
302    let total_fee = vm
303        .env
304        .base_fee_per_gas
305        .checked_add(fee_config.operator_fee_per_gas.into())
306        .ok_or(TxValidationError::InsufficientMaxFeePerGas)?;
307
308    if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < total_fee {
309        return Err(TxValidationError::InsufficientMaxFeePerGas);
310    }
311    Ok(())
312}
313
314/// Reserves L1 data availability gas from the execution budget.
315///
316/// By consuming l1_gas from gas_remaining during prepare_execution,
317/// execution physically cannot use the L1 fee portion. This guarantees
318/// the L1 fee vault always receives the full l1_gas payment, eliminating
319/// the griefing vector where a user sets gas_limit = intrinsic_gas.
320///
321/// If gas_limit < intrinsic_gas + l1_gas, increase_consumed_gas returns
322/// OutOfGas, which we map to L1GasReservationTooLow to reject the tx upfront.
323///
324/// On Prague+, also validates gas_limit >= floor + l1_gas (EIP-7623).
325/// Finalize computes actual_gas_used = max(execution_gas, floor) + l1_gas,
326/// so without this check a tx with heavy calldata could pass validation
327/// but underflow in refund_sender.
328///
329/// Both failures use `L1GasReservationTooLow` rather than `IntrinsicGasTooLow`
330/// because `l1_gas` tracks the L1 fee config and the block's gas price: a tx can
331/// fail here purely because L1 data availability was momentarily expensive and
332/// succeed in the next block. Reusing `IntrinsicGasTooLow` would make it
333/// indistinguishable from the tx's own immutable intrinsic-gas shortfall, which
334/// the payload builders evict on.
335fn reserve_l1_gas(vm: &mut VM<'_>, l1_fee_config: &Option<L1FeeConfig>) -> Result<(), VMError> {
336    let l1_gas = calculate_l1_fee_gas(vm, l1_fee_config)?;
337
338    // On Prague+, the EIP-7623 gas floor can raise execution_gas above
339    // intrinsic_gas at finalize time. Since actual_gas_used = execution_gas + l1_gas,
340    // we must ensure gas_limit can cover floor + l1_gas.
341    if vm.env.config.fork >= Fork::Prague {
342        let floor = vm.get_min_gas_used()?;
343        let floor_plus_l1 = floor.checked_add(l1_gas).ok_or(InternalError::Overflow)?;
344        if vm.env.gas_limit < floor_plus_l1 {
345            return Err(TxValidationError::L1GasReservationTooLow.into());
346        }
347    }
348
349    vm.current_call_frame
350        .increase_consumed_gas(l1_gas)
351        .map_err(|_| TxValidationError::L1GasReservationTooLow)?;
352    Ok(())
353}
354
355/// Pays the coinbase the priority fee per gas for the gas used.
356/// If an operator fee config is provided, the priority fee is reduced by the operator fee per gas.
357/// If use_fee_token is true, the fee is paid using the fee token contract.
358fn pay_coinbase_l2(
359    vm: &mut VM<'_>,
360    gas_to_pay: u64,
361    operator_fee_config: &Option<OperatorFeeConfig>,
362    use_fee_token: bool,
363) -> Result<(), crate::errors::VMError> {
364    if operator_fee_config.is_none() && !use_fee_token {
365        // No operator fee configured, operator fee is not paid
366        return default_hook::pay_coinbase(vm, gas_to_pay);
367    }
368
369    let priority_fee_per_gas = compute_priority_fee_per_gas(vm, operator_fee_config)?;
370
371    let coinbase_fee = U256::from(gas_to_pay)
372        .checked_mul(priority_fee_per_gas)
373        .ok_or(InternalError::Overflow)?;
374
375    // Per EIP-7928: Coinbase must appear in BAL when there's a user transaction,
376    // even if the priority fee is zero. In L2, this function is only called for
377    // non-privileged (user) transactions, so no gas_price check is needed.
378    if let Some(recorder) = vm.db.bal_recorder.as_mut() {
379        recorder.record_touched_address(vm.env.coinbase);
380    }
381
382    if !coinbase_fee.is_zero() {
383        if use_fee_token {
384            pay_fee_token(vm, vm.env.coinbase, coinbase_fee)?;
385        } else {
386            vm.increase_account_balance(vm.env.coinbase, coinbase_fee)?;
387        }
388    }
389
390    Ok(())
391}
392
393/// Computes the priority fee per gas to be paid to the coinbase.
394/// If an operator fee config is provided, the priority fee is reduced by the operator fee per gas.
395fn compute_priority_fee_per_gas(
396    vm: &VM<'_>,
397    operator_fee_config: &Option<OperatorFeeConfig>,
398) -> Result<U256, InternalError> {
399    let priority_fee = vm
400        .env
401        .gas_price
402        .checked_sub(vm.env.base_fee_per_gas)
403        .ok_or(InternalError::Underflow)?;
404
405    if let Some(fee_config) = operator_fee_config {
406        priority_fee
407            .checked_sub(U256::from(fee_config.operator_fee_per_gas))
408            .ok_or(InternalError::Underflow)
409    } else {
410        Ok(priority_fee)
411    }
412}
413
414/// Pays the base fee to the base fee vault for the gas used.
415/// This is calculated as gas_used * base_fee_per_gas.
416/// If use_fee_token is true, the fee is paid using the fee token contract.
417fn pay_base_fee_vault(
418    vm: &mut VM<'_>,
419    gas_to_pay: u64,
420    base_fee_vault: Address,
421    use_fee_token: bool,
422) -> Result<(), crate::errors::VMError> {
423    let base_fee = U256::from(gas_to_pay)
424        .checked_mul(vm.env.base_fee_per_gas)
425        .ok_or(InternalError::Overflow)?;
426
427    if use_fee_token {
428        pay_fee_token(vm, base_fee_vault, base_fee)?;
429    } else {
430        vm.increase_account_balance(base_fee_vault, base_fee)?;
431    }
432    Ok(())
433}
434
435/// Pays the operator fee to the operator fee vault for the gas used.
436/// This is calculated as gas_used * operator_fee_per_gas.
437/// If use_fee_token is true, the fee is paid using the fee token contract.
438fn pay_operator_fee(
439    vm: &mut VM<'_>,
440    gas_to_pay: u64,
441    operator_fee_config: OperatorFeeConfig,
442    use_fee_token: bool,
443) -> Result<(), crate::errors::VMError> {
444    let operator_fee = U256::from(gas_to_pay)
445        .checked_mul(U256::from(operator_fee_config.operator_fee_per_gas))
446        .ok_or(InternalError::Overflow)?;
447
448    if use_fee_token {
449        pay_fee_token(vm, operator_fee_config.operator_fee_vault, operator_fee)?;
450    } else {
451        vm.increase_account_balance(operator_fee_config.operator_fee_vault, operator_fee)?;
452    }
453    Ok(())
454}
455
456/// Prepares the execution of a privileged transaction.
457/// This includes skipping certain checks and validations that are not applicable to privileged transactions.
458/// See the comments for details.
459fn prepare_execution_privileged(vm: &mut VM<'_>) -> Result<(), crate::errors::VMError> {
460    let sender_address = vm.env.origin;
461    let sender_balance = vm.db.get_account(sender_address)?.info.balance;
462
463    let mut tx_should_fail = false;
464
465    // The bridge is allowed to mint ETH.
466    // This is done by not decreasing it's balance when it's the source of a transfer.
467    // For other privileged transactions, insufficient balance can't cause an error
468    // since they must always be accepted, and an error would mark them as invalid
469    // Instead, we make them revert by inserting a revert2
470    if sender_address != COMMON_BRIDGE_L2_ADDRESS {
471        let value = vm.current_call_frame.msg_value;
472        if value > sender_balance {
473            tx_should_fail = true;
474        }
475        // NOTE: Balance is NOT debited here — it is deferred until after all
476        // validation passes. This avoids permanent fund loss if a later check
477        // (e.g. intrinsic gas) triggers the failure path, which zeroes msg_value
478        // and makes undo_value_transfer a no-op.
479    }
480
481    // if fork > prague: default_hook::validate_min_gas_limit
482    // NOT CHECKED: the l1 makes spamming privileged transactions not economical
483
484    // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
485    // NOT CHECKED: privileged transactions do not pay for gas
486
487    // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
488    // NOT CHECKED: the blob price does not matter, privileged transactions do not support blobs
489
490    // (3) INSUFFICIENT_ACCOUNT_FUNDS
491    // NOT CHECKED: privileged transactions do not pay for gas
492
493    // (4) INSUFFICIENT_MAX_FEE_PER_GAS
494    // NOT CHECKED: privileged transactions do not pay for gas, the gas price is irrelevant
495
496    // (5) INITCODE_SIZE_EXCEEDED
497    // NOT CHECKED: privileged transactions can't be of "create" type
498
499    // (6) INTRINSIC_GAS_TOO_LOW
500    // CHANGED: the gas should be charged, but the transaction shouldn't error
501    let intrinsic_failed = match vm.get_intrinsic_gas() {
502        Ok(intrinsic) => vm.add_intrinsic_gas(&intrinsic).is_err(),
503        Err(_) => true,
504    };
505    if intrinsic_failed {
506        tx_should_fail = true;
507    }
508
509    // (7) NONCE_IS_MAX
510    // NOT CHECKED: privileged transactions don't use the account nonce
511
512    // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS
513    // NOT CHECKED: privileged transactions do not pay for gas, the gas price is irrelevant
514
515    // (9) SENDER_NOT_EOA
516    // NOT CHECKED: contracts can also send privileged transactions
517
518    // (10) GAS_ALLOWANCE_EXCEEDED
519    // CHECKED: we don't want to exceed block limits
520    default_hook::validate_gas_allowance(vm)?;
521
522    // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
523    // NOT CHECKED: privileged transactions are not type 3
524
525    // Transaction is type 4 if authorization_list is Some
526    // NOT CHECKED: privileged transactions are not type 4
527
528    if tx_should_fail {
529        // If the transaction failed some validation, but it must still be included
530        // To prevent it from taking effect, we force it to revert
531        vm.current_call_frame.msg_value = U256::zero();
532        vm.current_call_frame
533            .set_code(Code::from_bytecode_unchecked(
534                vec![Opcode::INVALID.into()].into(),
535                H256::zero(),
536            ))?;
537        return Ok(());
538    }
539
540    // Debit sender balance now that all validation has passed.
541    // This must happen after the tx_should_fail checks above to avoid
542    // permanent fund loss when the failure path zeroes msg_value.
543    if sender_address != COMMON_BRIDGE_L2_ADDRESS {
544        let value = vm.current_call_frame.msg_value;
545        vm.decrease_account_balance(sender_address, value)
546            .map_err(|_| {
547                InternalError::Custom("Insufficient funds in privileged transaction".to_string())
548            })?;
549    }
550
551    default_hook::transfer_value(vm)?;
552
553    default_hook::set_bytecode_and_code_address(vm)
554}
555
556/// Prepares the execution of a fee token transaction.
557/// Similar to default_hook preparation but allows paying fees with ERC20 tokens.
558/// Maintains separation between L1 and L2 functionality.
559fn prepare_execution_fee_token(vm: &mut VM<'_>) -> Result<U256, crate::errors::VMError> {
560    let fee_token = vm
561        .env
562        .fee_token
563        .ok_or(VMError::Internal(InternalError::Custom(
564            "Fee token address not provided".to_owned(),
565        )))?;
566
567    let (execution_result, _) = simulate_common_bridge_call(
568        vm,
569        FEE_TOKEN_REGISTRY_ADDRESS,
570        encode_is_fee_token_call(fee_token),
571    )?;
572
573    if !execution_result.is_success() {
574        return Err(VMError::TxValidation(
575            TxValidationError::InsufficientAccountFunds,
576        ));
577    }
578    // Here we want to check if the token is actually registered as valid.
579    // To do this we see if the last byte is 1 or 0.
580    // The contract returns a bool that is padded to 32 bytes.
581    if execution_result.output.len() != 32
582        || execution_result.output.get(31).is_none_or(|&b| b == 0)
583    {
584        return Err(VMError::TxValidation(
585            TxValidationError::InsufficientAccountFunds,
586        ));
587    }
588    let fee_token_ratio = get_fee_token_ratio(vm, fee_token)?;
589
590    let sender_address = vm.env.origin;
591    let sender_info = vm.db.get_account(sender_address)?.info.clone();
592
593    // Compute intrinsic gas once; reused by the min-gas-limit validation and
594    // `add_intrinsic_gas` below (mirrors the default hook).
595    let intrinsic = vm.get_intrinsic_gas()?;
596
597    if vm.env.config.fork >= Fork::Prague {
598        default_hook::validate_min_gas_limit(vm, &intrinsic)?;
599        // EIP-7825 (Prague to pre-Amsterdam): reject tx if gas_limit > TX_MAX_GAS_LIMIT_AMSTERDAM.
600        // Amsterdam removes this restriction (EIP-8037 reservoir model).
601        if !vm.env.disable_gas_allowance_check
602            && vm.env.config.fork < Fork::Amsterdam
603            && vm.tx.gas_limit() > TX_MAX_GAS_LIMIT_AMSTERDAM
604        {
605            return Err(VMError::TxValidation(
606                TxValidationError::TxMaxGasLimitExceeded {
607                    tx_hash: vm.tx.hash(vm.crypto),
608                    tx_gas_limit: vm.tx.gas_limit(),
609                },
610            ));
611        }
612        if !vm.env.disable_gas_allowance_check
613            && vm.env.config.fork >= Fork::Osaka
614            && vm.env.config.fork < Fork::Amsterdam
615            && vm.tx.gas_limit() > POST_OSAKA_GAS_LIMIT_CAP
616        {
617            return Err(VMError::TxValidation(
618                TxValidationError::TxMaxGasLimitExceeded {
619                    tx_hash: vm.tx.hash(vm.crypto),
620                    tx_gas_limit: vm.tx.gas_limit(),
621                },
622            ));
623        }
624    }
625
626    // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
627    let gaslimit_price_product = vm
628        .env
629        .gas_price
630        .checked_mul(vm.env.gas_limit.into())
631        .ok_or(TxValidationError::GasLimitPriceProductOverflow)?;
632
633    // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
634    // NOT CHECKED: the blob price does not matter, fee token transactions do not support blobs
635
636    // (3) INSUFFICIENT_ACCOUNT_FUNDS
637    deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;
638
639    // (4) INSUFFICIENT_MAX_FEE_PER_GAS
640    default_hook::validate_sufficient_max_fee_per_gas(vm)?;
641
642    // (5) INITCODE_SIZE_EXCEEDED
643    if vm.is_create()? {
644        default_hook::validate_init_code_size(vm)?;
645    }
646
647    // (6) INTRINSIC_GAS_TOO_LOW
648    vm.add_intrinsic_gas(&intrinsic)?;
649
650    // (7) NONCE_IS_MAX
651    vm.increment_account_nonce(sender_address)
652        .map_err(|_| TxValidationError::NonceIsMax)?;
653
654    // check for nonce mismatch
655    if !vm.env.disable_nonce_check && sender_info.nonce != vm.env.tx_nonce {
656        return Err(TxValidationError::NonceMismatch {
657            expected: sender_info.nonce,
658            actual: vm.env.tx_nonce,
659        }
660        .into());
661    }
662
663    // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS
664    if let (Some(tx_max_priority_fee), Some(tx_max_fee_per_gas)) = (
665        vm.env.tx_max_priority_fee_per_gas,
666        vm.env.tx_max_fee_per_gas,
667    ) && tx_max_priority_fee > tx_max_fee_per_gas
668    {
669        return Err(TxValidationError::PriorityGreaterThanMaxFeePerGas {
670            priority_fee: tx_max_priority_fee,
671            max_fee_per_gas: tx_max_fee_per_gas,
672        }
673        .into());
674    }
675
676    // (9) SENDER_NOT_EOA
677    if !vm.env.disable_sender_eoa_check {
678        let code = vm.db.get_code(sender_info.code_hash)?;
679        default_hook::validate_sender(sender_address, code.code())?;
680    }
681
682    // (10) GAS_ALLOWANCE_EXCEEDED
683    default_hook::validate_gas_allowance(vm)?;
684
685    // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
686    // NOT CHECKED: fee token transactions are not type 3
687
688    // Transaction is type 4 if authorization_list is Some
689    // NOT CHECKED: fee token transactions are not type 4
690
691    default_hook::transfer_value(vm)?;
692
693    default_hook::set_bytecode_and_code_address(vm)?;
694    Ok(fee_token_ratio)
695}
696
697/// Deducts the caller's balance in the fee token for the upfront gas cost.
698/// This is calculated as gas_limit * gas_price.
699/// This is done through a call to the fee token contract's lockFee function.
700pub fn deduct_caller_fee_token(
701    vm: &mut VM<'_>,
702    gas_limit_price_product: U256,
703) -> Result<(), VMError> {
704    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice (in ERC20) + value
705    let sender_address = vm.env.origin;
706    let value = vm.current_call_frame.msg_value;
707
708    // First, try to deduct the value sent
709    vm.decrease_account_balance(sender_address, value)
710        .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
711
712    // Then, deduct the gas cost in the fee token by locking it in the l2 bridge
713    lock_fee_token(vm, sender_address, gas_limit_price_product)?;
714
715    Ok(())
716}
717
718/// Helper function to encode the calldata for the fee token contract calls.
719/// <function>(address,uint256)
720fn encode_fee_token_call(selector: [u8; 4], address: Address, amount: U256) -> Bytes {
721    let mut data = Vec::with_capacity(4 + 32 + 32);
722    data.extend_from_slice(&selector);
723    data.extend_from_slice(&[0u8; 12]);
724    data.extend_from_slice(&address.0);
725    data.extend_from_slice(&amount.to_big_endian());
726    data.into()
727}
728
729fn encode_is_fee_token_call(token: Address) -> Bytes {
730    let mut data = Vec::with_capacity(4 + 32);
731    data.extend_from_slice(&IS_FEE_TOKEN_SELECTOR);
732    data.extend_from_slice(&[0u8; 12]);
733    data.extend_from_slice(&token.0);
734    data.into()
735}
736
737fn encode_fee_token_ratio_call(token: Address) -> Bytes {
738    let mut data = Vec::with_capacity(4 + 32);
739    data.extend_from_slice(&FEE_TOKEN_RATIO_SELECTOR);
740    data.extend_from_slice(&[0u8; 12]);
741    data.extend_from_slice(&token.0);
742    data.into()
743}
744
745/// Locks the fee token amount from the payer's balance.
746fn lock_fee_token(vm: &mut VM<'_>, payer: Address, amount: U256) -> Result<(), VMError> {
747    transfer_fee_token(vm, encode_fee_token_call(LOCK_FEE_SELECTOR, payer, amount))
748}
749
750/// Pays the fee token amount to the receiver's balance.
751fn pay_fee_token(vm: &mut VM<'_>, receiver: Address, amount: U256) -> Result<(), VMError> {
752    transfer_fee_token(
753        vm,
754        encode_fee_token_call(PAY_FEE_SELECTOR, receiver, amount),
755    )
756}
757
758/// Executes a call to the fee token contract for fee-related operations.
759///
760/// - This function is only called when locking the fees, refunding unspent gas, and paying the fees to the vaults.
761/// - Disable checks as we want to simulate the transaction and get only the updates of the contract storage slots.
762/// - This simulation makes a transaction with the calldata provided in `data`, this will be used to call the `payFee` and `lockFee` functions.
763///   `lockFee(payer, max_gas_cost)` - locks upfront gas cost from sender
764///   `payFee(receiver, amount)` - pays coinbase, vaults, or refunds sender
765/// - Uses `COMMON_BRIDGE_L2_ADDRESS` as origin to restrict access. No user can change this address.
766/// - Creates a new VM with cloned database; only fee token storage is synced back.
767/// - Uses the same contract address as the one set in the transaction.
768fn transfer_fee_token(vm: &mut VM<'_>, data: Bytes) -> Result<(), VMError> {
769    let fee_token = vm
770        .env
771        .fee_token
772        .ok_or(VMError::Internal(InternalError::Custom(
773            "No fee token address provided, this is a bug".to_owned(),
774        )))?;
775
776    let (execution_result, mut db_clone) = simulate_common_bridge_call(vm, fee_token, data)?;
777
778    if !execution_result.is_success() {
779        return Err(VMError::TxValidation(
780            TxValidationError::InsufficientAccountFunds,
781        ));
782    }
783    let new_storage = db_clone.get_account(fee_token)?.storage.clone();
784    let current_storage = vm.db.get_account(fee_token)?.storage.clone();
785
786    // Back up original values for changed slots so restore_cache_state can revert them
787    for (key, new_value) in &new_storage {
788        let old_value = current_storage.get(key).copied().unwrap_or_default();
789        if old_value != *new_value {
790            vm.backup_storage_slot(fee_token, *key, old_value)?;
791        }
792    }
793    // Back up slots that will be removed by the bulk replacement
794    for (key, &old_value) in &current_storage {
795        if !new_storage.contains_key(key) {
796            vm.backup_storage_slot(fee_token, *key, old_value)?;
797        }
798    }
799
800    // Apply new storage
801    vm.db.get_account_mut(fee_token)?.storage = new_storage;
802
803    // update the initial state account
804    let initial_state_fee_token = db_clone
805        .initial_accounts_state
806        .get(&fee_token)
807        .cloned()
808        .ok_or(VMError::Internal(InternalError::Custom(
809            "No initial state found for fee token".to_owned(),
810        )))?;
811    // We have to merge, not insert
812    vm.db
813        .initial_accounts_state
814        .insert(fee_token, initial_state_fee_token);
815
816    Ok(())
817}
818
819/// Executes an L2 call as if it originated from the common bridge, returning
820/// both the execution report and the mutated database snapshot.
821fn simulate_common_bridge_call(
822    vm: &VM<'_>,
823    to: Address,
824    data: Bytes,
825) -> Result<(ExecutionReport, GeneralizedDatabase), VMError> {
826    let mut db_clone = vm.db.clone(); // expensive but necessary to simulate call
827    let origin = COMMON_BRIDGE_L2_ADDRESS; // We set the common bridge to restrict access to the contract
828    let nonce = db_clone.get_account(origin)?.info.nonce;
829    let simulation_tx = EIP1559Transaction {
830        // we are simulating the transaction
831        chain_id: u64::try_from(vm.env.chain_id).map_err(|_| {
832            VMError::Internal(InternalError::Custom("chain_id overflows u64".to_string()))
833        })?,
834        nonce,
835        max_priority_fee_per_gas: SIMULATION_MAX_FEE,
836        max_fee_per_gas: SIMULATION_MAX_FEE,
837        gas_limit: SIMULATION_GAS_LIMIT,
838        to: TxKind::Call(to),
839        value: U256::zero(),
840        data,
841        ..Default::default()
842    };
843    let tx = Transaction::EIP1559Transaction(simulation_tx);
844    let mut env_clone = vm.env.clone();
845    // Disable fee checks and update fields
846    env_clone.base_fee_per_gas = U256::zero();
847    env_clone.block_excess_blob_gas = None;
848    env_clone.gas_price = U256::zero();
849    env_clone.origin = origin;
850    env_clone.fee_token = None;
851    env_clone.gas_limit = SIMULATION_GAS_LIMIT;
852
853    let mut new_vm = VM::new(
854        env_clone,
855        &mut db_clone,
856        &tx,
857        LevmCallTracer::disabled(),
858        VMType::L2(Default::default()),
859        vm.crypto,
860        None,
861    )?;
862    new_vm.hooks = vec![];
863    default_hook::set_bytecode_and_code_address(&mut new_vm)?;
864    let execution_result = new_vm.execute()?;
865
866    Ok((execution_result, db_clone))
867}
868
869/// Refunds the sender the unspent gas in fee tokens.
870/// Works similarly to refund_sender but uses the fee token contract
871/// But we don't want to be mixing L2 logic inside the default hook.
872fn refund_sender_fee_token(
873    vm: &mut VM<'_>,
874    ctx_result: &mut ContextResult,
875    refunded_gas: u64,
876    gas_spent: u64,
877    gas_used_pre_refund: u64,
878    fee_token_ratio: u64,
879) -> Result<(), VMError> {
880    vm.substate.refunded_gas = refunded_gas;
881
882    // EIP-7778: Separate block vs user gas accounting for Amsterdam+
883    if vm.env.config.fork >= Fork::Amsterdam {
884        // Block accounting uses pre-refund gas
885        ctx_result.gas_used = gas_used_pre_refund;
886        // User pays post-refund gas
887        ctx_result.gas_spent = gas_spent;
888    } else {
889        // Pre-Amsterdam: both use post-refund value
890        ctx_result.gas_used = gas_spent;
891        ctx_result.gas_spent = gas_spent;
892    }
893
894    // Return unspent gas to the sender.
895    let gas_to_return = vm
896        .env
897        .gas_limit
898        .checked_sub(gas_spent)
899        .ok_or(InternalError::Underflow)?;
900
901    let erc20_return_amount = vm
902        .env
903        .gas_price
904        .checked_mul(U256::from(gas_to_return))
905        .ok_or(InternalError::Overflow)?;
906    let sender_address = vm.env.origin;
907
908    pay_fee_token(
909        vm,
910        sender_address,
911        erc20_return_amount.saturating_mul(fee_token_ratio.into()),
912    )?;
913
914    Ok(())
915}
916
917/// Calculates the L1 fee based on the account diffs size and the L1 fee config.
918/// This is done according to the formula:
919/// L1 Fee = (L1 Fee per Blob Gas * GAS_PER_BLOB / SAFE_BYTES_PER_BLOB) * account_diffs_size
920fn calculate_l1_fee(
921    fee_config: &L1FeeConfig,
922    transaction_size: usize,
923) -> Result<U256, crate::errors::VMError> {
924    let l1_fee_per_blob: U256 = fee_config
925        .l1_fee_per_blob_gas
926        .checked_mul(GAS_PER_BLOB.into())
927        .ok_or(InternalError::Overflow)?
928        .into();
929
930    let l1_fee_per_blob_byte = l1_fee_per_blob
931        .checked_div(U256::from(SAFE_BYTES_PER_BLOB))
932        .ok_or(InternalError::DivisionByZero)?;
933
934    let l1_fee = l1_fee_per_blob_byte
935        .checked_mul(U256::from(transaction_size))
936        .ok_or(InternalError::Overflow)?;
937
938    Ok(l1_fee)
939}
940
941/// Calculates the L1 fee gas based on the account diffs size and the L1 fee config.
942/// Returns 0 if no L1 fee config is provided.
943fn calculate_l1_fee_gas(
944    vm: &VM<'_>,
945    l1_fee_config: &Option<L1FeeConfig>,
946) -> Result<u64, crate::errors::VMError> {
947    let Some(fee_config) = l1_fee_config else {
948        // No l1 fee configured, l1 fee gas is zero
949        return Ok(0);
950    };
951
952    let tx_size = vm.tx.length();
953
954    let l1_fee = calculate_l1_fee(fee_config, tx_size)?;
955    let mut l1_fee_gas = l1_fee
956        .checked_div(vm.env.gas_price)
957        .ok_or(InternalError::DivisionByZero)?;
958
959    // Ensure at least 1 gas is charged if there is a non-zero l1 fee
960    if l1_fee_gas == U256::zero() && l1_fee > U256::zero() {
961        l1_fee_gas = U256::one();
962    }
963
964    Ok(l1_fee_gas.try_into().map_err(|_| InternalError::Overflow)?)
965}
966
967/// Pays the L1 fee to the L1 fee vault for the gas used.
968/// This is calculated as gas_to_pay * gas_price.
969fn pay_to_l1_fee_vault(
970    vm: &mut VM<'_>,
971    gas_to_pay: u64,
972    l1_fee_config: L1FeeConfig,
973    use_fee_token: bool,
974) -> Result<(), crate::errors::VMError> {
975    let l1_fee = U256::from(gas_to_pay)
976        .checked_mul(vm.env.gas_price)
977        .ok_or(InternalError::Overflow)?;
978
979    if use_fee_token {
980        pay_fee_token(vm, l1_fee_config.l1_fee_vault, l1_fee)?;
981    } else {
982        vm.increase_account_balance(l1_fee_config.l1_fee_vault, l1_fee)
983            .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
984    }
985    Ok(())
986}
987
988fn get_fee_token_ratio(vm: &mut VM<'_>, fee_token: H160) -> Result<U256, VMError> {
989    let fee_token_ratio = simulate_common_bridge_call(
990        vm,
991        FEE_TOKEN_RATIO_ADDRESS,
992        encode_fee_token_ratio_call(fee_token),
993    )?
994    .0;
995    if !fee_token_ratio.is_success() || fee_token_ratio.output.len() != 32 {
996        return Err(VMError::Internal(InternalError::Custom(
997            "Failed to get fee token ratio".to_owned(),
998        )));
999    }
1000    Ok(U256::from_big_endian(
1001        fee_token_ratio
1002            .output
1003            .get(0..32)
1004            .ok_or(InternalError::Custom(
1005                "Failed to parse fee token ratio".to_owned(),
1006            ))?,
1007    ))
1008}