From dd0e234c63207ebfcbf853345814364c564512b8 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:54:42 +0200 Subject: [PATCH 1/2] fix: reject fractional wei in toWei unitLengths computed each unit's baseLength as `value.length - 1 || 1`. wei's raw value is '1' (length 1), so `1 - 1` is `0`, and the `|| 1` fallback (meant defensively for units that legitimately need it, but none do) silently coerced wei's baseLength from the correct 0 up to 1. toWei('0.5', 'wei') returned 5n instead of throwing "too many decimal places", since a single fractional digit fit inside the inflated baseLength of 1. Dropping the `|| 1` alone isn't enough: toWei() also unconditionally defaulted a missing fraction to the string '0' before checking its length against baseLength, which was harmless while baseLength was always >= 1 but throws on ordinary whole-wei input (toWei(0, 'wei'), toWei('5', 'wei')) once baseLength is correctly 0. Fixed by only running the length check/pad when the input actually supplied a fraction; a genuinely absent fraction is always 0 regardless of the unit's precision. One existing test needed updating: toWei('0.0', 'wei') was asserted to return 0n, but that was the same bug in disguise - wei has zero decimal places, so an explicit fractional digit (even a literal zero) should be rejected exactly like it would be for any other unit given more fractional digits than it supports. Guard-validated: reverting the source change makes the new "should reject fractional wei" test fail; restoring it passes again. Co-Authored-By: Claude --- src/unitsConversion.test.ts | 20 +++++++++++++++++++- src/unitsConversion.ts | 19 ++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/unitsConversion.test.ts b/src/unitsConversion.test.ts index f8cd58b41..b008674a5 100644 --- a/src/unitsConversion.test.ts +++ b/src/unitsConversion.test.ts @@ -151,7 +151,10 @@ describe('getValueOfUnit', () => { describe('toWei', () => { it('should handle edge cases', () => { expect(toWei(0, 'wei').toString(10)).toBe('0'); - expect(toWei('0.0', 'wei').toString(10)).toBe('0'); + // wei has zero decimal places, so an explicit fractional digit (even a + // literal zero) is "too many decimal places", same as it would be for + // any other unit given more fractional digits than it supports. + expect(() => toWei('0.0', 'wei')).toThrow(Error); expect(toWei('.3', 'ether').toString(10)).toBe('300000000000000000'); expect(() => toWei('.', 'wei')).toThrow(Error); expect(() => toWei('1.243842387924387924897423897423', 'ether')).toThrow( @@ -160,6 +163,21 @@ describe('toWei', () => { expect(() => toWei('8723.98234.98234', 'ether')).toThrow(Error); }); + it('should reject fractional wei (wei has zero decimal places)', () => { + // Regression test: unitLengths used to compute `value.length - 1 || 1`, + // so wei's baseLength was wrongly coerced from 0 to 1, letting a single + // fractional digit silently pass through as whole wei. + expect(() => toWei('0.5', 'wei')).toThrow('too many decimal places'); + expect(() => toWei('0.9', 'wei')).toThrow('too many decimal places'); + + // Whole wei values are unaffected by the fix. + expect(toWei('5', 'wei')).toBe(BigInt(5)); + expect(toWei('0', 'wei')).toBe(BigInt(0)); + + // Sanity-check another unit (baseLength 9) still works after the fix. + expect(toWei('1.5', 'gwei')).toBe(BigInt('1500000000')); + }); + it('should handle BigInt inputs with fast path optimizations', () => { // Fast path: BigInt + 'wei' unit (should return input directly) expect(toWei(BigInt(123), 'wei')).toBe(BigInt(123)); diff --git a/src/unitsConversion.ts b/src/unitsConversion.ts index 4f1b1b34c..bc6b6ea1e 100644 --- a/src/unitsConversion.ts +++ b/src/unitsConversion.ts @@ -68,7 +68,7 @@ const unitMapBigInt = Object.fromEntries( ) as Record; const unitLengths = Object.fromEntries( - Object.entries(unitMap).map(([key, value]) => [key, value.length - 1 || 1]), + Object.entries(unitMap).map(([key, value]) => [key, value.length - 1]), ) as Record; const NUMBER_REGEX = /^-?[0-9.]+$/u; @@ -266,16 +266,17 @@ export function toWei( if (!whole) { whole = '0'; } - if (!fraction) { + + if (fraction) { + if (fraction.length > baseLength) { + throw new Error( + `While converting number ${etherInput} to wei, too many decimal places`, + ); + } + fraction = fraction.padEnd(baseLength, '0'); + } else { fraction = '0'; } - if (fraction.length > baseLength) { - throw new Error( - `While converting number ${etherInput} to wei, too many decimal places`, - ); - } - - fraction = fraction.padEnd(baseLength, '0'); const wholeBigInt = BigInt(whole); const fractionBigInt = BigInt(fraction); From d981b118240b1fc2e6b7cc23168c42c2c3c77041 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:07:18 +0200 Subject: [PATCH 2/2] docs(changelog): note the toWei fractional-wei fix --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5bbb115..4e41772cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `convertMnemonicToWordlistIndices` converts a mnemonic string into little-endian `Uint16` wordlist indices encoded as a `Uint8Array`. - Add `@metamask/scure-bip39` as a dependency ([#300](https://github.com/MetaMask/utils/pull/300)) +### Fixed + +- `toWei` now rejects fractional wei values (e.g. `toWei('0.5', 'wei')`), which previously returned an incorrect whole-wei value instead of throwing ([#303](https://github.com/MetaMask/utils/pull/303)) + - This is a caller-visible change: `toWei('0.0', 'wei')` now throws instead of returning `0n`, consistent with how the library already rejects an over-precision fraction for every other unit. + ## [11.11.0] ### Added