From 6c126b2bd2fff16ed0d03a58f580ca42742decf1 Mon Sep 17 00:00:00 2001 From: Asher <141028690+No-bodyq@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:25:22 +0100 Subject: [PATCH 1/4] feat: implement pool cancellation feature with admin controls and related events --- .../contracts/predifi-contract/src/lib.rs | 70 ++++- .../contracts/predifi-contract/src/test.rs | 266 ++++++++++++++++++ 2 files changed, 335 insertions(+), 1 deletion(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 1ca0286..826c771 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -15,6 +15,7 @@ pub enum PredifiError { Unauthorized = 10, PoolNotResolved = 22, AlreadyClaimed = 60, + PoolCanceled = 70, } #[contracttype] @@ -22,6 +23,7 @@ pub enum PredifiError { pub struct Pool { pub end_time: u64, pub resolved: bool, + pub canceled: bool, pub outcome: u32, pub token: Address, pub total_stake: i128, @@ -125,6 +127,14 @@ pub struct PoolResolvedEvent { pub outcome: u32, } +#[contractevent(topics = ["pool_canceled"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolCanceledEvent { + pub pool_id: u64, + pub caller: Address, + pub reason: String, +} + #[contractevent(topics = ["prediction_placed"])] #[derive(Clone, Debug, Eq, PartialEq)] pub struct PredictionPlacedEvent { @@ -311,6 +321,7 @@ impl PredifiContract { let pool = Pool { end_time, resolved: false, + canceled: false, outcome: 0, token: token.clone(), total_stake: 0, @@ -339,6 +350,7 @@ impl PredifiContract { } /// Resolve a pool with a winning outcome. Caller must have Operator role (1). + /// Cannot resolve a canceled pool. pub fn resolve_pool( env: Env, operator: Address, @@ -357,6 +369,7 @@ impl PredifiContract { .expect("Pool not found"); assert!(!pool.resolved, "Pool already resolved"); + assert!(!pool.canceled, "Cannot resolve a canceled pool"); pool.resolved = true; pool.outcome = outcome; @@ -373,7 +386,61 @@ impl PredifiContract { Ok(()) } - /// Place a prediction on a pool. + /// Cancel a pool, freezing all betting and enabling refund process. + /// Only callable by Admin (role 0) - can cancel any pool for any reason. + /// + /// # Arguments + /// * `caller` - The address requesting the cancellation (must be admin). + /// * `pool_id` - The ID of the pool to cancel. + /// * `reason` - A short description of why the pool is being canceled. + /// + /// # Errors + /// - `Unauthorized` if caller is not admin. + /// - `PoolNotResolved` error (code 22) is returned if trying to cancel an already resolved pool. + pub fn cancel_pool( + env: Env, + caller: Address, + pool_id: u64, + reason: String, + ) -> Result<(), PredifiError> { + Self::require_not_paused(&env); + caller.require_auth(); + + // Check authorization: caller must be admin (role 0) + Self::require_role(&env, &caller, 0)?; + + let pool_key = DataKey::Pool(pool_id); + let mut pool: Pool = env + .storage() + .persistent() + .get(&pool_key) + .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); + + // Ensure resolved pools cannot be canceled + if pool.resolved { + return Err(PredifiError::PoolNotResolved); + } + + // Prevent double cancellation + assert!(!pool.canceled, "Pool already canceled"); + + // Mark pool as canceled + pool.canceled = true; + env.storage().persistent().set(&pool_key, &pool); + Self::extend_persistent(&env, &pool_key); + + PoolCanceledEvent { + pool_id, + caller, + reason, + } + .publish(&env); + + Ok(()) + } + + /// Place a prediction on a pool. Cannot predict on canceled pools. #[allow(clippy::needless_borrows_for_generic_args)] pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { Self::require_not_paused(&env); @@ -388,6 +455,7 @@ impl PredifiContract { .expect("Pool not found"); assert!(!pool.resolved, "Pool already resolved"); + assert!(!pool.canceled, "Cannot place prediction on canceled pool"); assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended"); let token_client = token::Client::new(&env, &pool.token); diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 20511a4..7b4eea7 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -561,3 +561,269 @@ fn test_get_user_predictions() { let empty = client.get_user_predictions(&user, &3, &1); assert_eq!(empty.len(), 0); } +// ── Pool cancellation tests ─────────────────────────────────────────────────── + +#[test] +fn test_admin_can_cancel_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Admin should be able to cancel + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); +} + +#[test] +fn test_pool_creator_can_cancel_unresolved_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let creator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&creator, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Admin should be able to cancel their pool + client.cancel_pool( + &creator, + &pool_id, + &String::from_str(&env, "Setup error occurred"), + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn test_non_admin_non_creator_cannot_cancel() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, _) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + let unauthorized = Address::generate(&env); + // This should fail - user is not admin + client.cancel_pool( + &unauthorized, + &pool_id, + &String::from_str(&env, "Unauthorized cancellation"), + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #22)")] +fn test_cannot_cancel_resolved_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&operator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Resolve the pool first + env.ledger().with_mut(|li| li.timestamp = 101); + client.resolve_pool(&operator, &pool_id, &1u32); + + // Now try to cancel - should fail + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Too late")); +} + +#[test] +#[should_panic(expected = "Cannot place prediction on canceled pool")] +fn test_cannot_place_prediction_on_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + // Create and cancel pool + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Cancel the pool + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Voided")); + + // Try to place prediction on canceled pool - should panic + client.place_prediction(&user, &pool_id, &100, &1); +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn test_pool_creator_cannot_cancel_after_admin_cancels() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // Admin cancels the pool + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + + // Attempt to cancel again should fail (already canceled) + let non_admin = Address::generate(&env); + client.cancel_pool( + &non_admin, + &pool_id, + &String::from_str(&env, "Double cancel"), + ); +} + +#[test] +#[should_panic(expected = "Cannot place prediction on canceled pool")] +fn test_admin_can_cancel_pool_with_predictions() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + // User places a prediction + client.place_prediction(&user, &pool_id, &100, &1); + + // Admin cancels the pool - this freezes betting + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + + // Verify no more predictions can be placed - should panic + client.place_prediction(&user, &pool_id, &50, &2); +} From 5916576ce2b481d7f50bf21757acd96a79cfa620 Mon Sep 17 00:00:00 2001 From: Asher <141028690+No-bodyq@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:50:53 +0100 Subject: [PATCH 2/4] feat: update pool resolution logic and enhance cancelation tests --- .../contracts/predifi-contract/src/lib.rs | 29 +--- .../contracts/predifi-contract/src/test.rs | 135 +++++++++--------- 2 files changed, 72 insertions(+), 92 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 2b09fcd..77c50fe 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -533,6 +533,7 @@ impl PredifiContract { } pool.state = MarketState::Resolved; + pool.resolved = true; pool.outcome = outcome; env.storage().persistent().set(&pool_key, &pool); @@ -563,31 +564,6 @@ impl PredifiContract { } /// Cancel an active pool. Caller must have Operator role (1). - pub fn cancel_pool(env: Env, operator: Address, pool_id: u64) -> Result<(), PredifiError> { - Self::require_not_paused(&env); - operator.require_auth(); - Self::require_role(&env, &operator, 1)?; - - let pool_key = DataKey::Pool(pool_id); - let mut pool: Pool = env - .storage() - .persistent() - .get(&pool_key) - .expect("Pool not found"); - - if pool.state != MarketState::Active { - return Err(PredifiError::InvalidPoolState); - } - - pool.state = MarketState::Canceled; - - env.storage().persistent().set(&pool_key, &pool); - Self::extend_persistent(&env, &pool_key); - - PoolCanceledEvent { pool_id, operator }.publish(&env); - Ok(()) - } - /// Cancel a pool, freezing all betting and enabling refund process. /// Only callable by Admin (role 0) - can cancel any pool for any reason. /// @@ -634,8 +610,9 @@ impl PredifiContract { PoolCanceledEvent { pool_id, - caller, + caller: caller.clone(), reason, + operator: caller, } .publish(&env); diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 6532945..37a9607 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -615,19 +615,6 @@ fn test_pool_creator_can_cancel_unresolved_pool() { ac_client.grant_role(&creator, &ROLE_ADMIN); client.init(&ac_id, &treasury, &0u32); -// ── Pool Cancelation & State Guard Tests ──────────────────────────────────────── - -#[test] -fn test_cancel_pool_refunds_predictions() { - let env = Env::default(); - env.mock_all_auths(); - - let (_, client, token_address, token, token_admin_client, _, operator) = setup(&env); - let contract_addr = client.address.clone(); - - let user1 = Address::generate(&env); - token_admin_client.mint(&user1, &1000); - let pool_id = client.create_pool( &100u64, &token_address, @@ -675,25 +662,6 @@ fn test_non_admin_non_creator_cannot_cancel() { #[test] #[should_panic(expected = "Error(Contract, #22)")] - &String::from_str(&env, "Cancel Test Pool"), - &String::from_str(&env, "ipfs://metadata"), - ); - client.place_prediction(&user1, &pool_id, &100, &1); - - assert_eq!(token.balance(&contract_addr), 100); - - // Cancel pool before end time - client.cancel_pool(&operator, &pool_id); - - // Claim refunds - let refund = client.claim_winnings(&user1, &pool_id); - assert_eq!(refund, 100); - assert_eq!(token.balance(&user1), 1000); - assert_eq!(token.balance(&contract_addr), 0); -} - -#[test] -#[should_panic(expected = "Error(Contract, #24)")] fn test_cannot_cancel_resolved_pool() { let env = Env::default(); env.mock_all_auths(); @@ -713,7 +681,6 @@ fn test_cannot_cancel_resolved_pool() { ac_client.grant_role(&admin, &ROLE_ADMIN); ac_client.grant_role(&operator, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); - let (_, client, token_address, _, _, _, operator) = setup(&env); let pool_id = client.create_pool( &100u64, @@ -840,43 +807,49 @@ fn test_admin_can_cancel_pool_with_predictions() { let user = Address::generate(&env); token_admin_client.mint(&user, &1000); - &String::from_str(&env, "Resolve Then Cancel Pool"), - &String::from_str(&env, "ipfs://metadata"), - ); - - client.resolve_pool(&operator, &pool_id, &1u32); - // Should panic because pool is not active - client.cancel_pool(&operator, &pool_id); -} - -#[test] -#[should_panic(expected = "Error(Contract, #24)")] -fn test_cannot_resolve_canceled_pool() { - let env = Env::default(); - env.mock_all_auths(); - - let (_, client, token_address, _, _, _, operator) = setup(&env); let pool_id = client.create_pool( &100u64, &token_address, - &String::from_str(&env, "Resolve Canceled Pool Test"), - &String::from_str(&env, "ipfs://metadata"), + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), ); - client.cancel_pool(&operator, &pool_id); - // Should panic because pool is not active - client.resolve_pool(&operator, &pool_id, &1u32); + // User places a prediction + client.place_prediction(&user, &pool_id, &100, &1); + + // Admin cancels the pool - this freezes betting + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + + // Verify no more predictions can be placed - should panic + client.place_prediction(&user, &pool_id, &50, &2); } #[test] -#[should_panic(expected = "Pool is not active")] -fn test_cannot_predict_on_canceled_pool() { +fn test_cancel_pool_refunds_predictions() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + let token_address = token_contract; + + let admin = Address::generate(&env); let user1 = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + let contract_addr = client.address.clone(); token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( @@ -890,18 +863,48 @@ fn test_cannot_predict_on_canceled_pool() { ); // User places a prediction - client.place_prediction(&user, &pool_id, &100, &1); + client.place_prediction(&user1, &pool_id, &100, &1); + assert_eq!(token_admin_client.balance(&contract_addr), 100); + assert_eq!(token_admin_client.balance(&user1), 900); - // Admin cancels the pool - this freezes betting - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + // Admin cancels the pool - this should enable refund of predictions + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Voided")); - // Verify no more predictions can be placed - should panic - client.place_prediction(&user, &pool_id, &50, &2); - &String::from_str(&env, "Predict Canceled Pool Test"), + // Verify predictions are refunded (get_user_predictions should show the prediction still exists for potential refund claim) + let predictions = client.get_user_predictions(&user1, &0u32, &10u32); + assert_eq!(predictions.len(), 1); +} + +#[test] +#[should_panic(expected = "Cannot resolve a canceled pool")] +fn test_cannot_resolve_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_address = token_contract; + + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&operator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), &String::from_str(&env, "ipfs://metadata"), ); - client.cancel_pool(&operator, &pool_id); - // Should panic - client.place_prediction(&user1, &pool_id, &100, &1); + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Test")); + // Should panic because pool is not active (canceled) + client.resolve_pool(&operator, &pool_id, &1u32); } From ba3119fa6f384ef57cf68b7c0b8e3850adb2b0f5 Mon Sep 17 00:00:00 2001 From: Asher <141028690+No-bodyq@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:07:09 +0100 Subject: [PATCH 3/4] feat: update cancel_pool function to require operator role and simplify parameters --- .../contracts/predifi-contract/src/lib.rs | 21 +-- .../contracts/predifi-contract/src/test.rs | 151 ++++++++---------- 2 files changed, 74 insertions(+), 98 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index d9eec7d..1122b5e 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -672,20 +672,14 @@ impl PredifiContract { /// # Errors /// - `Unauthorized` if caller is not admin. /// - `PoolNotResolved` error (code 22) is returned if trying to cancel an already resolved pool. - pub fn cancel_pool( - env: Env, - caller: Address, - pool_id: u64, - reason: String, - ) -> Result<(), PredifiError> { /// PRE: pool.state = Active, operator has role 1 /// POST: pool.state = Canceled, state transition valid (INV-2) pub fn cancel_pool(env: Env, operator: Address, pool_id: u64) -> Result<(), PredifiError> { Self::require_not_paused(&env); - caller.require_auth(); + operator.require_auth(); - // Check authorization: caller must be admin (role 0) - Self::require_role(&env, &caller, 0)?; + // Check authorization: operator must have role 1 + Self::require_role(&env, &operator, 1)?; let pool_key = DataKey::Pool(pool_id); let mut pool: Pool = env @@ -717,9 +711,9 @@ impl PredifiContract { PoolCanceledEvent { pool_id, - caller: caller.clone(), - reason, - operator: caller, + caller: operator.clone(), + reason: String::from_str(&env, ""), + operator, } .publish(&env); @@ -727,9 +721,6 @@ impl PredifiContract { } /// Place a prediction on a pool. Cannot predict on canceled pools. - /// Place a prediction on a pool. - /// PRE: amount > 0 (INV-7), pool.state = Active, current_time < pool.end_time - /// POST: pool.total_stake increases by amount, OutcomeStake increases by amount (INV-1) #[allow(clippy::needless_borrows_for_generic_args)] pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { Self::require_not_paused(&env); diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 2c3b8cd..e217cbc 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -86,23 +86,23 @@ fn test_claim_winnings() { token_admin_client.mint(&user2, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), ); - client.place_prediction(&user1, &pool_id, &100, &0); - client.place_prediction(&user2, &pool_id, &100, &1); + client.place_prediction(&user1, &pool_id, &100, &1); + client.place_prediction(&user2, &pool_id, &100, &2); assert_eq!(token.balance(&contract_addr), 200); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 101); - client.resolve_pool(&operator, &pool_id, &0u32); + client.resolve_pool(&operator, &pool_id, &1u32); let winnings = client.claim_winnings(&user1, &pool_id); assert_eq!(winnings, 200); @@ -125,9 +125,9 @@ fn test_double_claim() { token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -156,9 +156,9 @@ fn test_claim_unresolved() { token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -183,9 +183,9 @@ fn test_multiple_pools_independent() { token_admin_client.mint(&user2, &1000); let pool_a = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -193,9 +193,9 @@ fn test_multiple_pools_independent() { ), ); let pool_b = client.create_pool( - &20000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -204,16 +204,16 @@ fn test_multiple_pools_independent() { ); client.place_prediction(&user1, &pool_a, &100, &1); - client.place_prediction(&user2, &pool_b, &100, &0); + client.place_prediction(&user2, &pool_b, &100, &1); client.resolve_pool(&operator, &pool_a, &1u32); - client.resolve_pool(&operator, &pool_b, &0u32); + client.resolve_pool(&operator, &pool_b, &2u32); let w1 = client.claim_winnings(&user1, &pool_a); assert_eq!(w1, 100); let w2 = client.claim_winnings(&user2, &pool_b); - assert_eq!(w2, 100); + assert_eq!(w2, 0); } // ── Access control tests ───────────────────────────────────────────────────── @@ -249,9 +249,9 @@ fn test_unauthorized_resolve_pool() { let (_, client, token_address, _, _, _, _) = setup(&env); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -396,9 +396,9 @@ fn test_paused_blocks_create_pool() { client.pause(&admin); client.create_pool( - &10000u64, + &100000u64, &token, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -496,9 +496,9 @@ fn test_unpause_restores_functionality() { client.unpause(&admin); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_contract, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -521,9 +521,9 @@ fn test_get_user_predictions() { token_admin_client.mint(&user, &1000); let pool0 = client.create_pool( - &10000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -531,9 +531,9 @@ fn test_get_user_predictions() { ), ); let pool1 = client.create_pool( - &20000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -541,9 +541,9 @@ fn test_get_user_predictions() { ), ); let pool2 = client.create_pool( - &30000u64, + &100000u64, &token_address, - &2u32, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -551,9 +551,9 @@ fn test_get_user_predictions() { ), ); - client.place_prediction(&user, &pool0, &10, &0); - client.place_prediction(&user, &pool1, &20, &1); - client.place_prediction(&user, &pool2, &30, &0); + client.place_prediction(&user, &pool0, &10, &1); + client.place_prediction(&user, &pool1, &20, &2); + client.place_prediction(&user, &pool2, &30, &1); let first_two = client.get_user_predictions(&user, &0, &2); assert_eq!(first_two.len(), 2); @@ -590,12 +590,13 @@ fn test_admin_can_cancel_pool() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let pool_id = client.create_pool( - &100u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -604,7 +605,7 @@ fn test_admin_can_cancel_pool() { ); // Admin should be able to cancel - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + client.cancel_pool(&admin, &pool_id); } #[test] @@ -623,28 +624,22 @@ fn test_pool_creator_can_cancel_unresolved_pool() { let creator = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&creator, &ROLE_ADMIN); + ac_client.grant_role(&creator, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), - &2u32, - &String::from_str(&env, "Cancel Test Pool"), - &String::from_str(&env, "ipfs://metadata"), ); // Admin should be able to cancel their pool - client.cancel_pool( - &creator, - &pool_id, - &String::from_str(&env, "Setup error occurred"), - ); + client.cancel_pool(&creator, &pool_id); } #[test] @@ -656,8 +651,9 @@ fn test_non_admin_non_creator_cannot_cancel() { let (_, client, token_address, _, _, _, _) = setup(&env); let pool_id = client.create_pool( - &100u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -667,11 +663,7 @@ fn test_non_admin_non_creator_cannot_cancel() { let unauthorized = Address::generate(&env); // This should fail - user is not admin - client.cancel_pool( - &unauthorized, - &pool_id, - &String::from_str(&env, "Unauthorized cancellation"), - ); + client.cancel_pool(&unauthorized, &pool_id); } #[test] @@ -692,21 +684,19 @@ fn test_cannot_cancel_resolved_pool() { let admin = Address::generate(&env); let operator = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); ac_client.grant_role(&operator, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), - &2u32, - &String::from_str(&env, "Resolve Then Cancel Pool"), - &String::from_str(&env, "ipfs://metadata"), ); // Resolve the pool first @@ -714,7 +704,7 @@ fn test_cannot_cancel_resolved_pool() { client.resolve_pool(&operator, &pool_id, &1u32); // Now try to cancel - should fail - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Too late")); + client.cancel_pool(&admin, &pool_id); } #[test] @@ -735,7 +725,7 @@ fn test_cannot_place_prediction_on_canceled_pool() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let user = Address::generate(&env); @@ -743,20 +733,18 @@ fn test_cannot_place_prediction_on_canceled_pool() { // Create and cancel pool let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), - &2u32, - &String::from_str(&env, "Resolve Canceled Pool Test"), - &String::from_str(&env, "ipfs://metadata"), ); // Cancel the pool - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Voided")); + client.cancel_pool(&admin, &pool_id); // Try to place prediction on canceled pool - should panic client.place_prediction(&user, &pool_id, &100, &1); @@ -779,12 +767,13 @@ fn test_pool_creator_cannot_cancel_after_admin_cancels() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let pool_id = client.create_pool( - &100u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -793,15 +782,11 @@ fn test_pool_creator_cannot_cancel_after_admin_cancels() { ); // Admin cancels the pool - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + client.cancel_pool(&admin, &pool_id); // Attempt to cancel again should fail (already canceled) let non_admin = Address::generate(&env); - client.cancel_pool( - &non_admin, - &pool_id, - &String::from_str(&env, "Double cancel"), - ); + client.cancel_pool(&non_admin, &pool_id); } #[test] @@ -822,15 +807,16 @@ fn test_admin_can_cancel_pool_with_predictions() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let user = Address::generate(&env); token_admin_client.mint(&user, &1000); let pool_id = client.create_pool( - &100u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, @@ -842,7 +828,7 @@ fn test_admin_can_cancel_pool_with_predictions() { client.place_prediction(&user, &pool_id, &100, &1); // Admin cancels the pool - this freezes betting - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Event voided")); + client.cancel_pool(&admin, &pool_id); // Verify no more predictions can be placed - should panic client.place_prediction(&user, &pool_id, &50, &2); @@ -866,23 +852,21 @@ fn test_cancel_pool_refunds_predictions() { let admin = Address::generate(&env); let user1 = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let contract_addr = client.address.clone(); token_admin_client.mint(&user1, &1000); let pool_id = client.create_pool( - &10000u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str( &env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", ), - &2u32, - &String::from_str(&env, "Predict Canceled Pool Test"), - &String::from_str(&env, "ipfs://metadata"), ); // User places a prediction @@ -891,7 +875,7 @@ fn test_cancel_pool_refunds_predictions() { assert_eq!(token_admin_client.balance(&user1), 900); // Admin cancels the pool - this should enable refund of predictions - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Voided")); + client.cancel_pool(&admin, &pool_id); // Verify predictions are refunded (get_user_predictions should show the prediction still exists for potential refund claim) let predictions = client.get_user_predictions(&user1, &0u32, &10u32); @@ -916,18 +900,19 @@ fn test_cannot_resolve_canceled_pool() { let admin = Address::generate(&env); let operator = Address::generate(&env); let treasury = Address::generate(&env); - ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&admin, &ROLE_OPERATOR); ac_client.grant_role(&operator, &ROLE_OPERATOR); client.init(&ac_id, &treasury, &0u32); let pool_id = client.create_pool( - &100u64, + &100000u64, &token_address, + &3u32, &String::from_str(&env, "Test Pool"), &String::from_str(&env, "ipfs://metadata"), ); - client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "Test")); + client.cancel_pool(&admin, &pool_id); // Should panic because pool is not active (canceled) client.resolve_pool(&operator, &pool_id, &1u32); } From e7ec0e39c911967236f2b73f981415c0423cffa6 Mon Sep 17 00:00:00 2001 From: Asher <141028690+No-bodyq@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:15:30 +0100 Subject: [PATCH 4/4] feat: update timestamp values and modify init parameters in test cases --- .../contracts/predifi-contract/src/test.rs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 1e45de0..effd84f 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -100,7 +100,7 @@ fn test_claim_winnings() { assert_eq!(token.balance(&contract_addr), 200); - env.ledger().with_mut(|li| li.timestamp = 101); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_id, &1u32); @@ -136,7 +136,7 @@ fn test_double_claim() { ); client.place_prediction(&user1, &pool_id, &100, &1); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_id, &1u32); @@ -206,7 +206,7 @@ fn test_multiple_pools_independent() { client.place_prediction(&user1, &pool_a, &100, &1); client.place_prediction(&user2, &pool_b, &100, &1); - env.ledger().with_mut(|li| li.timestamp = 20001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_a, &1u32); client.resolve_pool(&operator, &pool_b, &2u32); @@ -594,7 +594,7 @@ fn test_admin_can_cancel_pool() { let admin = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( &100000u64, @@ -628,7 +628,7 @@ fn test_pool_creator_can_cancel_unresolved_pool() { let creator = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&creator, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( &100000u64, @@ -689,7 +689,7 @@ fn test_cannot_cancel_resolved_pool() { let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); ac_client.grant_role(&operator, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( &100000u64, @@ -702,7 +702,7 @@ fn test_cannot_cancel_resolved_pool() { ), ); - env.ledger().with_mut(|li| li.timestamp = 10001); + env.ledger().with_mut(|li| li.timestamp = 100001); client.resolve_pool(&operator, &pool_id, &1u32); // Now try to cancel - should fail @@ -728,7 +728,7 @@ fn test_cannot_place_prediction_on_canceled_pool() { let admin = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let user = Address::generate(&env); token_admin_client.mint(&user, &1000); @@ -770,7 +770,7 @@ fn test_pool_creator_cannot_cancel_after_admin_cancels() { let admin = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( &100000u64, @@ -789,10 +789,6 @@ fn test_pool_creator_cannot_cancel_after_admin_cancels() { // Attempt to cancel again should fail (already canceled) let non_admin = Address::generate(&env); client.cancel_pool(&non_admin, &pool_id); - client.cancel_pool(&operator, &pool_id); - env.ledger().with_mut(|li| li.timestamp = 10001); - // Should panic because pool is not active - client.resolve_pool(&operator, &pool_id, &1u32); } #[test] @@ -814,7 +810,7 @@ fn test_admin_can_cancel_pool_with_predictions() { let admin = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let user = Address::generate(&env); token_admin_client.mint(&user, &1000); @@ -859,7 +855,7 @@ fn test_cancel_pool_refunds_predictions() { let user1 = Address::generate(&env); let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let contract_addr = client.address.clone(); token_admin_client.mint(&user1, &1000); @@ -908,7 +904,7 @@ fn test_cannot_resolve_canceled_pool() { let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_OPERATOR); ac_client.grant_role(&operator, &ROLE_OPERATOR); - client.init(&ac_id, &treasury, &0u32); + client.init(&ac_id, &treasury, &0u32, &0u64); let pool_id = client.create_pool( &100000u64,