Skip to content

Commit 8babfeb

Browse files
CollinsC1OCollins C Augustine
andauthored
refactor: centralize pool active check into is_pool_active helper (#453)
Co-authored-by: Collins C Augustine <[email protected]>
1 parent acbea0f commit 8babfeb

2 files changed

Lines changed: 299 additions & 10 deletions

File tree

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,16 @@ impl PredifiContract {
882882
fee_bps <= 10_000
883883
}
884884

885+
/// Pure: Check if a pool is currently active.
886+
/// A pool is active iff it has not been resolved, not been canceled,
887+
/// and its state is explicitly `MarketState::Active`.
888+
///
889+
/// PRE: pool is a valid Pool instance
890+
/// POST: returns true only when all three conditions hold simultaneously
891+
fn is_pool_active(pool: &Pool) -> bool {
892+
!pool.resolved && !pool.canceled && pool.state == MarketState::Active
893+
}
894+
885895
/// Pure: Initialize outcome stakes vector with zeros
886896
/// Used for markets with many outcomes (e.g., 32+ teams tournament)
887897
#[allow(dead_code)]
@@ -1723,9 +1733,13 @@ impl PredifiContract {
17231733
}
17241734

17251735
// Pool must still be active and not ended
1726-
if pool.state != MarketState::Active || pool.resolved || pool.canceled {
1736+
// if pool.state != MarketState::Active || pool.resolved || pool.canceled {
1737+
// return Err(PredifiError::InvalidPoolState);
1738+
// }
1739+
if !Self::is_pool_active(&pool){
17271740
return Err(PredifiError::InvalidPoolState);
17281741
}
1742+
17291743
assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended");
17301744

17311745
// Must not set a cap below what is already staked
@@ -1772,8 +1786,11 @@ impl PredifiContract {
17721786

17731787
assert!(!pool.resolved, "Pool already resolved");
17741788
assert!(!pool.canceled, "Cannot resolve a canceled pool");
1775-
if pool.state != MarketState::Active {
1776-
return Err(PredifiError::InvalidPoolState);
1789+
// if pool.state != MarketState::Active {
1790+
// return Err(PredifiError::InvalidPoolState);
1791+
// }
1792+
if !Self::is_pool_active(&pool) {
1793+
return Err(PredifiError::InvalidPoolState)
17771794
}
17781795

17791796
let current_time = env.ledger().timestamp();
@@ -1945,14 +1962,17 @@ impl PredifiContract {
19451962
if pool.resolved {
19461963
return Err(PredifiError::PoolNotResolved);
19471964
}
1948-
1965+
19491966
// Prevent double cancellation
19501967
assert!(!pool.canceled, "Pool already canceled");
19511968
// Verify state transition validity (INV-2)
1952-
assert!(
1953-
Self::is_valid_state_transition(pool.state, MarketState::Canceled),
1954-
"Invalid state transition"
1955-
);
1969+
// assert!(
1970+
// Self::is_valid_state_transition(pool.state, MarketState::Canceled),
1971+
// "Invalid state transition"
1972+
// );
1973+
if !Self::is_pool_active(&pool) {
1974+
return Err(PredifiError::InvalidPoolState);
1975+
}
19561976

19571977
pool.state = MarketState::Canceled;
19581978

@@ -2014,7 +2034,10 @@ impl PredifiContract {
20142034

20152035
assert!(!pool.resolved, "Pool already resolved");
20162036
assert!(!pool.canceled, "Cannot place prediction on canceled pool");
2017-
assert!(pool.state == MarketState::Active, "Pool is not active");
2037+
// assert!(pool.state == MarketState::Active, "Pool is not active");
2038+
if !Self::is_pool_active(&pool) {
2039+
panic!("Pool is not active");
2040+
}
20182041
assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended");
20192042

20202043
// Check private pool authorization
@@ -3008,7 +3031,10 @@ impl OracleCallback for PredifiContract {
30083031

30093032
assert!(!pool.resolved, "Pool already resolved");
30103033
assert!(!pool.canceled, "Cannot resolve a canceled pool");
3011-
if pool.state != MarketState::Active {
3034+
// if pool.state != MarketState::Active {
3035+
// return Err(PredifiError::InvalidPoolState);
3036+
// }
3037+
if !Self::is_pool_active(&pool) {
30123038
return Err(PredifiError::InvalidPoolState);
30133039
}
30143040

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

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5145,6 +5145,27 @@ fn test_is_contract_paused_independent_per_instance() {
51455145
assert!(!client_2.is_contract_paused());
51465146
}
51475147

5148+
// ═══════════════════════════════════════════════════════════════════════════
5149+
// is_pool_active Helper Tests
5150+
// ═══════════════════════════════════════════════════════════════════════════
5151+
5152+
/// is_pool_active returns true for a freshly created pool.
5153+
#[test]
5154+
fn test_is_pool_active_returns_true_for_active_pool() {
5155+
let env = Env::default();
5156+
env.mock_all_auths();
5157+
5158+
let (_, client, token_address, _, _, _, _, creator) = setup(&env);
5159+
5160+
let pool_id = client.create_pool(
5161+
&creator,
5162+
&100_000u64,
5163+
&token_address,
5164+
&2u32,
5165+
&symbol_short!("Tech"),
5166+
&PoolConfig {
5167+
description: String::from_str(&env, "Active pool test"),
5168+
metadata_url: String::from_str(&env, "ipfs://active"),
51485169
// ── bump_ttl helper tests ────────────────────────────────────────────────────
51495170

51505171
/// Helper: create an env with predictable ledger settings for TTL assertions.
@@ -5192,6 +5213,248 @@ fn create_test_pool(
51925213
String::from_str(env, "Outcome 1"),
51935214
],
51945215
},
5216+
);
5217+
5218+
let pool = client.get_pool(&pool_id);
5219+
// All three conditions must hold for an active pool.
5220+
assert!(!pool.resolved);
5221+
assert!(!pool.canceled);
5222+
assert_eq!(pool.state, MarketState::Active);
5223+
}
5224+
5225+
/// is_pool_active returns false (via behavior) after pool is resolved —
5226+
/// resolve_pool on an already-resolved pool must panic.
5227+
#[test]
5228+
#[should_panic(expected = "Pool already resolved")]
5229+
fn test_is_pool_active_false_after_resolve() {
5230+
let env = Env::default();
5231+
env.mock_all_auths();
5232+
5233+
let (_, client, token_address, _, _, _, operator, creator) = setup(&env);
5234+
5235+
let pool_id = client.create_pool(
5236+
&creator,
5237+
&100_000u64,
5238+
&token_address,
5239+
&2u32,
5240+
&symbol_short!("Tech"),
5241+
&PoolConfig {
5242+
description: String::from_str(&env, "Resolve inactive test"),
5243+
metadata_url: String::from_str(&env, "ipfs://resolved"),
5244+
min_stake: 1i128,
5245+
max_stake: 0i128,
5246+
initial_liquidity: 0i128,
5247+
required_resolutions: 1u32,
5248+
private: false,
5249+
whitelist_key: None,
5250+
},
5251+
);
5252+
5253+
env.ledger().with_mut(|li| li.timestamp = 100_001);
5254+
client.resolve_pool(&operator, &pool_id, &0u32);
5255+
5256+
// Pool is now resolved — resolved == true, state == Resolved.
5257+
// is_pool_active would return false, so a second resolve attempt must panic.
5258+
client.resolve_pool(&operator, &pool_id, &0u32);
5259+
}
5260+
5261+
/// is_pool_active returns false (via behavior) after pool is canceled —
5262+
/// place_prediction on a canceled pool must panic with the correct message.
5263+
#[test]
5264+
#[should_panic(expected = "Cannot place prediction on canceled pool")]
5265+
fn test_is_pool_active_false_after_cancel() {
5266+
let env = Env::default();
5267+
env.mock_all_auths();
5268+
5269+
let (_, client, token_address, _, token_admin_client, _, operator, creator) = setup(&env);
5270+
5271+
let pool_id = client.create_pool(
5272+
&creator,
5273+
&100_000u64,
5274+
&token_address,
5275+
&2u32,
5276+
&symbol_short!("Tech"),
5277+
&PoolConfig {
5278+
description: String::from_str(&env, "Cancel inactive test"),
5279+
metadata_url: String::from_str(&env, "ipfs://canceled"),
5280+
min_stake: 1i128,
5281+
max_stake: 0i128,
5282+
initial_liquidity: 0i128,
5283+
required_resolutions: 1u32,
5284+
private: false,
5285+
whitelist_key: None,
5286+
},
5287+
);
5288+
5289+
client.cancel_pool(&operator, &pool_id);
5290+
5291+
let user = Address::generate(&env);
5292+
token_admin_client.mint(&user, &500);
5293+
5294+
// Pool is canceled — is_pool_active returns false.
5295+
// place_prediction must be blocked.
5296+
client.place_prediction(&user, &pool_id, &100, &0, &None, &None);
5297+
}
5298+
5299+
/// Resolving a canceled pool must be blocked — verifies is_pool_active
5300+
/// integration in resolve_pool.
5301+
#[test]
5302+
#[should_panic(expected = "Cannot resolve a canceled pool")]
5303+
fn test_is_pool_active_blocks_resolve_on_canceled_pool() {
5304+
let env = Env::default();
5305+
env.mock_all_auths();
5306+
5307+
let (_, client, token_address, _, _, _, operator, creator) = setup(&env);
5308+
5309+
let pool_id = client.create_pool(
5310+
&creator,
5311+
&100_000u64,
5312+
&token_address,
5313+
&2u32,
5314+
&symbol_short!("Tech"),
5315+
&PoolConfig {
5316+
description: String::from_str(&env, "Cancel then resolve test"),
5317+
metadata_url: String::from_str(&env, "ipfs://cancelresolve"),
5318+
min_stake: 1i128,
5319+
max_stake: 0i128,
5320+
initial_liquidity: 0i128,
5321+
required_resolutions: 1u32,
5322+
private: false,
5323+
whitelist_key: None,
5324+
},
5325+
);
5326+
5327+
client.cancel_pool(&operator, &pool_id);
5328+
5329+
env.ledger().with_mut(|li| li.timestamp = 100_001);
5330+
// is_pool_active == false → should panic
5331+
client.resolve_pool(&operator, &pool_id, &0u32);
5332+
}
5333+
5334+
/// Canceling a canceled pool a second time must be blocked — verifies
5335+
/// is_pool_active integration in cancel_pool.
5336+
#[test]
5337+
#[should_panic(expected = "Pool already canceled")]
5338+
fn test_is_pool_active_blocks_double_cancel() {
5339+
let env = Env::default();
5340+
env.mock_all_auths();
5341+
5342+
let (_, client, token_address, _, _, _, operator, creator) = setup(&env);
5343+
5344+
let pool_id = client.create_pool(
5345+
&creator,
5346+
&100_000u64,
5347+
&token_address,
5348+
&2u32,
5349+
&symbol_short!("Tech"),
5350+
&PoolConfig {
5351+
description: String::from_str(&env, "Double cancel test"),
5352+
metadata_url: String::from_str(&env, "ipfs://doublecancel"),
5353+
min_stake: 1i128,
5354+
max_stake: 0i128,
5355+
initial_liquidity: 0i128,
5356+
required_resolutions: 1u32,
5357+
private: false,
5358+
whitelist_key: None,
5359+
},
5360+
);
5361+
5362+
client.cancel_pool(&operator, &pool_id);
5363+
// Second cancel: is_pool_active == false → should panic
5364+
client.cancel_pool(&operator, &pool_id);
5365+
}
5366+
5367+
/// increase_max_total_stake on a resolved pool must return InvalidPoolState —
5368+
/// verifies is_pool_active integration in that function too.
5369+
#[test]
5370+
#[should_panic(expected = "Error(Contract, #24)")]
5371+
fn test_is_pool_active_blocks_increase_max_stake_on_resolved_pool() {
5372+
let env = Env::default();
5373+
env.mock_all_auths();
5374+
5375+
let (_, client, token_address, _, _, _, operator, creator) = setup(&env);
5376+
5377+
let pool_id = client.create_pool(
5378+
&creator,
5379+
&100_000u64,
5380+
&token_address,
5381+
&2u32,
5382+
&symbol_short!("Tech"),
5383+
&PoolConfig {
5384+
description: String::from_str(&env, "Max stake resolved test"),
5385+
metadata_url: String::from_str(&env, "ipfs://maxresolved"),
5386+
min_stake: 1i128,
5387+
max_stake: 0i128,
5388+
initial_liquidity: 0i128,
5389+
required_resolutions: 1u32,
5390+
private: false,
5391+
whitelist_key: None,
5392+
},
5393+
);
5394+
5395+
env.ledger().with_mut(|li| li.timestamp = 100_001);
5396+
client.resolve_pool(&operator, &pool_id, &0u32);
5397+
5398+
// Pool resolved → is_pool_active == false → must return InvalidPoolState (24)
5399+
client.increase_max_total_stake(&creator, &pool_id, &500_000);
5400+
}
5401+
5402+
/// Full lifecycle: active → predictions → resolve → claim.
5403+
/// Confirms is_pool_active correctly gates each phase without regression.
5404+
#[test]
5405+
fn test_is_pool_active_full_lifecycle() {
5406+
let env = Env::default();
5407+
env.mock_all_auths();
5408+
5409+
let (_, client, token_address, token, token_admin_client, _, operator, creator) = setup(&env);
5410+
let contract_addr = client.address.clone();
5411+
5412+
let pool_id = client.create_pool(
5413+
&creator,
5414+
&100_000u64,
5415+
&token_address,
5416+
&2u32,
5417+
&symbol_short!("Tech"),
5418+
&PoolConfig {
5419+
description: String::from_str(&env, "Lifecycle test"),
5420+
metadata_url: String::from_str(&env, "ipfs://lifecycle"),
5421+
min_stake: 1i128,
5422+
max_stake: 0i128,
5423+
initial_liquidity: 0i128,
5424+
required_resolutions: 1u32,
5425+
private: false,
5426+
whitelist_key: None,
5427+
},
5428+
);
5429+
5430+
// Phase 1: pool is active — predictions accepted.
5431+
let pool = client.get_pool(&pool_id);
5432+
assert!(!pool.resolved && !pool.canceled && pool.state == MarketState::Active);
5433+
5434+
let user_win = Address::generate(&env);
5435+
let user_lose = Address::generate(&env);
5436+
token_admin_client.mint(&user_win, &300);
5437+
token_admin_client.mint(&user_lose, &200);
5438+
5439+
client.place_prediction(&user_win, &pool_id, &300, &0, &None, &None);
5440+
client.place_prediction(&user_lose, &pool_id, &200, &1, &None, &None);
5441+
assert_eq!(token.balance(&contract_addr), 500);
5442+
5443+
// Phase 2: resolve — pool transitions to inactive.
5444+
env.ledger().with_mut(|li| li.timestamp = 100_001);
5445+
client.resolve_pool(&operator, &pool_id, &0u32);
5446+
5447+
let pool = client.get_pool(&pool_id);
5448+
assert!(pool.resolved);
5449+
assert_eq!(pool.state, MarketState::Resolved);
5450+
5451+
// Phase 3: claims work correctly post-resolution.
5452+
let w = client.claim_winnings(&user_win, &pool_id);
5453+
assert_eq!(w, 500);
5454+
let l = client.claim_winnings(&user_lose, &pool_id);
5455+
assert_eq!(l, 0);
5456+
assert_eq!(token.balance(&contract_addr), 0);
5457+
}
51955458
)
51965459
}
51975460

0 commit comments

Comments
 (0)