Skip to content

fix: reject fractional wei in toWei - #303

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

fix: reject fractional wei in toWei#303
gomesalexandre wants to merge 2 commits into
MetaMask:mainfrom
gomesalexandre:fix_towei_wei_baselength

Conversation

@gomesalexandre

@gomesalexandregomesalexandre commented Aug 19, 2026

Copy link
Copy Markdown

No filed issue, found by inspection.

unitLengths computed each unit's decimal precision as value.length - 1 || 1.
Wei's raw value is '1' (length 1), so 1 - 1 is 0 - the correct precision for
wei, since it's the base unit with zero decimal places. But the || 1 fallback
silently coerced that 0 up to 1, so toWei('0.5', 'wei') returned 5n
instead of throwing "too many decimal places" the way it does for any other unit
given more fractional digits than it supports.

Fix, and why it's more than dropping || 1

Dropping || 1 alone breaks 5 existing tests (toWei(0, 'wei'),
toWei('5', 'wei'), etc. start throwing). Root cause: toWei() unconditionally
defaulted a missing fraction to the string '0' before comparing its length
against baseLength - harmless while baseLength was always >= 1, but broken
once wei's is correctly 0. Restructured so the length-check/pad only runs when
the input actually supplied a fraction; a genuinely absent fraction is always 0
regardless of the unit's precision.

Caller-visible behavior change - flagging explicitly

toWei('0.0', 'wei') used to return 0n. It now throws. This is a real,
intentional change, not incidental: the library already rejects an all-zero
fraction that exceeds a unit's precision for every other unit -
toWei('0.' + '0'.repeat(19), 'ether') throws on unmodified main, no
zero-value special case exists anywhere in the file. Wei was the only unit
accepting an over-precision fraction, and only because of this bug. Matches how
ethers' parseUnits/parseFixed also reject fractional digits beyond a unit's
decimals regardless of whether those digits are zero. Happy to add a zero-fraction
special case across all units instead if maintainers would prefer that (it would
be a bigger, separate behavior change to ether/gwei/etc, not just wei), but
rejecting fractional wei consistently with every other over-precision case seemed
like the more defensible default.

One more pre-existing quirk this PR doesn't touch: unitLengths.noether also
flips from 1 to 0 under the same fix, but it's provably dead code - both
toWei and fromWei early-return on base === zero before baseLength is ever
read for that unit, so there's no behavior change there.

Testing

fromWei is unaffected: wei's base is 1n, so wei % base is always 0n,
making the fraction computation's baseLength irrelevant to its output -
confirmed identical results with and without the fix.

Guard-validated: reverted the source fix, confirmed the new "should reject
fractional wei" test fails, restored, confirmed it passes. Full test suite:
1985/1986 pass (1 pre-existing unrelated timing flake in bytes.test.ts,
unrelated to unitsConversion.ts). unitsConversion.ts at 100%
statement/branch/function/line coverage. tsc --noEmit: 7 pre-existing errors,
all in node_modules/web3-* typings (confirmed via git stash comparison against
unmodified main - identical count). eslint: clean.


Note

Cursor Bugbot is generating a summary for commit dd0e234. Configure here.

gomesalexandreand others added 2 commits August 19, 2026 09:54
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 <noreply@anthropic.com>
Sign up for freeto 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

@gomesalexandre