fix: F-2026-18145 | [Dual Defense] Missing OP-Stack L1 Data-Fee Accounting Under-Reports Outbound Cost - #304
Merged
Merged
Conversation
…eipt fetch (F-2026-18145)
… rpc_client (F-2026-18145)
… accounting (F-2026-18145)
…drop tx fetch (F-2026-18145)
…ned (F-2026-18145)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
F-2026-18145 — OP-Stack L1 data-fee under-reporting
Problem
Outbound settlement recorded
GasFeeUsed = gasUsed * gasPrice, capturing only the L2 execution cost. On OP-Stack destinations (Base, Optimism, …) a transaction also pays an L1 data fee, which was dropped. Core'sapplyGasRefundcomputesgasFee − GasFeeUsed, so the omitted L1 component was silently over-refunded to users, and the TSS/relayer balance absorbed it.Fix
GasFeeUsednow includes the L1 fee and uses the price actually paid, read from a singleeth_getTransactionReceiptcall:l1Fee— the OP-Stack L1 data fee. go-ethereum's typedtypes.Receiptdoesn't expose it, so we read it from the raw receipt JSON. Absent on non-OP chains →0, so those are unaffected.effectiveGasPrice— the price actually paid (from the receipt), correct for both legacy and EIP-1559 txs. This avoids thetx.GasPrice()pitfall, where a type-2 tx returnsmaxFeePerGas(the cap) and would over-report.effectiveGasPriceis present on every post-London chain (all targets), so it is not defended with a secondary lookup. If a receipt ever omits it, both paths return"0"/ skip-and-retry rather than silently emit an L2-less fee.Both settlement paths use it:
event_confirmer.goTxBuilder.GetGasFeeUsed(consumed bytxresolver)The receipt fetch was consolidated to one raw call returning the fields we use (
status,blockNumber,gasUsed,effectiveGasPrice,l1Fee); the separate transaction fetch was removed.Tests (
l1fee_test.go)GasFeeUsed = execFee + l1Fee.GasFeeUsed = execFeeonly.effectiveGasPrice→"0"(guard; never an L2-less fee)."0".Verified against live Base Sepolia receipts (real
l1Fee, e.g.0x2a38522d9).Out of scope (flagged separately)
getL1Feeupper bound in quotes) and payload-size caps for OP destinations — quoting/policy, partly core-side.Severity: Low (relayer gas accounting on OP/Base-class chains; no principal theft).