Skip to content

Replace floating-point USDC arithmetic in settlement and invoicing with bigint contract units #1512

Closed

Description

@greatest0fallt1me

Summary

Settlement batches, payout conversion and invoice totals add USDC amounts as JavaScript numbers. RevenueSettlementService sums e.amountUsdc floats to decide payouts and records the float total. convertUsdcToStroops turns that float into on-chain i128 units with Math.round(... + Number.EPSILON). InvoiceService totals line items with sum + Number(item.amount). With many micro-charges these sums drift by stroops, so payouts, invoices and the ledger stop reconciling exactly, and amounts above 2^53 stroops lose precision. This issue moves domain arithmetic to bigint contract units end to end.

Background and evidence

  • Float payout totals. src/services/revenueSettlementService.ts, runBatchOnce (about line 131): const totalAmount = events.reduce((sum, e) => sum + e.amountUsdc, 0);. That value is compared with minPayout, stored as amount, and emitted with amount.toFixed(7) (about line 421).
  • Float to on-chain units. src/services/sorobanSettlement.ts, convertUsdcToStroops(amountUsdc: number) (about lines 100-106), converts float to stroops with Math.round, and distribute(developerAddress: string, amountUsdc: number) (line 193) takes a float.
  • Number-typed domain objects. src/services/usageStore.ts, toNumber / mapUsageEventRow (about lines 182-200), map DECIMAL strings to number for UsageEvent.amountUsdc. settlementStore.ts (about lines 86-92) does the same for Settlement.amount.
  • Float invoice totals. src/services/InvoiceService.ts (about lines 78-81): items.reduce((sum, item) => sum + Number(item.amount), 0), then event.total.toFixed(7) (about line 145).
  • Example. Summing 10,000 charges of 0.0000001 in float gives 0.0010000000000000002-style drift. After rounding at different stages, the payout, the ledger sum and the invoice can differ by a stroop.

Scope

In scope

  1. Types. Change UsageEvent.amountUsdc, Settlement.amount, invoice totals and the settlement client's distribute amount to bigint contract units (1 USDC = 10,000,000). Use a shared src/lib/usdcUnits.ts with parseUsdcToUnits and formatUnitsToUsdc, reusing or moving the helpers in src/services/billing.ts.
  2. Arithmetic. All sums, comparisons (minPayout, also converted to units) and splits use bigint. Remove convertUsdcToStroops in favour of passing units through unchanged.
  3. Boundaries. Convert at the SQL boundary (DECIMAL or integer column, read as string) and at the API, event and webhook boundary, where the decimal string format is unchanged ("12.3456789").
  4. Lint guard. Add an ESLint no-restricted-syntax rule (or a focused unit test) that flags parseFloat, Number( and toFixed( in src/services/{revenueSettlementService,sorobanSettlement,InvoiceService,settlementStore,usageStore}.ts.
  5. Docs. Update docs/guides/SETTLEMENT_STORE_DOCUMENTATION.md.

Out of scope

  • The usage_events column type migration (a separate issue).
  • Invoice PDF rendering (invoicePdf.ts formats to 2 decimals for display only).

Design constraints

  • Exact conservation. For any set of events, payout amount == Σ event amounts == Σ invoice line items, exactly.
  • External formats unchanged. API and webhook payloads keep the decimal-string format; snapshot tests prove it.
  • Bigint-safe JSON. No JSON.stringify of raw bigints; convert explicitly.

Test plan

Extend src/services/revenueSettlementService.test.ts, src/services/sorobanSettlement.test.ts, src/services/InvoiceService.test.ts and src/services/usageStore.test.ts.

# Scenario Expected
1 10,000 events of 0.0000001 payout exactly 10000 units ("0.001")
2 Property (fast-check): random 7-decimal amounts Σ units equals the formatted total parsed back
3 Payout amount passed to the client bigint, equal to the sum of claimed rows
4 minPayout boundary equal to the total payout happens (≥ semantics)
5 Invoice with 3 line items total equals the exact sum; stored string matches
6 Amount above 2^53 units exact
7 settlement_completed webhook payload amount is a decimal string, unchanged format
8 Lint guard flags a reintroduced parseFloat

Regression proof. Row 1 (or a crafted case like it) produces a non-exact total on current main.

Acceptance criteria

  • No float arithmetic on USDC amounts remains in the listed files, enforced by the guard.
  • Every test-plan row passes.
  • npm run lint, npm run typecheck, npm run build and npm test pass, and CI is green.
  • At least 90% line coverage on src/lib/usdcUnits.ts and the touched functions.

Getting started

git checkout -b fix/settlement-bigint-amounts
npm ci && npx prisma generate
npm run test:unit -- src/services/revenueSettlementService src/services/InvoiceService src/services/sorobanSettlement

Read first: runBatchOnce in src/services/revenueSettlementService.ts, convertUsdcToStroops in src/services/sorobanSettlement.ts, src/services/InvoiceService.ts, and parseUsdcToContractUnits / formatContractUnitsToUsdc in src/services/billing.ts.

PR requirements

  • Link this issue (Closes #<n>).
  • Paste test output, including the property test, and coverage.
  • Example commit: fix(settlement): use bigint contract units for payout and invoice math.

Priority rationale

Medium. It is a well-bounded refactor across a few services, but it determines whether payouts and invoices reconcile to the stroop, which is a money-correctness guarantee.

Activity

  1. olubukolatanko209-doc commented on Oct 19, 2026

    @olubukolatanko209-doc
    Contributor

    @olubukolatanko209-doc has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    Hi Maintainer, I’d like to take this issue. I have experience with the relevant area and can work on implementing a fix. Please assign it to me.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @olubukolatanko209-doc to this issue.

  2. drips-wave commented on Oct 19, 2026

    @drips-wave

    Congratulations, @olubukolatanko209-doc! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.

    🧑‍💻 @olubukolatanko209-doc: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.

    Warning

    When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.

    🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.

    🌊 Happy Wave 🌊

  3. added a commit that references this issue on Oct 24, 2026
    adfb942
  4. grantfox-oss commented on Oct 24, 2026

    @grantfox-oss

    🎉 This issue has been marked as completed on GrantFox!

    @olubukolatanko209-doc's PR #1527 was approved and merged by @greatest0fallt1me.

    🏆 @olubukolatanko209-doc: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (175 total points). Track your full progress on GrantFox.

    👏 Great work, @olubukolatanko209-doc! Keep contributing to CalloraOrg.

  5. drips-wave commented on Oct 24, 2026

    @drips-wave

    This issue has been completed by @olubukolatanko209-doc as part of the Stellar Wave Program's 10th Wave 🥳

    😎 @olubukolatanko209-doc: You earned 100 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.

    🧑‍💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Stellar WaveStellar Wave Program issuebackendBackend service workbillingpriority:mediumMedium implementation difficultytestingTests and coverage

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions