@@ -451,6 +451,14 @@ pub struct TreasuryWithdrawnEvent {
451451 pub recipient : Address ,
452452 pub timestamp : u64 ,
453453}
454+ #[ contractevent( topics = [ "refund_claimed" ] ) ]
455+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
456+ pub struct RefundClaimedEvent {
457+ pub pool_id : u64 ,
458+ pub user : Address ,
459+ pub amount : i128 ,
460+ }
461+
454462#[ contractevent( topics = [ "upgrade" ] ) ]
455463#[ derive( Clone , Debug , Eq , PartialEq ) ]
456464pub struct UpgradeEvent {
@@ -1600,6 +1608,104 @@ impl PredifiContract {
16001608 Ok ( winnings)
16011609 }
16021610
1611+ /// Claim a refund from a canceled pool. Returns the refunded amount.
1612+ /// Only available for canceled pools. User receives their full original stake.
1613+ ///
1614+ /// PRE: pool.state = Canceled, user has a prediction on the pool
1615+ /// POST: HasClaimed(user, pool) = true (INV-3), user receives full stake amount
1616+ ///
1617+ /// # Arguments
1618+ /// * `user` - Address claiming the refund (must provide auth)
1619+ /// * `pool_id` - ID of the canceled pool
1620+ ///
1621+ /// # Returns
1622+ /// Ok(amount) - Refund successfully claimed, returns refunded amount
1623+ /// Err(PredifiError) - Operation failed with specific error code
1624+ ///
1625+ /// # Errors
1626+ /// - `InvalidPoolState` if pool doesn't exist or is not canceled
1627+ /// - `InsufficientBalance` if user has no prediction or zero stake
1628+ /// - `AlreadyClaimed` if user already claimed refund for this pool
1629+ /// - `PoolNotResolved` if pool is resolved (not canceled)
1630+ #[ allow( clippy:: needless_borrows_for_generic_args) ]
1631+ pub fn claim_refund ( env : Env , user : Address , pool_id : u64 ) -> Result < i128 , PredifiError > {
1632+ Self :: require_not_paused ( & env) ;
1633+ user. require_auth ( ) ;
1634+
1635+ Self :: enter_reentrancy_guard ( & env) ;
1636+
1637+ // --- CHECKS ---
1638+
1639+ let pool_key = DataKey :: Pool ( pool_id) ;
1640+ let pool: Pool = match env. storage ( ) . persistent ( ) . get ( & pool_key) {
1641+ Some ( p) => p,
1642+ None => {
1643+ Self :: exit_reentrancy_guard ( & env) ;
1644+ return Err ( PredifiError :: InvalidPoolState ) ;
1645+ }
1646+ } ;
1647+ Self :: extend_persistent ( & env, & pool_key) ;
1648+
1649+ // Verify pool is canceled
1650+ if pool. state != MarketState :: Canceled {
1651+ Self :: exit_reentrancy_guard ( & env) ;
1652+ return Err ( PredifiError :: InvalidPoolState ) ;
1653+ }
1654+
1655+ // Check if user already claimed refund
1656+ let claimed_key = DataKey :: Claimed ( user. clone ( ) , pool_id) ;
1657+ if env. storage ( ) . persistent ( ) . has ( & claimed_key) {
1658+ Self :: exit_reentrancy_guard ( & env) ;
1659+ return Err ( PredifiError :: AlreadyClaimed ) ;
1660+ }
1661+
1662+ // Get user's prediction
1663+ let pred_key = DataKey :: Pred ( user. clone ( ) , pool_id) ;
1664+ let prediction: Option < Prediction > = env. storage ( ) . persistent ( ) . get ( & pred_key) ;
1665+
1666+ if env. storage ( ) . persistent ( ) . has ( & pred_key) {
1667+ Self :: extend_persistent ( & env, & pred_key) ;
1668+ }
1669+
1670+ let prediction = match prediction {
1671+ Some ( p) => p,
1672+ None => {
1673+ Self :: exit_reentrancy_guard ( & env) ;
1674+ return Err ( PredifiError :: InsufficientBalance ) ;
1675+ }
1676+ } ;
1677+
1678+ // Verify user has a non-zero stake
1679+ if prediction. amount <= 0 {
1680+ Self :: exit_reentrancy_guard ( & env) ;
1681+ return Err ( PredifiError :: InsufficientBalance ) ;
1682+ }
1683+
1684+ // --- EFFECTS ---
1685+
1686+ // Mark as claimed immediately to prevent re-entrancy (INV-3)
1687+ env. storage ( ) . persistent ( ) . set ( & claimed_key, & true ) ;
1688+ Self :: extend_persistent ( & env, & claimed_key) ;
1689+
1690+ let refund_amount = prediction. amount ;
1691+
1692+ // --- INTERACTIONS ---
1693+
1694+ let token_client = token:: Client :: new ( & env, & pool. token ) ;
1695+ token_client. transfer ( & env. current_contract_address ( ) , & user, & refund_amount) ;
1696+
1697+ Self :: exit_reentrancy_guard ( & env) ;
1698+
1699+ RefundClaimedEvent {
1700+ pool_id,
1701+ user : user. clone ( ) ,
1702+ amount : refund_amount,
1703+ }
1704+ . publish ( & env) ;
1705+
1706+ Ok ( refund_amount)
1707+ }
1708+
16031709 /// Update the stake limits for an active pool. Caller must have Operator role (1).
16041710 /// PRE: pool.state = Active, operator has role 1
16051711 /// POST: pool.min_stake and pool.max_stake updated
0 commit comments