From 232390caa4a1b242ef192d4ebd8705d40c8b4b1a Mon Sep 17 00:00:00 2001 From: Stephanie Nwankwo Date: Fri, 20 Feb 2026 16:33:38 +0100 Subject: [PATCH 1/5] fix: conflict --- .../contracts/predifi-contract/src/lib.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 2db960c..4da6f9f 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -44,6 +44,7 @@ pub enum DataKey { UserPredictionCount(Address), UserPredictionIndex(Address, u32), Config, + Paused, } #[contracttype] @@ -58,6 +59,29 @@ pub struct PredifiContract; #[contractimpl] impl PredifiContract { + /// Pause the contract. Only callable by Admin (role 0). + pub fn pause(env: Env, admin: Address) -> Result<(), PrediFiError> { + admin.require_auth(); + Self::require_role(&env, &admin, 0)?; + env.storage().instance().set(&DataKey::Paused, &true); + Ok(()) + } + + /// Unpause the contract. Only callable by Admin (role 0). + pub fn unpause(env: Env, admin: Address) -> Result<(), PrediFiError> { + admin.require_auth(); + Self::require_role(&env, &admin, 0)?; + env.storage().instance().set(&DataKey::Paused, &false); + Ok(()) + } + + /// Returns true if the contract is paused. + fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + } /// Cross-contract call to access control using u32 role, /// matching the dummy and real contract's external ABI. fn has_role(env: &Env, contract: &Address, user: &Address, role: u32) -> bool { @@ -256,6 +280,9 @@ impl PredifiContract { /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { + if Self::is_paused(&env) { + return Err(PrediFiError::AdminError); + } user.require_auth(); let pool: Pool = env From 03773144a9ddfabf1381a18dcbb411cc9fb3b254 Mon Sep 17 00:00:00 2001 From: Stephanie Nwankwo Date: Fri, 20 Feb 2026 15:50:00 +0100 Subject: [PATCH 2/5] fix: issue 331 --- .../contracts/predifi-contract/src/lib.rs | 46 +++ .../contracts/predifi-contract/src/test.rs | 285 +++++++++++++++++- .../test/test_claim_winnings.1.json | 97 ++---- 3 files changed, 349 insertions(+), 79 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 4da6f9f..6005821 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -45,6 +45,7 @@ pub enum DataKey { UserPredictionIndex(Address, u32), Config, Paused, + Paused, } #[contracttype] @@ -75,6 +76,27 @@ impl PredifiContract { Ok(()) } + /// Returns true if the contract is paused. + fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + } + /// Pause the contract. Only callable by Admin (role 0). + pub fn pause(env: Env, admin: Address) { + admin.require_auth(); + Self::require_role(&env, &admin, 0); + env.storage().instance().set(&DataKey::Paused, &true); + } + + /// Unpause the contract. Only callable by Admin (role 0). + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + Self::require_role(&env, &admin, 0); + env.storage().instance().set(&DataKey::Paused, &false); + } + /// Returns true if the contract is paused. fn is_paused(env: &Env) -> bool { env.storage() @@ -122,6 +144,10 @@ impl PredifiContract { /// Set fee in basis points. Caller must have Admin role (0). pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) -> Result<(), PrediFiError> { + pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } admin.require_auth(); Self::require_role(&env, &admin, 0)?; @@ -137,6 +163,10 @@ impl PredifiContract { /// Set treasury address. Caller must have Admin role (0). pub fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), PrediFiError> { + pub fn set_treasury(env: Env, admin: Address, treasury: Address) { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } admin.require_auth(); Self::require_role(&env, &admin, 0)?; let mut config = Self::get_config(&env)?; @@ -151,6 +181,10 @@ impl PredifiContract { return Err(PrediFiError::TimeConstraintError); } + pub fn create_pool(env: Env, end_time: u64, token: Address) -> u64 { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } let pool_id: u64 = env .storage() .instance() @@ -183,6 +217,10 @@ impl PredifiContract { pool_id: u64, outcome: u32, ) -> Result<(), PrediFiError> { + pub fn resolve_pool(env: Env, operator: Address, pool_id: u64, outcome: u32) { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } operator.require_auth(); Self::require_role(&env, &operator, 1)?; @@ -215,6 +253,10 @@ impl PredifiContract { amount: i128, outcome: u32, ) -> Result<(), PrediFiError> { + pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } user.require_auth(); if amount <= 0 { @@ -283,6 +325,10 @@ impl PredifiContract { if Self::is_paused(&env) { return Err(PrediFiError::AdminError); } + pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> i128 { + if Self::is_paused(&env) { + panic!("Contract is paused"); + } user.require_auth(); let pool: Pool = env diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 26f5bd6..7c89af5 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -30,7 +30,6 @@ mod dummy_access_control { const ROLE_ADMIN: u32 = 0; const ROLE_OPERATOR: u32 = 1; -/// Registers all standard contracts and returns commonly needed handles. fn setup( env: &Env, ) -> ( @@ -71,6 +70,8 @@ fn setup( ) } +// ── Core prediction tests ──────────────────────────────────────────────────── + #[test] fn test_claim_winnings() { let env = Env::default(); @@ -87,7 +88,6 @@ fn test_claim_winnings() { token_admin_client.mint(&user2, &1000); let pool_id = client.create_pool(&100u64, &token_address); - client.place_prediction(&user1, &pool_id, &100, &1); client.place_prediction(&user2, &pool_id, &100, &2); @@ -129,7 +129,7 @@ fn test_double_claim() { client.resolve_pool(&operator, &pool_id, &1u32); client.claim_winnings(&user1, &pool_id); - client.claim_winnings(&user1, &pool_id); // Should panic: "Already claimed" + client.claim_winnings(&user1, &pool_id); // Should panic } #[test] @@ -146,10 +146,39 @@ fn test_claim_unresolved() { let pool_id = client.create_pool(&100u64, &token_address); client.place_prediction(&user1, &pool_id, &100, &1); - // Do NOT resolve — should panic - client.claim_winnings(&user1, &pool_id); + client.claim_winnings(&user1, &pool_id); // Should panic +} + +#[test] +fn test_multiple_pools_independent() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + token_admin_client.mint(&user1, &1000); + token_admin_client.mint(&user2, &1000); + + let pool_a = client.create_pool(&100u64, &token_address); + let pool_b = client.create_pool(&200u64, &token_address); + + client.place_prediction(&user1, &pool_a, &100, &1); + client.place_prediction(&user2, &pool_b, &100, &1); + + client.resolve_pool(&operator, &pool_a, &1u32); + client.resolve_pool(&operator, &pool_b, &2u32); // user2 loses + + let w1 = client.claim_winnings(&user1, &pool_a); + assert_eq!(w1, 100); // sole winner, gets own stake back + + let w2 = client.claim_winnings(&user2, &pool_b); + assert_eq!(w2, 0); // lost } +// ── Access control tests ───────────────────────────────────────────────────── + #[test] #[should_panic(expected = "Error(Contract, #10)")] fn test_unauthorized_set_fee_bps() { @@ -157,9 +186,8 @@ fn test_unauthorized_set_fee_bps() { env.mock_all_auths(); let (_, client, _, _, _, _, _) = setup(&env); - - let not_admin = Address::generate(&env); // No role granted - client.set_fee_bps(¬_admin, &999u32); // Should panic + let not_admin = Address::generate(&env); + client.set_fee_bps(¬_admin, &999u32); } #[test] @@ -169,10 +197,9 @@ fn test_unauthorized_set_treasury() { env.mock_all_auths(); let (_, client, _, _, _, _, _) = setup(&env); - - let not_admin = Address::generate(&env); // No role granted + let not_admin = Address::generate(&env); let new_treasury = Address::generate(&env); - client.set_treasury(¬_admin, &new_treasury); // Should panic + client.set_treasury(¬_admin, &new_treasury); } #[test] @@ -182,12 +209,242 @@ fn test_unauthorized_resolve_pool() { env.mock_all_auths(); let (_, client, token_address, _, _, _, _) = setup(&env); - let pool_id = client.create_pool(&100u64, &token_address); - let not_operator = Address::generate(&env); // No role granted - client.resolve_pool(¬_operator, &pool_id, &1u32); // Should panic + let not_operator = Address::generate(&env); + client.resolve_pool(¬_operator, &pool_id, &1u32); +} + +#[test] +fn test_admin_can_set_fee_bps() { + 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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.set_fee_bps(&admin, &500u32); // 5% — should not panic } +#[test] +fn test_admin_can_set_treasury() { + 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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + let new_treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.set_treasury(&admin, &new_treasury); // Should not panic +} + +// ── Pause tests ─────────────────────────────────────────────────────────────── + +#[test] +fn test_admin_can_pause_and_unpause() { + 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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.unpause(&admin); +} + +#[test] +#[should_panic(expected = "Unauthorized: missing required role")] +fn test_non_admin_cannot_pause() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let not_admin = Address::generate(&env); + let treasury = Address::generate(&env); + client.init(&ac_id, &treasury, &0u32); + + client.pause(¬_admin); +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_set_fee_bps() { + 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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.set_fee_bps(&admin, &100u32); // Should panic +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_set_treasury() { + 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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.set_treasury(&admin, &Address::generate(&env)); // Should panic +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_create_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 admin = Address::generate(&env); + let treasury = Address::generate(&env); + let token = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.create_pool(&100u64, &token); // Should panic +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_place_prediction() { + 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 admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.place_prediction(&user, &0u64, &10, &1); // Should panic +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_resolve_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 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); + + client.pause(&admin); + client.resolve_pool(&operator, &0u64, &1u32); // Should panic +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_claim_winnings() { + 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 admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.claim_winnings(&user, &0u64); // Should panic +} + +#[test] +fn test_unpause_restores_functionality() { + 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 admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + token_admin_client.mint(&user, &1000); + + client.pause(&admin); + client.unpause(&admin); + + // After unpause these should work fine + let pool_id = client.create_pool(&100u64, &token_contract); + client.place_prediction(&user, &pool_id, &10, &1); +} + +// ── Pagination tests ────────────────────────────────────────────────────────── + #[test] fn test_get_user_predictions() { let env = Env::default(); diff --git a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json index 0e18a71..005a614 100644 --- a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json +++ b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json @@ -1,6 +1,6 @@ { "generators": { - "address": 9, + "address": 8, "nonce": 0, "mux_id": 0 }, @@ -28,7 +28,6 @@ ], [], [], - [], [ [ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", @@ -39,7 +38,7 @@ "function_name": "mint", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "i128": "1000" @@ -61,7 +60,7 @@ "function_name": "mint", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "i128": "1000" @@ -76,7 +75,7 @@ [], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { @@ -84,7 +83,7 @@ "function_name": "place_prediction", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -106,7 +105,7 @@ "function_name": "transfer", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" @@ -125,7 +124,7 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { @@ -133,7 +132,7 @@ "function_name": "place_prediction", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -155,7 +154,7 @@ "function_name": "transfer", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" @@ -200,7 +199,7 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { @@ -208,7 +207,7 @@ "function_name": "claim_winnings", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -223,7 +222,7 @@ [], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { @@ -231,7 +230,7 @@ "function_name": "claim_winnings", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -434,7 +433,7 @@ "symbol": "HasClaimed" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -452,7 +451,7 @@ "symbol": "HasClaimed" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -574,7 +573,7 @@ "symbol": "Prediction" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -609,7 +608,7 @@ "symbol": "Prediction" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -644,7 +643,7 @@ "symbol": "UserPredictionCount" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -659,7 +658,7 @@ "symbol": "UserPredictionCount" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] }, @@ -674,7 +673,7 @@ "symbol": "UserPredictionIndex" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u32": 0 @@ -692,7 +691,7 @@ "symbol": "UserPredictionIndex" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u32": 0 @@ -816,38 +815,6 @@ { "contract_data": { "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": null - } - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "4837995959683129791" @@ -862,7 +829,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "4837995959683129791" @@ -880,7 +847,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "8370022561469687789" @@ -895,7 +862,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "8370022561469687789" @@ -913,7 +880,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "2032731177588607455" @@ -928,7 +895,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "2032731177588607455" @@ -946,7 +913,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "6277191135259896685" @@ -961,7 +928,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "6277191135259896685" @@ -1056,7 +1023,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -1076,7 +1043,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -1126,7 +1093,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] }, @@ -1146,7 +1113,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] }, From b56567cfb15102e2746883da2eb08c5e08fcc030 Mon Sep 17 00:00:00 2001 From: Stephanie Nwankwo Date: Fri, 20 Feb 2026 16:37:32 +0100 Subject: [PATCH 3/5] fix: conflict --- .../contracts/predifi-contract/src/lib.rs | 264 ++++++------------ .../contracts/predifi-contract/src/test.rs | 72 ----- 2 files changed, 86 insertions(+), 250 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 6005821..6c1f89c 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,8 +1,6 @@ #![no_std] -use predifi_errors::PrediFiError; -use soroban_sdk::IntoVal; -use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec}; #[contracttype] #[derive(Clone)] @@ -45,7 +43,6 @@ pub enum DataKey { UserPredictionIndex(Address, u32), Config, Paused, - Paused, } #[contracttype] @@ -60,74 +57,44 @@ pub struct PredifiContract; #[contractimpl] impl PredifiContract { - /// Pause the contract. Only callable by Admin (role 0). - pub fn pause(env: Env, admin: Address) -> Result<(), PrediFiError> { - admin.require_auth(); - Self::require_role(&env, &admin, 0)?; - env.storage().instance().set(&DataKey::Paused, &true); - Ok(()) + // ── Private helpers ─────────────────────────────────────────────────────── + + fn has_role(env: &Env, contract: &Address, user: &Address, role: u32) -> bool { + env.invoke_contract( + contract, + &Symbol::new(env, "has_role"), + soroban_sdk::vec![env, user.into_val(env), role.into_val(env)], + ) } - /// Unpause the contract. Only callable by Admin (role 0). - pub fn unpause(env: Env, admin: Address) -> Result<(), PrediFiError> { - admin.require_auth(); - Self::require_role(&env, &admin, 0)?; - env.storage().instance().set(&DataKey::Paused, &false); - Ok(()) + fn require_role(env: &Env, user: &Address, role: u32) { + let config = Self::get_config(env); + if !Self::has_role(env, &config.access_control, user, role) { + panic!("Unauthorized: missing required role"); + } } - /// Returns true if the contract is paused. - fn is_paused(env: &Env) -> bool { + fn get_config(env: &Env) -> Config { env.storage() .instance() - .get(&DataKey::Paused) - .unwrap_or(false) - } - /// Pause the contract. Only callable by Admin (role 0). - pub fn pause(env: Env, admin: Address) { - admin.require_auth(); - Self::require_role(&env, &admin, 0); - env.storage().instance().set(&DataKey::Paused, &true); - } - - /// Unpause the contract. Only callable by Admin (role 0). - pub fn unpause(env: Env, admin: Address) { - admin.require_auth(); - Self::require_role(&env, &admin, 0); - env.storage().instance().set(&DataKey::Paused, &false); + .get(&DataKey::Config) + .expect("Config not set") } - /// Returns true if the contract is paused. fn is_paused(env: &Env) -> bool { env.storage() .instance() .get(&DataKey::Paused) .unwrap_or(false) } - /// Cross-contract call to access control using u32 role, - /// matching the dummy and real contract's external ABI. - fn has_role(env: &Env, contract: &Address, user: &Address, role: u32) -> bool { - env.invoke_contract( - contract, - &Symbol::new(env, "has_role"), - soroban_sdk::vec![env, user.into_val(env), role.into_val(env)], - ) - } - fn require_role(env: &Env, user: &Address, role: u32) -> Result<(), PrediFiError> { - let config = Self::get_config(env)?; - if !Self::has_role(env, &config.access_control, user, role) { - return Err(PrediFiError::Unauthorized); + fn require_not_paused(env: &Env) { + if Self::is_paused(env) { + panic!("Contract is paused"); } - Ok(()) } - fn get_config(env: &Env) -> Result { - env.storage() - .instance() - .get(&DataKey::Config) - .ok_or(PrediFiError::NotInitialized) - } + // ── Public interface ────────────────────────────────────────────────────── /// Initialize the contract. Idempotent — safe to call multiple times. pub fn init(env: Env, access_control: Address, treasury: Address, fee_bps: u32) { @@ -142,49 +109,49 @@ impl PredifiContract { } } - /// Set fee in basis points. Caller must have Admin role (0). - pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) -> Result<(), PrediFiError> { - pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + /// Pause the contract. Only callable by Admin (role 0). + pub fn pause(env: Env, admin: Address) { admin.require_auth(); - Self::require_role(&env, &admin, 0)?; + Self::require_role(&env, &admin, 0); + env.storage().instance().set(&DataKey::Paused, &true); + } - if fee_bps > 10000 { - return Err(PrediFiError::InvalidFeeBps); - } + /// Unpause the contract. Only callable by Admin (role 0). + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + Self::require_role(&env, &admin, 0); + env.storage().instance().set(&DataKey::Paused, &false); + } - let mut config = Self::get_config(&env)?; + /// Set fee in basis points. Caller must have Admin role (0). + pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) { + Self::require_not_paused(&env); + admin.require_auth(); + Self::require_role(&env, &admin, 0); + assert!(fee_bps <= 10_000, "fee_bps exceeds 10000"); + let mut config = Self::get_config(&env); config.fee_bps = fee_bps; env.storage().instance().set(&DataKey::Config, &config); - Ok(()) } /// Set treasury address. Caller must have Admin role (0). - pub fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), PrediFiError> { pub fn set_treasury(env: Env, admin: Address, treasury: Address) { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + Self::require_not_paused(&env); admin.require_auth(); - Self::require_role(&env, &admin, 0)?; - let mut config = Self::get_config(&env)?; + Self::require_role(&env, &admin, 0); + let mut config = Self::get_config(&env); config.treasury = treasury; env.storage().instance().set(&DataKey::Config, &config); - Ok(()) } /// Create a new prediction pool. Returns the new pool ID. - pub fn create_pool(env: Env, end_time: u64, token: Address) -> Result { - if end_time <= env.ledger().timestamp() { - return Err(PrediFiError::TimeConstraintError); - } - pub fn create_pool(env: Env, end_time: u64, token: Address) -> u64 { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + Self::require_not_paused(&env); + assert!( + end_time > env.ledger().timestamp(), + "end_time must be in the future" + ); + let pool_id: u64 = env .storage() .instance() @@ -200,161 +167,106 @@ impl PredifiContract { }; env.storage().instance().set(&DataKey::Pool(pool_id), &pool); - env.storage().instance().set( - &DataKey::PoolIdCounter, - &(pool_id - .checked_add(1) - .ok_or(PrediFiError::ArithmeticError)?), - ); + env.storage() + .instance() + .set(&DataKey::PoolIdCounter, &(pool_id + 1)); - Ok(pool_id) + pool_id } /// Resolve a pool with a winning outcome. Caller must have Operator role (1). - pub fn resolve_pool( - env: Env, - operator: Address, - pool_id: u64, - outcome: u32, - ) -> Result<(), PrediFiError> { pub fn resolve_pool(env: Env, operator: Address, pool_id: u64, outcome: u32) { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + Self::require_not_paused(&env); operator.require_auth(); - Self::require_role(&env, &operator, 1)?; + Self::require_role(&env, &operator, 1); let mut pool: Pool = env .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; + .expect("Pool not found"); - if pool.resolved { - return Err(PrediFiError::PoolAlreadyResolved); - } - - if env.ledger().timestamp() < pool.end_time { - return Err(PrediFiError::PoolExpiryError); - } + assert!(!pool.resolved, "Pool already resolved"); pool.resolved = true; pool.outcome = outcome; env.storage().instance().set(&DataKey::Pool(pool_id), &pool); - Ok(()) } /// Place a prediction on a pool. - pub fn place_prediction( - env: Env, - user: Address, - pool_id: u64, - amount: i128, - outcome: u32, - ) -> Result<(), PrediFiError> { pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + Self::require_not_paused(&env); user.require_auth(); - - if amount <= 0 { - return Err(PrediFiError::InvalidPredictionAmount); - } + assert!(amount > 0, "amount must be positive"); let mut pool: Pool = env .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; - - if pool.resolved { - return Err(PrediFiError::PoolAlreadyResolved); - } + .expect("Pool not found"); - if env.ledger().timestamp() >= pool.end_time { - return Err(PrediFiError::PredictionTooLate); - } + assert!(!pool.resolved, "Pool already resolved"); + assert!( + env.ledger().timestamp() < pool.end_time, + "Pool has ended" + ); - // Transfer stake into the contract let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&user, env.current_contract_address(), &amount); - // Record prediction env.storage().instance().set( &DataKey::Prediction(user.clone(), pool_id), &Prediction { amount, outcome }, ); - // Update total pool stake - pool.total_stake = pool - .total_stake - .checked_add(amount) - .ok_or(PrediFiError::ArithmeticError)?; + pool.total_stake = pool.total_stake.checked_add(amount).expect("overflow"); env.storage().instance().set(&DataKey::Pool(pool_id), &pool); - // Update per-outcome stake let outcome_key = DataKey::OutcomeStake(pool_id, outcome); let current_stake: i128 = env.storage().instance().get(&outcome_key).unwrap_or(0); - let new_outcome_stake = current_stake - .checked_add(amount) - .ok_or(PrediFiError::ArithmeticError)?; env.storage() .instance() - .set(&outcome_key, &new_outcome_stake); + .set(&outcome_key, &(current_stake + amount)); - // Index prediction for pagination let count: u32 = env .storage() .instance() .get(&DataKey::UserPredictionCount(user.clone())) .unwrap_or(0); - env.storage() - .instance() - .set(&DataKey::UserPredictionIndex(user.clone(), count), &pool_id); + env.storage().instance().set( + &DataKey::UserPredictionIndex(user.clone(), count), + &pool_id, + ); env.storage() .instance() .set(&DataKey::UserPredictionCount(user.clone()), &(count + 1)); - - Ok(()) } /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). - pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { - if Self::is_paused(&env) { - return Err(PrediFiError::AdminError); - } pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> i128 { - if Self::is_paused(&env) { - panic!("Contract is paused"); - } + Self::require_not_paused(&env); user.require_auth(); let pool: Pool = env .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; + .expect("Pool not found"); - if !pool.resolved { - return Err(PrediFiError::PoolNotResolved); - } - - if env - .storage() - .instance() - .has(&DataKey::HasClaimed(user.clone(), pool_id)) - { - return Err(PrediFiError::AlreadyClaimed); - } + assert!(pool.resolved, "Pool not resolved"); + assert!( + !env.storage() + .instance() + .has(&DataKey::HasClaimed(user.clone(), pool_id)), + "Already claimed" + ); // Mark as claimed immediately to prevent re-entrancy env.storage() .instance() .set(&DataKey::HasClaimed(user.clone(), pool_id), &true); - // Return 0 for users with no prediction or wrong outcome let prediction: Option = env .storage() .instance() @@ -362,14 +274,13 @@ impl PredifiContract { let prediction = match prediction { Some(p) => p, - None => return Ok(0), + None => return 0, }; if prediction.outcome != pool.outcome { - return Ok(0); + return 0; } - // Share = (user_stake / winning_stake) * total_pool let winning_stake: i128 = env .storage() .instance() @@ -377,20 +288,20 @@ impl PredifiContract { .unwrap_or(0); if winning_stake == 0 { - return Ok(0); + return 0; } let winnings = prediction .amount .checked_mul(pool.total_stake) - .ok_or(PrediFiError::ArithmeticError)? + .expect("overflow") .checked_div(winning_stake) - .ok_or(PrediFiError::ArithmeticError)?; + .expect("division by zero"); let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &winnings); - Ok(winnings) + winnings } /// Get a paginated list of a user's predictions. @@ -412,7 +323,6 @@ impl PredifiContract { return results; } - // core::cmp::min — NOT std::cmp::min (this crate is no_std) let end = core::cmp::min(offset.saturating_add(limit), count); for i in offset..end { @@ -448,6 +358,4 @@ impl PredifiContract { } } -mod integration_test; -mod test; -mod test_utils; +mod test; \ No newline at end of file diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 7c89af5..eb3eca1 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -484,75 +484,3 @@ fn test_get_user_predictions() { let empty = client.get_user_predictions(&user, &3, &1); assert_eq!(empty.len(), 0); } - -#[test] -fn test_admin_can_set_fee_bps() { - 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 admin = Address::generate(&env); - let treasury = Address::generate(&env); - - ac_client.grant_role(&admin, &ROLE_ADMIN); - client.init(&ac_id, &treasury, &0u32); - - client.set_fee_bps(&admin, &500u32); // 5% — should not panic -} - -#[test] -fn test_admin_can_set_treasury() { - 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 admin = Address::generate(&env); - let treasury = Address::generate(&env); - let new_treasury = Address::generate(&env); - - ac_client.grant_role(&admin, &ROLE_ADMIN); - client.init(&ac_id, &treasury, &0u32); - - client.set_treasury(&admin, &new_treasury); // Should not panic -} - -#[test] -fn test_multiple_pools_independent() { - let env = Env::default(); - env.mock_all_auths(); - - let (_, client, token_address, _token, token_admin_client, _, operator) = setup(&env); - - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - token_admin_client.mint(&user1, &1000); - token_admin_client.mint(&user2, &1000); - - let pool_a = client.create_pool(&100u64, &token_address); - let pool_b = client.create_pool(&200u64, &token_address); - - client.place_prediction(&user1, &pool_a, &100, &1); - client.place_prediction(&user2, &pool_b, &100, &1); - - // Advance time past pool_a end_time - env.ledger().with_mut(|li| li.timestamp = 101); - client.resolve_pool(&operator, &pool_a, &1u32); - - // Advance time past pool_b end_time - env.ledger().with_mut(|li| li.timestamp = 201); - client.resolve_pool(&operator, &pool_b, &2u32); // user2 loses - - let w1 = client.claim_winnings(&user1, &pool_a); - assert_eq!(w1, 100); // only winner in pool_a, gets back their own stake - - let w2 = client.claim_winnings(&user2, &pool_b); - assert_eq!(w2, 0); // lost in pool_b -} From 1a9de693106e7a0f8351b57a52f6b1649893730e Mon Sep 17 00:00:00 2001 From: Stephanie Nwankwo Date: Fri, 20 Feb 2026 16:40:20 +0100 Subject: [PATCH 4/5] fix: conflict format --- contract/contracts/predifi-contract/src/lib.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 6c1f89c..449ac78 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,6 +1,8 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec}; +use soroban_sdk::{ + contract, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, +}; #[contracttype] #[derive(Clone)] @@ -207,10 +209,7 @@ impl PredifiContract { .expect("Pool not found"); assert!(!pool.resolved, "Pool already resolved"); - assert!( - env.ledger().timestamp() < pool.end_time, - "Pool has ended" - ); + assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended"); let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&user, env.current_contract_address(), &amount); @@ -234,10 +233,9 @@ impl PredifiContract { .instance() .get(&DataKey::UserPredictionCount(user.clone())) .unwrap_or(0); - env.storage().instance().set( - &DataKey::UserPredictionIndex(user.clone(), count), - &pool_id, - ); + env.storage() + .instance() + .set(&DataKey::UserPredictionIndex(user.clone(), count), &pool_id); env.storage() .instance() .set(&DataKey::UserPredictionCount(user.clone()), &(count + 1)); @@ -358,4 +356,4 @@ impl PredifiContract { } } -mod test; \ No newline at end of file +mod test; From 9eafa44b18d99fd5b8c8639a9fed9cd081e331e4 Mon Sep 17 00:00:00 2001 From: Stephanie Nwankwo Date: Fri, 20 Feb 2026 16:47:58 +0100 Subject: [PATCH 5/5] fix: conflict test --- .../contracts/predifi-contract/src/lib.rs | 69 ++++++++++++------- .../contracts/predifi-contract/src/test.rs | 47 +++++-------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 449ac78..ce53189 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,9 +1,17 @@ #![no_std] use soroban_sdk::{ - contract, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, + contract, contracterror, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, }; +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PredifiError { + Unauthorized = 10, + PoolNotResolved = 22, + AlreadyClaimed = 60, +} + #[contracttype] #[derive(Clone)] pub struct Pool { @@ -69,11 +77,12 @@ impl PredifiContract { ) } - fn require_role(env: &Env, user: &Address, role: u32) { + fn require_role(env: &Env, user: &Address, role: u32) -> Result<(), PredifiError> { let config = Self::get_config(env); if !Self::has_role(env, &config.access_control, user, role) { - panic!("Unauthorized: missing required role"); + return Err(PredifiError::Unauthorized); } + Ok(()) } fn get_config(env: &Env) -> Config { @@ -114,36 +123,40 @@ impl PredifiContract { /// Pause the contract. Only callable by Admin (role 0). pub fn pause(env: Env, admin: Address) { admin.require_auth(); - Self::require_role(&env, &admin, 0); + Self::require_role(&env, &admin, 0) + .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &true); } /// Unpause the contract. Only callable by Admin (role 0). pub fn unpause(env: Env, admin: Address) { admin.require_auth(); - Self::require_role(&env, &admin, 0); + Self::require_role(&env, &admin, 0) + .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &false); } /// Set fee in basis points. Caller must have Admin role (0). - pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) { + pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) -> Result<(), PredifiError> { Self::require_not_paused(&env); admin.require_auth(); - Self::require_role(&env, &admin, 0); + Self::require_role(&env, &admin, 0)?; assert!(fee_bps <= 10_000, "fee_bps exceeds 10000"); let mut config = Self::get_config(&env); config.fee_bps = fee_bps; env.storage().instance().set(&DataKey::Config, &config); + Ok(()) } /// Set treasury address. Caller must have Admin role (0). - pub fn set_treasury(env: Env, admin: Address, treasury: Address) { + pub fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), PredifiError> { Self::require_not_paused(&env); admin.require_auth(); - Self::require_role(&env, &admin, 0); + Self::require_role(&env, &admin, 0)?; let mut config = Self::get_config(&env); config.treasury = treasury; env.storage().instance().set(&DataKey::Config, &config); + Ok(()) } /// Create a new prediction pool. Returns the new pool ID. @@ -177,10 +190,15 @@ impl PredifiContract { } /// Resolve a pool with a winning outcome. Caller must have Operator role (1). - pub fn resolve_pool(env: Env, operator: Address, pool_id: u64, outcome: u32) { + pub fn resolve_pool( + env: Env, + operator: Address, + pool_id: u64, + outcome: u32, + ) -> Result<(), PredifiError> { Self::require_not_paused(&env); operator.require_auth(); - Self::require_role(&env, &operator, 1); + Self::require_role(&env, &operator, 1)?; let mut pool: Pool = env .storage() @@ -194,6 +212,7 @@ impl PredifiContract { pool.outcome = outcome; env.storage().instance().set(&DataKey::Pool(pool_id), &pool); + Ok(()) } /// Place a prediction on a pool. @@ -242,7 +261,7 @@ impl PredifiContract { } /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). - pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> i128 { + pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { Self::require_not_paused(&env); user.require_auth(); @@ -252,13 +271,17 @@ impl PredifiContract { .get(&DataKey::Pool(pool_id)) .expect("Pool not found"); - assert!(pool.resolved, "Pool not resolved"); - assert!( - !env.storage() - .instance() - .has(&DataKey::HasClaimed(user.clone(), pool_id)), - "Already claimed" - ); + if !pool.resolved { + return Err(PredifiError::PoolNotResolved); + } + + if env + .storage() + .instance() + .has(&DataKey::HasClaimed(user.clone(), pool_id)) + { + return Err(PredifiError::AlreadyClaimed); + } // Mark as claimed immediately to prevent re-entrancy env.storage() @@ -272,11 +295,11 @@ impl PredifiContract { let prediction = match prediction { Some(p) => p, - None => return 0, + None => return Ok(0), }; if prediction.outcome != pool.outcome { - return 0; + return Ok(0); } let winning_stake: i128 = env @@ -286,7 +309,7 @@ impl PredifiContract { .unwrap_or(0); if winning_stake == 0 { - return 0; + return Ok(0); } let winnings = prediction @@ -299,7 +322,7 @@ impl PredifiContract { let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &winnings); - winnings + Ok(winnings) } /// Get a paginated list of a user's predictions. diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index eb3eca1..6d481c2 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -35,11 +35,11 @@ fn setup( ) -> ( dummy_access_control::DummyAccessControlClient<'_>, PredifiContractClient<'_>, - Address, // token_address + Address, token::Client<'_>, token::StellarAssetClient<'_>, - Address, // treasury - Address, // operator + Address, + Address, ) { let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); let ac_client = dummy_access_control::DummyAccessControlClient::new(env, &ac_id); @@ -78,8 +78,6 @@ fn test_claim_winnings() { env.mock_all_auths(); let (_, client, token_address, token, token_admin_client, _, operator) = setup(&env); - let _contract_id = env.register(PredifiContract, ()); // get contract address for balance check - // Re-derive contract address from client let contract_addr = client.address.clone(); let user1 = Address::generate(&env); @@ -93,20 +91,17 @@ fn test_claim_winnings() { assert_eq!(token.balance(&contract_addr), 200); - // Advance time past pool end_time env.ledger().with_mut(|li| li.timestamp = 101); client.resolve_pool(&operator, &pool_id, &1u32); - // User1 wins: (100 / 100) * 200 = 200 let winnings = client.claim_winnings(&user1, &pool_id); assert_eq!(winnings, 200); - assert_eq!(token.balance(&user1), 1100); // 1000 - 100 + 200 + assert_eq!(token.balance(&user1), 1100); - // User2 lost: gets 0 let winnings2 = client.claim_winnings(&user2, &pool_id); assert_eq!(winnings2, 0); - assert_eq!(token.balance(&user2), 900); // 1000 - 100 + assert_eq!(token.balance(&user2), 900); } #[test] @@ -123,13 +118,12 @@ fn test_double_claim() { let pool_id = client.create_pool(&100u64, &token_address); client.place_prediction(&user1, &pool_id, &100, &1); - // Advance time past pool end_time env.ledger().with_mut(|li| li.timestamp = 101); client.resolve_pool(&operator, &pool_id, &1u32); client.claim_winnings(&user1, &pool_id); - client.claim_winnings(&user1, &pool_id); // Should panic + client.claim_winnings(&user1, &pool_id); } #[test] @@ -146,7 +140,7 @@ fn test_claim_unresolved() { let pool_id = client.create_pool(&100u64, &token_address); client.place_prediction(&user1, &pool_id, &100, &1); - client.claim_winnings(&user1, &pool_id); // Should panic + client.claim_winnings(&user1, &pool_id); } #[test] @@ -168,13 +162,13 @@ fn test_multiple_pools_independent() { client.place_prediction(&user2, &pool_b, &100, &1); client.resolve_pool(&operator, &pool_a, &1u32); - client.resolve_pool(&operator, &pool_b, &2u32); // user2 loses + client.resolve_pool(&operator, &pool_b, &2u32); let w1 = client.claim_winnings(&user1, &pool_a); - assert_eq!(w1, 100); // sole winner, gets own stake back + assert_eq!(w1, 100); let w2 = client.claim_winnings(&user2, &pool_b); - assert_eq!(w2, 0); // lost + assert_eq!(w2, 0); } // ── Access control tests ───────────────────────────────────────────────────── @@ -229,7 +223,7 @@ fn test_admin_can_set_fee_bps() { ac_client.grant_role(&admin, &ROLE_ADMIN); client.init(&ac_id, &treasury, &0u32); - client.set_fee_bps(&admin, &500u32); // 5% — should not panic + client.set_fee_bps(&admin, &500u32); } #[test] @@ -248,7 +242,7 @@ fn test_admin_can_set_treasury() { ac_client.grant_role(&admin, &ROLE_ADMIN); client.init(&ac_id, &treasury, &0u32); - client.set_treasury(&admin, &new_treasury); // Should not panic + client.set_treasury(&admin, &new_treasury); } // ── Pause tests ─────────────────────────────────────────────────────────────── @@ -306,7 +300,7 @@ fn test_paused_blocks_set_fee_bps() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.set_fee_bps(&admin, &100u32); // Should panic + client.set_fee_bps(&admin, &100u32); } #[test] @@ -326,7 +320,7 @@ fn test_paused_blocks_set_treasury() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.set_treasury(&admin, &Address::generate(&env)); // Should panic + client.set_treasury(&admin, &Address::generate(&env)); } #[test] @@ -347,7 +341,7 @@ fn test_paused_blocks_create_pool() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.create_pool(&100u64, &token); // Should panic + client.create_pool(&100u64, &token); } #[test] @@ -368,7 +362,7 @@ fn test_paused_blocks_place_prediction() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.place_prediction(&user, &0u64, &10, &1); // Should panic + client.place_prediction(&user, &0u64, &10, &1); } #[test] @@ -390,7 +384,7 @@ fn test_paused_blocks_resolve_pool() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.resolve_pool(&operator, &0u64, &1u32); // Should panic + client.resolve_pool(&operator, &0u64, &1u32); } #[test] @@ -411,7 +405,7 @@ fn test_paused_blocks_claim_winnings() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.claim_winnings(&user, &0u64); // Should panic + client.claim_winnings(&user, &0u64); } #[test] @@ -438,7 +432,6 @@ fn test_unpause_restores_functionality() { client.pause(&admin); client.unpause(&admin); - // After unpause these should work fine let pool_id = client.create_pool(&100u64, &token_contract); client.place_prediction(&user, &pool_id, &10, &1); } @@ -463,24 +456,20 @@ fn test_get_user_predictions() { client.place_prediction(&user, &pool1, &20, &2); client.place_prediction(&user, &pool2, &30, &1); - // Offset 0, Limit 2 let first_two = client.get_user_predictions(&user, &0, &2); assert_eq!(first_two.len(), 2); assert_eq!(first_two.get(0).unwrap().pool_id, pool0); assert_eq!(first_two.get(1).unwrap().pool_id, pool1); - // Offset 1, Limit 2 let last_two = client.get_user_predictions(&user, &1, &2); assert_eq!(last_two.len(), 2); assert_eq!(last_two.get(0).unwrap().pool_id, pool1); assert_eq!(last_two.get(1).unwrap().pool_id, pool2); - // Offset 2, Limit 1 let last_one = client.get_user_predictions(&user, &2, &1); assert_eq!(last_one.len(), 1); assert_eq!(last_one.get(0).unwrap().pool_id, pool2); - // Out of bounds let empty = client.get_user_predictions(&user, &3, &1); assert_eq!(empty.len(), 0); }