Skip to content

fix(txpool): include operator fee in post-Isthmus affordability check#2645

Draft
0x00101010 wants to merge 2 commits into
mainfrom
fix/txpool-operator-fee-affordability
Draft

fix(txpool): include operator fee in post-Isthmus affordability check#2645
0x00101010 wants to merge 2 commits into
mainfrom
fix/txpool-operator-fee-affordability

Conversation

@0x00101010
Copy link
Copy Markdown
Contributor

@0x00101010 0x00101010 commented May 11, 2026

Summary

Mirror the execution-side cost in the txpool affordability check.

After Isthmus, BaseHandler deducts tx.cost + l1_data_fee + operator_fee, but the txpool only added l1_data_fee. A sender funded for tx.cost + l1_data_fee (but not the operator fee) was admitted by the pool and rejected by execution. The payload builder's mark_invalid is iterator-local, so the tx persists across builds.

apply_base_checks now calls L1BlockInfo::tx_cost, the same function execution uses.

Adds a regression test asserting Invalid(InsufficientFunds) at admission for an operator-fee-underfunded EIP-1559 tx.

Reported via HackerOne #74725.

After Isthmus, BaseHandler deducts L1 data fee + operator fee from the
sender, but the txpool admission check was only adding L1 data fee. A
sender funded for tx.cost + l1_data_fee but not the operator fee was
admitted by the pool and then permanently rejected by execution, since
mark_invalid in the payload builder is iterator-local and does not evict
the underlying transaction.

Mirror the execution-side cost in apply_base_checks by calling
L1BlockInfo::tx_cost, which adds the operator fee when post-Isthmus.

Adds a regression test asserting InsufficientFunds at admission for an
operator-fee-underfunded EIP-1559 transaction.
@cb-heimdall
Copy link
Copy Markdown
Collaborator

cb-heimdall commented May 11, 2026

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
base Ignored Ignored Preview May 11, 2026 11:54pm

Request Review

Comment on lines +257 to +261
let cost_addition = l1_block_info.tx_cost(
&encoded,
false,
) {
Ok(cost) => cost,
Err(err) => {
return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err));
}
};
U256::from(valid_tx.transaction().gas_limit()),
spec_id,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential panic on Isthmus+ chains before the first L1 info deposit is processed.

tx_costoperator_fee_chargeoperator_fee_charge_inner calls .expect() on operator_fee_scalar / operator_fee_constant. At genesis (block 0), the validator initializes L1BlockInfo with Default (both fields are None), and only the timestamp is set — update_l1_block_info is skipped because genesis has no transactions (see new() above, lines 130–131).

If the chain activates Isthmus at genesis (or before the first update_l1_block_info call populates the fields), BaseSpecId::from_timestamp will return an Isthmus+ spec, and the .expect() will panic on the first transaction validation.

The execution-side handler (BaseHandler::validate_against_state_and_deduct_caller) has the same latent issue, so this PR isn't making things worse — it's aligning txpool with execution. But it's worth noting that this code path is now reachable from the txpool for the first time.

Consider either:

  • Falling back to zero operator fee when the scalars are None (matching pre-Isthmus behavior), or
  • Returning TransactionValidationOutcome::Error if the scalars are missing.

@github-actions
Copy link
Copy Markdown
Contributor

Review Summary

The fix correctly addresses the HackerOne #74725 vulnerability by switching from l1_tx_data_fee (L1 data fee only) to tx_cost (L1 data fee + operator fee), mirroring the execution-side deduction in BaseHandler::validate_against_state_and_deduct_caller. The regression test is well-constructed with proper assertions that the fixture produces a non-zero operator fee gap.

Finding

Latent panic on Isthmus+ chains at genesis (low severity): tx_cost internally calls operator_fee_charge_inner, which uses .expect() on operator_fee_scalar / operator_fee_constant. At genesis, these fields are None because update_l1_block_info is skipped for block 0. If the chain spec activates Isthmus at genesis, the first txpool validation will panic. The execution handler has the same latent issue, so this PR aligns behavior rather than introducing a new bug — but it does make this panic path reachable from the txpool for the first time. See inline comment for suggested mitigations.

What looks good

  • Using gas_limit (not gas_used) for the operator fee matches the pre-execution deduction in validate_against_state_and_deduct_callertx_cost_with_tx.
  • The old l1_tx_data_fee Result handling was dead code (it always returned Ok), so removing the Err arm is correct.
  • The cloned L1BlockInfo avoids holding the RwLock read guard across the computation.
  • The test correctly constructs a balance that covers tx.cost + l1_data_fee but not tx.cost + l1_data_fee + operator_fee, verifying the exact gap the bug exploited.

@danyalprout
Copy link
Copy Markdown
Collaborator

As Base doesn't have an operator fee and probably won't, should we just come up with a plan to remove this in a future hard fork too?

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.

3 participants