Skip to content

fix: enforce systemic configuration invariants (#100) - #108

Open
fredericklamar342-prog wants to merge 1 commit into
SPulse-Org:mainfrom
fredericklamar342-prog:fix/issue-100-systemic-configuration
Open

fix: enforce systemic configuration invariants (#100)#108
fredericklamar342-prog wants to merge 1 commit into
SPulse-Org:mainfrom
fredericklamar342-prog:fix/issue-100-systemic-configuration

Conversation

@fredericklamar342-prog

Copy link
Copy Markdown
Contributor

Summary

The protocol's constants were each individually reasonable but their interactions were never constrained: fee bookkeeping could silently invert on cancellation, the leaderboard's min cache could be wrong at capacity, and combined welcome/bet minting had no bound. This PR makes the safe operating envelope explicit and machine-verified (compile-time invariant matrix + provenance ledgers + caps) and fixes the concrete interaction failures the issue identifies, rather than tuning any single constant.

Root Cause

Constants were hardcoded literals with implicit relationships (e.g. NET_NUMERATOR == BPS_DENOM - TOTAL_FEE_BPS was only true by coincidence of typing 9_800), and accounting state was global: cancel_market reclaimed fees with net * 200bps / (10000 - 200bps) — treating ALL 2% as reclaimable even when the referrer had already been paid — and clamped the whole accumulator, so cancelling one referrer-backed market stole other markets' fees. cancel_refund refunded gross, which the contract may no longer hold. The leaderboard min-cache could disagree with reality (issue #43 family), and PULSE minting was unbounded (issues #29/#46 family).

Impact

  • Fee theft / accounting inversion (the group-1 interaction): cancelling a market with referrer-backed bets zeroes the entire accumulator; the contract's balance and AccumulatedFees can never reconcile.
  • Wrong leaderboard evictions (group-2): with MAX_TOP_PLAYERS and a stale min cache, a low-points player can displace a high-points player once the list fills.
  • Unbounded supply (group-4): welcome bonus + per-bet rewards + registration/referral loops can mint PULSE without bound.
  • No mechanism prevented a future developer from introducing an unsafe constant combination.

Solution

1. Single source of truth + compile-time invariant matrix (all four contracts)

  • NET_NUMERATOR is now derived: BPS_DENOM - TOTAL_FEE_BPS — the cash-accounting identity net + total_fee == denom can no longer drift.
  • New compile-time assertions encode every cross-constant relationship the protocol depends on across the four crates:
    • fee group: 0 < fees < denom, platform <= total, net + total == denom
    • limits: MIN_BET > 0, MAX_BETS_PER_USER > 0, MAX_MARKETS_PER_HOUR > 0
    • withdrawal: 0 < MAX_WITHDRAWAL_BPS <= BPS_DENOM, WITHDRAW_DELAY_SECS > 0
    • TTL group: 0 < TTL_BUMP <= TTL_HIGH (persistent bump never outruns the instance key)
    • rewards: WELCOME_BONUS_POINTS/TOKENS > 0, REFERRAL_BET_POINTS > 0
    • supply: MAX_SUPPLY > 0
      An unsafe combination now fails the compilation, not production.

2. Exact per-market fee accounting (Group 1 + the cancel interaction)

  • New DataKey::MarketFees(market_id) ledger records exactly what each market contributed to AccumulatedFees (platform fee + referral fee when never paid out + swept pools/dust at resolution).
  • BetEntry.refundable tracks per user the amount the contract actually holds (net + platform + referral iff unpaid).
  • cancel_refund now pays refundable (not gross) and drains that bet's fee share from AccumulatedFees — so refunds self-balance: Σ refundable == pool + MarketFees[market], and no cancellation can touch another market's fees.
  • get_market_fees(market_id) gives full provenance: Σ_markets MarketFees == AccumulatedFees (fees + held + sweep).

3. Leaderboard capacity invariant (Group 2)

  • update_top_players now also refreshes MinPoints/MinSlot when an in-list player's updated points drop below the cached min (the stale-cache hole): the min cache always equals the true minimum of the leaderboard.

4. Bounded reward economics (Group 3/4)

  • PULSE gains a hard MAX_SUPPLY (1e9 PULSE, 7 decimals) enforced in mint before any state change (SupplyCapExceeded). Welcome bonuses, betting rewards and referral flows are all bounded by it.

Implementation Details

File Change
prediction_market/src/lib.rs Derived NET_NUMERATOR + compile-time invariant matrix; MarketFees(u64) ledger; BetEntry.refundable; exact cancel_refund (refundable + per-bet fee drain); removed the net*200bps cancel clamp; sweep provenance in resolve_market; get_market_fees() view; renames rewardadd_pts + direct PULSE mint in claim (pre-existing baseline repair, see note).
prediction_market/src/tests.rs 5 new invariant tests; 3 existing cancel tests updated to exact-provenance semantics; 2 pre-existing broken tests adapted.
leaderboard/src/lib.rs Min-cache fix + invariant asserts; dead imports/const removed.
leaderboard/src/tests.rs New capacity-invariant test; reward_bonusadd_bonus_pts stale calls repaired.
pulse_token/src/lib.rs MAX_SUPPLY + SupplyCapExceeded + mint guard.
pulse_token/src/tests.rs Supply-cap boundary tests.
referral_registry/src/lib.rs Invariant matrix; pre-existing reward_bonusadd_bonus_pts + welcome mint repair.
referral_registry/src/tests.rs set_tokenset_token_contract (baseline compile repair).

Baseline repair note (honest): upstream main did not compile because an earlier leaderboard rename left dangling callers (set_token/reward_bonus/reward). These mechanical repairs are included so the workspace is a runnable baseline; they are behavior-preserving and identical in scope to the baseline repairs on the sibling PRs.

Security / Invariant Considerations

  • Fee conservation: every bet's refundable == net + platform + held_referral; Σ over bettors of refundable == pool + MarketFees[m]; after full cancellation the contract returns exactly what it held and AccumulatedFees is drained only by this market's amounts — provable (and tested) that no other market's fees can move.
  • No over/under refunds: referrer-paid referral fees are never double-refunded; no-referrer bets get full gross back.
  • Compile-time guarantees: the whole interaction matrix is enforced at build time.
  • Bounded supply: no minting vector can exceed MAX_SUPPLY.
  • Storage compatible: only additive keys (MarketFees, BetEntry.refundable), no existing key reshaped; old refunds on upgraded contracts simply refund the exact held amount.

Test Coverage

  • test_cancel_refund_isolates_market_fees — two markets: cancelling+refunding one never touches the other's fees.
  • test_cancel_refund_respects_referrer_holdings — referrer-backed refund = gross − paid referral (99.5%); no-referrer = 100%; referrer not clawed back.
  • test_fee_provenance_invariant_holds — Σ fees == AccumulatedFees == get_market_fees(); refunds return exactly the held gross.
  • test_min_bet_net_threshold_boundary — the net-vs-gross MIN_BET interaction pinned exactly (gross yields net == MIN_BET accepted; one stroop below rejected).
  • test_sweep_provenance_recorded — swept user principal is attributed to the market's fee ledger.
  • test_min_cache_matches_true_minimum_at_capacity (leaderboard) — fill to 50, below-min rejected, above-min admitted, list stays sorted.
  • test_mint_enforces_supply_cap / test_supply_cap_partial_is_allowed (pulse) — cap hit exactly, over-cap mint fails #9.

Acceptance Criteria ↔ Evidence

Criterion Evidence
Config relationships validated compile-time const _: () = assert!(...) matrix in all 4 contracts
Fee accounting invariants enforced MarketFees + refundable ledgers; isolation/provenance tests
Leaderboard invariant min-cache fix + capacity test
TTL relationship compile-time TTL_BUMP <= TTL_HIGH + existing TTL suites
Referral/reward bounds WELCOME_* > 0 asserts + MAX_SUPPLY + cap tests
No new exploit from changes full suite + focused invariant tests

Validation

  • cargo test --workspace104/104 pass (leaderboard 6, prediction_market 65, pulse_token 18, referral_registry 15) — zero warnings, zero errors.
  • No CI workflows exist in the repository (verified via Actions API: 0 runs) — no CI status to report, honestly.
  • cargo fmt: upstream main is not rustfmt-clean; branch keeps style-consistent changes only (no repo-wide reformat).

Regression Analysis

  • All pre-existing tests pass with the new exact accounting; the only updated expectations are the three cancel tests that had asserted the old (incorrect) "zero accumulator at cancel" behavior — they now assert the correct per-market release.
  • MIN_BET/fee/withdrawal/TTL/leaderboard existing suites unaffected.
  • Both previously-broken upstream tests (net-vs-gross MIN_BET and the mock footprint limit) fixed minimally.

Review Checklist

  • focused changes + mechanical baseline repairs
  • authorization untouched
  • accounting invariants (ledger + refundable + per-market drain)
  • storage additive-only
  • tests per invariant
  • no warnings, clean build
  • CI: not configured in repo (verified)

Issue Reference

Closes #100

Turns the implicit cross-constant interactions into explicit,
machine-verified constraints and fixes the concrete interaction
failures:
- Fee group: NET_NUMERATOR is now DERIVED from the fee constants
  (single source of truth) plus a compile-time invariant matrix in
  every contract (fee, limits, timelock, TTL relationships).
- Fee accounting: per-market MarketFees ledger + per-bet refundable
  tracking. Cancellation no longer reclaims via the naive
  net*200bps formula (which zeroed the accumulator and stole other
  markets' fees); cancel_refund releases exactly what the contract
  holds (net + platform + referral-if-unpaid) and drains fees per
  market, keeping Sigma(market fees) == AccumulatedFees.
- Leaderboard: MinPoints/MinSlot cache now tracks a player whose
  points drop below the cached min, keeping evictions correct at
  MAX_TOP_PLAYERS.
- Reward economics: hard PULSE supply cap bounds combined
  welcome-bonus and betting-reward minting.

Closes SPulse-Org#100
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.

[CRITICAL] Configuration parameter interactions create hidden systemic risks — no safe combination of constants exists

1 participant