Skip to content

fix(revm): restore original_value after token fee reimbursement#43

Closed
panos-xyz wants to merge 4 commits intomainfrom
codex/token-fee-reimburse-fix
Closed

fix(revm): restore original_value after token fee reimbursement#43
panos-xyz wants to merge 4 commits intomainfrom
codex/token-fee-reimburse-fix

Conversation

@panos-xyz
Copy link
Contributor

@panos-xyz panos-xyz commented Mar 10, 2026

Summary

  • align the main-branch fix with the current runtime-tracker implementation instead of keeping a reimbursement-only patch
  • add MorphTxRuntime to track forced-cold slot originals across token-fee deduction, EVM execution, and reimbursement
  • cover both direct journal reimbursement and first-access SSTORE with regression tests

Context

main already had a partial fix based on MorphTxEnv::fee_slot_original_values, but that only covered the EVM SLOAD path. The reimbursement path still called transfer_erc20_with_slot directly and bypassed sload_morph, which let revm overwrite original_value for the fee-token balance slots.

This PR now matches the current in-progress implementation used on the working branch:

  • move tx-scoped tracking out of MorphTxEnv and into a dedicated MorphTxRuntime
  • wire that runtime through MorphContext
  • restore tracked originals from a single source in all affected paths
  • add a custom SSTORE override so forced-cold slots are also protected when the first main-execution touch is a write instead of a read

This replaces the earlier minimal reimbursement-only patch on this PR branch.

Test Plan

  • cargo fmt
  • cargo test -p morph-revm
  • cargo test -p morph-evm

Summary by CodeRabbit

  • New Features

    • Token-based fee reimbursement (ERC20 or EVM transfer paths) and cached per-transaction L1 data fee computation
    • L1 fee cap for circuit compatibility
    • Per-transaction runtime to track/restore forced-cold storage and new accessors for cached fees and pre/post-fee logs
  • Bug Fixes

    • Restore original storage slot values and reset per-transaction caches/logs between iterations
    • Skip token reimbursement during fee-less simulations (e.g., eth_call)

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6b9e0d2-be17-4f40-82c8-6c4cde006def

📥 Commits

Reviewing files that changed from the base of the PR and between c61633f and b141eb6.

📒 Files selected for processing (3)
  • crates/revm/src/handler.rs
  • crates/revm/src/runtime.rs
  • crates/txpool/src/validator.rs

📝 Walkthrough

Walkthrough

Adds per-transaction runtime state and L1-data fee caching; extends fee handling to support ERC20 token reimbursement (slot-based or internal EVM-call), introduces helpers to perform/restore fee transfers and preserve original storage values, and updates morph sload/sstore behavior and tests.

Changes

Cohort / File(s) Summary
Handler / Token-fee flow
crates/revm/src/handler.rs
Expanded fee handling to support token-based payments alongside ETH, added per-tx L1 data fee calculation/caching, token reimbursement via slot writes or internal EVM CALLs, early-exit for eth_call scenarios, and new helpers (evm_call, evm_call_balance_of, transfer_erc20_with_evm, transfer_erc20_with_slot, restore_tracked_original_values, calculate_caller_fee_with_l1_cost).
EVM surface & accessors
crates/evm/src/evm.rs
Now constructs an inner MorphEvm, derives precompiles from it, wires chain config for MorphTxRuntime, and exposes new accessors: cached_token_fee_info, cached_l1_data_fee, take_pre_fee_logs, take_post_fee_logs plus inspector/context helpers.
revm core: runtime, morph instrs & journals
crates/revm/src/evm.rs, crates/revm/src/runtime.rs, crates/revm/src/lib.rs
Introduced MorphTxRuntime (per-tx forced-cold slot originals) and added runtime module export; replaced prior fee-slot tracking with runtime-based tracking; added sload_morph/sstore_morph and restore_tracked_original_value; added cached_token_fee_info, cached_l1_data_fee, pre_fee_logs and post_fee_logs fields and accessors.
L1 fee handling
crates/revm/src/l1block.rs
Made Curie L1 fee fields concrete U256, added L1_FEE_CAP and applied cap in calculate_tx_l1_cost, updated related calculations and tests.
Tx envelope / MorphTx parsing
crates/revm/src/tx.rs
Removed fee_slot_original_values from MorphTxEnv; eliminated RLP-based extraction helper and now extract fields directly from MorphTxEnvelope; adjusted constructor/initialization accordingly.
Tests / scaffolding
crates/revm/src/handler.rs (cfg(test))
Added tests and harness code covering forced-cold slot preservation, token-fee reimbursements, and EVM-based token transfers.
Small changes
crates/txpool/src/validator.rs
Replaced clone-from-lock with copy/dereference for L1BlockInfo retrieval.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller/Tx
    participant Handler as revm::handler
    participant EVM as MorphEvm
    participant Runtime as MorphTxRuntime
    participant DB as Journal/State

    Caller->>Handler: Submit transaction (may include token fee)
    Handler->>EVM: validate_and_deduct_fee (compute L1 data fee)
    EVM->>Runtime: cache per-tx L1 data fee & TokenFeeInfo
    Handler->>Runtime: track_forced_cold_slot(address, slot, original)
    alt token reimbursement via slot
      Handler->>EVM: transfer_erc20_with_slot(from,to,token,amt)
      EVM->>DB: write slot changes (journal)
      Runtime->>DB: restore_tracked_original_value for forced-cold slots
    else token reimbursement via internal CALL
      Handler->>EVM: evm_call(balanceOf/transfer calldata)
      EVM-->>Handler: result + logs (pre/post fee logs separated)
      Runtime->>DB: restore_tracked_original_value if needed
    end
    Handler->>Caller: continue execution with restored originals and fee logs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • anylots
  • chengwenxi

