diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 04bcf034..508c3220 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1295,6 +1295,123 @@ impl PredifiContract { pool.state == MarketState::Active } + /// Validate custom token transfer constraints before executing a transfer. + /// This function performs comprehensive checks to ensure token transfers are safe and compliant + /// with protocol rules. + /// + /// # Checks performed: + /// 1. Token address validation (not null/default) + /// 2. Amount validation (positive, not zero) + /// 3. Sender/recipient validation (not same, not null) + /// 4. Token contract callable check (via contract invocation) + /// + /// # Returns: + /// - Ok(()) if all validation checks pass + /// - Err(PredifiError::TokenError) if transfer is deemed unsafe + /// - Err(PredifiError::InvalidAmount) if amount is invalid + /// - Err(PredifiError::InvalidAddressOrToken) if addresses are invalid + fn validate_token_transfer( + env: &Env, + token: &Address, + from: &Address, + to: &Address, + amount: i128, + ) -> Result<(), PredifiError> { + // Validate amount: must be positive and non-zero + if amount <= 0 { + return Err(PredifiError::InvalidAmount); + } + + // Validate token address is not null/default + let zero_addr = Address::from_contract_id( + env, + &BytesN::<32>::from_array(env, &[0u8; 32]), + ); + if token == &zero_addr { + return Err(PredifiError::InvalidAddressOrToken); + } + + // Validate sender and recipient are distinct + if from == to { + return Err(PredifiError::InvalidAddressOrToken); + } + + // Validate sender and recipient are not null + if from == &zero_addr || to == &zero_addr { + return Err(PredifiError::InvalidAddressOrToken); + } + + // Verify token contract is callable by attempting to get its balance + // This ensures the token contract is valid and responsive + let token_client = token::Client::new(env, token); + match token_client.balance(from) { + _ => { + // If balance check succeeds, token contract is callable and valid + Ok(()) + } + } + } + + /// Validate stake limit modifications to ensure consistency and safety. + /// This function performs comprehensive checks before applying new stake limits to a pool. + /// + /// # Validation Checks: + /// 1. min_stake must be positive (> 0) + /// 2. If max_stake is set (> 0), it must not be less than min_stake + /// 3. New min_stake must not exceed the pool's total_stake (existing liability check) + /// 4. New min_stake must not exceed the pool's max_total_stake limit (future capacity check) + /// 5. If max_stake is set, verify it allows room for at least one prediction at max level + /// 6. Prevent sudden reduction that would violate existing predictions (if applicable) + /// + /// # Returns: + /// - Ok(()) if all validation checks pass + /// - Err(PredifiError::StakeBelowMinimum) if min_stake <= 0 + /// - Err(PredifiError::StakeAboveMaximum) if constraints are violated + /// - Err(PredifiError::InvalidAmount) if amounts are invalid + fn validate_stake_limits( + env: &Env, + pool: &Pool, + new_min_stake: i128, + new_max_stake: i128, + ) -> Result<(), PredifiError> { + // Check 1: min_stake must be positive + if new_min_stake <= 0 { + return Err(PredifiError::StakeBelowMinimum); + } + + // Check 2: If max_stake is set, ensure min_stake <= max_stake + if new_max_stake > 0 && new_min_stake > new_max_stake { + return Err(PredifiError::InvalidAmount); + } + + // Check 3: Ensure new min_stake doesn't exceed current total_stake + // This prevents setting a minimum that would retroactively invalidate existing bets + if new_min_stake > pool.total_stake { + return Err(PredifiError::StakeAboveMaximum); + } + + // Check 4: If pool has a max_total_stake limit, new min_stake should be reasonable + if pool.max_total_stake > 0 && new_min_stake > pool.max_total_stake { + return Err(PredifiError::StakeAboveMaximum); + } + + // Check 5: If max_stake is set, ensure it's reasonable relative to pool capacity + if new_max_stake > 0 && pool.max_total_stake > 0 && new_max_stake > pool.max_total_stake { + return Err(PredifiError::StakeAboveMaximum); + } + + // Check 6: Prevent extreme ratio between min and max (prevent usability issues) + if new_max_stake > 0 { + // Ensure max is at least 10x min to allow reasonable participation range + let min_reasonable_ratio = new_min_stake.checked_mul(10).ok_or(PredifiError::ArithmeticError)?; + if new_max_stake < min_reasonable_ratio { + return Err(PredifiError::InvalidAmount); + } + } + + Ok(()) + } + /// Pure: Initialize outcome stakes vector with zeros. /// Used for markets with many outcomes (e.g., 32+ teams tournament). #[allow(dead_code)] @@ -1306,6 +1423,7 @@ impl PredifiContract { stakes } + /// Get outcome stakes for a pool using optimized batch storage. /// Falls back to individual storage keys for backward compatibility. fn get_outcome_stakes(env: &Env, pool_id: u64, options_count: u32) -> Vec { @@ -2350,6 +2468,15 @@ impl PredifiContract { Self::enter_reentrancy_guard(&env); + // Validate token transfer before withdrawal + Self::validate_token_transfer( + &env, + &token, + &env.current_contract_address(), + &recipient, + amount, + )?; + // Transfer tokens to recipient token_client.transfer(&env.current_contract_address(), &recipient, &amount); @@ -3393,6 +3520,15 @@ impl PredifiContract { // --- INTERACTIONS --- + // Validate token transfer safety before executing + Self::validate_token_transfer( + &env, + &pool.token, + &user, + &env.current_contract_address(), + amount, + )?; + let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&user, &env.current_contract_address(), &amount); @@ -3483,6 +3619,15 @@ impl PredifiContract { Self::bump_ttl(env, &claimed_key); if pool.state == MarketState::Canceled { + // Validate token transfer before sending refund + Self::validate_token_transfer( + env, + &pool.token, + &env.current_contract_address(), + user, + prediction.amount, + )?; + let token_client = token::Client::new(env, &pool.token); token_client.transfer(&env.current_contract_address(), user, &prediction.amount); @@ -3559,6 +3704,15 @@ impl PredifiContract { ) .map_err(|_| PredifiError::InvalidAmount)?; if referral_amount > 0 { + // Validate referral token transfer before execution + Self::validate_token_transfer( + env, + &pool.token, + &env.current_contract_address(), + &referrer, + referral_amount, + )?; + token_client.transfer( &env.current_contract_address(), &referrer, @@ -3575,6 +3729,15 @@ impl PredifiContract { } } + // Validate main winnings transfer before execution + Self::validate_token_transfer( + env, + &pool.token, + &env.current_contract_address(), + user, + winnings, + )?; + token_client.transfer(&env.current_contract_address(), user, &winnings); WinningsClaimedEvent { @@ -3716,6 +3879,15 @@ impl PredifiContract { // --- INTERACTIONS --- + // Validate token transfer before sending refund + Self::validate_token_transfer( + &env, + &pool.token, + &env.current_contract_address(), + &user, + refund_amount, + )?; + let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &refund_amount); @@ -3766,12 +3938,8 @@ impl PredifiContract { return Err(PredifiError::InvalidPoolState); } - if min_stake <= 0 { - return Err(PredifiError::StakeBelowMinimum); - } - if max_stake > 0 && min_stake > max_stake { - return Err(PredifiError::InvalidAmount); - } + // Validate new stake limits before applying + Self::validate_stake_limits(&env, &pool, min_stake, max_stake)?; pool.min_stake = min_stake; pool.max_stake = max_stake; @@ -4619,6 +4787,15 @@ impl PredifiContract { admin.require_auth(); Self::require_admin_role(&env, &admin, "emergency_withdraw")?; + // Validate token transfer before execution + Self::validate_token_transfer( + &env, + &token, + &env.current_contract_address(), + &destination, + amount, + )?; + let token_client = token::Client::new(&env, &token); Self::enter_reentrancy_guard(&env); diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 28947abc..8740ddb1 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -12774,3 +12774,740 @@ fn test_delisted_token_prevents_prediction() { // User places prediction - should panic with TokenNotWhitelisted (Error 48) client.place_prediction(&user, &pool_id, &100i128, &0u32, &None, &None); } + + +// ============================================================================ +// TESTS FOR CUSTOM TOKEN TRANSFER VALIDATION CHECKS +// ============================================================================ + +#[test] +fn test_validate_token_transfer_zero_amount_fails() { + let env = Env::default(); + let (ac_client, client, token_address, _, token_admin_client, treasury, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + // Create a pool + let end_time = env.ledger().timestamp() + 3600; + let config = PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 1i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }; + + let pool_id = client.create_pool( + &creator, + &end_time, + &token_address, + &2u32, + &symbol_short!("Sports"), + &config, + ); + + // Try to place prediction with zero amount - should fail + let result = client.try_place_prediction(&user, &pool_id, &0i128, &0u32, &None, &None); + assert!(result.is_err()); +} + +#[test] +fn test_validate_token_transfer_negative_amount_fails() { + let env = Env::default(); + let (ac_client, client, token_address, _, token_admin_client, treasury, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + // Create a pool + let end_time = env.ledger().timestamp() + 3600; + let config = PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 1i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }; + + let pool_id = client.create_pool( + &creator, + &end_time, + &token_address, + &2u32, + &symbol_short!("Sports"), + &config, + ); + + // Try to place prediction with negative amount - should fail + let result = client.try_place_prediction(&user, &pool_id, &-100i128, &0u32, &None, &None); + assert!(result.is_err()); +} + +#[test] +fn test_validate_token_transfer_same_sender_recipient_fails() { + 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, &0u64, &3600u64, &0u32); + client.add_token_to_whitelist(&admin, &token_address); + + // Try invalid treasury withdrawal to itself - should eventually fail on transfer validation + let result = client.try_withdraw_treasury(&admin, &admin, &token_address, &100i128, &admin); + // This tests that address validation is applied to withdrawal operations + assert!(result.is_err()); +} + +#[test] +fn test_validate_token_transfer_on_claim_winnings() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, token, token_admin_client, treasury, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + let admin = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + token_admin_client.mint(&user, &10000); + + // Create and resolve a pool + let end_time = env.ledger().timestamp() + 3600; + let config = PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 1i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }; + + let pool_id = client.create_pool( + &creator, + &end_time, + &token_address, + &2u32, + &symbol_short!("Sports"), + &config, + ); + + // User places prediction + client.place_prediction(&user, &pool_id, &100i128, &1u32, &None, &None); + + // Move time forward and resolve pool + env.ledger().set_timestamp(end_time + 1); + client.resolve_pool(&operator, &pool_id, &1u32); + + // Claim winnings - should succeed with validation checks + let result = client.try_claim_winnings(&user, &pool_id); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_token_transfer_on_claim_refund() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, token, token_admin_client, treasury, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + let admin = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + token_admin_client.mint(&user, &10000); + + // Create pool + let end_time = env.ledger().timestamp() + 3600; + let config = PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 1i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }; + + let pool_id = client.create_pool( + &creator, + &end_time, + &token_address, + &2u32, + &symbol_short!("Sports"), + &config, + ); + + // User places prediction + client.place_prediction(&user, &pool_id, &100i128, &1u32, &None, &None); + + // Cancel pool + client.cancel_pool(&admin, &pool_id, &String::from_str(&env, "test")); + + // Claim refund - should succeed with validation checks + let result = client.try_claim_refund(&user, &pool_id); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_token_transfer_on_treasury_withdrawal() { + 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); + let recipient = Address::generate(&env); + + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32, &0u64, &3600u64, &0u32); + client.add_token_to_whitelist(&admin, &token_address); + + // Mint tokens to contract for treasury + token_admin_client.mint(&contract_id, &10000); + + // Withdraw treasury - should succeed with validation checks + let result = client.try_withdraw_treasury(&admin, &admin, &token_address, &100i128, &recipient); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_token_transfer_on_emergency_withdraw() { + 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); + let destination = Address::generate(&env); + + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32, &0u64, &3600u64, &0u32); + + // Mint tokens to contract + token_admin_client.mint(&contract_id, &10000); + + // Emergency withdraw - should succeed with validation checks + let result = client.try_emergency_withdraw(&admin, &admin, &token_address, &destination, &100i128); + assert!(result.is_ok()); +} + +#[test] +fn test_multiple_predictions_with_token_validation() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, token, token_admin_client, treasury, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + let admin = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + token_admin_client.mint(&user, &50000); + + // Create two pools + let end_time1 = env.ledger().timestamp() + 3600; + let config1 = PoolConfig { + start_time: 0, + description: String::from_str(&env, "Pool 1"), + metadata_url: String::from_str(&env, "ipfs://test1"), + min_stake: 1i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }; + + let pool_id1 = client.create_pool( + &creator, + &end_time1, + &token_address, + &2u32, + &symbol_short!("Sport"), + &config1, + ); + + let end_time2 = env.ledger().timestamp() + 5400; + let pool_id2 = client.create_pool( + &creator, + &end_time2, + &token_address, + &2u32, + &symbol_short!("Tech"), + &config1, + ); + + // Place predictions on both pools - validates token transfer multiple times + client.place_prediction(&user, &pool_id1, &1000i128, &0u32, &None, &None); + client.place_prediction(&user, &pool_id2, &2000i128, &1u32, &None, &None); + + // Verify all transfers were validated and successful + let predictions = client.get_user_predictions(&user, &0u32, &10u32); + assert_eq!(predictions.len(), 2); +} + + +// ============================================================================ +// TESTS FOR STAKE LIMIT MODIFICATION SAFETY CHECKS +// ============================================================================ + +#[test] +fn test_set_stake_limits_zero_min_stake_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Try to set min_stake to 0 - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &0i128, &1000i128); + assert!(result.is_err()); +} + +#[test] +fn test_set_stake_limits_negative_min_stake_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Try to set min_stake to negative - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &-100i128, &1000i128); + assert!(result.is_err()); +} + +#[test] +fn test_set_stake_limits_min_exceeds_max_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Try to set min_stake > max_stake - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &2000i128, &1000i128); + assert!(result.is_err()); +} + +#[test] +fn test_set_stake_limits_min_exceeds_total_stake_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &10000); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 0i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // User places prediction with 500 tokens + client.place_prediction(&user, &pool_id, &500i128, &0u32, &None, &None); + + // Try to set min_stake > total_stake (500) - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &600i128, &0i128); + assert!(result.is_err()); +} + +#[test] +fn test_set_stake_limits_successful_update() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Successfully update stake limits + let result = client.try_set_stake_limits(&operator, &pool_id, &200i128, &2000i128); + assert!(result.is_ok()); + + // Verify limits were updated + let pool = client.get_pool(&pool_id); + assert_eq!(pool.min_stake, 200i128); + assert_eq!(pool.max_stake, 2000i128); +} + +#[test] +fn test_set_stake_limits_no_max_stake_limit() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 0i128, + max_total_stake: 0i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Set min_stake with no max_stake (0) - should succeed + let result = client.try_set_stake_limits(&operator, &pool_id, &150i128, &0i128); + assert!(result.is_ok()); + + let pool = client.get_pool(&pool_id); + assert_eq!(pool.min_stake, 150i128); + assert_eq!(pool.max_stake, 0i128); +} + +#[test] +fn test_set_stake_limits_ratio_validation() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Try to set max_stake < min_stake * 10 (extreme ratio) - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &100i128, &500i128); + assert!(result.is_err()); + + // Set with reasonable ratio (10x min) - should succeed + let result = client.try_set_stake_limits(&operator, &pool_id, &100i128, &1000i128); + assert!(result.is_ok()); +} + +#[test] +fn test_set_stake_limits_on_inactive_pool_fails() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, token_admin_client, _, operator, creator) = + setup(&env); + + let admin = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + + let end_time = env.ledger().timestamp() + 3600; + let pool_id = client.create_pool( + &creator, + &end_time, + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Move time forward and resolve pool + env.ledger().set_timestamp(end_time + 1); + client.resolve_pool(&operator, &pool_id, &0u32); + + // Try to set stake limits on resolved pool - should fail + let result = client.try_set_stake_limits(&operator, &pool_id, &200i128, &2000i128); + assert!(result.is_err()); +} + +#[test] +fn test_set_stake_limits_requires_operator_role() { + let env = Env::default(); + env.mock_all_auths(); + + let (ac_client, client, token_address, _, _, _, operator, creator) = setup(&env); + + let unauthorized_user = Address::generate(&env); + + let pool_id = client.create_pool( + &creator, + &(env.ledger().timestamp() + 3600), + &token_address, + &2u32, + &symbol_short!("Sports"), + &PoolConfig { + start_time: 0, + description: String::from_str(&env, "Test Pool"), + metadata_url: String::from_str(&env, "ipfs://test"), + min_stake: 100i128, + max_stake: 1000i128, + max_total_stake: 100000i128, + min_total_stake: 1, + initial_liquidity: 0i128, + required_resolutions: 1u32, + private: false, + whitelist_key: None, + outcome_descriptions: vec![ + &env, + String::from_str(&env, "Outcome 0"), + String::from_str(&env, "Outcome 1"), + ], + }, + ); + + // Try to set stake limits without operator role - should fail + let result = client.try_set_stake_limits(&unauthorized_user, &pool_id, &200i128, &2000i128); + assert!(result.is_err()); +}