Skip to content

fix(prediction-market): systemic accounting — solvent cancel refunds, live referral lookup, reentrancy guard - #33

Open
fredericklamar342-prog wants to merge 2 commits into
SPulse-Org:mainfrom
fredericklamar342-prog:fix/issue-1-systemic-accounting
Open

fix(prediction-market): systemic accounting — solvent cancel refunds, live referral lookup, reentrancy guard#33
fredericklamar342-prog wants to merge 2 commits into
SPulse-Org:mainfrom
fredericklamar342-prog:fix/issue-1-systemic-accounting

Conversation

@fredericklamar342-prog

@fredericklamar342-prog fredericklamar342-prog commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Three interacting accounting/state-invariant defects let a cancellation corrupt the platform's fee bookkeeping, defraud referrers, and expose the contract to cross-contract reentrancy:

  1. cancel_market reclaimed fees with a global formula (fees_in_pool = net_pool * 200 / 9800) that could zero out the entire accumulator — including fees earned by unrelated, non-cancelled markets — and refunded the full gross even for referrer-backed bets whose referral fee had already been paid out (insolvent refund).
  2. The HasReferrer cache was never invalidated, so a user who registered a referrer after their first bet permanently diverted the 50 bps referral fee away from the referrer.
  3. place_bet performed external calls (XLM transfers, referral.credit) before writing bet state, enabling reentrancy that observed partially-updated state.

Root Cause

Cancellation refunds were not funded by the market's actual physical funds (deposits minus referral payouts), the referrer lookup was cached forever with no invalidation path, and state writes were ordered after external calls.

Implementation

  • Per-user refundable ledger: every bet records Refundable(market, user) = gross − referral fee paid out — exactly what the market physically holds for that bettor.
  • Solvent cancellation: cancel_market sums the market's refundable, releases only this market's fee share from the global accumulator (acc −= refundable − net_pool), and tracks the outstanding balance in CancelState; cancel_refund pays exactly the refundable, so a cancelled market can never drain other markets' funds or the platform's accumulator.
  • Live referral lookup: the HasReferrer cache is removed; the registry is consulted on every bet, so a late-registered referrer is honored from then on.
  • Reentrancy guard + CEI: a Lock guards every external-calling entry point (place_bet, resolve_market, cancel_market, cancel_refund, claim, withdraw_fees), and place_bet writes bet/market state before calling out.

Security / Accounting Invariant

For every market, Σ refundable == physical funds held by the market; cancellation releases exactly the market's own fee share; and no entry point can be re-entered mid-transaction.

Tests

  • test_cancel_preserves_unrelated_market_fees — cancelling A leaves B's fees intact.
  • test_cancel_referrer_backed_refund_excludes_paid_fee — solvent refund; referrer keeps the fee.
  • test_referral_registered_after_first_bet_honored — late referrer is paid.
  • test_reentrancy_guard_rejects_reentrant_bet — an adversarial referral contract re-enters place_bet mid-execution; the re-entrant call is rejected and the bet is counted exactly once (no double-counting). Note: the Soroban host rejects same-contract re-entry (Error(Context, InvalidAction)), so the contract's own Lock guard is defense-in-depth behind the host's protection — the test proves the end-to-end property holds.

Verification

Scope

Only prediction_market/src/lib.rs, prediction_market/src/tests.rs, and the new test snapshot fixtures are changed. No other crate or issue is touched.

Issue

Partially addresses #1 — this PR resolves defects 1, 3, and 4 of #1 (accounting/referral/reentrancy in prediction_market). Defects 2 and 5 of #1 concern leaderboard and pulse_token respectively and are out of scope for this PR; they should be tracked separately.

@Muyideen-js

Copy link
Copy Markdown
Contributor

@fredericklamar342-prog
The issue covers five distinct defects:

  1. ✅ Cancellation fee accounting — addressed by the new refundable ledger and market-specific fee release.
  2. ❌ Stale MinPoints / MinSlot leaderboard state — not addressed; this lives in leaderboard/src/lib.rs.
  3. ✅ Stale HasReferrer cache — addressed by doing a live referral lookup.
  4. ⚠️ Reentrancy / CEI — the lock + state-before-external-calls approach looks like the intended fix, but I don't see a regression test exercising an actual reentrant contract/call path.
  5. ❌ Unbounded PULSE minting / divergent reward and reward_bonus accounting — not addressed; this requires changes outside prediction_market.

Since the PR scope only changes prediction_market, it cannot fully close the original issue, which explicitly includes the leaderboard and token-supply problems.

I'd suggest either:

I would also add an adversarial reentrancy regression test before considering defect 4 fully verified.

The 48 passing tests are good for the cases covered by this patch, but they don't demonstrate resolution of the two issue areas that aren't touched by the PR.

… live referral lookup, reentrancy guard

cancel_market reclaimed fees with a global formula that could zero out fees
earned by unrelated markets and refunded gross even when the referrer had
already been paid (insolvent). Each bet now records its per-user refundable
(= gross minus any referral fee already paid out), cancellation releases only
this market's fee share and tracks the outstanding refund balance, and
cancel_refund pays exactly the refundable. The HasReferrer cache is removed
so a referrer registered after the first bet is honored. All external-calling
entry points are wrapped in a reentrancy lock and place_bet writes state
before calling out (CEI).
@fredericklamar342-prog
fredericklamar342-prog force-pushed the fix/issue-1-systemic-accounting branch from bccc88c to b6da62c Compare August 17, 2026 00:34
@fredericklamar342-prog

Copy link
Copy Markdown
Contributor Author

@Muyideen-js
PR #33 — Partially addresses #1 — Systemic accounting — CLEAN

Branch: fix/issue-1-systemic-accounting

What was fixed:
(1) cancel_market used a global fee formula that could zero the whole accumulator and refunded referrer-backed gross it didn’t physically hold — now a per-user Refundable ledger makes refunds exactly solvent.
(2) The HASREFERRER cache was never invalidated, so late-registered referrers never got paid — now the registry is consulted live on every bet.
(3) place_bet called out before writing state — now CEI ordering + a Lock reentrancy guard on every external-calling entry point.

Tests: test_cancel_preserves_unrelated_market_fees, test_cancel_referrer_backed_refund_excludes_paid_fee, test_referral_registered_after_first_bet_honored, plus the adversarial test_reentrancy_guard_rejects_reentrant_bet (a malicious referral re-enters place_bet; the call is rejected and the bet counted once). Verified 51/51.

Body changed to: Partially addresses #1 — defects 2 & 5 concern leaderboard / pulse_token and are tracked separately.

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.

2 participants