Skip to content

Commit b8d1270

Browse files
authored
fix(transaction-pay-controller): convert exchange rate numbers to strings before passing to BigNumber (#8808)
## Explanation Fixes a crash in `transaction-pay-controller` caused by `BigNumber` throwing when receiving a JavaScript `number` with more than 15 significant digits. ### Root Cause `BigNumber` enforces a strict 15 significant digit limit on `number` inputs — both in the constructor (`new BigNumber(number)`) and in math methods (`.multipliedBy(number)`, `.times(number)`, `.plus(number)`, etc.) — because IEEE 754 floats lose precision beyond that. In `getTokenFiatRate` ([`token.ts`](https://github.com/MetaMask/core/blob/main/packages/transaction-pay-controller/src/utils/token.ts#L215-L221)), three raw `number` values from external sources are passed directly to BigNumber: ```typescript // tokenToNativeRate: number — from marketData[chainId][address].price // nativeToUsdRate: number — from CurrencyRateController state // nativeToFiatRate: number — from CurrencyRateController state const usdRate = new BigNumber(tokenToNativeRate ?? 1) // ❌ constructor .multipliedBy(nativeToUsdRate) // ❌ method arg .toString(10); const fiatRate = new BigNumber(tokenToNativeRate ?? 1) // ❌ constructor .multipliedBy(nativeToFiatRate) // ❌ method arg .toString(10); ``` ### When does this crash? **Case 1: Large conversion rate (weak-currency locales like VND, IDR)** ```js new BigNumber(40115252.21304121) // ❌ 16 significant digits → THROWS new BigNumber("40115252.21304121") // ✅ string input → no limit // .multipliedBy() also validates: bn.multipliedBy(40115252.21304121) // ❌ THROWS bn.multipliedBy("40115252.21304121") // ✅ no limit ``` **Case 2: Very precise small price (micro-cap tokens)** ```js new BigNumber(0.00000123456789012345) // ❌ 17 significant digits → THROWS new BigNumber("0.00000123456789012345") // ✅ string input → no limit ``` **Case 3: Near-peg stablecoin with API floating point noise** ```js new BigNumber(1.0000000000000002) // ❌ 17 significant digits → THROWS new BigNumber("1.0000000000000002") // ✅ string input → no limit ``` Real user state logs confirm CurrencyRateController stores values exceeding 15 significant digits for weak-currency locales: ```json "currencyRates": { "ETH": { "conversionRate": 40115252.21304121 }, "BNB": { "conversionRate": 11259865.090939905 } } ``` This happens because `CurrencyRateController.boundedPrecisionNumber` uses `toFixed(9)` which bounds **decimal** digits, not **significant** digits. For large numbers, 8+ integer digits + 9 decimal digits = 17+ significant digits. ### The Fix Wrap all three values in `String()` before passing to BigNumber. BigNumber's string constructor and string method arguments have no precision limit. `getTokenFiatRate` is the single entry point where raw `number` values from CurrencyRateController and marketData enter the transaction-pay-controller. Everything downstream operates on `FiatRates` (`{ fiatRate: string, usdRate: string }`), so fixing this one function protects the entire controller. ### Related - Extension fix: MetaMask/metamask-extension#42674 - Upstream root cause: [`CurrencyRateController.boundedPrecisionNumber`](https://github.com/MetaMask/core/blob/main/packages/assets-controllers/src/CurrencyRateController.ts#L111-L112) should use `toPrecision(15)` instead of `toFixed(9)` (separate issue for @metamask/assets-controllers) ## Changelog ### `@metamask/transaction-pay-controller` - **Fixed**: Crash when exchange rate numbers exceed 15 significant digits by converting to strings before passing to BigNumber <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: small, localized change to exchange-rate arithmetic that only affects how values are passed into `BigNumber` (number→string) and should preserve outputs while preventing runtime throws. > > **Overview** > Prevents a runtime crash in `getTokenFiatRate` by converting exchange-rate inputs to strings before constructing/operating on `BigNumber`, avoiding the 15-significant-digit limit on JS `number` inputs. > > Adds a matching `Unreleased` changelog entry noting the fix. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d140b4e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 10ae080 commit b8d1270

2 files changed

Lines changed: 5 additions & 4 deletions

File tree

packages/transaction-pay-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- Fix BigNumber crash when exchange rate numbers exceed 15 significant digits ([#8808](https://github.com/MetaMask/core/pull/8808))
1617
- Handle gas-station and prefunded gas-estimate edge cases for Across Predict withdraw quotes ([#8762](https://github.com/MetaMask/core/pull/8762))
1718

1819
## [22.5.0]

packages/transaction-pay-controller/src/utils/token.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,12 @@ export function getTokenFiatRate(
212212

213213
const usdRate = isStablecoin
214214
? '1'
215-
: new BigNumber(tokenToNativeRate ?? 1)
216-
.multipliedBy(nativeToUsdRate)
215+
: new BigNumber(String(tokenToNativeRate ?? 1))
216+
.multipliedBy(String(nativeToUsdRate))
217217
.toString(10);
218218

219-
const fiatRate = new BigNumber(tokenToNativeRate ?? 1)
220-
.multipliedBy(nativeToFiatRate)
219+
const fiatRate = new BigNumber(String(tokenToNativeRate ?? 1))
220+
.multipliedBy(String(nativeToFiatRate))
221221
.toString(10);
222222

223223
return { usdRate, fiatRate };

0 commit comments

Comments
 (0)