Repository navigation
All monetary amounts are JavaScript number — fee and routing maths accumulate floating-point error on value #225
Description
Priority: High · Area: Numeric correctness / money handling · Est. effort: 10–14 h
📌 Problem
Every monetary field in src/models/liquidity.ts is a JavaScript number — an IEEE-754 double:
amount: number; // :12, :30, :49, :63
remainingBalance: number; // :32
total: number; // :40
portion: number; // :57src/services/quoteService.ts does arithmetic on them:
const taken = Math.min(remaining, entry.amount); // :49 (inside the routing loop)
const fee = Math.ceil((amount * this.feeBps) / BPS_DIVISOR); // :54Two distinct problems.
1. Precision loss above 2^53. Doubles represent integers exactly only up to 9,007,199,254,740,991. Stellar amounts are commonly held in stroops (1 XLM = 10,000,000 stroops), so 900,719,925 XLM exceeds the safe range. Past it, addition silently stops being exact.
2. Accumulated error in the routing loop. The service selects liquidity largest-first, subtracting taken from remaining each iteration. Repeated float subtraction accumulates representation error, so the sum of the route portions can fail to equal the requested amount by a small residue — and Math.ceil on the fee then rounds a value that is already slightly wrong.
The service's own doc comment calls the quote "deterministic". Float arithmetic across a variable number of iterations, in a variable order, is not reliably so.
🎯 Design decision required
Post your approach in a comment on this issue before writing code — this touches the models, the services and the API boundary. State and defend:
- Representation.
bigintfor integer minor units, or a decimal library?biginthas no dependency and is exact, but does not serialise to JSON natively and cannot express fractions. Argue one, and state the unit you standardise on. - API compatibility. JSON has no integer type beyond double precision, so large values must serialise as strings to survive the round trip. That is a breaking API change. State it plainly and describe the migration.
- Rounding policy.
Math.ceilon the fee rounds in the protocol's favour. Whatever representation you choose, state the rounding rule explicitly, apply it consistently, and prove the route portions sum exactly to the requested amount.
🧩 Requirements and context
- Add tests demonstrating the current defect first — a value above
Number.MAX_SAFE_INTEGER, and a multi-anchor route whose portions do not sum exactly. Those tests are the evidence the change is warranted. - The invariant "sum of route portions + fee equals the requested total, exactly" must hold after the change, with a property-style test over many inputs.
src/models/settlement.tsand the settlement path use the same amounts — check and include them, or scope explicitly and say what you left.- All 42 test files must pass.
- Update
src/openapi.tsto reflect any serialisation change.
🛠️ Suggested execution
- Write the two failing tests (large value, non-summing route).
- Post your representation and API-compatibility plan; wait for agreement.
- Convert models, then services.
- Add the exact-sum property test.
- Update the OpenAPI spec and document the breaking change.
✅ Acceptance criteria
- Tests demonstrate precision loss and route-sum drift against the current code.
- Monetary values use an exact representation; the unit is stated.
- Route portions plus fee sum exactly to the requested amount, proven by a property-style test.
- The rounding rule is documented and applied consistently.
- Any API serialisation change is reflected in
src/openapi.tsand documented as breaking. - All existing tests pass.
- Anything deliberately left out of scope is listed.
🚫 Out of scope
- The persistence layer — separate issue.
- Changing the largest-first selection strategy.
- Changing the fee rate.
🧪 Verification
npm ci
npm test src/services
npm run lint && npm run build && npm test📤 What your PR must include
- The two failing tests and their output.
- A link to the agreed representation plan.
- Your rounding policy.
- The breaking-change description and migration.
Closes #<n>.
🔒 Security notes
Float arithmetic on money is a correctness failure with financial consequences: a route whose portions do not sum to the requested amount means the system's accounting does not balance, and the discrepancy is small enough to escape notice while being systematic rather than random. Math.ceil on the fee compounds this by always rounding the same direction. Any residue that repeatedly favours one side of a settlement is a value leak, whether or not anyone is deliberately exploiting it.
📋 Guidelines
- Minimum 95% test coverage on changed lines
- Clear documentation
- Timeframe: 96 hours from assignment
- One logical change per commit; no merge commits
💬 Join our community
Working on this, or want to sanity-check your approach before you start? Come and ask — the maintainers are there and happy to help.
Telegram: https://t.me/Grainlify
Activity
gramseostudio-dev commentedon Aug 23, 2026
Hi team. I would love to tackle this issue. Relying on IEEE-754 floats for financial routing is a well-known vulnerability, and I completely agree with the urgency to migrate to an exact representation.
Here is my proposed design architecture to solve the precision loss and drift:
I propose using native JavaScript bigint to represent all monetary amounts internally rather than introducing a heavy third-party decimal library.
Unit Standard: All amounts will be strictly standardized and operated on as stroops (the integer minor unit, where 1 XLM = 10,000,000 stroops).
Why: Native bigint is dependency-free, memory-efficient, mathematically exact for integers, and natively supported across modern Node.js/TypeScript environments.
Since JSON does not support integers beyond Number.MAX_SAFE_INTEGER, we cannot safely transmit these values as numbers.
Migration: All monetary fields (amount, remainingBalance, total, portion) will be serialized to string at the API boundary before being sent out, and parsed from string to bigint on incoming requests.
Documentation: I will update src/openapi.ts to reflect this breaking change, documenting the fields as type: string to ensure client consumers handle the stringified large numbers correctly.
const fee = (amount * feeBps + (BPS_DIVISOR - 1n)) / BPS_DIVISOR;. This guarantees exact upward rounding without float conversion.
Execution Plan:
Following your guidelines, I will first write the failing tests to demonstrate the >2^53 precision loss and the routing residue. Once proven, I will execute the refactor across the models and services, and finally add a property-style test verifying the exact-sum invariant.
Please let me know if this architectural approach is approved so I can be assigned and begin execution!
Jagadeeshftw commentedon Aug 23, 2026
Assignment update — exact monetary arithmetic
Welcome, @gramseostudio-dev, and good luck with the monetary-precision issue. Please read the entire description before implementation, including the exact stroop representation, API serialization decision, rounding invariant, defect-first tests, and property-style verification. Your PR must address every requirement with a high-quality, behavior-conscious change, preserve the routing policy, document any breaking API implications, and prove that unsafe floating-point drift is gone.
Please raise the PR within 48 hours of assignment. If no PR is opened by then, the issue may be unassigned for reassignment. Please join the Telegram group to coordinate with the maintainers: https://t.me/+u5qmu35nZ7I0OTU1
grantfox-oss commentedon Aug 23, 2026
🦊 GrantFox — @gramseostudio-dev has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #225)Good luck! Track your progress on GrantFox.
fix(AnchorNet-Org#225): replace float arithmetic with bigint for exac…
fix(AnchorNet-Org#225): add all bigint migration files missing from p…
fix(#225): replace float arithmetic with bigint for exact monetary pr…
grantfox-oss commentedon Aug 29, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@gramseostudio-dev's PR #236 was approved and merged by @Jagadeeshftw.
🏆 @gramseostudio-dev: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (252 total points). Track your full progress on GrantFox.
👏 Great work, @gramseostudio-dev! Keep contributing to AnchorNet-Org.
feat(config): fail-fast validation + standalone typecheck script (#230)…