fix(rpc): align eth_estimateGas with geth on fee tokens and L1 data fee - #182
fix(rpc): align eth_estimateGas with geth on fee tokens and L1 data fee#182panos-xyz wants to merge 6 commits into
Conversation
Simulation paths (eth_call / eth_estimateGas) with is_fee_charge_disabled() previously short-circuited validate_and_deduct_token_fee before checking whether the specified fee_token_id was registered and active in the L2 Token Registry. This caused eth_estimateGas to incorrectly return 21000 for unregistered fee tokens like 65535 instead of rejecting the transaction. Move the token registration and active checks prior to the simulation short-circuit so that invalid tokens are rejected consistently across both execution and simulation paths.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughSimulation paths now validate that fee tokens are registered and active before skipping fee deduction. Unit and RPC integration tests verify rejection of unregistered and inactive tokens. ChangesFee token validation
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The simulation token-validation, legacy gas-price conversion, and L1 fee envelope changes have no identified merge-blocking risk. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
…ookup Decouple fee-token registry metadata loading into TokenRegistryEntry so simulation paths (eth_call / eth_estimateGas) only validate token existence and active status without reading the caller's token balance or invoking balanceOf. Move the caller balance lookup and fee deduction to non-simulation execution only, and verify that simulation paths never touch fee-token contract storage.
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
|
Thanks for addressing the fee token validation in simulation paths! Decoupling Comparing this with 1. Also validate non-zero
|
|
Regarding observation 2, I’m going to keep the current validation order and not make this change. Without treating geth’s error precedence as a compatibility requirement, checking the caller’s ETH value balance first is preferable for real transactions: the caller account has already been loaded for nonce/code validation, so the balance check is effectively free and avoids unnecessary Token Registry storage reads for transactions that cannot transfer their requested value anyway. It also preserves the real-execution ordering that existed on Simulation intentionally disables fee/balance charging checks, so it can validate token eligibility first without requiring identical error precedence to real execution. Both paths still reject invalid transactions, and the ordering has no consensus-state impact. Observation 1 is valid and will be addressed separately with an explicit invalid-token-configuration error rather than reporting a zero |
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
…Price try_into_tx_env gated MorphTx detection on `gas_price.is_none()`, so any eth_call / eth_estimateGas request that also carried a legacy `gasPrice` had its feeTokenID silently dropped and was priced as a plain ETH transaction. A caller holding only the fee token then failed with an ETH-funding error, and an invalid token id was never rejected at all. geth applies the "legacy gasPrice forces a standard transaction" rule only in toTransaction (real transaction construction); ToMessage — the path behind eth_call and eth_estimateGas — keys MorphTx detection off isMorphTxArgs() alone and never inspects GasPrice. Drop the gas_price condition from try_into_tx_env to match, leaving try_build_morph_tx_from_request (the toTransaction analogue) unchanged. Note this is not covered by the earlier simulation-path fix: with feeTokenID stripped during conversion, the handler never sees a token id, so the request still returned 0x5208. Closes #184
…tions The MorphTx encoding used to size the L1 data fee reads `max_priority_fee_per_gas` off the TxEnv. alloy models a legacy request as `gas_priority_fee: None`, so `build_morph_tx_for_l1_fee` fell back to `unwrap_or_default()` and encoded a zero tip: 0x80 instead of 0x84 3B9ACA00, four non-zero bytes short of what geth sizes. Since the estimate feeds `caller_gas_allowance`, an under-sized L1 fee widens the allowance and can hand back a gas estimate the caller cannot actually pay for. geth's `ToMessage` maps a legacy `gasPrice` onto both EIP-1559 caps (`gasFeeCap, gasTipCap = gasPrice, gasPrice`, inherited from upstream go-ethereum's original EIP-1559 work), and `asUnsignedMorphTx` then sizes the L1 fee from that tip. Carry the same mapping into the TxEnv when the request is a MorphTx so both clients size identical bytes — they serve the same endpoint, so a divergent estimate is directly visible as a non-deterministic result. `effective_gas_price` is unchanged either way (`None` and `Some(gas_price)` both resolve to `gas_price`), and this encoding is only reached from RPC simulation: real execution sizes the L1 fee from the signed transaction's own `encoded_2718()` bytes.
Dispatching on tx_type rebuilt Legacy/EIP-2930 envelopes for common gasPrice-only eth_estimateGas requests, under-sizing the L1 data fee versus go-ethereum's post-London asUnsignedTx path.
Observation: asymmetry in priority fee fallback between MorphTx and Ethereum L1-fee envelopesIn In // geth maps a legacy `gasPrice` onto both EIP-1559 caps (`ToMessage`).
let max_priority_fee_per_gas = self.max_priority_fee_per_gas().unwrap_or(self.gas_price());However, in Some(TxMorph {
chain_id: self.chain_id().unwrap_or(fallback_chain_id),
nonce: self.inner.nonce,
gas_limit: self.gas_limit(),
max_fee_per_gas: self.max_fee_per_gas(),
max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or_default(),
...While commit 4645667 backfills If a Suggestion: max_priority_fee_per_gas: self.max_priority_fee_per_gas().unwrap_or(self.gas_price()),This ensures both envelope builders are consistent and self-contained without relying on upstream callers to populate |
Closes #181
Closes #184
Closes #189
Summary
Align
eth_estimateGas/eth_callwith morph-geth on Morph fee-token handling and simulated L1 data fee sizing. Real execution is unchanged: signed transactions still size L1 fees from their ownencoded_2718()bytes, andeth_sendTransaction/eth_signTransactionstill follow geth'stoTransaction(legacygasPriceforces a standard ETH tx — see #190).What was wrong
Several independent simulation bugs stacked:
feeTokenID(fix(revm): eth_estimateGas incorrectly succeeds for unregistered feeTokenID #181).validate_and_deduct_token_feeshort-circuited ondisable_fee_chargebefore the L2 Token Registry check. Upstream'sis_basic_transferpath then returned0x5208(21000) instead of rejecting.feeTokenIDdropped whengasPricewas also set (fix(rpc): eth_estimateGas and eth_call strip feeTokenID when gasPrice is present #184).try_into_tx_envtreatedgas_price.is_some()as "not a MorphTx", so the same request was priced as a plain ETH tx. geth'sToMessage(the call/estimate path) ignoresGasPriceand keys MorphTx offisMorphTxArgs()alone.gasPrice. alloy leavesgas_priority_fee = None; the MorphTx encoder fell back to a zero tip, four non-zero bytes short of geth (ToMessagemapsgasPriceonto both caps).build_ethereum_envelope_for_l1_feedispatched ontx_typefromminimal_tx_type(), so agasPrice-only estimate was sized as Legacy while geth's post-LondonasUnsignedTxalways uses DynamicFeeTx.(3) and (4) both feed
caller_gas_allowance, so an under-sizedl1_feecan return a gas estimate the caller cannot actually pay.Changes
balanceOfoneth_call/eth_estimateGas.gas_price.is_none()gate fromtry_into_tx_envso simulation keeps Morph fields.try_build_morph_tx_from_request(thetoTransactionanalogue) is unchanged.gasPrice, map it onto both EIP-1559 caps in the TxEnv so L1-fee encoding matches geth.gasPriceonto both caps) unless the request is a MorphTx or an executable EIP-7702. The statetest harness no longer needs its own "untyped + baseFee → 1559" override.Test plan
try_into_tx_envwhengasPriceis set;try_build_morph_tx_from_requeststill drops themencode_for_l1_feematch geth DynamicFee byte lengths forgasPrice-only shapesestimate_gas_rejects_unregistered_fee_tokensimulation_rpcs_keep_fee_token_with_legacy_gas_priceSummary by CodeRabbit
Bug Fixes
Tests