feat(contracts): systematic fuzz + invariant testing suite - #266
Merged
Sendi0011 merged 1 commit intoAug 31, 2026
Merged
Conversation
…-org#262) Add property-based fuzz and invariant tests across all seven Soroban contracts, wired into a dedicated CI job with a fixed seed for reproducible failures. ## New fuzz test files - contracts/rotational/src/fuzz_tests.rs (pre-existing, kept as-is) - contracts/flexible/src/fuzz_tests.rs - contracts/target/src/fuzz_tests.rs - contracts/microloan/src/fuzz_tests.rs - contracts/governance/src/fuzz_tests.rs - contracts/yield-strategy/src/fuzz_tests.rs - contracts/reputation/src/fuzz_tests.rs Each file uses proptest with pure-Rust simulation structs that mirror the on-chain arithmetic without requiring a live Soroban Env. ## Invariants covered (per contract) rotational fee split sums to total; round index validity after member removal; deposit unlock is sticky flexible withdrawal fee: fee+net==amount, monotone in bps; yield share: no over-distribution, proportionality; aggregate balance reconciliation; withdrawal cap target balance reconciliation; unlock sticky; unlock threshold; deadline enforcement; refund sum == deposited total microloan total_owed >= principal; remaining never negative; MAX_ACTIVE_LOANS per member; no overpayment; no double-repay; terminal state irreversibility governance quorum math: monotone in votes and threshold, boundary precision, overflow-safe saturating arithmetic; MAX_ACTIVE_PROPOSALS cap; no double-vote; terminal state irreversibility yield-strat position_value >= deployed for ratio>=10_000; yield monotone in ratio; no overflow at i128::MAX boundaries; emergency_withdraw resets deployed_amount to 0; total_harvested never decreases reputation total_score in [0,1000]; reliability/recency/completion monotone; provisional threshold; multi-call invariants; saturating_add on deposit totals ## CI job (test.yml: fuzz-invariant) New job runs on every push/PR with PROPTEST_SEED=0x5a3f9c1d7b2e4068 (deterministic). Each contract step tees output to /tmp/fuzz-*.log and fails the build with the seed in the error message on any test failure. Logs are uploaded as artifacts (30-day retention) on failure. ## Real bugs found and fixed ### reputation: integer overflow in compute_deposit_reliability (lib.rs) let total_rounds = total_deposits + missed_deposits; // was plain + At (successful=u32::MAX, missed=1) this panics with 'attempt to add with overflow'. Fixed with saturating_add in both lib.rs and the fuzz mirror. Regression test: regression_reliability_no_overflow_at_u32_max. ### reputation: MemberSim pools_completed could exceed pools_joined The simulation incremented pools_completed without incrementing pools_joined. Fixed by also incrementing pools_joined on each pool_completed event. Regression test: regression_pools_completed_never_exceeds_pools_joined. ## proptest-regressions files Saved counterexamples committed for flexible, governance, target, and reputation so every future run re-checks the shrunk failure cases. Closes JointSave-org#262
Sendi0011
self-requested a review
August 31, 2026 07:31
Sendi0011
approved these changes
Aug 31, 2026
Sendi0011
left a comment
Contributor
There was a problem hiding this comment.
✅ Approve — excellent coverage, well-integrated
Strong, senior-quality PR that directly addresses the gap in #262. The fuzz/invariant suite is thoughtfully designed and the CI wiring is clean. A few observations to note (non-blocking):
What's done well
- Pure-arithmetic simulators mirroring on-chain logic (
total_owed,remaining, state machine) drive thousands of proptest cases fast, without needing a liveEnv— pragmatic and effective for catching arithmetic/overflow/state-transition bugs. - Invariants match the actual contract constraints:
total_owed ≥ principal,remaining ≥ 0,repaid ≤ owed, no double-repay, terminalCancelled/Repaidstates,MAX_ACTIVE_LOANSper member, and overflow safety ati128::MAX/10_000. - Multi-call fuzz sequences (create/accept/repay/cancel/default) assert invariants after every operation — this is exactly the stress that unit tests miss.
- Deterministic + reproducible CI: fixed
PROPTEST_SEED, per-contract jobs,grep "test result: ok"gate (so a skipped/crashed run fails the job), and upload of counterexample logs on failure. Reproducible seeds are the right call.
Non-blocking observations (no change required to merge)
- Mirror-based testing can share bugs with
lib.rs. Because the fuzz target re-implements the arithmetic in a pure simulator, a bug that is consistent between the simulator and the contract (e.g., both round interest down incorrectly) will not be detected — it only catches cases where the contract diverges from the intended spec. This is a known trade-off of avoided-Envs fuzzing and is fine for now. Suggest a future task: a smaller end-to-end invariant set usingtestutilsagainst the realEnv(likerotational's existingfuzz_tests.rs) to cross-check the simulators. proptest-regressions/fuzz_tests.txtfiles are committed (good practice — they'll replay any discovered counterexample). Just confirm no seeded regression currently encodes a failure.- Workflow toolchain pin (
1.85.0) and the--precisepins match the existing deploy workflow — consistent with the repo.
Minor: consider cargo test split so the prop_ jobs also compile-check the non-fuzz tests, but not required.
Great work — the suite materially de-risks the contract set.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #262
Summary
Implements the full scope of issue #262 — systematic property-based fuzz and invariant testing across all seven Soroban contracts, wired into a dedicated CI job with a fixed seed for deterministic failures.
New fuzz test files
All use pure-Rust simulation structs (no live Soroban Env) so proptest drives thousands of iterations without a network or wallet.
CI job added (test.yml: fuzz-invariant)
PROPTEST_SEED=0x5a3f9c1d7b2e4068— deterministic, reproducible failuresReal bugs found and fixed
🐛 reputation: integer overflow in
compute_deposit_reliability(lib.rs)Minimal counterexample found by proptest:
successful = 4294967295, missed = 1.Regression test:
regression_reliability_no_overflow_at_u32_max🐛 reputation fuzz:
pools_completedcould exceedpools_joinedMemberSimincrementedpools_completedwithout ever growingpools_joined, breaking the invariant after just twopool_completed=trueevents. Fixed by incrementingpools_joinedalongsidepools_completed.Regression test:
regression_pools_completed_never_exceeds_pools_joinedproptest-regressions committed
Saved shrunk counterexamples for flexible, governance, target, and reputation — all future runs automatically re-verify these exact inputs before exploring new ones.
Results
96 properties across 7 contracts — all green.