Skip to content

Conversation

@ilitteri
Copy link
Collaborator

Motivation

Addresses audit finding: fee token storage changes are not rolled back when validation fails after fee deduction.

In prepare_execution_fee_token, fee deduction via deduct_caller_fee_token was happening BEFORE several validation checks (e.g., validate_sufficient_max_fee_per_gas, validate_priority_fee). Since transfer_fee_token uses vm.db.get_account_mut directly (bypassing the backup mechanism), if any subsequent validation failed, the fee token storage changes could not be rolled back by restore_cache_state.

This could lead to fee tokens being locked in the fee token contract without a corresponding transfer to the sequencer.

Description

Moves deduct_caller_fee_token to AFTER all validation checks that can fail, ensuring fee tokens are never deducted for transactions that will fail validation.

The key insight is that vm.db.get_account_mut (used by transfer_fee_token) does NOT backup storage changes, while vm.get_account_mut does. The code comment explicitly warns: "Use directly only if outside of the EVM, otherwise use vm.get_account_mut because it contemplates call frame backups."

Checklist

  • Blockchain tests pass
  • Lint passes
  • Code review

In `prepare_execution_fee_token`, fee deduction via `deduct_caller_fee_token`
was happening BEFORE several validation checks. Since `transfer_fee_token`
uses `vm.db.get_account_mut` directly (bypassing the backup mechanism),
if any subsequent validation failed, the fee token storage changes could
not be rolled back by `restore_cache_state`.

This moves `deduct_caller_fee_token` to AFTER all validation checks,
ensuring fee tokens are never deducted for transactions that will fail
validation. The fix prevents a scenario where fee tokens could be locked
in the fee token contract without a corresponding transfer to the sequencer.
Copilot AI review requested due to automatic review settings January 27, 2026 19:47
@ilitteri ilitteri added the audit label Jan 27, 2026
@github-actions github-actions bot added the levm Lambda EVM implementation label Jan 27, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a critical audit finding in the LEVM implementation where fee token storage changes were not properly rolled back when validation failed. The fix reorders the execution flow in prepare_execution_fee_token to ensure all transaction validations occur before fee token deduction.

Changes:

  • Moved deduct_caller_fee_token call from before validations to after all validation checks
  • Added comprehensive warning comments explaining why this ordering is critical
  • Documented that fee token storage changes bypass the backup mechanism and cannot be rolled back

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 522 to 527
// 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.
@github-actions
Copy link