Poem

🐰
I hopped through slots both cold and deep,
Saved originals while others sleep,
Token fees twirled beneath the moon,
I called and wrote, then fixed each rune,
Carrots, logs, and state — all neat. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective: restoring original storage values after token fee reimbursement, which is central to this substantial refactor introducing MorphTxRuntime for tracking forced-cold slots.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/token-fee-reimburse-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/revm/src/handler.rs`:
- Around line 629-633: The conditionals with the let-chains involving
tx.fee_slot_original_values and state.get_mut(&address) are misformatted and
failing rustfmt; reformat these blocks to match rustfmt style (e.g., split the
chained let/if into rustfmt-friendly lines or use parentheses) and run cargo fmt
--all to ensure CI passes; locate the affected expressions around
tx.fee_slot_original_values.iter().find(|(saved_address, saved_slot, _)|
*saved_address == address && *saved_slot == slot_key) and the subsequent let
Some(account) = state.get_mut(&address) and apply the same reformatting to the
other similar block later (the one with the same pattern at the other
occurrence).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57274f97-8334-4285-9795-bbc350892473

📥 Commits

Reviewing files that changed from the base of the PR and between 7d7f090 and 5480095.

📒 Files selected for processing (1)
  • crates/revm/src/handler.rs

@panos-xyz
Copy link
Contributor Author

Code review

Found 2 issues:

  1. post_fee_logs assigned unconditionally even when EVM-path refund fails

In reimburse_caller_token_fee, the else branch (no balance_slot) sets evm.post_fee_logs = refund_logs before the error is checked. If transfer_erc20_with_evm emits a Transfer log during the EVM call and then fails a post-transfer validation (return value check, balance delta check), those logs are drained into post_fee_logs and merged into the receipt by the block executor. Go-ethereum's refundGas() is silent on failure — no logs are emitted. This would produce receipt hash mismatches when a token reimbursement fails via the EVM-call path.

Fix: only assign evm.post_fee_logs = refund_logs when the result is Ok.

let log_count_before = evm.ctx_mut().journal_mut().logs.len();
let result = transfer_erc20_with_evm(
evm,
beneficiary,
caller,
token_fee_info.token_address,
token_amount_required,
None,
);
let refund_logs: Vec<_> = evm
.ctx_mut()
.journal_mut()
.logs
.drain(log_count_before..)
.collect();
evm.post_fee_logs = refund_logs;
result
};

  1. track_forced_cold_slot_from_state is dead code

MorphTxRuntime::track_forced_cold_slot_from_state is defined but never called anywhere in the codebase. All call sites use track_forced_cold_slot directly after manually extracting slot.original_value. This method should be removed or used to replace the manual extraction pattern.

#[inline]
pub fn track_forced_cold_slot_from_state(
&mut self,
state: &EvmState,
address: Address,
slot: U256,
) -> Option<U256> {
let original_value = state
.get(&address)
.and_then(|account| account.storage.get(&slot))
.map(|slot_state| slot_state.original_value)?;
self.track_forced_cold_slot(address, slot, original_value);
Some(original_value)
}


Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@panos-xyz panos-xyz closed this Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant