Skip to content

chore(tests): fuzz the financial arithmetic with proptest - #72

Merged
codemagician1949 merged 1 commit into
IntensifierLab:mainfrom
oluwagbemigaphilip:chore/proptest-arithmetic
Aug 25, 2026
Merged

codemagician1949 merged 1 commit into
IntensifierLab:mainfrom
oluwagbemigaphilip:chore/proptest-arithmetic

Conversation

@oluwagbemigaphilip

Copy link
Copy Markdown
Contributor

Adds nine property tests over the repayment waterfall and pool-manager share
minting, run by cargo test --workspace and so already covered by CI.

Waterfall splits

  • the four buckets sum to exactly the repayment, for any combination of
    amount and dues — money falling out of the split would be silently
    unallocated, which is the invariant the waterfall exists to provide
  • no bucket is ever negative, and none exceeds what it was due (lender
    yield excepted, which is deliberately uncapped)
  • strict priority order: a later bucket is only paid once every earlier one
    is satisfied in full
  • non-positive repayments are always rejected

Share minting

  • a positive deposit always mints positive shares, 1:1 at the initial NAV —
    a lender who deposited real capital and received zero shares has been
    diluted to nothing by a rounding bug
  • share totals accumulate exactly across lenders, with nothing lost between
    them
  • non-positive deposits are always rejected

Why every call goes through try_

A panic inside a Soroban contract aborts the whole test process rather than
unwinding. Driving these through the plain client methods would mean one bad
generated case takes the entire suite down and reports nothing useful, so
every call uses the try_ variant, where the same condition surfaces as an
Err a property can assert on.

Two real overflows found — reported, not fixed here

The fuzzing found genuine unchecked-overflow bugs. Above
i128::MAX / NAV_SCALE (~1.7e32):

  • pool-manager::join_pool computes amount * NAV_SCALE / nav with
    unchecked multiplication
  • pool-manager::apply_reserve_delta, which the waterfall forwards to,
    accumulates into the reserve the same way

Both trap the host rather than returning a contract error, which aborts
the test process outright — so a property cannot assert on them from inside
the same binary, and an unbounded probe simply kills the suite.

The properties therefore verify correctness across the entire representable
domain up to that bound, and the bound is recorded in the test file as a
known limit with the reasoning.

Making overflow return a contract error instead of trapping means changing
the arithmetic in pool-manager, which is contract behaviour rather than
test coverage — and that file is being edited by the event-emission work for
#4, so changing it here would create exactly the merge conflict this
sequence of PRs is meant to avoid. Worth its own issue; happy to open one.

Verification

  • cargo test --test properties — 9 properties passing, ~43s
  • cargo test --workspace — all suites passing

proptest is pinned to 1.5 so a release of it cannot change the generated
cases and turn CI red on a run that touched no contract code.

Closes #9

Adds nine property tests over the repayment waterfall and pool-manager share
minting, run by `cargo test --workspace` and so already covered by CI.

Waterfall splits
----------------
- the four buckets sum to *exactly* the repayment, for any combination of
  amount and dues — money falling out of the split would be silently
  unallocated, which is the invariant the waterfall exists to provide
- no bucket is ever negative, and none exceeds what it was due (lender
  yield excepted, which is deliberately uncapped)
- strict priority order: a later bucket is only paid once every earlier one
  is satisfied in full
- non-positive repayments are always rejected

Share minting
-------------
- a positive deposit always mints positive shares, 1:1 at the initial NAV —
  a lender who deposited real capital and received zero shares has been
  diluted to nothing by a rounding bug
- share totals accumulate exactly across lenders, with nothing lost between
  them
- non-positive deposits are always rejected

Why every call goes through `try_`
----------------------------------
A panic inside a Soroban contract aborts the whole test process rather than
unwinding. Driving these through the plain client methods would mean one bad
generated case takes the entire suite down and reports nothing useful, so
every call uses the `try_` variant, where the same condition surfaces as an
`Err` a property can assert on.

Two real overflows found — reported, not fixed here
---------------------------------------------------
The fuzzing found genuine unchecked-overflow bugs. Above
`i128::MAX / NAV_SCALE` (~1.7e32):

- `pool-manager::join_pool` computes `amount * NAV_SCALE / nav` with
  unchecked multiplication
- `pool-manager::apply_reserve_delta`, which the waterfall forwards to,
  accumulates into the reserve the same way

Both **trap the host** rather than returning a contract error, which aborts
the test process outright — so a property cannot assert on them from inside
the same binary, and an unbounded probe simply kills the suite.

The properties therefore verify correctness across the entire representable
domain *up to* that bound, and the bound is recorded in the test file as a
known limit with the reasoning.

Making overflow return a contract error instead of trapping means changing
the arithmetic in `pool-manager`, which is contract behaviour rather than
test coverage — and that file is being edited by the event-emission work for
IntensifierLab#4, so changing it here would create exactly the merge conflict this
sequence of PRs is meant to avoid. Worth its own issue; happy to open one.

Verification
------------
- `cargo test --test properties` — 9 properties passing, ~43s
- `cargo test --workspace` — all suites passing

`proptest` is pinned to 1.5 so a release of it cannot change the generated
cases and turn CI red on a run that touched no contract code.

Closes IntensifierLab#9
@codemagician1949
codemagician1949 merged commit d6240ea into IntensifierLab:main Aug 25, 2026
3 checks passed
@codemagician1949

Copy link
Copy Markdown
Contributor

Nice job @oluwagbemigaphilip

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.

chore: add property-based tests for all arithmetic

2 participants