Skip to content

fix(fee-primitives): overflow and rounding invariant audit for i128 fee arithmetic - #243

Open
Hexor-Hash wants to merge 1 commit into
Afro-Pay:mainfrom
Hexor-Hash:fix/issue-193-fee-primitives-overflow-audit
Open

fix(fee-primitives): overflow and rounding invariant audit for i128 fee arithmetic#243
Hexor-Hash wants to merge 1 commit into
Afro-Pay:mainfrom
Hexor-Hash:fix/issue-193-fee-primitives-overflow-audit

Conversation

@Hexor-Hash

@Hexor-Hash Hexor-Hash commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Closes #193. Full audit of every arithmetic expression in contracts/fee-primitives/src/lib.rs, covering overflow safety and rounding direction, plus tests proving the invariants the issue calls for.

Findings & fixes

  • Overflow: both multiplication sites (amount * base_fee_rate in calculate_fee, amount * rate.rate in convert_currency) used raw *, which is undefined/incorrect on overflow for a plain (non-checked) build. Both now use checked_mul:
    • calculate_fee: on overflow, the true fee is necessarily far above config.max_fee (which already bounds protocol revenue per call), so the result clamps straight to max_fee instead of computing an unrepresentable product.
    • convert_currency: there's no safe ceiling to fall back to for a currency conversion, so overflow now panics with a descriptive message instead of silently wrapping.
  • Rounding direction: calculate_fee now uses ceiling division — any non-zero remainder rounds the fee up — so the protocol never under-collects due to truncation (documented in code and in docs/fee-primitives.md). convert_currency intentionally keeps floor division since it's a value transform, not a charge; this is documented too.
  • New validation: amount, min_fee, max_fee, base_fee_rate must be non-negative, min_fee <= max_fee, and base_fee_rate <= 10_000 bps (100%) — all panic with descriptive messages otherwise.
  • New invariant: the computed fee can never exceed the principal amount, even if min_fee is configured above it.

Pre-existing bugs found while getting cargo test -p fee-primitives to actually run

The crate did not compile before this change (so its tests had never run):

  • #[contractimpl] was used without importing it from soroban_sdk.
  • env.storage().get/set(...) called the SDK's now-private raw storage methods; switched to env.storage().persistent().get/set(...).
  • Contract-exported functions took config structs by reference (&FeeConfig, &ConversionRate), which Soroban's #[contractimpl] macro doesn't support for exported functions — changed to owned parameters.
  • fee-primitives wasn't a member of the contracts/ Cargo workspace, so cargo test -p fee-primitives (the acceptance criterion's exact command) couldn't resolve the package; added it to workspace.members.
  • The existing test_currency_conversion test asserted a result that was inconsistent with the documented basis-points formula (and had never actually run to catch it).

Tests

16 tests total in fee-primitives/src/lib.rs, including 5 property-based tests (deterministic PRNG-driven, since this is a #![no_std] contract crate and proptest/quickcheck require std) covering the acceptance criteria exactly:

  • fee of 0 amount = 0
  • fee of i128::MAX does not overflow (clamps to max_fee)
  • fee + principal is well-formed / doesn't overflow for bounded amounts
  • rounding is always ≥ 0 and favors the protocol
  • fee can never exceed principal

Plus edge-case unit tests for invalid config (rate > 100%, min_fee > max_fee), conversion overflow, and the conversion+fee pipeline.

cargo test -p fee-primitives
running 16 tests
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

cargo fmt -p fee-primitives and cargo clippy -p fee-primitives --all-targets -- -D warnings are both clean. Verified the rest of the contracts/ workspace (escrow, governor, payment_registry) still builds after adding fee-primitives as a workspace member.

Docs

docs/fee-primitives.md now documents the rounding convention, overflow handling, and invariants.

Full audit of every arithmetic expression in fee-primitives/src/lib.rs
per Afro-Pay#193.

- Add overflow-safe checked_mul at both multiplication sites
  (calculate_fee, convert_currency). On overflow, calculate_fee clamps
  to config.max_fee (the true fee is necessarily far above it);
  convert_currency panics with a descriptive message since it has no
  safe ceiling to fall back to.
- Round fee calculation up (ceiling division) so the protocol never
  under-collects due to truncation; document why convert_currency
  intentionally keeps floor division (it's a value transform, not a
  charge).
- Validate amount/min_fee/max_fee/base_fee_rate are non-negative,
  min_fee <= max_fee, and base_fee_rate <= 10_000 bps (100%).
- Guarantee the computed fee never exceeds the principal amount, even
  if min_fee is configured above it.
- Add 16 tests: edge cases (zero amount, i128::MAX, invalid config)
  and 5 property-based tests covering the acceptance-criteria
  invariants (zero fee at zero amount, no overflow at i128::MAX,
  fee+principal well-formed, rounding always >= 0 and favors the
  protocol, fee never exceeds principal).
- Fix pre-existing compile errors that meant this crate had never
  actually built or run its tests: missing `contractimpl` import,
  private storage.get/set calls (now via storage().persistent()), and
  contract-exported functions taking references (Soroban contract
  functions must take owned Val types).
- Add fee-primitives to the Cargo workspace members so `cargo test -p
  fee-primitives` (the acceptance criterion's exact command) works.
- Document the rounding convention and invariants in
  docs/fee-primitives.md.

Closes Afro-Pay#193

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

@Hexor-Hash is attempting to deploy a commit to the milah's projects Team on Vercel.

A member of the Team first needs to authorize it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fee-primitives: overflow and rounding invariant audit for i128 fee arithmetic

1 participant