Skip to content

EscrowService uses f64 for amount validation, risking precision loss on financial values #29

Description

@abayomicornelius

Problem

In src/services/escrow.rs, EscrowService::create validates the escrow amount by parsing it as f64:

let amount: f64 = req
    .amount
    .parse()
    .map_err(|_| AppError::Validation("amount must be a valid decimal string".into()))?;
if amount <= 0.0 {
    return Err(AppError::Validation("amount must be positive".into()));
}

The amount is stored as a string (good — avoids float storage), but validation itself goes through f64, which cannot exactly represent most decimal fractions. This means:

  • A value like "0.1" or amounts with more than ~15-17 significant decimal digits can silently round during the <= 0.0 positivity check, in edge cases producing incorrect accept/reject decisions (e.g. a value that should be treated as zero/negative passing the check, or a tiny positive value being treated as zero due to float underflow after arithmetic elsewhere that reuses this pattern).
  • Stellar/Soroban amounts are fixed-point (7 decimal places for classic Stellar assets), so f64 parsing is the wrong validation primitive: it doesn't reject amounts with excess precision (e.g. "1.123456789" parses fine as f64 but is not a valid Stellar amount).
  • The same anti-pattern reappears in src/services/batch.rs::execute_batch (let amount: f64 = leg.amount.parse()...), so a fix here should be applied consistently across both call sites (see EscrowService uses f64 for amount validation, risking precision loss on financial values #29, filed alongside this one, for the batch-specific consistency issue).

Why it matters

This is a money-transfer backend. Any amount-validation logic that doesn't use a fixed-point/decimal type is a correctness risk for a financial system — even rare rounding edge cases can mean an invalid amount is submitted to a Soroban contract or an escrow is created with an amount that doesn't match what the client believes was validated.

Proposed fix

  • Introduce a shared parse_stellar_amount(&str) -> AppResult<i128> (or use a decimal crate, e.g. rust_decimal) that parses to a fixed-point representation with at most 7 decimal places, matching Stellar's Int128Parts/i64 stroop representation, and rejects excess precision explicitly.
  • Replace the f64 parse/compare in EscrowService::create and BatchPaymentService::execute_batch with this shared validator.
  • Add unit tests for boundary cases: "0.0000001" (min valid stroop), "0.00000001" (too precise, should reject), "0", "-1", huge amounts near i64::MAX stroops, and strings with locale-style separators ("1,000") which should be rejected, not silently parsed as 1.0.

Additional Notes

Edge cases

  • Scientific notation strings ("1e10") parse fine as f64 but are almost certainly not intended as valid user input — a decimal-based validator should reject them outright.
  • NaN/Infinity: "nan".parse::<f64>() succeeds and produces f64::NAN, which fails <= 0.0 (false) and would incorrectly pass validation as "positive". This is a real, exploitable gap: submitting amount: "nan" currently bypasses the positivity check.
  • Locale formatting (thousands separators, comma decimal points) should be explicitly rejected rather than silently mis-parsed.

Testing strategy

  • Property-based test (e.g. proptest) generating random decimal strings and asserting the validator never accepts values with >7 fractional digits or non-finite results.
  • Regression test specifically for the "nan"/"inf" bypass described above.

Cross-references

  • Same pattern in src/services/batch.rs (BatchPaymentService::execute_batch) — see the batch payment crash-consistency issue filed alongside this one for related durability concerns in that file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26backendBackend service logicbugSomething isn't workingvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions