deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

rust_decimal's default parse returns Ok while dropping the last wei of token amounts

A dev.to comparison of Rust money types shows rust_decimal's from_str accepting a 30-digit token amount while silently discarding its final digit; from_str_exact returns Err(Underflow) on the same input.

rust_decimal's default parse returns Ok while dropping the last wei of token amounts

A parse that succeeds with fewer digits than you sent

According to a comparison of Rust money types published on dev.to by Tom Wang, a fintech and crypto-payments developer, rust_decimal's default string constructor reports success on input it cannot fully store.

The demonstration: calling Decimal::from_str on "100000000000.000000000000000001" returns Ok, but the value held afterwards is 100000000000.00000000000000000. The input carries twelve integer digits and eighteen fractional digits — thirty significant digits — while rust_decimal's 96-bit mantissa holds twenty-eight. The trailing 1, which in an 18-decimal token amount is the last wei, is discarded to make the value fit, with no error raised. Running the same string through Decimal::from_str_exact produces Err(Underflow) instead.

Wang says he verified the behaviour in a scratch crate against rust_decimal 1.43.0, bigdecimal 0.4.11 and Rust 1.98, with every output pasted from that run. The dev.to post summarises a longer write-up on the author's own site, so the comparison is one developer's benchmark rather than an upstream bug report.

Why the type behaves this way

rust_decimal fits a decimal number into 16 fixed bytes: a 96-bit integer, a scale between 0 and 28, and a sign bit. That buys exact values up to ±79,228,162,514,264,337,593,543,950,335, Copy semantics and no heap allocation, with 28 significant digits available in most cases — 29 in some, per the article. The limit is hard: anything past the twenty-eighth significant digit must be rounded away or rejected, and the default parse path rounds. The crate's default rounding mode is half-even. A strict variant exists, since from_str_exact turns the same condition into an error, but nothing about the default path's Ok result hints that digits were lost.

The fiat/token gap that produces such strings

The article's wider argument is that the usual search-result summary — "fixed-size and fast versus arbitrary precision" — is true but not enough to choose a type with. Its practical breakdown:

  • f64 fails the first test anyone runs: adding 0.1 ten times gives 0.9999999999999999, and 0.1 + 0.2 == 0.3 evaluates to false.
  • i64 minor units are exact and are what Stripe and Adyen put on the wire; one i64 holds 92,233,720,368,547,758.07 in pence, or 9.223372036854775807 ETH when counted in wei.
  • i128/u128 minor units cover token balances and TigerBeetle-style ledgers, up to roughly 3.4 × 10³⁸.
  • rust_decimal suits rates, fees, tax, FX and API amounts.
  • bigdecimal — a heap-backed BigInt with an i64 scale and unbounded precision — belongs at boundaries where inputs can exceed 28 digits.

The truncation example sits exactly at that boundary: an amount with twelve integer digits leaves only sixteen fractional digits of room inside a 28-digit budget, while 18-decimal token amounts demand eighteen.

The rest of the crate picture

The post also tabs fastnum 0.7.5: fixed precision tiers of 19, 38, 77 and 154 digits, half-up as the default rounding mode, support for NaN and Infinity, and serde settings that emit strings but only accept strings. rust_decimal and bigdecimal both serialise to strings and accept either strings or numbers on input. For Postgres, rust_decimal supports sqlx, Diesel and tokio-postgres; bigdecimal covers sqlx and Diesel; fastnum covers all three. As of 3 November 2026 the article lists total downloads of 169.1M for rust_decimal, 139.5M for bigdecimal and 10.6M for fastnum, with latest releases on 2 September 2026, 28 September 2026 and 11 June 2026 respectively, fastnum still pre-1.0.

Why it matters

Silent precision loss in a type whose entire purpose is exact money arithmetic is close to the worst failure mode available, because it is invisible at the point of parsing. A ledger that records a deposit using from_str stores one wei less than the sender moved; nothing errors, and the discrepancy only surfaces later as a reconciliation break. The mitigation is cheap and local: parse untrusted or chain-derived amounts with from_str_exact, keep balances in integer minor units, and use bigdecimal at the edges where inputs can outrun 28 digits. The deeper point the article makes is that "exact decimal" is a range claim, not a guarantee — 28 digits comfortably covers fiat amounts and still loses the tail of an 18-decimal token with a twelve-digit integer part.

  • #rust
  • #rust-decimal
  • #bigdecimal
  • #money
  • #precision
  • #fintech

Related posts