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 IntrinsicGasTooLow.
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 IntrinsicGasTooLow 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.
328fn reserve_l1_gas(vm: &mut VM<'_>, l1_fee_config: &Option<L1FeeConfig>) -> Result<(), VMError> {
329    let l1_gas = calculate_l1_fee_gas(vm, l1_fee_config)?;
330
331    // On Prague+, the EIP-7623 gas floor can raise execution_gas above
332    // intrinsic_gas at finalize time. Since actual_gas_used = execution_gas + l1_gas,
333    // we must ensure gas_limit can cover floor + l1_gas.
334    if vm.env.config.fork >= Fork::Prague {
335        let floor = vm.get_min_gas_used()?;
336        let floor_plus_l1 = floor.checked_add(l1_gas).ok_or(InternalError::Overflow)?;
337        if vm.env.gas_limit < floor_plus_l1 {
338            return Err(TxValidationError::IntrinsicGasTooLow.into());
339        }
340    }
341
342    vm.current_call_frame
343        .increase_consumed_gas(l1_gas)
344        .map_err(|_| TxValidationError::IntrinsicGasTooLow)?;
345    Ok(())
346}
347
348/// Pays the coinbase the priority fee per gas for the gas used.
349/// If an operator fee config is provided, the priority fee is reduced by the operator fee per gas.
350/// If use_fee_token is true, the fee is paid using the fee token contract.
351fn pay_coinbase_l2(
352    vm: &mut VM<'_>,
353    gas_to_pay: u64,
354    operator_fee_config: &Option<OperatorFeeConfig>,
355    use_fee_token: bool,
356) -> Result<(), crate::errors::VMError> {
357    if operator_fee_config.is_none() && !use_fee_token {
358        // No operator fee configured, operator fee is not paid
359        return default_hook::pay_coinbase(vm, gas_to_pay);
360    }
361
362    let priority_fee_per_gas = compute_priority_fee_per_gas(vm, operator_fee_config)?;
363
364    let coinbase_fee = U256::from(gas_to_pay)
365        .checked_mul(priority_fee_per_gas)
366        .ok_or(InternalError::Overflow)?;
367
368    // Per EIP-7928: Coinbase must appear in BAL when there's a user transaction,
369    // even if the priority fee is zero. In L2, this function is only called for
370    // non-privileged (user) transactions, so no gas_price check is needed.
371    if let Some(recorder) = vm.db.bal_recorder.as_mut() {
372        recorder.record_touched_address(vm.env.coinbase);
373    }
374
375    if !coinbase_fee.is_zero() {
376        if use_fee_token {
377            pay_fee_token(vm, vm.env.coinbase, coinbase_fee)?;
378        } else {
379            vm.increase_account_balance(vm.env.coinbase, coinbase_fee)?;
380        }
381    }
382
383    Ok(())
384}
385
386/// Computes the priority fee per gas to be paid to the coinbase.
387/// If an operator fee config is provided, the priority fee is reduced by the operator fee per gas.
388fn compute_priority_fee_per_gas(
389    vm: &VM<'_>,
390    operator_fee_config: &Option<OperatorFeeConfig>,
391) -> Result<U256, InternalError> {
392    let priority_fee = vm
393        .env
394        .gas_price
395        .checked_sub(vm.env.base_fee_per_gas)
396        .ok_or(InternalError::Underflow)?;
397
398    if let Some(fee_config) = operator_fee_config {
399        priority_fee
400            .checked_sub(U256::from(fee_config.operator_fee_per_gas))
401            .ok_or(InternalError::Underflow)
402    } else {
403        Ok(priority_fee)
404    }
405}
406
407/// Pays the base fee to the base fee vault for the gas used.
408/// This is calculated as gas_used * base_fee_per_gas.
409/// If use_fee_token is true, the fee is paid using the fee token contract.
410fn pay_base_fee_vault(
411    vm: &mut VM<'_>,
412    gas_to_pay: u64,
413    base_fee_vault: Address,
414    use_fee_token: bool,
415) -> Result<(), crate::errors::VMError> {
416    let base_fee = U256::from(gas_to_pay)
417        .checked_mul(vm.env.base_fee_per_gas)
418        .ok_or(InternalError::Overflow)?;
419
420    if use_fee_token {
421        pay_fee_token(vm, base_fee_vault, base_fee)?;
422    } else {
423        vm.increase_account_balance(base_fee_vault, base_fee)?;
424    }
425    Ok(())
426}
427
428/// Pays the operator fee to the operator fee vault for the gas used.
429/// This is calculated as gas_used * operator_fee_per_gas.
430/// If use_fee_token is true, the fee is paid using the fee token contract.
431fn pay_operator_fee(
432    vm: &mut VM<'_>,
433    gas_to_pay: u64,
434    operator_fee_config: OperatorFeeConfig,
435    use_fee_token: bool,
436) -> Result<(), crate::errors::VMError> {
437    let operator_fee = U256::from(gas_to_pay)
438        .checked_mul(U256::from(operator_fee_config.operator_fee_per_gas))
439        .ok_or(InternalError::Overflow)?;
440
441    if use_fee_token {
442        pay_fee_token(vm, operator_fee_config.operator_fee_vault, operator_fee)?;
443    } else {
444        vm.increase_account_balance(operator_fee_config.operator_fee_vault, operator_fee)?;
445    }
446    Ok(())
447}
448
449/// Prepares the execution of a privileged transaction.
450/// This includes skipping certain checks and validations that are not applicable to privileged transactions.
451/// See the comments for details.
452fn prepare_execution_privileged(vm: &mut VM<'_>) -> Result<(), crate::errors::VMError> {
453    let sender_address = vm.env.origin;
454    let sender_balance = vm.db.get_account(sender_address)?.info.balance;
455
456    let mut tx_should_fail = false;
457
458    // The bridge is allowed to mint ETH.
459    // This is done by not decreasing it's balance when it's the source of a transfer.
460    // For other privileged transactions, insufficient balance can't cause an error
461    // since they must always be accepted, and an error would mark them as invalid
462    // Instead, we make them revert by inserting a revert2
463    if sender_address != COMMON_BRIDGE_L2_ADDRESS {
464        let value = vm.current_call_frame.msg_value;
465        if value > sender_balance {
466            tx_should_fail = true;
467        }
468        // NOTE: Balance is NOT debited here — it is deferred until after all
469        // validation passes. This avoids permanent fund loss if a later check
470        // (e.g. intrinsic gas) triggers the failure path, which zeroes msg_value
471        // and makes undo_value_transfer a no-op.
472    }
473
474    // if fork > prague: default_hook::validate_min_gas_limit
475    // NOT CHECKED: the l1 makes spamming privileged transactions not economical
476
477    // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
478    // NOT CHECKED: privileged transactions do not pay for gas
479
480    // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
481    // NOT CHECKED: the blob price does not matter, privileged transactions do not support blobs
482
483    // (3) INSUFFICIENT_ACCOUNT_FUNDS
484    // NOT CHECKED: privileged transactions do not pay for gas
485
486    // (4) INSUFFICIENT_MAX_FEE_PER_GAS
487    // NOT CHECKED: privileged transactions do not pay for gas, the gas price is irrelevant
488
489    // (5) INITCODE_SIZE_EXCEEDED
490    // NOT CHECKED: privileged transactions can't be of "create" type
491
492    // (6) INTRINSIC_GAS_TOO_LOW
493    // CHANGED: the gas should be charged, but the transaction shouldn't error
494    let intrinsic_failed = match vm.get_intrinsic_gas() {
495        Ok(intrinsic) => vm.add_intrinsic_gas(&intrinsic).is_err(),
496        Err(_) => true,
497    };
498    if intrinsic_failed {
499        tx_should_fail = true;
500    }
501
502    // (7) NONCE_IS_MAX
503    // NOT CHECKED: privileged transactions don't use the account nonce
504
505    // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS
506    // NOT CHECKED: privileged transactions do not pay for gas, the gas price is irrelevant
507
508    // (9) SENDER_NOT_EOA
509    // NOT CHECKED: contracts can also send privileged transactions
510
511    // (10) GAS_ALLOWANCE_EXCEEDED
512    // CHECKED: we don't want to exceed block limits
513    default_hook::validate_gas_allowance(vm)?;
514
515    // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
516    // NOT CHECKED: privileged transactions are not type 3
517
518    // Transaction is type 4 if authorization_list is Some
519    // NOT CHECKED: privileged transactions are not type 4
520
521    if tx_should_fail {
522        // If the transaction failed some validation, but it must still be included
523        // To prevent it from taking effect, we force it to revert
524        vm.current_call_frame.msg_value = U256::zero();
525        vm.current_call_frame
526            .set_code(Code::from_bytecode_unchecked(
527                vec![Opcode::INVALID.into()].into(),
528                H256::zero(),
529            ))?;
530        return Ok(());
531    }
532
533    // Debit sender balance now that all validation has passed.
534    // This must happen after the tx_should_fail checks above to avoid
535    // permanent fund loss when the failure path zeroes msg_value.
536    if sender_address != COMMON_BRIDGE_L2_ADDRESS {
537        let value = vm.current_call_frame.msg_value;
538        vm.decrease_account_balance(sender_address, value)
539            .map_err(|_| {
540                InternalError::Custom("Insufficient funds in privileged transaction".to_string())
541            })?;
542    }
543
544    default_hook::transfer_value(vm)?;
545
546    default_hook::set_bytecode_and_code_address(vm)
547}
548
549/// Prepares the execution of a fee token transaction.
550/// Similar to default_hook preparation but allows paying fees with ERC20 tokens.
551/// Maintains separation between L1 and L2 functionality.
552fn prepare_execution_fee_token(vm: &mut VM<'_>) -> Result<U256, crate::errors::VMError> {
553    let fee_token = vm
554        .env
555        .fee_token
556        .ok_or(VMError::Internal(InternalError::Custom(
557            "Fee token address not provided".to_owned(),
558        )))?;
559
560    let (execution_result, _) = simulate_common_bridge_call(
561        vm,
562        FEE_TOKEN_REGISTRY_ADDRESS,
563        encode_is_fee_token_call(fee_token),
564    )?;
565
566    if !execution_result.is_success() {
567        return Err(VMError::TxValidation(
568            TxValidationError::InsufficientAccountFunds,
569        ));
570    }
571    // Here we want to check if the token is actually registered as valid.
572    // To do this we see if the last byte is 1 or 0.
573    // The contract returns a bool that is padded to 32 bytes.
574    if execution_result.output.len() != 32
575        || execution_result.output.get(31).is_none_or(|&b| b == 0)
576    {
577        return Err(VMError::TxValidation(
578            TxValidationError::InsufficientAccountFunds,
579        ));
580    }
581    let fee_token_ratio = get_fee_token_ratio(vm, fee_token)?;
582
583    let sender_address = vm.env.origin;
584    let sender_info = vm.db.get_account(sender_address)?.info.clone();
585
586    // Compute intrinsic gas once; reused by the min-gas-limit validation and
587    // `add_intrinsic_gas` below (mirrors the default hook).
588    let intrinsic = vm.get_intrinsic_gas()?;
589
590    if vm.env.config.fork >= Fork::Prague {
591        default_hook::validate_min_gas_limit(vm, &intrinsic)?;
592        // EIP-7825 (Prague to pre-Amsterdam): reject tx if gas_limit > TX_MAX_GAS_LIMIT_AMSTERDAM.
593        // Amsterdam removes this restriction (EIP-8037 reservoir model).
594        if vm.env.config.fork < Fork::Amsterdam && vm.tx.gas_limit() > TX_MAX_GAS_LIMIT_AMSTERDAM {
595            return Err(VMError::TxValidation(
596                TxValidationError::TxMaxGasLimitExceeded {
597                    tx_hash: vm.tx.hash(),
598                    tx_gas_limit: vm.tx.gas_limit(),
599                },
600            ));
601        }
602        if vm.env.config.fork >= Fork::Osaka
603            && vm.env.config.fork < Fork::Amsterdam
604            && vm.tx.gas_limit() > POST_OSAKA_GAS_LIMIT_CAP
605        {
606            return Err(VMError::TxValidation(
607                TxValidationError::TxMaxGasLimitExceeded {
608                    tx_hash: vm.tx.hash(),
609                    tx_gas_limit: vm.tx.gas_limit(),
610                },
611            ));
612        }
613    }
614
615    // (1) GASLIMIT_PRICE_PRODUCT_OVERFLOW
616    let gaslimit_price_product = vm
617        .env
618        .gas_price
619        .checked_mul(vm.env.gas_limit.into())
620        .ok_or(TxValidationError::GasLimitPriceProductOverflow)?;
621
622    // (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
623    // NOT CHECKED: the blob price does not matter, fee token transactions do not support blobs
624
625    // (3) INSUFFICIENT_ACCOUNT_FUNDS
626    deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;
627
628    // (4) INSUFFICIENT_MAX_FEE_PER_GAS
629    default_hook::validate_sufficient_max_fee_per_gas(vm)?;
630
631    // (5) INITCODE_SIZE_EXCEEDED
632    if vm.is_create()? {
633        default_hook::validate_init_code_size(vm)?;
634    }
635
636    // (6) INTRINSIC_GAS_TOO_LOW
637    vm.add_intrinsic_gas(&intrinsic)?;
638
639    // (7) NONCE_IS_MAX
640    vm.increment_account_nonce(sender_address)
641        .map_err(|_| TxValidationError::NonceIsMax)?;
642
643    // check for nonce mismatch
644    if sender_info.nonce != vm.env.tx_nonce {
645        return Err(TxValidationError::NonceMismatch {
646            expected: sender_info.nonce,
647            actual: vm.env.tx_nonce,
648        }
649        .into());
650    }
651
652    // (8) PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS
653    if let (Some(tx_max_priority_fee), Some(tx_max_fee_per_gas)) = (
654        vm.env.tx_max_priority_fee_per_gas,
655        vm.env.tx_max_fee_per_gas,
656    ) && tx_max_priority_fee > tx_max_fee_per_gas
657    {
658        return Err(TxValidationError::PriorityGreaterThanMaxFeePerGas {
659            priority_fee: tx_max_priority_fee,
660            max_fee_per_gas: tx_max_fee_per_gas,
661        }
662        .into());
663    }
664
665    // (9) SENDER_NOT_EOA
666    let code = vm.db.get_code(sender_info.code_hash)?;
667    default_hook::validate_sender(sender_address, code.code())?;
668
669    // (10) GAS_ALLOWANCE_EXCEEDED
670    default_hook::validate_gas_allowance(vm)?;
671
672    // Transaction is type 3 if tx_max_fee_per_blob_gas is Some
673    // NOT CHECKED: fee token transactions are not type 3
674
675    // Transaction is type 4 if authorization_list is Some
676    // NOT CHECKED: fee token transactions are not type 4
677
678    default_hook::transfer_value(vm)?;
679
680    default_hook::set_bytecode_and_code_address(vm)?;
681    Ok(fee_token_ratio)
682}
683
684/// Deducts the caller's balance in the fee token for the upfront gas cost.
685/// This is calculated as gas_limit * gas_price.
686/// This is done through a call to the fee token contract's lockFee function.
687pub fn deduct_caller_fee_token(
688    vm: &mut VM<'_>,
689    gas_limit_price_product: U256,
690) -> Result<(), VMError> {
691    // Up front cost is the maximum amount of wei that a user is willing to pay for. Gaslimit * gasprice (in ERC20) + value
692    let sender_address = vm.env.origin;
693    let value = vm.current_call_frame.msg_value;
694
695    // First, try to deduct the value sent
696    vm.decrease_account_balance(sender_address, value)
697        .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
698
699    // Then, deduct the gas cost in the fee token by locking it in the l2 bridge
700    lock_fee_token(vm, sender_address, gas_limit_price_product)?;
701
702    Ok(())
703}
704
705/// Helper function to encode the calldata for the fee token contract calls.
706/// <function>(address,uint256)
707fn encode_fee_token_call(selector: [u8; 4], address: Address, amount: U256) -> Bytes {
708    let mut data = Vec::with_capacity(4 + 32 + 32);
709    data.extend_from_slice(&selector);
710    data.extend_from_slice(&[0u8; 12]);
711    data.extend_from_slice(&address.0);
712    data.extend_from_slice(&amount.to_big_endian());
713    data.into()
714}
715
716fn encode_is_fee_token_call(token: Address) -> Bytes {
717    let mut data = Vec::with_capacity(4 + 32);
718    data.extend_from_slice(&IS_FEE_TOKEN_SELECTOR);
719    data.extend_from_slice(&[0u8; 12]);
720    data.extend_from_slice(&token.0);
721    data.into()
722}
723
724fn encode_fee_token_ratio_call(token: Address) -> Bytes {
725    let mut data = Vec::with_capacity(4 + 32);
726    data.extend_from_slice(&FEE_TOKEN_RATIO_SELECTOR);
727    data.extend_from_slice(&[0u8; 12]);
728    data.extend_from_slice(&token.0);
729    data.into()
730}
731
732/// Locks the fee token amount from the payer's balance.
733fn lock_fee_token(vm: &mut VM<'_>, payer: Address, amount: U256) -> Result<(), VMError> {
734    transfer_fee_token(vm, encode_fee_token_call(LOCK_FEE_SELECTOR, payer, amount))
735}
736
737/// Pays the fee token amount to the receiver's balance.
738fn pay_fee_token(vm: &mut VM<'_>, receiver: Address, amount: U256) -> Result<(), VMError> {
739    transfer_fee_token(
740        vm,
741        encode_fee_token_call(PAY_FEE_SELECTOR, receiver, amount),
742    )
743}
744
745/// Executes a call to the fee token contract for fee-related operations.
746///
747/// - This function is only called when locking the fees, refunding unspent gas, and paying the fees to the vaults.
748/// - Disable checks as we want to simulate the transaction and get only the updates of the contract storage slots.
749/// - This simulation makes a transaction with the calldata provided in `data`, this will be used to call the `payFee` and `lockFee` functions.
750///   `lockFee(payer, max_gas_cost)` - locks upfront gas cost from sender
751///   `payFee(receiver, amount)` - pays coinbase, vaults, or refunds sender
752/// - Uses `COMMON_BRIDGE_L2_ADDRESS` as origin to restrict access. No user can change this address.
753/// - Creates a new VM with cloned database; only fee token storage is synced back.
754/// - Uses the same contract address as the one set in the transaction.
755fn transfer_fee_token(vm: &mut VM<'_>, data: Bytes) -> Result<(), VMError> {
756    let fee_token = vm
757        .env
758        .fee_token
759        .ok_or(VMError::Internal(InternalError::Custom(
760            "No fee token address provided, this is a bug".to_owned(),
761        )))?;
762
763    let (execution_result, mut db_clone) = simulate_common_bridge_call(vm, fee_token, data)?;
764
765    if !execution_result.is_success() {
766        return Err(VMError::TxValidation(
767            TxValidationError::InsufficientAccountFunds,
768        ));
769    }
770    let new_storage = db_clone.get_account(fee_token)?.storage.clone();
771    let current_storage = vm.db.get_account(fee_token)?.storage.clone();
772
773    // Back up original values for changed slots so restore_cache_state can revert them
774    for (key, new_value) in &new_storage {
775        let old_value = current_storage.get(key).copied().unwrap_or_default();
776        if old_value != *new_value {
777            vm.backup_storage_slot(fee_token, *key, old_value)?;
778        }
779    }
780    // Back up slots that will be removed by the bulk replacement
781    for (key, &old_value) in &current_storage {
782        if !new_storage.contains_key(key) {
783            vm.backup_storage_slot(fee_token, *key, old_value)?;
784        }
785    }
786
787    // Apply new storage
788    vm.db.get_account_mut(fee_token)?.storage = new_storage;
789
790    // update the initial state account
791    let initial_state_fee_token = db_clone
792        .initial_accounts_state
793        .get(&fee_token)
794        .cloned()
795        .ok_or(VMError::Internal(InternalError::Custom(
796            "No initial state found for fee token".to_owned(),
797        )))?;
798    // We have to merge, not insert
799    vm.db
800        .initial_accounts_state
801        .insert(fee_token, initial_state_fee_token);
802
803    Ok(())
804}
805
806/// Executes an L2 call as if it originated from the common bridge, returning
807/// both the execution report and the mutated database snapshot.
808fn simulate_common_bridge_call(
809    vm: &VM<'_>,
810    to: Address,
811    data: Bytes,
812) -> Result<(ExecutionReport, GeneralizedDatabase), VMError> {
813    let mut db_clone = vm.db.clone(); // expensive but necessary to simulate call
814    let origin = COMMON_BRIDGE_L2_ADDRESS; // We set the common bridge to restrict access to the contract
815    let nonce = db_clone.get_account(origin)?.info.nonce;
816    let simulation_tx = EIP1559Transaction {
817        // we are simulating the transaction
818        chain_id: u64::try_from(vm.env.chain_id).map_err(|_| {
819            VMError::Internal(InternalError::Custom("chain_id overflows u64".to_string()))
820        })?,
821        nonce,
822        max_priority_fee_per_gas: SIMULATION_MAX_FEE,
823        max_fee_per_gas: SIMULATION_MAX_FEE,
824        gas_limit: SIMULATION_GAS_LIMIT,
825        to: TxKind::Call(to),
826        value: U256::zero(),
827        data,
828        ..Default::default()
829    };
830    let tx = Transaction::EIP1559Transaction(simulation_tx);
831    let mut env_clone = vm.env.clone();
832    // Disable fee checks and update fields
833    env_clone.base_fee_per_gas = U256::zero();
834    env_clone.block_excess_blob_gas = None;
835    env_clone.gas_price = U256::zero();
836    env_clone.origin = origin;
837    env_clone.fee_token = None;
838    env_clone.gas_limit = SIMULATION_GAS_LIMIT;
839
840    let mut new_vm = VM::new(
841        env_clone,
842        &mut db_clone,
843        &tx,
844        LevmCallTracer::disabled(),
845        VMType::L2(Default::default()),
846        vm.crypto,
847    )?;
848    new_vm.hooks = vec![];
849    default_hook::set_bytecode_and_code_address(&mut new_vm)?;
850    let execution_result = new_vm.execute()?;
851
852    Ok((execution_result, db_clone))
853}
854
855/// Refunds the sender the unspent gas in fee tokens.
856/// Works similarly to refund_sender but uses the fee token contract
857/// But we don't want to be mixing L2 logic inside the default hook.
858fn refund_sender_fee_token(
859    vm: &mut VM<'_>,
860    ctx_result: &mut ContextResult,
861    refunded_gas: u64,
862    gas_spent: u64,
863    gas_used_pre_refund: u64,
864    fee_token_ratio: u64,
865) -> Result<(), VMError> {
866    vm.substate.refunded_gas = refunded_gas;
867
868    // EIP-7778: Separate block vs user gas accounting for Amsterdam+
869    if vm.env.config.fork >= Fork::Amsterdam {
870        // Block accounting uses pre-refund gas
871        ctx_result.gas_used = gas_used_pre_refund;
872        // User pays post-refund gas
873        ctx_result.gas_spent = gas_spent;
874    } else {
875        // Pre-Amsterdam: both use post-refund value
876        ctx_result.gas_used = gas_spent;
877        ctx_result.gas_spent = gas_spent;
878    }
879
880    // Return unspent gas to the sender.
881    let gas_to_return = vm
882        .env
883        .gas_limit
884        .checked_sub(gas_spent)
885        .ok_or(InternalError::Underflow)?;
886
887    let erc20_return_amount = vm
888        .env
889        .gas_price
890        .checked_mul(U256::from(gas_to_return))
891        .ok_or(InternalError::Overflow)?;
892    let sender_address = vm.env.origin;
893
894    pay_fee_token(
895        vm,
896        sender_address,
897        erc20_return_amount.saturating_mul(fee_token_ratio.into()),
898    )?;
899
900    Ok(())
901}
902
903/// Calculates the L1 fee based on the account diffs size and the L1 fee config.
904/// This is done according to the formula:
905/// L1 Fee = (L1 Fee per Blob Gas * GAS_PER_BLOB / SAFE_BYTES_PER_BLOB) * account_diffs_size
906fn calculate_l1_fee(
907    fee_config: &L1FeeConfig,
908    transaction_size: usize,
909) -> Result<U256, crate::errors::VMError> {
910    let l1_fee_per_blob: U256 = fee_config
911        .l1_fee_per_blob_gas
912        .checked_mul(GAS_PER_BLOB.into())
913        .ok_or(InternalError::Overflow)?
914        .into();
915
916    let l1_fee_per_blob_byte = l1_fee_per_blob
917        .checked_div(U256::from(SAFE_BYTES_PER_BLOB))
918        .ok_or(InternalError::DivisionByZero)?;
919
920    let l1_fee = l1_fee_per_blob_byte
921        .checked_mul(U256::from(transaction_size))
922        .ok_or(InternalError::Overflow)?;
923
924    Ok(l1_fee)
925}
926
927/// Calculates the L1 fee gas based on the account diffs size and the L1 fee config.
928/// Returns 0 if no L1 fee config is provided.
929fn calculate_l1_fee_gas(
930    vm: &VM<'_>,
931    l1_fee_config: &Option<L1FeeConfig>,
932) -> Result<u64, crate::errors::VMError> {
933    let Some(fee_config) = l1_fee_config else {
934        // No l1 fee configured, l1 fee gas is zero
935        return Ok(0);
936    };
937
938    let tx_size = vm.tx.length();
939
940    let l1_fee = calculate_l1_fee(fee_config, tx_size)?;
941    let mut l1_fee_gas = l1_fee
942        .checked_div(vm.env.gas_price)
943        .ok_or(InternalError::DivisionByZero)?;
944
945    // Ensure at least 1 gas is charged if there is a non-zero l1 fee
946    if l1_fee_gas == U256::zero() && l1_fee > U256::zero() {
947        l1_fee_gas = U256::one();
948    }
949
950    Ok(l1_fee_gas.try_into().map_err(|_| InternalError::Overflow)?)
951}
952
953/// Pays the L1 fee to the L1 fee vault for the gas used.
954/// This is calculated as gas_to_pay * gas_price.
955fn pay_to_l1_fee_vault(
956    vm: &mut VM<'_>,
957    gas_to_pay: u64,
958    l1_fee_config: L1FeeConfig,
959    use_fee_token: bool,
960) -> Result<(), crate::errors::VMError> {
961    let l1_fee = U256::from(gas_to_pay)
962        .checked_mul(vm.env.gas_price)
963        .ok_or(InternalError::Overflow)?;
964
965    if use_fee_token {
966        pay_fee_token(vm, l1_fee_config.l1_fee_vault, l1_fee)?;
967    } else {
968        vm.increase_account_balance(l1_fee_config.l1_fee_vault, l1_fee)
969            .map_err(|_| TxValidationError::InsufficientAccountFunds)?;
970    }
971    Ok(())
972}
973
974fn get_fee_token_ratio(vm: &mut VM<'_>, fee_token: H160) -> Result<U256, VMError> {
975    let fee_token_ratio = simulate_common_bridge_call(
976        vm,
977        FEE_TOKEN_RATIO_ADDRESS,
978        encode_fee_token_ratio_call(fee_token),
979    )?
980    .0;
981    if !fee_token_ratio.is_success() || fee_token_ratio.output.len() != 32 {
982        return Err(VMError::Internal(InternalError::Custom(
983            "Failed to get fee token ratio".to_owned(),
984        )));
985    }
986    Ok(U256::from_big_endian(
987        fee_token_ratio
988            .output
989            .get(0..32)
990            .ok_or(InternalError::Custom(
991                "Failed to parse fee token ratio".to_owned(),
992            ))?,
993    ))
994}