Skip to content

Commit 43a264f

Browse files
authored
Merge pull request #381 from 0xVida/main
2 parents ea6957b + 14888e4 commit 43a264f

File tree

5 files changed

+323
-78
lines changed

5 files changed

+323
-78
lines changed

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ fn test_full_market_lifecycle() {
9999
);
100100

101101
// 2. Place Predictions
102-
client.place_prediction(&user1, &pool_id, &100, &1); // User 1 bets 100 on Outcome 1
103-
client.place_prediction(&user2, &pool_id, &200, &2); // User 2 bets 200 on Outcome 2
104-
client.place_prediction(&user3, &pool_id, &300, &1); // User 3 bets 300 on Outcome 1 (Total Outcome 1 = 400)
102+
client.place_prediction(&user1, &pool_id, &100, &1, &None); // User 1 bets 100 on Outcome 1
103+
client.place_prediction(&user2, &pool_id, &200, &2, &None); // User 2 bets 200 on Outcome 2
104+
client.place_prediction(&user3, &pool_id, &300, &1, &None); // User 3 bets 300 on Outcome 1 (Total Outcome 1 = 400)
105105

106106
// Total stake = 100 + 200 + 300 = 600
107107
assert_eq!(token_ctx.token.balance(&client.address), 600);
@@ -181,11 +181,11 @@ fn test_multi_user_betting_and_balance_verification() {
181181
// U4: 500 on 1
182182
// Total 1: 1500, Total 2: 1000, Total 3: 1500. Total Stake: 4000.
183183

184-
client.place_prediction(&users.get(0).unwrap(), &pool_id, &500, &1);
185-
client.place_prediction(&users.get(1).unwrap(), &pool_id, &1000, &2);
186-
client.place_prediction(&users.get(2).unwrap(), &pool_id, &500, &1);
187-
client.place_prediction(&users.get(3).unwrap(), &pool_id, &1500, &3);
188-
client.place_prediction(&users.get(4).unwrap(), &pool_id, &500, &1);
184+
client.place_prediction(&users.get(0).unwrap(), &pool_id, &500, &1, &None);
185+
client.place_prediction(&users.get(1).unwrap(), &pool_id, &1000, &2, &None);
186+
client.place_prediction(&users.get(2).unwrap(), &pool_id, &500, &1, &None);
187+
client.place_prediction(&users.get(3).unwrap(), &pool_id, &1500, &3, &None);
188+
client.place_prediction(&users.get(4).unwrap(), &pool_id, &500, &1, &None);
189189

190190
assert_eq!(token_ctx.token.balance(&client.address), 4000);
191191

@@ -248,9 +248,9 @@ fn test_market_resolution_multiple_winners() {
248248
// U3: 500 on 2
249249
// Total 1: 500, Total 2: 500. Total Stake: 1000.
250250

251-
client.place_prediction(&user1, &pool_id, &200, &1);
252-
client.place_prediction(&user2, &pool_id, &300, &1);
253-
client.place_prediction(&user3, &pool_id, &500, &2);
251+
client.place_prediction(&user1, &pool_id, &200, &1, &None);
252+
client.place_prediction(&user2, &pool_id, &300, &1, &None);
253+
client.place_prediction(&user3, &pool_id, &500, &2, &None);
254254

255255
// Advance time past end_time=3600, then resolve
256256
env.ledger().with_mut(|li| li.timestamp = 3601);

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

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ fn test_high_volume_predictions_single_pool() {
106106

107107
// Split users between outcome 0 and 1
108108
let outcome = i % 2;
109-
client.place_prediction(&user, &pool_id, &stake_per_user, &outcome);
109+
client.place_prediction(&user, &pool_id, &stake_per_user, &outcome, &None);
110110
}
111111

112112
// Use client to get details instead of direct storage access
@@ -140,7 +140,7 @@ fn test_bulk_claim_winnings() {
140140
for _ in 0..num_users {
141141
let user = Address::generate(&env);
142142
token_admin_client.mint(&user, &stake_per_user);
143-
client.place_prediction(&user, &pool_id, &stake_per_user, &0); // All on outcome 0
143+
client.place_prediction(&user, &pool_id, &stake_per_user, &0, &None); // All on outcome 0
144144
users.push(user);
145145
}
146146

@@ -207,7 +207,7 @@ fn test_max_outcomes_high_volume() {
207207
for i in 0..max_options {
208208
let user = Address::generate(&env);
209209
token_admin_client.mint(&user, &1000);
210-
client.place_prediction(&user, &pool_id, &1000, &i);
210+
client.place_prediction(&user, &pool_id, &1000, &i, &None);
211211
}
212212

213213
let pool = client.get_pool(&pool_id);
@@ -239,7 +239,7 @@ fn test_prediction_throughput_measurement() {
239239
for _ in 0..num_predictions {
240240
let user = Address::generate(&env);
241241
token_admin_client.mint(&user, &100);
242-
client.place_prediction(&user, &pool_id, &100, &0);
242+
client.place_prediction(&user, &pool_id, &100, &0, &None);
243243
}
244244

245245
let end_ledger = env.ledger().timestamp();
@@ -277,7 +277,7 @@ fn test_resolution_under_load() {
277277
for &pid in &pool_ids {
278278
let user = Address::generate(&env);
279279
token_admin_client.mint(&user, &1000);
280-
client.place_prediction(&user, &pid, &1000, &0);
280+
client.place_prediction(&user, &pid, &1000, &0, &None);
281281
}
282282

283283
// Advance time

0 commit comments

Comments
 (0)