feat: add emergency pause / circuit breaker to market, registry and token (#95) - #107
Open
fredericklamar342-prog wants to merge 1 commit into
Conversation
…oken (SPulse-Org#95) Adds an admin-controlled Paused flag with set_paused()/paused() to prediction_market, referral_registry and pulse_token. While paused, all risk-creating, settlement and withdrawal operations are blocked (place_bet, create_market, resolve_market, cancel_market, all three withdrawal paths; register_referral/credit; mint/burn) with a clear Paused error. User recovery paths intentionally stay available so an emergency pause never locks user funds: claim and cancel_refund stay open on the market, and PULSE transfers/allowances are untouched. The leaderboard is intentionally not pausable — pausing its points ledger would block claims (recovery) with no security benefit since it only listens to market/referral calls. Closes SPulse-Org#95
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.
Summary
None of the four contracts had any way to halt operations in an emergency. If a critical vulnerability was discovered (fee theft, reentrancy, payout bug), funds would continue flowing until an
upgradewas deployed — there was no way to contain an active exploit, no "kill switch", and no way for users to know operations were frozen. This adds a coherent admin-controlled circuit breaker to the three contracts that move value or create exposure.Root Cause
No contract implemented a
pausedstate,pause()/resume(), apaused()query, or any guard on state-changing entry points. The only incident response tool wasupgrade, which is heavy, irreversible, and slow to prepare — leaving an uncontained exploit window.Impact
place_bet/cancel_market/resolve_market/withdrawals could drain funds over many blocks until an upgrade shipped.Solution
A coherent, per-contract circuit breaker (not one flag on one function), designed across the four contracts with an explicit security model:
Which contracts get a pause state and why:
prediction_marketreferral_registryregister_referralcreates fee obligations;creditdisburses XLM.pulse_tokenmint/burnchange supply/value.leaderboardLocked while paused (risk-creating / settlement / withdrawal, each with a clear
Pausederror):prediction_market:create_market,place_bet,resolve_market,cancel_market,withdraw_fees,request_withdraw_fees,execute_withdraw_fees.referral_registry:register_referral,credit.pulse_token:mint,burn.Intentionally still available while paused (user recovery — a pause must never become a second fund-locking vulnerability, per the issue's own analysis):
prediction_market:claim,cancel_refund,cancel_withdrawal_request, all views, admin config/upgrade.pulse_token:transfer,transfer_from,approve— users can always move their own PULSE.Mechanics (identical shape in all three contracts):
set_paused(caller, paused)— admin-only, caller explicitly compared against the stored admin (idempotent: repeated pause/resume is fine).paused()— public query so users/dashboards can see the breaker state.require_not_paused()invoked at the very top of each protected entry point, before auth — so the error is deterministic and no partial state is ever written.All storage is a single instance-storage
Pausedbool per contract — no layout migration, no impact on TTL conventions, upgrade-compatible.Implementation Details
prediction_market/src/lib.rsMarketError::Paused = 26,DataKey::Paused,set_paused/paused/require_not_paused, guards on 7 risk-creating/withdrawal entry points; renamesreward→add_pts+ direct PULSE mint inclaim(pre-existing compile/runtime breakage onmain); removed deadtotal_poolbinding.referral_registry/src/lib.rsReferralError::Paused = 7,DataKey::Paused,set_paused/paused/require_not_paused, guards onregister_referral+credit;reward_bonus→add_bonus_pts+ restored welcome PULSE mint (pre-existing rename breakage).pulse_token/src/lib.rsTokenError::Paused = 9,DataKey::Paused,set_paused(caller must equal stored admin →NotAdminotherwise),paused, guards onmint+burn.prediction_market/src/tests.rstoo_many_betsMIN_BET sizing, bettor-index footprint) andset_token→set_token_contract.pulse_token/src/tests.rsreferral_registry/src/tests.rsset_token→set_token_contract.leaderboard/src/{lib,tests}.rsreward_bonus→add_bonus_ptsin stale tests (compile repair only).Baseline repairs note (honest): upstream
maindid not compile (set_token/reward/reward_bonuscallers left dangling by an earlier rename) and several tests were broken at the tip; the minimal mechanical repairs listed above are included so the workspace is a runnable baseline — verified, not assumed.Security / Invariant Considerations
credit/add_pts/add_bonus_ptsare only reachable via the (paused) market path and additionally guarded in the registry.Paused([MEDIUM] register_referral doesn't validate that the referrer is registered — unregistered referrers get paid #26/[HIGH] No event emission anywhere — off-chain indexers, UIs, and auditors cannot track state transitions #7/[CRITICAL] Persistent storage TTLs can expire before claims/refunds — user funds permanently locked #9), never partial state.set_pausedcompares the caller against the stored admin (like the other admin entry points) →NotAdminfor everyone else; repeated pause/re-sume is idempotent.paused()on all three contracts.Test Coverage (adversarial)
prediction_market:
test_pause_rejects_non_admin/test_resume_rejects_non_admin(NotAdmin [CRITICAL] resolve_market lets a resolver sweep the entire pool to fees by resolving to the empty side — griefing / fund theft #3)test_pause_blocks_place_bet_then_resume— paused bet fails, resume, bet workstest_pause_blocks_create_market— create blockedtest_pause_blocks_resolve_and_cancel— resolve+cancel blockedtest_pause_blocks_all_withdrawal_paths— withdraw/request/execute blockedtest_pause_keeps_claim_available+test_pause_keeps_cancel_refund_available— recovery open while pausedtest_repeated_pause_resume_idempotentpulse_token:
test_pause_requires_admin(NotAdmin [CRITICAL] set_config can re-point the market to malicious token/referral/leaderboard contracts with no validation — total fund theft #6)test_pause_blocks_mint_and_burn_but_not_transfer— mint/burn blocked; transfer still works; resume restores mintingreferral_registry:
test_registry_pause_requires_admin(NotAdmin [CRITICAL] set_config can re-point the market to malicious token/referral/leaderboard contracts with no validation — total fund theft #6)test_registry_pause_blocks_registration_and_credit_then_resume— registration+credit blocked; views alive; resume restores workflowAcceptance Criteria ↔ Evidence
Pausedkey +paused()on 3 contractsValidation
cargo test --workspace→ 110/110 pass (leaderboard 6, prediction_market 69, pulse_token 18, referral_registry 17) — zero warnings, zero errors.cargo fmt: upstreammainis not rustfmt-clean; this branch keeps its changes style-consistent without reformatting untouched files.Regression Analysis
Pausedrejection on risk-creating entry points while the breaker is engaged — that is the feature.Review Checklist
Issue Reference
Closes #95