@@ -198,6 +198,12 @@ pub enum DataKey {
198198 /// Token whitelist: TokenWhitelist(token_address) -> true if allowed for betting.
199199 TokenWl ( Address ) ,
200200 PartCnt ( u64 ) ,
201+ /// Referrer for a (user, pool): Referrer(user, pool_id) -> referrer Address.
202+ Referrer ( Address , u64 ) ,
203+ /// Referred volume per (referrer, pool) for payout/analytics: ReferredVolume(referrer, pool_id) -> i128.
204+ ReferredVolume ( Address , u64 ) ,
205+ /// Referral cut in bps (e.g. 5000 = 50% of referrer's fee share). Instance storage, default 5000.
206+ ReferralCutBps ,
201207}
202208
203209#[ contracttype]
@@ -330,6 +336,15 @@ pub struct WinningsClaimedEvent {
330336 pub amount : i128 ,
331337}
332338
339+ #[ contractevent( topics = [ "referral_paid" ] ) ]
340+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
341+ pub struct ReferralPaidEvent {
342+ pub pool_id : u64 ,
343+ pub referrer : Address ,
344+ pub referred_user : Address ,
345+ pub amount : i128 ,
346+ }
347+
333348// ── Monitoring & Alert Events ─────────────────────────────────────────────────
334349// These events are classified by severity and are intended for consumption by
335350// off-chain monitoring tools (Horizon event streaming, Grafana, SIEM, etc.).
@@ -690,6 +705,17 @@ impl PredifiContract {
690705 config
691706 }
692707
708+ /// Referral cut in basis points (e.g. 5000 = 50% of referrer's fee share to referrer). Default 5000.
709+ fn read_referral_cut_bps ( env : & Env ) -> u32 {
710+ let bps = env
711+ . storage ( )
712+ . instance ( )
713+ . get ( & DataKey :: ReferralCutBps )
714+ . unwrap_or ( 5000u32 ) ;
715+ Self :: extend_instance ( env) ;
716+ bps
717+ }
718+
693719 fn is_paused ( env : & Env ) -> bool {
694720 let paused = env
695721 . storage ( )
@@ -871,6 +897,35 @@ impl PredifiContract {
871897 Ok ( ( ) )
872898 }
873899
900+ /// Set referral cut in basis points (e.g. 5000 = 50% of referrer's fee share). Caller must have Admin role (0).
901+ /// Must be ≤ 10_000.
902+ pub fn set_referral_cut_bps (
903+ env : Env ,
904+ admin : Address ,
905+ referral_cut_bps : u32 ,
906+ ) -> Result < ( ) , PredifiError > {
907+ Self :: require_not_paused ( & env) ;
908+ admin. require_auth ( ) ;
909+ if let Err ( e) = Self :: require_role ( & env, & admin, 0 ) {
910+ UnauthorizedAdminAttemptEvent {
911+ caller : admin,
912+ operation : Symbol :: new ( & env, "set_referral_cut_bps" ) ,
913+ timestamp : env. ledger ( ) . timestamp ( ) ,
914+ }
915+ . publish ( & env) ;
916+ return Err ( e) ;
917+ }
918+ assert ! (
919+ referral_cut_bps <= 10_000 ,
920+ "referral_cut_bps must be at most 10000"
921+ ) ;
922+ env. storage ( )
923+ . instance ( )
924+ . set ( & DataKey :: ReferralCutBps , & referral_cut_bps) ;
925+ Self :: extend_instance ( & env) ;
926+ Ok ( ( ) )
927+ }
928+
874929 /// Add a token to the allowed betting whitelist. Caller must have Admin role (0).
875930 pub fn add_token_to_whitelist (
876931 env : Env ,
@@ -962,6 +1017,21 @@ impl PredifiContract {
9621017 Self :: is_token_whitelisted ( & env, & token)
9631018 }
9641019
1020+ /// Get referral cut in basis points (e.g. 5000 = 50% of referrer's fee share).
1021+ pub fn get_referral_cut_bps ( env : Env ) -> u32 {
1022+ Self :: read_referral_cut_bps ( & env)
1023+ }
1024+
1025+ /// Get total referred volume for a (referrer, pool_id) in base token units.
1026+ pub fn get_referred_volume ( env : Env , referrer : Address , pool_id : u64 ) -> i128 {
1027+ let key = DataKey :: ReferredVolume ( referrer, pool_id) ;
1028+ let vol = env. storage ( ) . persistent ( ) . get ( & key) . unwrap_or ( 0 ) ;
1029+ if env. storage ( ) . persistent ( ) . has ( & key) {
1030+ Self :: extend_persistent ( & env, & key) ;
1031+ }
1032+ vol
1033+ }
1034+
9651035 /// Withdraw accumulated protocol fees or unused liquidity from the contract.
9661036 /// Only callable by Admin (role 0).
9671037 ///
@@ -1431,15 +1501,33 @@ impl PredifiContract {
14311501 }
14321502
14331503 /// Place a prediction on a pool. Cannot predict on canceled or resolved pools.
1504+ /// Optional `referrer`: if set, that address will receive a referral cut of the protocol fee
1505+ /// when this user claims winnings. Stored only on first prediction for (user, pool_id).
14341506 /// PRE: amount > 0 (INV-7), pool.state = Active, current_time < pool.end_time
14351507 /// PRE: pool.min_stake <= amount <= pool.max_stake (unless max_stake == 0)
14361508 /// POST: pool.total_stake increases by amount, OutcomeStake increases by amount (INV-1)
14371509 #[ allow( clippy:: needless_borrows_for_generic_args) ]
1438- pub fn place_prediction ( env : Env , user : Address , pool_id : u64 , amount : i128 , outcome : u32 ) {
1510+ pub fn place_prediction (
1511+ env : Env ,
1512+ user : Address ,
1513+ pool_id : u64 ,
1514+ amount : i128 ,
1515+ outcome : u32 ,
1516+ referrer : Option < Address > ,
1517+ ) {
14391518 Self :: require_not_paused ( & env) ;
14401519 user. require_auth ( ) ;
14411520 assert ! ( amount > 0 , "amount must be positive" ) ;
14421521
1522+ // Validate referrer if provided: cannot be self or contract
1523+ if let Some ( ref r) = referrer {
1524+ assert ! ( r != & user, "referrer cannot be self" ) ;
1525+ assert ! (
1526+ r != & env. current_contract_address( ) ,
1527+ "referrer cannot be contract"
1528+ ) ;
1529+ }
1530+
14431531 Self :: enter_reentrancy_guard ( & env) ;
14441532
14451533 let pool_key = DataKey :: Pool ( pool_id) ;
@@ -1483,21 +1571,43 @@ impl PredifiContract {
14831571 }
14841572
14851573 let pred_key = DataKey :: Pred ( user. clone ( ) , pool_id) ;
1486- if let Some ( mut existing_pred) = env. storage ( ) . persistent ( ) . get :: < _ , Prediction > ( & pred_key)
1487- {
1574+ let existing_pred = env. storage ( ) . persistent ( ) . get :: < _ , Prediction > ( & pred_key) ;
1575+ if let Some ( mut existing_pred ) = existing_pred {
14881576 assert ! (
14891577 existing_pred. outcome == outcome,
14901578 "Cannot change prediction outcome"
14911579 ) ;
14921580 existing_pred. amount = existing_pred. amount . checked_add ( amount) . expect ( "overflow" ) ;
14931581 env. storage ( ) . persistent ( ) . set ( & pred_key, & existing_pred) ;
14941582 Self :: extend_persistent ( & env, & pred_key) ;
1583+
1584+ // Track referred volume: if this user already has a referrer, add to their volume
1585+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1586+ if let Some ( referrer_addr) = env. storage ( ) . persistent ( ) . get :: < _ , Address > ( & referrer_key)
1587+ {
1588+ Self :: extend_persistent ( & env, & referrer_key) ;
1589+ let vol_key = DataKey :: ReferredVolume ( referrer_addr. clone ( ) , pool_id) ;
1590+ let vol: i128 = env. storage ( ) . persistent ( ) . get ( & vol_key) . unwrap_or ( 0 ) ;
1591+ env. storage ( ) . persistent ( ) . set ( & vol_key, & ( vol + amount) ) ;
1592+ Self :: extend_persistent ( & env, & vol_key) ;
1593+ }
14951594 } else {
14961595 env. storage ( )
14971596 . persistent ( )
14981597 . set ( & pred_key, & Prediction { amount, outcome } ) ;
14991598 Self :: extend_persistent ( & env, & pred_key) ;
15001599
1600+ // Store referrer on first prediction and track referred volume
1601+ if let Some ( ref referrer_addr) = referrer {
1602+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1603+ env. storage ( ) . persistent ( ) . set ( & referrer_key, referrer_addr) ;
1604+ Self :: extend_persistent ( & env, & referrer_key) ;
1605+ let vol_key = DataKey :: ReferredVolume ( referrer_addr. clone ( ) , pool_id) ;
1606+ let vol: i128 = env. storage ( ) . persistent ( ) . get ( & vol_key) . unwrap_or ( 0 ) ;
1607+ env. storage ( ) . persistent ( ) . set ( & vol_key, & ( vol + amount) ) ;
1608+ Self :: extend_persistent ( & env, & vol_key) ;
1609+ }
1610+
15011611 let pc_key = DataKey :: PartCnt ( pool_id) ;
15021612 let pc: u32 = env. storage ( ) . persistent ( ) . get ( & pc_key) . unwrap_or ( 0 ) ;
15031613 env. storage ( ) . persistent ( ) . set ( & pc_key, & ( pc + 1 ) ) ;
@@ -1653,14 +1763,62 @@ impl PredifiContract {
16531763 return Ok ( 0 ) ;
16541764 }
16551765
1656- // Use pure function for winnings calculation (verifiable)
1657- let winnings = Self :: calculate_winnings ( prediction. amount , winning_stake, pool. total_stake ) ;
1766+ // Protocol fee: deducted from pool before distribution (flat fee_bps, no dependency on 317)
1767+ let config = Self :: get_config ( & env) ;
1768+ let fee_bps_i = config. fee_bps as i128 ;
1769+ let protocol_fee_total =
1770+ SafeMath :: percentage ( pool. total_stake , fee_bps_i, RoundingMode :: ProtocolFavor )
1771+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1772+ let payout_pool = pool
1773+ . total_stake
1774+ . checked_sub ( protocol_fee_total)
1775+ . ok_or ( PredifiError :: InvalidAmount ) ?;
1776+
1777+ // Winnings = user's share of the payout pool (after fee)
1778+ let winnings = Self :: calculate_winnings ( prediction. amount , winning_stake, payout_pool) ;
16581779
16591780 // Verify invariant: winnings ≤ total_stake (INV-4)
16601781 assert ! ( winnings <= pool. total_stake, "Winnings exceed total stake" ) ;
16611782
1662- // --- INTERACTIONS (Winnings Payout) ---
1783+ // --- INTERACTIONS ---
16631784 let token_client = token:: Client :: new ( & env, & pool. token ) ;
1785+
1786+ // Referral: portion of protocol fee attributable to this user goes to referrer
1787+ let referrer_key = DataKey :: Referrer ( user. clone ( ) , pool_id) ;
1788+ if let Some ( referrer) = env. storage ( ) . persistent ( ) . get :: < _ , Address > ( & referrer_key) {
1789+ Self :: extend_persistent ( & env, & referrer_key) ;
1790+ if protocol_fee_total > 0 && pool. total_stake > 0 {
1791+ let protocol_fee_share = SafeMath :: proportion (
1792+ prediction. amount ,
1793+ pool. total_stake ,
1794+ protocol_fee_total,
1795+ RoundingMode :: Neutral ,
1796+ )
1797+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1798+ let referral_cut_bps = Self :: read_referral_cut_bps ( & env) as i128 ;
1799+ let referral_amount = SafeMath :: percentage (
1800+ protocol_fee_share,
1801+ referral_cut_bps,
1802+ RoundingMode :: Neutral ,
1803+ )
1804+ . map_err ( |_| PredifiError :: InvalidAmount ) ?;
1805+ if referral_amount > 0 {
1806+ token_client. transfer (
1807+ & env. current_contract_address ( ) ,
1808+ & referrer,
1809+ & referral_amount,
1810+ ) ;
1811+ ReferralPaidEvent {
1812+ pool_id,
1813+ referrer : referrer. clone ( ) ,
1814+ referred_user : user. clone ( ) ,
1815+ amount : referral_amount,
1816+ }
1817+ . publish ( & env) ;
1818+ }
1819+ }
1820+ }
1821+
16641822 token_client. transfer ( & env. current_contract_address ( ) , & user, & winnings) ;
16651823
16661824 Self :: exit_reentrancy_guard ( & env) ;
0 commit comments