Problem
compute_credits chains three unchecked i128 multiplications (compute_total_stake(...) * credit_rate * ledgers_elapsed as i128, itself internally computing amount * allocation_pct as i128 / 100 and boosted * multiplier as i128), and there is currently zero test coverage anywhere near the boundaries of i128:
$ grep -n "i128::MAX\|checked_mul\|overflow" soroban/contracts/farming-pool/src/test.rs
# (no matches)
Every existing test in farming-pool/src/test.rs uses small, hand-picked values (amount in the hundreds/thousands, multiplier in 1-10, elapsed in single/double digits), which never come close to exercising the overflow behavior the workspace's overflow-checks = true release profile setting implies should be tested (see the companion "unchecked i128 multiplication chain traps the whole contract on overflow" issue). Since global_multiplier and credit_rate are both admin-settable to arbitrary values post-initialization with no upper bound, and amount is bounded only by a user's token balance, the actual production input space is far larger than anything the test suite currently exercises.
Impact
- The most consequential arithmetic bug class in the contract is completely untested: whether
compute_credits/compute_total_stake trap gracefully, trap ungracefully (losing the whole transaction with an opaque host error), or — if checked_mul migration from the companion overflow issue is only partially applied — silently produce a wrong result, is currently unverified by any automated test.
- Regression risk for the overflow-checked fix: once
compute_credits/compute_total_stake are migrated to checked_mul (per the companion issue), there is no fuzz/boundary test to prove the fix actually covers the full input space rather than just the specific values chosen ad hoc for a couple of example tests.
Fix
Add targeted boundary-value tests plus a bounded fuzz sweep near the extremes of each input:
#[test]
fn test_compute_credits_near_i128_max_amount_does_not_wrap() {
// amount chosen so that amount * allocation_pct/100 * multiplier * credit_rate * elapsed
// is just below i128::MAX, and a slightly larger variant just above it.
let result = std::panic::catch_unwind(|| {
compute_credits(i128::MAX / 4, 100, 4, 1, 1)
});
// Once the checked_mul migration lands, assert this returns Err(PoolError::CreditOverflow)
// instead of panicking/trapping.
}
// Plus, if proptest is added per the path-independence issue:
proptest! {
#[test]
fn compute_credits_never_wraps_or_panics_silently(
amount in 1i128..i128::MAX/1000,
allocation_pct in 1u32..=100,
multiplier in 1u32..10_000,
credit_rate in 1i128..1_000_000,
elapsed in 1u32..1_000_000,
) {
// Assert the function either returns a correct-magnitude result or a typed overflow
// error — never a value smaller than a naively-scaled lower bound, which would indicate
// silent wraparound.
}
}
Acceptance Criteria
Further Investigation
Why this is a spike: the right shape of the fix this test suite is meant to validate is itself undecided. compute_credits (soroban/contracts/farming-pool/src/lib.rs:138-146) and compute_total_stake (lib.rs:131-136) currently use raw */-// operators on i128, which under this workspace's overflow-checks profile setting either panics (traps the whole transaction with an opaque host error) or wraps silently depending on build profile — the same profile-dependency risk flagged in #82 for the factory's u32 PoolCount. Whether the eventual fix is (a) checked_mul/checked_add returning a new PoolError::CreditOverflow variant, (b) saturating arithmetic that clamps to i128::MAX instead of erroring, or (c) a redesign that keeps intermediate values in a wider/scaled representation to push the overflow boundary out further, is an open design decision this issue's own body defers to a "companion issue" — meaning the tests this issue asks for can't be finalized until that decision is made, and the test design should itself help surface which of (a)/(b)/(c) is preferable by characterizing exactly where today's implementation breaks.
Tradeoff approaches for the boundary-test design
- Pure characterization tests first (no fix assumed): write
#[should_panic] tests that document today's actual behavior (does i128::MAX/4 * 100 / 100 * 4 * 1 * 1 panic via debug-mode overflow-checks, or does the release-profile setting mean the workspace's cargo test run — which uses the dev/test profile, not [profile.release] — actually panics today regardless of the release profile flag?). This is important to verify empirically rather than assumed: confirm via cargo test -p farming-pool whether overflow-checks is even active in the default test profile before designing the "graceful error" tests around an assumption. If it already panics in tests today, that's arguably an existing (if undocumented) DoS/panic-safety gap in its own right.
- Property-based sweep bounded near realistic production ranges, per the issue's own
proptest sketch — cheap to write but only useful once the ranges chosen actually reach the overflow boundary for the current un-fixed arithmetic; today's implementation will hit the panic boundary at fairly modest combined inputs (e.g. amount ~ 10^15, multiplier ~ 10^4, credit_rate ~ 10^6, elapsed ~ 10^6 already exceeds i128::MAX ~ 1.7*10^38 when multiplied together... actually verify the exact magnitude threshold by computation rather than estimating, since i128::MAX is large enough that four chained multiplications of "reasonable-looking" per-admin-settable values may or may not reach it — this needs to be computed precisely as part of the spike, not assumed).
- Golden/regression boundary tests keyed to the eventual error variant name: once the overflow-issue fix lands (introducing e.g.
PoolError::CreditOverflow), add exact boundary tests (i128::MAX / N for a precisely derived N) asserting the typed error is returned at N+1 and a valid result at N. This is the most precise but has to be written after, not concurrently with, the arithmetic fix — sequencing risk if this issue is picked up before its companion overflow-fix issue.
Edge cases
Test plan
Problem
compute_creditschains three uncheckedi128multiplications (compute_total_stake(...) * credit_rate * ledgers_elapsed as i128, itself internally computingamount * allocation_pct as i128 / 100andboosted * multiplier as i128), and there is currently zero test coverage anywhere near the boundaries ofi128:Every existing test in
farming-pool/src/test.rsuses small, hand-picked values (amountin the hundreds/thousands,multiplierin 1-10,elapsedin single/double digits), which never come close to exercising the overflow behavior the workspace'soverflow-checks = truerelease profile setting implies should be tested (see the companion "unchecked i128 multiplication chain traps the whole contract on overflow" issue). Sinceglobal_multiplierandcredit_rateare both admin-settable to arbitrary values post-initialization with no upper bound, andamountis bounded only by a user's token balance, the actual production input space is far larger than anything the test suite currently exercises.Impact
compute_credits/compute_total_staketrap gracefully, trap ungracefully (losing the whole transaction with an opaque host error), or — ifchecked_mulmigration from the companion overflow issue is only partially applied — silently produce a wrong result, is currently unverified by any automated test.compute_credits/compute_total_stakeare migrated tochecked_mul(per the companion issue), there is no fuzz/boundary test to prove the fix actually covers the full input space rather than just the specific values chosen ad hoc for a couple of example tests.Fix
Add targeted boundary-value tests plus a bounded fuzz sweep near the extremes of each input:
Acceptance Criteria
i128::MAXforamount/multiplier/credit_rate/ledgers_elapsedcombinations.(amount, allocation_pct, multiplier, credit_rate, elapsed)combinations asserting no silent wraparound occurs.cargo test --workspacetime budget.Further Investigation
Why this is a spike: the right shape of the fix this test suite is meant to validate is itself undecided.
compute_credits(soroban/contracts/farming-pool/src/lib.rs:138-146) andcompute_total_stake(lib.rs:131-136) currently use raw*/-//operators oni128, which under this workspace'soverflow-checksprofile setting either panics (traps the whole transaction with an opaque host error) or wraps silently depending on build profile — the same profile-dependency risk flagged in #82 for the factory'su32PoolCount. Whether the eventual fix is (a)checked_mul/checked_addreturning a newPoolError::CreditOverflowvariant, (b) saturating arithmetic that clamps toi128::MAXinstead of erroring, or (c) a redesign that keeps intermediate values in a wider/scaled representation to push the overflow boundary out further, is an open design decision this issue's own body defers to a "companion issue" — meaning the tests this issue asks for can't be finalized until that decision is made, and the test design should itself help surface which of (a)/(b)/(c) is preferable by characterizing exactly where today's implementation breaks.Tradeoff approaches for the boundary-test design
#[should_panic]tests that document today's actual behavior (doesi128::MAX/4 * 100 / 100 * 4 * 1 * 1panic via debug-mode overflow-checks, or does the release-profile setting mean the workspace'scargo testrun — which uses the dev/test profile, not[profile.release]— actually panics today regardless of the release profile flag?). This is important to verify empirically rather than assumed: confirm viacargo test -p farming-poolwhetheroverflow-checksis even active in the default test profile before designing the "graceful error" tests around an assumption. If it already panics in tests today, that's arguably an existing (if undocumented) DoS/panic-safety gap in its own right.proptestsketch — cheap to write but only useful once the ranges chosen actually reach the overflow boundary for the current un-fixed arithmetic; today's implementation will hit the panic boundary at fairly modest combined inputs (e.g.amount ~ 10^15, multiplier ~ 10^4, credit_rate ~ 10^6, elapsed ~ 10^6already exceedsi128::MAX ~ 1.7*10^38when multiplied together... actually verify the exact magnitude threshold by computation rather than estimating, sincei128::MAXis large enough that four chained multiplications of "reasonable-looking" per-admin-settable values may or may not reach it — this needs to be computed precisely as part of the spike, not assumed).PoolError::CreditOverflow), add exact boundary tests (i128::MAX / Nfor a precisely derivedN) asserting the typed error is returned atN+1and a valid result atN. This is the most precise but has to be written after, not concurrently with, the arithmetic fix — sequencing risk if this issue is picked up before its companion overflow-fix issue.Edge cases
allocation_pct = 100combined with very largemultiplier(removes the/100truncation's downward pressure, reaching overflow fastest).credit_rateisi128and only validated> 0atset_credit_rate/initialize, so a fuzz sweep should also confirm no combination of positive inputs can ever produce a negative result via wraparound (a negative credits balance would be a distinct, worse bug than a panic — silent value corruption rather than a loud failure).set_global_multiplier/set_credit_rategain upper ceilings, add a specific test asserting that the maximum allowedmultiplier/credit_ratecombined with a generous but realisticamountand multi-yearelapsed(e.g. ~6.3M ledgers/year at 5s/ledger) still does not overflow — this is the concrete acceptance test tying farming-pool: set_global_multiplier and set_credit_rate accept unbounded values with no sanity ceiling, guaranteeing compute_credits overflow at scale #89's chosen ceiling constants back to this issue's fuzzing.proptestas a shared dev-dependency; coordinate constants/ranges (e.g. a sharedproptestconfig module) rather than duplicating generator bounds independently in two places.Test plan
test_compute_credits_near_i128_max_amount_does_not_wrap(named in the issue body) plus a first characterization testtest_compute_credits_current_behavior_at_i128_max_boundarydocumenting today's actual (panic vs. wrap) behavior before any fix lands.compute_credits_never_wraps_or_panics_silentlyproptest (named in the issue body), gated behind the sameproptestdev-dependency added for farming-pool: no property-based test enforces that total accrued credits are independent of checkpoint frequency #75.test_set_credit_rate_and_multiplier_ceiling_prevents_overflowonce farming-pool: set_global_multiplier and set_credit_rate accept unbounded values with no sanity ceiling, guaranteeing compute_credits overflow at scale #89's ceilings exist, computing the actual numeric bound viai128::MAXdivided by the chosen ceiling constants and a realistic maxelapsed.