Skip to content

fix: linea gas fee flow and user-op gas buffer truncate fractional muln multipliers - #10045

Open
gomesalexandre wants to merge 2 commits into
MetaMask:mainfrom
gomesalexandre:fix_linea_gas_fractional_muln
Open

fix: linea gas fee flow and user-op gas buffer truncate fractional muln multipliers#10045
gomesalexandre wants to merge 2 commits into
MetaMask:mainfrom
gomesalexandre:fix_linea_gas_fractional_muln

Conversation

@gomesalexandre

@gomesalexandre gomesalexandre commented Sep 1, 2026

Copy link
Copy Markdown

What

LineaGasFeeFlow (@metamask/transaction-controller) and normalizeGasEstimate (@metamask/user-operation-controller) both apply a decimal multiplier to a BN via BN.muln(fractionalNumber). bn.js's imuln requires an integer multiplier but never validates that — it does w & 0x3ffffff per 26-bit word, so a fractional multiplier gets silently truncated instead of throwing. The value that comes out the other end is wrong and under-computed, and on the Linea side it reaches what actually gets signed by default (RandomisedEstimationsGasFeeFlow only supersedes this flow behind a remote feature flag).

Repro (pinned bn.js@5.2.2, exactly what ships in this repo)

1 gwei  x1.05 (muln) -> 1003023795   (exact 1050000000, short 46976205 wei = 4.47% low)
30 gwei x1.35 (muln) -> 40469801011  (exact 40500000000)
100000000 x1.5 (muln) -> 116445568  (exact 150000000)

The transaction-controller side is live right now: LineaGasFeeFlow.test.ts's existing pinned expectations (0x23a3d70a3, 0x25658bf25 for priority fees, 0x3a7ae1479, 0x42428f5c1 for max fees) are snapshots of the buggy output, not independently-verified correct values — I confirmed this by computing the old muln output directly and it matches the pinned hex exactly.

The user-operation-controller side (GAS_ESTIMATE_MULTIPLIER = 1.5) needs the estimate to cross 2^26 (~67.1M) before it's wrong, which is above typical UserOperation gas but not impossible (heavy calldata/batched ops, chains with higher gas costs). Included as the same bug class with the same trivial fix, not as the headline issue.

Fix

Both now use fractionBN (already exported from @metamask/controller-utils, and already used correctly for exactly this purpose at packages/transaction-controller/src/utils/gas.ts:401) instead of muln. fractionBN does target.mul(numerator).div(denominator) — full-precision BN multiplication, not the fixed-width imuln path — so it has no truncation boundary. The decimal multiplier is converted to an integer numerator/denominator pair via Math.round(multiplier * PRECISION) / PRECISION (guards against multiplier * 100 landing on a non-integer float, e.g. 1.1 * 100 === 110.00000000000001 in JS).

Tests

  • Recomputed and corrected the two pinned hex expectations in LineaGasFeeFlow.test.ts that were snapshotting the buggy output (see repro above for how I know the old values were wrong, not just different).
  • Added a new test to each affected suite that doesn't rely on a pinned snapshot — it computes the expected value independently via BigInt/exact decimal math and uses an input magnitude chosen specifically to fail against the old muln implementation. Verified this directly: stashed just the source fix (kept the new tests) and confirmed 3 tests fail with the exact truncated values shown above; restored the fix and all pass.
$ yarn jest LineaGasFeeFlow --no-coverage
Test Suites: 1 passed, 1 total
Tests:       8 passed, 8 total

$ yarn jest (full transaction-controller package)
Test Suites: 44 passed, 44 total
Tests:       1141 passed, 1141 total

$ yarn jest gas.test.ts --no-coverage (user-operation-controller)
Test Suites: 1 passed, 1 total
Tests:       6 passed, 6 total

$ yarn jest (full user-operation-controller package)
Test Suites: 10 passed, 10 total
Tests:       217 passed, 217 total

$ yarn lint:tsc   -> clean, no output
$ yarn eslint <4 changed files>  -> clean, no output

Risk

Low — this only ever increases the previously-under-computed medium/high estimates toward the mathematically-intended value; it never changes the low level (multiplier 1, unaffected either way) and never produces a value the code wasn't already trying to produce.

Scope note

Kept to the two muln-with-fractional-multiplier call sites found via rg. Did not touch other BN arithmetic in either package.


Note

Medium Risk
Linea default gas fees and user-op gas buffers increase toward previously intended values, affecting what users see and sign; behavior is corrective but touches fee-critical paths.

Overview
Fixes silent under-estimation when applying decimal gas multipliers with BN.muln, which truncates fractional inputs per 26-bit word instead of multiplying correctly.

LineaGasFeeFlow now scales base and priority fees for medium/high via fractionBN through a new #applyMultiplier helper (replacing base.muln(1.35), 1.7, 1.05, 1.1). Medium and high max fee and priority fee values increase toward the intended math; low (multiplier 1) is unchanged. Tests update previously wrong pinned hex expectations and add a case that asserts exact fractional math.

normalizeGasEstimate in user-operation-controller applies the same pattern for the 1.5× gas buffer, fixing wrong buffered limits when estimates exceed ~67M gas. A regression test covers a large callGasLimit.

Changelogs document both fixes (#10045).

Reviewed by Cursor Bugbot for commit 4a6d230. Bugbot is set up for automated code reviews on this repo. Configure here.

…ln multipliers

BN.muln() requires an integer multiplier but never validates that. It
silently truncates non-integer input per 26-bit word instead of throwing,
so LineaGasFeeFlow's 1.35/1.7/1.05/1.1 multipliers and the user-operation
controller's 1.5 gas buffer multiplier were producing wrong, under-computed
values instead of the intended ones.

On Linea this reaches what actually gets signed by default. On the
user-operation side it only bites once an estimate needs more than one
26-bit BN word (~67.1M+), so it's a real but lower-frequency instance of
the same class.

Fixed both to use fractionBN (target.mul(numerator).div(denominator)),
which does full-precision BN math instead of muln's fixed-width path -
the same helper already used correctly for this exact purpose at
transaction-controller/src/utils/gas.ts:401.

Recomputed the two pinned LineaGasFeeFlow.test.ts hex expectations that
were snapshotting the buggy truncated output, and added a test to each
affected suite that computes its expected value independently (BigInt
exact math / a magnitude chosen to fail under the old muln) rather than
asserting another pinned snapshot - verified these fail against the old
implementation with the exact wrong values.
@gomesalexandre
gomesalexandre marked this pull request as ready for review September 1, 2026 12:04
@gomesalexandre
gomesalexandre requested review from a team as code owners September 1, 2026 12:04
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