Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions crates/vm/levm/src/hooks/l2_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,11 @@ fn prepare_execution_fee_token(vm: &mut VM<'_>) -> Result<(), crate::errors::VME
// (2) INSUFFICIENT_MAX_FEE_PER_BLOB_GAS
// NOT CHECKED: the blob price does not matter, fee token transactions do not support blobs

// (3) INSUFFICIENT_ACCOUNT_FUNDS
deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;
// ═══════════════════════════════════════════════════════════════════════════
// IMPORTANT: All validations that can fail MUST happen BEFORE fee deduction.
// Fee token storage changes via `transfer_fee_token` bypass the backup
// mechanism, so they cannot be rolled back if validation fails later.
// ═══════════════════════════════════════════════════════════════════════════

// (4) INSUFFICIENT_MAX_FEE_PER_GAS
Comment on lines 462 to 465
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now there's a gap in the numbering.

default_hook::validate_sufficient_max_fee_per_gas(vm)?;
Expand Down Expand Up @@ -515,6 +518,11 @@ fn prepare_execution_fee_token(vm: &mut VM<'_>) -> Result<(), crate::errors::VME
// Transaction is type 4 if authorization_list is Some
// NOT CHECKED: fee token transactions are not type 4

// (3) INSUFFICIENT_ACCOUNT_FUNDS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This number is inconsistent.

// NOTE: Fee deduction is intentionally placed AFTER all validations above.
// This ensures that if any validation fails, fee tokens are never locked.
deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;

default_hook::transfer_value(vm)?;

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this PR correctly moves fee deduction after all transaction validations, there's still a theoretical edge case: if transfer_value fails due to recipient balance overflow (line 526), the fee token storage changes cannot be rolled back by restore_cache_state because they bypass the backup mechanism via vm.db.get_account_mut.

Though highly unlikely in practice (requires recipient balance near U256::MAX), consider either moving deduct_caller_fee_token after transfer_value, or documenting this limitation. The same pattern exists in the default hook, but only affects ETH which can be rolled back, whereas fee token storage changes cannot.

Suggested change
// NOTE: Fee deduction is intentionally placed AFTER all validations above.
// This ensures that if any validation fails, fee tokens are never locked.
deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;
default_hook::transfer_value(vm)?;
// NOTE: Fee deduction is intentionally placed AFTER all validations above
// and AFTER value transfer. This ensures that if any validation or value
// transfer fails, fee tokens are never locked.
default_hook::transfer_value(vm)?;
deduct_caller_fee_token(vm, gaslimit_price_product.saturating_mul(fee_token_ratio))?;

Copilot uses AI. Check for mistakes.
default_hook::set_bytecode_and_code_address(vm)?;
Expand Down