github-actions bot commented Jan 27, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 2.933 ± 0.019 2.916 2.965 1.01 ± 0.01
main_levm_BubbleSort 3.032 ± 0.014 3.017 3.056 1.04 ± 0.01
pr_revm_BubbleSort 2.918 ± 0.025 2.893 2.960 1.00
pr_levm_BubbleSort 3.027 ± 0.027 3.002 3.083 1.04 ± 0.01

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 981.0 ± 9.8 969.0 994.7 1.02 ± 0.01
main_levm_ERC20Approval 1077.6 ± 16.0 1060.9 1110.7 1.12 ± 0.02
pr_revm_ERC20Approval 966.2 ± 9.8 957.9 986.4 1.00
pr_levm_ERC20Approval 1052.1 ± 10.8 1041.7 1071.4 1.09 ± 0.02

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 135.1 ± 1.3 133.7 137.5 1.04 ± 0.02
main_levm_ERC20Mint 160.8 ± 1.7 158.2 163.2 1.23 ± 0.02
pr_revm_ERC20Mint 130.4 ± 1.9 128.4 133.3 1.00
pr_levm_ERC20Mint 159.0 ± 2.1 156.8 162.5 1.22 ± 0.02

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 234.2 ± 7.1 229.4 252.5 1.02 ± 0.03
main_levm_ERC20Transfer 269.7 ± 1.5 267.2 271.7 1.18 ± 0.01
pr_revm_ERC20Transfer 229.3 ± 2.2 226.1 232.6 1.00
pr_levm_ERC20Transfer 263.8 ± 2.0 261.1 266.6 1.15 ± 0.01

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 222.4 ± 3.0 217.4 229.9 1.00
main_levm_Factorial 254.0 ± 12.0 247.0 287.8 1.14 ± 0.06
pr_revm_Factorial 223.6 ± 1.6 222.4 227.5 1.01 ± 0.02
pr_levm_Factorial 251.4 ± 5.4 246.4 265.7 1.13 ± 0.03

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.558 ± 0.105 1.301 1.663 1.00
main_levm_FactorialRecursive 8.479 ± 0.049 8.407 8.576 5.44 ± 0.37
pr_revm_FactorialRecursive 1.586 ± 0.022 1.559 1.623 1.02 ± 0.07
pr_levm_FactorialRecursive 8.462 ± 0.057 8.392 8.535 5.43 ± 0.37

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 200.8 ± 4.0 197.7 210.9 1.00 ± 0.02
main_levm_Fibonacci 222.0 ± 2.7 220.0 228.3 1.11 ± 0.02
pr_revm_Fibonacci 200.6 ± 2.4 199.1 207.1 1.00
pr_levm_Fibonacci 223.9 ± 4.9 219.7 236.4 1.12 ± 0.03

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 838.6 ± 10.5 827.6 864.9 1.21 ± 0.02
main_levm_FibonacciRecursive 703.0 ± 11.7 693.9 734.5 1.02 ± 0.02
pr_revm_FibonacciRecursive 835.1 ± 6.6 824.8 847.7 1.21 ± 0.01
pr_levm_FibonacciRecursive 692.6 ± 4.4 687.7 700.4 1.00

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.3 ± 0.1 8.2 8.4 1.00
main_levm_ManyHashes 9.5 ± 0.1 9.4 9.8 1.15 ± 0.02
pr_revm_ManyHashes 8.3 ± 0.1 8.2 8.4 1.00 ± 0.01
pr_levm_ManyHashes 9.5 ± 0.2 9.3 10.0 1.15 ± 0.03

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 260.7 ± 8.0 253.5 272.6 1.21 ± 0.04
main_levm_MstoreBench 216.6 ± 4.7 212.2 228.8 1.00 ± 0.02
pr_revm_MstoreBench 259.2 ± 6.9 253.5 271.9 1.20 ± 0.03
pr_levm_MstoreBench 216.1 ± 1.5 213.0 218.2 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 287.0 ± 4.9 284.2 300.1 1.08 ± 0.02
main_levm_Push 266.1 ± 2.7 261.1 270.6 1.00 ± 0.01
pr_revm_Push 286.4 ± 3.7 284.1 296.4 1.08 ± 0.02
pr_levm_Push 265.2 ± 1.7 263.2 268.2 1.00

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 160.9 ± 2.2 157.2 163.1 1.71 ± 0.04
main_levm_SstoreBench_no_opt 94.1 ± 1.9 91.4 98.1 1.00
pr_revm_SstoreBench_no_opt 161.3 ± 1.8 157.1 163.1 1.71 ± 0.04
pr_levm_SstoreBench_no_opt 96.8 ± 7.9 91.2 117.3 1.03 ± 0.09

// 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.

… value transfer failure

Addresses PR review feedback:
- Remove inconsistent (3) numbering since the check is no longer in position 3
- Split deduct_caller_fee_token: ETH value deduction stays before transfer_value
  (correct accounting), but lock_fee_token moves after it so fee tokens are
  never locked if transfer_value fails (fee token storage bypasses cache backup)
- Remove now-unused deduct_caller_fee_token function
@github-actions
Copy link

Lines of code report

Total lines added: 0
Total lines removed: 5
Total lines changed: 5

Detailed view
+--------------------------------------------+-------+------+
| File                                       | Lines | Diff |
+--------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/l2_hook.rs | 560   | -5   |
+--------------------------------------------+-------+------+

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit levm Lambda EVM implementation

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants