Skip to content

Coefficient and exponent signs use skip-many for an optional-single dash: multi-dash input is rejected only by accidental arithmetic wrap, with a misleading overflow selector #272

Description

@thedavidmeister

Unit

src/lib/parse/LibParseDecimalFloat.sol parseDecimalFloatInline: coefficient sign at ~line 46 and exponent sign at ~line 144 are both consumed with LibParseChar.skipMask(cursor, end, CMASK_NEGATIVE_SIGN) — skip-MANY — for a token that is optional-single.

Violated property

A sign is zero-or-one dash; --5, ---5, 1e--2 should be malformed-input errors. What actually happens (traced against current source):

  1. skipMask consumes every dash; isNegative = cursor != start collapses any count to a boolean (used only to sign the fractional part).
  2. The coefficient region handed to LibParseDecimal.unsafeDecimalStringToSignedInt is (start, cursor) — dash-inclusive. The signed parser consumes exactly ONE leading dash; each surviving dash flows into the inner unsigned parser's unchecked byte - '0' arithmetic as −3·10^k, wrapping the accumulated value into the top of the uint256 range, which then trips the negative overflow bound.
  3. Result: --5(ParseDecimalOverflow.selector, ...). Rejection is reliable for every multi-dash form, but only by accident of the wrap landing above the bound, and the selector says "overflow" for input that is malformed, not large.

Why it deserves a real guard

The rejection chain couples three unrelated behaviors (skip-many width, the signed parser's single-dash contract, unchecked-wrap magnitudes). Any refactor of the inner parser's garbage handling — e.g. validating chars, or changing failure-value conventions — can silently turn accidental rejection into acceptance. rain.string's campaign already touched those overflow paths this week (rain.string PR #60).

Proposed fix

Replace both skip-many calls with a single-char check (isMask + cursor += isNeg, mirroring unsafeDecimalStringToSignedInt itself), so a second dash lands in the digit check and returns the existing malformed/empty selectors. Tests: --5, ---5, 1e--2, -.5, lone - pinned to exact selectors; -5, 1e-2 unchanged.

Found by the 2026-08-24 rain.string CMask consumer-oracle audit (CMASK_NEGATIVE_SIGN's value is correct; the primitive choice at this consumer is the defect).

🤖 Generated with Claude Code

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions