Skip to content

Commit 14888e4

Browse files
committed
Merge branch 'main' of https://github.com/0xVida/predifi
2 parents 2eb8212 + 4c092b3 commit 14888e4

File tree

2 files changed

+175
-2
lines changed

2 files changed

+175
-2
lines changed

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

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
471483
pub 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

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ mod dummy_access_control {
2828
}
2929
}
3030

31-
const ROLE_ADMIN: u32 = 0;
32-
const ROLE_OPERATOR: u32 = 1;
31+
const ROLE_ADMIN: u32 = 0; // i am testing this
32+
const ROLE_OPERATOR: u32 = 1; // i am testing this the second one
3333
const ROLE_ORACLE: u32 = 3;
3434

3535
fn setup(

0 commit comments

Comments
 (0)