@@ -104,6 +104,8 @@ pub enum PredifiError {
104104 PriceDataInvalid = 102 ,
105105 /// Price condition not set for pool.
106106 PriceConditionNotSet = 103 ,
107+ /// Total pool stake cap reached or would be exceeded.
108+ MaxTotalStakeExceeded = 104 ,
107109}
108110
109111#[ contracttype]
@@ -136,6 +138,8 @@ pub struct Pool {
136138 pub min_stake : i128 ,
137139 /// Maximum stake amount per prediction (0 = no limit).
138140 pub max_stake : i128 ,
141+ /// Maximum total stake amount across the entire pool (0 = no limit).
142+ pub max_total_stake : i128 ,
139143 /// Initial liquidity provided by the pool creator (house money).
140144 /// This is part of total_stake but excluded from fee calculations.
141145 pub initial_liquidity : i128 ,
@@ -466,6 +470,14 @@ pub struct TreasuryWithdrawnEvent {
466470 pub recipient : Address ,
467471 pub timestamp : u64 ,
468472}
473+ #[ contractevent( topics = [ "refund_claimed" ] ) ]
474+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
475+ pub struct RefundClaimedEvent {
476+ pub pool_id : u64 ,
477+ pub user : Address ,
478+ pub amount : i128 ,
479+ }
480+
469481#[ contractevent( topics = [ "upgrade" ] ) ]
470482#[ derive( Clone , Debug , Eq , PartialEq ) ]
471483pub struct UpgradeEvent {
@@ -1196,6 +1208,7 @@ impl PredifiContract {
11961208 options_count,
11971209 min_stake,
11981210 max_stake,
1211+ max_total_stake : 0 , // default: no cap
11991212 initial_liquidity,
12001213 creator : creator. clone ( ) ,
12011214 category : category. clone ( ) ,
@@ -1263,6 +1276,59 @@ impl PredifiContract {
12631276 pool_id
12641277 }
12651278
1279+ /// Increase the maximum total stake cap for a pool.
1280+ /// Only the pool creator can increase it, and only before the market ends.
1281+ ///
1282+ /// - `new_max_total_stake` must be >= current `pool.total_stake`.
1283+ /// - Setting to 0 means "no cap" (only allowed if current cap is 0 or increasing from a non-zero).
1284+ pub fn increase_max_total_stake (
1285+ env : Env ,
1286+ creator : Address ,
1287+ pool_id : u64 ,
1288+ new_max_total_stake : i128 ,
1289+ ) -> Result < ( ) , PredifiError > {
1290+ Self :: require_not_paused ( & env) ;
1291+ creator. require_auth ( ) ;
1292+
1293+ let pool_key = DataKey :: Pool ( pool_id) ;
1294+ let mut pool: Pool = env
1295+ . storage ( )
1296+ . persistent ( )
1297+ . get ( & pool_key)
1298+ . expect ( "Pool not found" ) ;
1299+ Self :: extend_persistent ( & env, & pool_key) ;
1300+
1301+ if pool. creator != creator {
1302+ return Err ( PredifiError :: Unauthorized ) ;
1303+ }
1304+
1305+ // Pool must still be active and not ended
1306+ if pool. state != MarketState :: Active || pool. resolved || pool. canceled {
1307+ return Err ( PredifiError :: InvalidPoolState ) ;
1308+ }
1309+ assert ! ( env. ledger( ) . timestamp( ) < pool. end_time, "Pool has ended" ) ;
1310+
1311+ // Must not set a cap below what is already staked
1312+ assert ! (
1313+ new_max_total_stake == 0 || new_max_total_stake >= pool. total_stake,
1314+ "new_max_total_stake must be zero (unlimited) or >= total_stake"
1315+ ) ;
1316+
1317+ // Only allow increasing the cap (or setting unlimited)
1318+ if pool. max_total_stake > 0 && new_max_total_stake > 0 {
1319+ assert ! (
1320+ new_max_total_stake >= pool. max_total_stake,
1321+ "new_max_total_stake must be >= current max_total_stake"
1322+ ) ;
1323+ }
1324+
1325+ pool. max_total_stake = new_max_total_stake;
1326+ env. storage ( ) . persistent ( ) . set ( & pool_key, & pool) ;
1327+ Self :: extend_persistent ( & env, & pool_key) ;
1328+
1329+ Ok ( ( ) )
1330+ }
1331+
12661332 /// Resolve a pool with a winning outcome. Caller must have Operator role (1).
12671333 /// Cannot resolve a canceled pool.
12681334 /// PRE: pool.state = Active, operator has role 1
@@ -1495,6 +1561,15 @@ impl PredifiContract {
14951561 ) ;
14961562 }
14971563
1564+ // Enforce global pool cap (max total stake)
1565+ if pool. max_total_stake > 0 {
1566+ let new_total = pool. total_stake . checked_add ( amount) . expect ( "overflow" ) ;
1567+ if new_total > pool. max_total_stake {
1568+ Self :: exit_reentrancy_guard ( & env) ;
1569+ soroban_sdk:: panic_with_error!( & env, PredifiError :: MaxTotalStakeExceeded ) ;
1570+ }
1571+ }
1572+
14981573 let pred_key = DataKey :: Pred ( user. clone ( ) , pool_id) ;
14991574 let existing_pred = env. storage ( ) . persistent ( ) . get :: < _ , Prediction > ( & pred_key) ;
15001575 if let Some ( mut existing_pred) = existing_pred {
@@ -1758,6 +1833,104 @@ impl PredifiContract {
17581833 Ok ( winnings)
17591834 }
17601835
1836+ /// Claim a refund from a canceled pool. Returns the refunded amount.
1837+ /// Only available for canceled pools. User receives their full original stake.
1838+ ///
1839+ /// PRE: pool.state = Canceled, user has a prediction on the pool
1840+ /// POST: HasClaimed(user, pool) = true (INV-3), user receives full stake amount
1841+ ///
1842+ /// # Arguments
1843+ /// * `user` - Address claiming the refund (must provide auth)
1844+ /// * `pool_id` - ID of the canceled pool
1845+ ///
1846+ /// # Returns
1847+ /// Ok(amount) - Refund successfully claimed, returns refunded amount
1848+ /// Err(PredifiError) - Operation failed with specific error code
1849+ ///
1850+ /// # Errors
1851+ /// - `InvalidPoolState` if pool doesn't exist or is not canceled
1852+ /// - `InsufficientBalance` if user has no prediction or zero stake
1853+ /// - `AlreadyClaimed` if user already claimed refund for this pool
1854+ /// - `PoolNotResolved` if pool is resolved (not canceled)
1855+ #[ allow( clippy:: needless_borrows_for_generic_args) ]
1856+ pub fn claim_refund ( env : Env , user : Address , pool_id : u64 ) -> Result < i128 , PredifiError > {
1857+ Self :: require_not_paused ( & env) ;
1858+ user. require_auth ( ) ;
1859+
1860+ Self :: enter_reentrancy_guard ( & env) ;
1861+
1862+ // --- CHECKS ---
1863+
1864+ let pool_key = DataKey :: Pool ( pool_id) ;
1865+ let pool: Pool = match env. storage ( ) . persistent ( ) . get ( & pool_key) {
1866+ Some ( p) => p,
1867+ None => {
1868+ Self :: exit_reentrancy_guard ( & env) ;
1869+ return Err ( PredifiError :: InvalidPoolState ) ;
1870+ }
1871+ } ;
1872+ Self :: extend_persistent ( & env, & pool_key) ;
1873+
1874+ // Verify pool is canceled
1875+ if pool. state != MarketState :: Canceled {
1876+ Self :: exit_reentrancy_guard ( & env) ;
1877+ return Err ( PredifiError :: InvalidPoolState ) ;
1878+ }
1879+
1880+ // Check if user already claimed refund
1881+ let claimed_key = DataKey :: Claimed ( user. clone ( ) , pool_id) ;
1882+ if env. storage ( ) . persistent ( ) . has ( & claimed_key) {
1883+ Self :: exit_reentrancy_guard ( & env) ;
1884+ return Err ( PredifiError :: AlreadyClaimed ) ;
1885+ }
1886+
1887+ // Get user's prediction
1888+ let pred_key = DataKey :: Pred ( user. clone ( ) , pool_id) ;
1889+ let prediction: Option < Prediction > = env. storage ( ) . persistent ( ) . get ( & pred_key) ;
1890+
1891+ if env. storage ( ) . persistent ( ) . has ( & pred_key) {
1892+ Self :: extend_persistent ( & env, & pred_key) ;
1893+ }
1894+
1895+ let prediction = match prediction {
1896+ Some ( p) => p,
1897+ None => {
1898+ Self :: exit_reentrancy_guard ( & env) ;
1899+ return Err ( PredifiError :: InsufficientBalance ) ;
1900+ }
1901+ } ;
1902+
1903+ // Verify user has a non-zero stake
1904+ if prediction. amount <= 0 {
1905+ Self :: exit_reentrancy_guard ( & env) ;
1906+ return Err ( PredifiError :: InsufficientBalance ) ;
1907+ }
1908+
1909+ // --- EFFECTS ---
1910+
1911+ // Mark as claimed immediately to prevent re-entrancy (INV-3)
1912+ env. storage ( ) . persistent ( ) . set ( & claimed_key, & true ) ;
1913+ Self :: extend_persistent ( & env, & claimed_key) ;
1914+
1915+ let refund_amount = prediction. amount ;
1916+
1917+ // --- INTERACTIONS ---
1918+
1919+ let token_client = token:: Client :: new ( & env, & pool. token ) ;
1920+ token_client. transfer ( & env. current_contract_address ( ) , & user, & refund_amount) ;
1921+
1922+ Self :: exit_reentrancy_guard ( & env) ;
1923+
1924+ RefundClaimedEvent {
1925+ pool_id,
1926+ user : user. clone ( ) ,
1927+ amount : refund_amount,
1928+ }
1929+ . publish ( & env) ;
1930+
1931+ Ok ( refund_amount)
1932+ }
1933+
17611934 /// Update the stake limits for an active pool. Caller must have Operator role (1).
17621935 /// PRE: pool.state = Active, operator has role 1
17631936 /// POST: pool.min_stake and pool.max_stake updated
0 commit comments