Skip to content

Commit 54b704b

Browse files
authored
feat: Add Reentrancy Protection (#366)
1 parent 775e022 commit 54b704b

File tree

1 file changed

+46
-8
lines changed
  • contract/contracts/predifi-contract/src

1 file changed

+46
-8
lines changed

contract/contracts/predifi-contract/src/lib.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)