@@ -155,6 +155,7 @@ pub enum DataKey {
155155 UserPredictionIndex ( Address , u32 ) ,
156156 Config ,
157157 Paused ,
158+ ReentrancyGuard ,
158159 CategoryPoolCount ( Symbol ) ,
159160 CategoryPoolIndex ( Symbol , u32 ) ,
160161 /// Token whitelist: TokenWhitelist(token_address) -> true if allowed for betting.
@@ -632,6 +633,18 @@ impl PredifiContract {
632633 }
633634 }
634635
636+ fn enter_reentrancy_guard ( env : & Env ) {
637+ let key = DataKey :: ReentrancyGuard ;
638+ if env. storage ( ) . temporary ( ) . has ( & key) {
639+ panic ! ( "Reentrancy detected" ) ;
640+ }
641+ env. storage ( ) . temporary ( ) . set ( & key, & true ) ;
642+ }
643+
644+ fn exit_reentrancy_guard ( env : & Env ) {
645+ env. storage ( ) . temporary ( ) . remove ( & DataKey :: ReentrancyGuard ) ;
646+ }
647+
635648 /// Returns true if the token is on the allowed betting whitelist.
636649 fn is_token_whitelisted ( env : & Env , token : & Address ) -> bool {
637650 let key = DataKey :: TokenWhitelist ( token. clone ( ) ) ;
@@ -1294,6 +1307,8 @@ impl PredifiContract {
12941307 user. require_auth ( ) ;
12951308 assert ! ( amount > 0 , "amount must be positive" ) ;
12961309
1310+ Self :: enter_reentrancy_guard ( & env) ;
1311+
12971312 let pool_key = DataKey :: Pool ( pool_id) ;
12981313 let mut pool: Pool = env
12991314 . storage ( )
@@ -1312,6 +1327,7 @@ impl PredifiContract {
13121327 "outcome exceeds options_count"
13131328 ) ;
13141329
1330+ // --- INTERNAL CHECKS & EFFECTS ---
13151331 // Validate: per-pool stake limits
13161332 assert ! (
13171333 amount >= pool. min_stake,
@@ -1324,9 +1340,6 @@ impl PredifiContract {
13241340 ) ;
13251341 }
13261342
1327- let token_client = token:: Client :: new ( & env, & pool. token ) ;
1328- token_client. transfer ( & user, & env. current_contract_address ( ) , & amount) ;
1329-
13301343 let pred_key = DataKey :: Prediction ( user. clone ( ) , pool_id) ;
13311344 if !env. storage ( ) . persistent ( ) . has ( & pred_key) {
13321345 let pc_key = DataKey :: ParticipantsCount ( pool_id) ;
@@ -1358,6 +1371,13 @@ impl PredifiContract {
13581371 env. storage ( ) . persistent ( ) . set ( & count_key, & ( count + 1 ) ) ;
13591372 Self :: extend_persistent ( & env, & count_key) ;
13601373
1374+ // --- INTERACTIONS ---
1375+
1376+ let token_client = token:: Client :: new ( & env, & pool. token ) ;
1377+ token_client. transfer ( & user, & env. current_contract_address ( ) , & amount) ;
1378+
1379+ Self :: exit_reentrancy_guard ( & env) ;
1380+
13611381 PredictionPlacedEvent {
13621382 pool_id,
13631383 user : user. clone ( ) ,
@@ -1399,6 +1419,8 @@ impl PredifiContract {
13991419 Self :: require_not_paused ( & env) ;
14001420 user. require_auth ( ) ;
14011421
1422+ Self :: enter_reentrancy_guard ( & env) ;
1423+
14021424 let pool_key = DataKey :: Pool ( pool_id) ;
14031425 let pool: Pool = env
14041426 . storage ( )
@@ -1408,6 +1430,7 @@ impl PredifiContract {
14081430 Self :: extend_persistent ( & env, & pool_key) ;
14091431
14101432 if pool. state == MarketState :: Active {
1433+ Self :: exit_reentrancy_guard ( & env) ;
14111434 return Err ( PredifiError :: PoolNotResolved ) ;
14121435 }
14131436
@@ -1420,12 +1443,11 @@ impl PredifiContract {
14201443 timestamp : env. ledger ( ) . timestamp ( ) ,
14211444 }
14221445 . publish ( & env) ;
1446+ Self :: exit_reentrancy_guard ( & env) ;
14231447 return Err ( PredifiError :: AlreadyClaimed ) ;
14241448 }
14251449
1426- // Mark as claimed immediately to prevent re-entrancy (INV-3)
1427- env. storage ( ) . persistent ( ) . set ( & claimed_key, & true ) ;
1428- Self :: extend_persistent ( & env, & claimed_key) ;
1450+ // --- CHECKS ---
14291451
14301452 let pred_key = DataKey :: Prediction ( user. clone ( ) , pool_id) ;
14311453 let prediction: Option < Prediction > = env. storage ( ) . persistent ( ) . get ( & pred_key) ;
@@ -1436,14 +1458,25 @@ impl PredifiContract {
14361458
14371459 let prediction = match prediction {
14381460 Some ( p) => p,
1439- None => return Ok ( 0 ) ,
1461+ None => {
1462+ Self :: exit_reentrancy_guard ( & env) ;
1463+ return Ok ( 0 ) ;
1464+ }
14401465 } ;
14411466
1467+ // --- EFFECTS ---
1468+
1469+ // Mark as claimed immediately to prevent re-entrancy (INV-3)
1470+ env. storage ( ) . persistent ( ) . set ( & claimed_key, & true ) ;
1471+ Self :: extend_persistent ( & env, & claimed_key) ;
1472+
14421473 if pool. state == MarketState :: Canceled {
1443- // Refunds: user gets exactly what they put in.
1474+ // --- INTERACTIONS (Refund) ---
14441475 let token_client = token:: Client :: new ( & env, & pool. token ) ;
14451476 token_client. transfer ( & env. current_contract_address ( ) , & user, & prediction. amount ) ;
14461477
1478+ Self :: exit_reentrancy_guard ( & env) ;
1479+
14471480 WinningsClaimedEvent {
14481481 pool_id,
14491482 user : user. clone ( ) ,
@@ -1455,6 +1488,7 @@ impl PredifiContract {
14551488 }
14561489
14571490 if prediction. outcome != pool. outcome {
1491+ Self :: exit_reentrancy_guard ( & env) ;
14581492 return Ok ( 0 ) ;
14591493 }
14601494
@@ -1463,6 +1497,7 @@ impl PredifiContract {
14631497 let winning_stake: i128 = stakes. get ( pool. outcome ) . unwrap_or ( 0 ) ;
14641498
14651499 if winning_stake == 0 {
1500+ Self :: exit_reentrancy_guard ( & env) ;
14661501 return Ok ( 0 ) ;
14671502 }
14681503
@@ -1472,9 +1507,12 @@ impl PredifiContract {
14721507 // Verify invariant: winnings ≤ total_stake (INV-4)
14731508 assert ! ( winnings <= pool. total_stake, "Winnings exceed total stake" ) ;
14741509
1510+ // --- INTERACTIONS (Winnings Payout) ---
14751511 let token_client = token:: Client :: new ( & env, & pool. token ) ;
14761512 token_client. transfer ( & env. current_contract_address ( ) , & user, & winnings) ;
14771513
1514+ Self :: exit_reentrancy_guard ( & env) ;
1515+
14781516 WinningsClaimedEvent {
14791517 pool_id,
14801518 user,
0 commit comments