You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
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.
Depends on: #214. Blocks: #221, #223, #224, #225, #227.
Description
Amounts are
i64stroops 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. Thisencodes 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^18andi64::MAX ≈ 9.22 × 10^18,so a balance of ~9.3 ETH overflows. Token amounts are
uint256, up to ~1.16 × 10^77. An overflowhere does not throw — it silently credits the wrong amount, which is a fund-loss bug.
Introduce an arbitrary-precision
Amounttype and migrate storage and serialisation to it.Requirements and context
NUMERIC(78,0)— integer base units, no fractional component. Decimals live in thetoken registry (feat(store): ERC-20 token registry #223), never inferred from the value.
rust_decimalis not acceptable (96-bit mantissa). Usesqlx::types::BigDecimal(enable the
bigdecimalfeature) at the storage boundary andU256in EVM-facing code. Justifyyour choice of the
U256implementation (alloy-primitives,ruint,primitive-types) againstthe MSRV constraint — coordinate with feat(evm-core): secp256k1 keys, BIP-44 derivation, and EIP-55 addresses #217.
uint256in a JSON number silently losesprecision 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.
format_amountincrates/api/src/routes/submit.rscurrently round-tripsthrough
f64. That is tolerable for 7-decimal stroops and not tolerable at 18 decimals.Replace it with integer/string formatting.
wallet-coreandcryptolint walls already denyclippy::cast_possible_truncationandcast_sign_loss— extend that posture to any crate handling amounts.as i64or a silent saturation is a credit-amount vulnerability.Conversions must be fallible and return an error, never truncate.
Suggested execution
Branch:
feat/arbitrary-precision-amountsImplement changes
Amountnewtype inocto-chainwrapping an unsigned big integer, with a documentedinvariant that it is always a non-negative integer count of base units, plus fallible
TryFrom<i64>/to_i64()conversions for the Stellar path.00NN_numeric_amounts.sql: addamount_base_units NUMERIC(78,0), backfill fromamount_stroops, add the> 0check, and keep the old column through one release for rollback.Serialize/Deserializeas strings, with a deserializer that rejects JSON numbersoutright rather than accepting and truncating them.
format_amount'sf64path with exact integer→decimal-string formatting parameterisedby
decimals.octo-storemodels and queries, andocto-ingest's amount parsing.Test and commit
proptestis already a workspace dependency): round-tripAmount → NUMERIC → AmountandAmount → JSON string → Amountfor the fulluint256range.0,1,i64::MAX,i64::MAX + 1,u64::MAX,2^256 - 1.10^18(1 ETH) survives a full store→API→client round-tripbit-identically — this is the case the old
i64schema could not represent.docs/api.mdanddocs/openapi.yamlfor the stringrepresentation.
Example commit message
Guidelines
Breaking API change: coordinate with #227 on versioning before merge. Call out every remaining
as i64in the diff and explain why each is safe.