Skip to content

feat(store): Arbitrary-precision amounts — replace i64 stroops with NUMERIC(78,0) #215

Description

@Emmyt24

Depends on: #214. Blocks: #221, #223, #224, #225, #227.

Description

Amounts are i64 stroops everywhere — schema (amount_stroops BIGINT CHECK (amount_stroops > 0)),
models (crates/store/src/models.rs), the ingest amount parser
(crates/ingest/src/amount.rs), and API serialisation. This
encodes two Stellar-specific assumptions: 7 decimal places, and values that fit in 64 bits.

Both fail on EVM. ETH and most ERC-20s use 18 decimals; 1 ETH = 10^18 and i64::MAX ≈ 9.22 × 10^18,
so a balance of ~9.3 ETH overflows. Token amounts are uint256, up to ~1.16 × 10^77. An overflow
here does not throw — it silently credits the wrong amount, which is a fund-loss bug.

Introduce an arbitrary-precision Amount type and migrate storage and serialisation to it.

Requirements and context

  • Storage: NUMERIC(78,0) — integer base units, no fractional component. Decimals live in the
    token registry (feat(store): ERC-20 token registry #223), never inferred from the value.
  • Rust type: rust_decimal is not acceptable (96-bit mantissa). Use sqlx::types::BigDecimal
    (enable the bigdecimal feature) at the storage boundary and U256 in EVM-facing code. Justify
    your choice of the U256 implementation (alloy-primitives, ruint, primitive-types) against
    the MSRV constraint — coordinate with feat(evm-core): secp256k1 keys, BIP-44 derivation, and EIP-55 addresses #217.
  • API representation: JSON strings, per AD-3. A uint256 in a JSON number silently loses
    precision in every JavaScript client. This is a breaking API change and must be versioned — see
    feat(api): Multi-chain API surface, OpenAPI, and webhooks #227.
  • No floats, ever. Note that format_amount in
    crates/api/src/routes/submit.rs currently round-trips
    through f64. That is tolerable for 7-decimal stroops and not tolerable at 18 decimals.
    Replace it with integer/string formatting.
  • The wallet-core and crypto lint walls already deny clippy::cast_possible_truncation and
    cast_sign_loss — extend that posture to any crate handling amounts.
  • Security: an unchecked as i64 or a silent saturation is a credit-amount vulnerability.
    Conversions must be fallible and return an error, never truncate.

Suggested execution

Branch: feat/arbitrary-precision-amounts

Implement changes

  • Add an Amount newtype in octo-chain wrapping an unsigned big integer, with a documented
    invariant that it is always a non-negative integer count of base units, plus fallible
    TryFrom<i64> / to_i64() conversions for the Stellar path.
  • Migration 00NN_numeric_amounts.sql: add amount_base_units NUMERIC(78,0), backfill from
    amount_stroops, add the > 0 check, and keep the old column through one release for rollback.
  • Implement Serialize/Deserialize as strings, with a deserializer that rejects JSON numbers
    outright rather than accepting and truncating them.
  • Replace format_amount's f64 path with exact integer→decimal-string formatting parameterised
    by decimals.
  • Update octo-store models and queries, and octo-ingest's amount parsing.

Test and commit

  • Property tests (proptest is already a workspace dependency): round-trip
    Amount → NUMERIC → Amount and Amount → JSON string → Amount for the full uint256 range.
  • Explicit boundary cases: 0, 1, i64::MAX, i64::MAX + 1, u64::MAX, 2^256 - 1.
  • A regression test asserting that a JSON number amount is rejected, not coerced.
  • A test that the exact value 10^18 (1 ETH) survives a full store→API→client round-trip
    bit-identically — this is the case the old i64 schema could not represent.
  • Update docs/api.md and docs/openapi.yaml for the string
    representation.

Example commit message

feat(store): arbitrary-precision amounts for EVM compatibility

i64 stroops assume 7 decimals and 64-bit range; ETH is 18 decimals and
ERC-20 amounts are uint256, so ~9.3 ETH would silently overflow.

Amounts move to NUMERIC(78,0) with decimals owned by the token registry,
and cross the API as JSON strings — a uint256 in a JSON number loses
precision in every JS client.

BREAKING CHANGE: amount fields serialise as strings.

Refs #215

Guidelines

Breaking API change: coordinate with #227 on versioning before merge. Call out every remaining
as i64 in the diff and explain why each is safe.


Activity

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

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea/backendBackend crates: api, store, ingest, webhooks, bin/servercompat/breaking-changeChanges a public API or on-disk contractdifficulty/hardHard, complex, cross-cuttingstatus/blockingOther issues depend on this one landing firsttype/epicTracking issue for a multi-issue epic

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions