@@ -194,6 +194,12 @@ pub enum DataKey {
194194 /// Token whitelist: TokenWhitelist(token_address) -> true if allowed for betting.
195195 TokenWl ( Address ) ,
196196 PartCnt ( u64 ) ,
197+ /// Referrer for a (user, pool): Referrer(user, pool_id) -> referrer Address.
198+ Referrer ( Address , u64 ) ,
199+ /// Referred volume per (referrer, pool) for payout/analytics: ReferredVolume(referrer, pool_id) -> i128.
200+ ReferredVolume ( Address , u64 ) ,
201+ /// Referral cut in bps (e.g. 5000 = 50% of referrer's fee share). Instance storage, default 5000.
202+ ReferralCutBps ,
197203}
198204
199205#[ contracttype]
@@ -326,6 +332,15 @@ pub struct WinningsClaimedEvent {
326332 pub amount : i128 ,
327333}
328334
335+ #[ contractevent( topics = [ "referral_paid" ] ) ]
336+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
337+ pub struct ReferralPaidEvent {
338+ pub pool_id : u64 ,
339+ pub referrer : Address ,
340+ pub referred_user : Address ,
341+ pub amount : i128 ,
342+ }
343+
329344// ── Monitoring & Alert Events ─────────────────────────────────────────────────
330345// These events are classified by severity and are intended for consumption by
331346// off-chain monitoring tools (Horizon event streaming, Grafana, SIEM, etc.).
@@ -678,6 +693,17 @@ impl PredifiContract {
678693 config
679694 }
680695
696+ /// Referral cut in basis points (e.g. 5000 = 50% of referrer's fee share to referrer). Default 5000.
697+ fn read_referral_cut_bps ( env : & Env ) -> u32 {
698+ let bps = env
699+ . storage ( )
700+ . instance ( )
701+ . get ( & DataKey :: ReferralCutBps )
702+ . unwrap_or ( 5000u32 ) ;
703+ Self :: extend_instance ( env) ;
704+ bps
705+ }
706+
681707 fn is_paused ( env : & Env ) -> bool {
682708 let paused = env
683709 . storage ( )
@@ -859,6 +885,35 @@ impl PredifiContract {
859885 Ok ( ( ) )
860886 }
861887
888+ /// Set referral cut in basis points (e.g. 5000 = 50% of referrer's fee share). Caller must have Admin role (0).
889+ /// Must be ≤ 10_000.
890+ pub fn set_referral_cut_bps (
891+ env : Env ,
892+ admin : Address ,
893+ referral_cut_bps : u32 ,
894+ ) -> Result < ( ) , PredifiError > {
895+ Self :: require_not_paused ( & env) ;
896+ admin. require_auth ( ) ;
897+ if let Err ( e) = Self :: require_role ( & env, & admin, 0 ) {
898+ UnauthorizedAdminAttemptEvent {
899+ caller : admin,
900+ operation : Symbol :: new ( & env, "set_referral_cut_bps" ) ,
901+ timestamp : env. ledger ( ) . timestamp ( ) ,
902+ }
903+ . publish ( & env) ;
904+ return Err ( e) ;
905+ }
906+ assert ! (
907+ referral_cut_bps <= 10_000 ,
908+ "referral_cut_bps must be at most 10000"
909+ ) ;
910+ env. storage ( )
911+ . instance ( )
912+ . set ( & DataKey :: ReferralCutBps , & referral_cut_bps) ;
913+ Self :: extend_instance ( & env) ;
914+ Ok ( ( ) )
915+ }
916+
862917 /// Add a token to the allowed betting whitelist. Caller must have Admin role (0).
863918 pub fn add_token_to_whitelist (
864919 env : Env ,
@@ -950,6 +1005,21 @@ impl PredifiContract {
9501005 Self :: is_token_whitelisted ( & env, & token)
9511006 }
9521007
1008+ /// Get referral cut in basis points (e.g. 5000 = 50% of referrer's fee share).
1009+ pub fn get_referral_cut_bps ( env : Env ) -> u32 {
1010+ Self :: read_referral_cut_bps ( & env)
1011+ }
1012+
1013+ /// Get total referred volume for a (referrer, pool_id) in base token units.
1014+ pub fn get_referred_volume ( env : Env , referrer : Address , pool_id : u64 ) -> i128 {
1015+ let key = DataKey :: ReferredVolume ( referrer, pool_id) ;
1016+ let vol = env. storage ( ) . persistent ( ) . get ( & key) . unwrap_or ( 0 ) ;
1017+ if env. storage ( ) . persistent ( ) . has ( & key) {
1018+ Self :: extend_persistent ( & env, & key) ;
1019+ }
1020+ vol
1021+ }
1022+
9531023 /// Withdraw accumulated protocol fees or unused liquidity from the contract.
9541024 /// Only callable by Admin (role 0).
9551025 ///
@@ -1365,15 +1435,33 @@ impl PredifiContract {
13651435 }
13661436
13671437 /// Place a prediction on a pool. Cannot predict on canceled or resolved pools.
1438+ /// Optional `referrer`: if set, that address will receive a referral cut of the protocol fee
1439+ /// when this user claims winnings. Stored only on first prediction for (user, pool_id).
13681440 /// PRE: amount > 0 (INV-7), pool.state = Active, current_time < pool.end_time
13691441 /// PRE: pool.min_stake <= amount <= pool.max_stake (unless max_stake == 0)
13701442 /// POST: pool.total_stake increases by amount, OutcomeStake increases by amount (INV-1)
13711443 #[ allow( clippy:: needless_borrows_for_generic_args) ]
1372- pub fn place_prediction ( env : Env , user : Address , pool_id : u64 , amount : i128 , outcome : u32 ) {
1444+ pub fn place_prediction (
1445+ env : Env ,
1446+ user : Address ,
1447+ pool_id : u64 ,
1448+ amount : i128 ,
1449+ outcome : u32 ,
1450+ referrer : Option < Address > ,
1451+ ) {
13731452 Self :: require_not_paused ( & env) ;
13741453 user. require_auth ( ) ;
13751454 assert ! ( amount > 0 , "amount must be positive" ) ;
13761455
1456+ // Validate referrer if provided: cannot be self or contract
1457+ if let Some ( ref r) = referrer {
1458+ assert ! ( r != & user, "referrer cannot be self" ) ;
1459+ assert ! (
1460+ r != & env. current_contract_address( ) ,
1461+ "referrer cannot be contract"
1462+ ) ;
1463+ }
1464+
13771465 Self :: enter_reentrancy_guard ( & env) ;
13781466
13791467 let pool_key = DataKey :: Pool ( pool_id) ;
@@ -1408,21 +1496,43 @@ impl PredifiContract {
14081496 }
14091497
14101498 let pred_key = DataKey :: Pred ( user. clone ( ) , pool_id) ;
1411- if let Some ( mut existing_pred) = env. storage ( ) . persistent ( ) . get :: < _ , Prediction > ( & pred_key)
1412- {
1499+ let existing_pred = env. storage ( ) . persistent ( ) . get :: < _ , Prediction > ( & pred_key) ;
1500+ if let Some ( mut existing_pred ) = existing_pred {
14131501 assert ! (
14141502 existing_pred. outcome == outcome,
14151503 "Cannot change prediction outcome"
14161504 ) ;
14171505 existing_pred. amount = existing_pred. amount . checked_add ( amount) . expect ( "overflow" ) ;
14181506 env. storage ( ) . persistent ( ) . set ( & pred_key, & existing_pred) ;
14191507 Self :: extend_persistent ( & env, & pred_key) ;
1508+
1509+ // Track referred volume: if this user already has a referrer, add to their volume
1510+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1511+ if let Some ( referrer_addr) = env. storage ( ) . persistent ( ) . get :: < _ , Address > ( & referrer_key)
1512+ {
1513+ Self :: extend_persistent ( & env, & referrer_key) ;
1514+ let vol_key = DataKey :: ReferredVolume ( referrer_addr. clone ( ) , pool_id) ;
1515+ let vol: i128 = env. storage ( ) . persistent ( ) . get ( & vol_key) . unwrap_or ( 0 ) ;
1516+ env. storage ( ) . persistent ( ) . set ( & vol_key, & ( vol + amount) ) ;
1517+ Self :: extend_persistent ( & env, & vol_key) ;
1518+ }
14201519 } else {
14211520 env. storage ( )
14221521 . persistent ( )
14231522 . set ( & pred_key, & Prediction { amount, outcome } ) ;
14241523 Self :: extend_persistent ( & env, & pred_key) ;
14251524
1525+ // Store referrer on first prediction and track referred volume
1526+ if let Some ( ref referrer_addr) = referrer {
1527+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1528+ env. storage ( ) . persistent ( ) . set ( & referrer_key, referrer_addr) ;
1529+ Self :: extend_persistent ( & env, & referrer_key) ;
1530+ let vol_key = DataKey :: ReferredVolume ( referrer_addr. clone ( ) , pool_id) ;
1531+ let vol: i128 = env. storage ( ) . persistent ( ) . get ( & vol_key) . unwrap_or ( 0 ) ;
1532+ env. storage ( ) . persistent ( ) . set ( & vol_key, & ( vol + amount) ) ;
1533+ Self :: extend_persistent ( & env, & vol_key) ;
1534+ }
1535+
14261536 let pc_key = DataKey :: PartCnt ( pool_id) ;
14271537 let pc: u32 = env. storage ( ) . persistent ( ) . get ( & pc_key) . unwrap_or ( 0 ) ;
14281538 env. storage ( ) . persistent ( ) . set ( & pc_key, & ( pc + 1 ) ) ;
@@ -1578,14 +1688,66 @@ impl PredifiContract {
15781688 return Ok ( 0 ) ;
15791689 }
15801690
1581- // Use pure function for winnings calculation (verifiable)
1582- let winnings = Self :: calculate_winnings ( prediction. amount , winning_stake, pool. total_stake ) ;
1691+ // Protocol fee: deducted from pool before distribution (flat fee_bps, no dependency on 317)
1692+ let config = Self :: get_config ( & env) ;
1693+ let fee_bps_i = config. fee_bps as i128 ;
1694+ let protocol_fee_total = SafeMath :: percentage (
1695+ pool. total_stake ,
1696+ fee_bps_i,
1697+ RoundingMode :: ProtocolFavor ,
1698+ )
1699+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1700+ let payout_pool = pool
1701+ . total_stake
1702+ . checked_sub ( protocol_fee_total)
1703+ . ok_or ( PredifiError :: InvalidAmount ) ?;
1704+
1705+ // Winnings = user's share of the payout pool (after fee)
1706+ let winnings =
1707+ Self :: calculate_winnings ( prediction. amount , winning_stake, payout_pool) ;
15831708
15841709 // Verify invariant: winnings ≤ total_stake (INV-4)
15851710 assert ! ( winnings <= pool. total_stake, "Winnings exceed total stake" ) ;
15861711
1587- // --- INTERACTIONS (Winnings Payout) ---
1712+ // --- INTERACTIONS ---
15881713 let token_client = token:: Client :: new ( & env, & pool. token ) ;
1714+
1715+ // Referral: portion of protocol fee attributable to this user goes to referrer
1716+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1717+ if let Some ( referrer) = env. storage ( ) . persistent ( ) . get :: < _ , Address > ( & referrer_key) {
1718+ Self :: extend_persistent ( & env, & referrer_key) ;
1719+ if protocol_fee_total > 0 && pool. total_stake > 0 {
1720+ let protocol_fee_share = SafeMath :: proportion (
1721+ prediction. amount ,
1722+ pool. total_stake ,
1723+ protocol_fee_total,
1724+ RoundingMode :: Neutral ,
1725+ )
1726+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1727+ let referral_cut_bps = Self :: read_referral_cut_bps ( & env) as i128 ;
1728+ let referral_amount = SafeMath :: percentage (
1729+ protocol_fee_share,
1730+ referral_cut_bps,
1731+ RoundingMode :: Neutral ,
1732+ )
1733+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1734+ if referral_amount > 0 {
1735+ token_client. transfer (
1736+ & env. current_contract_address ( ) ,
1737+ & referrer,
1738+ & referral_amount,
1739+ ) ;
1740+ ReferralPaidEvent {
1741+ pool_id,
1742+ referrer : referrer. clone ( ) ,
1743+ referred_user : user. clone ( ) ,
1744+ amount : referral_amount,
1745+ }
1746+ . publish ( & env) ;
1747+ }
1748+ }
1749+ }
1750+
15891751 token_client. transfer ( & env. current_contract_address ( ) , & user, & winnings) ;
15901752
15911753 Self :: exit_reentrancy_guard ( & env) ;
0 commit comments