use super::helpers::*; #[test] #[should_panic(expected = "Cannot crystallize fees before vault is live")] fn test_crystallization_fails_vault_not_live() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // 90 days, 7% hurdle // Configure fees contract.vault_config.performance_fee_bps = 2000; // 20% // Do NOT start vault (live_at = 0) assert_eq!(contract.live_at.0, 0); // Set up state set_total_shares(&mut contract, 1_000_000 * USDC_UNIT); set_share_price(&mut contract, RATE_PRECISION); // Fast forward past crystallization interval fast_forward_time(ONE_DAY_NS * 90); // Try to crystallize - should panic contract.internal_crystallize_performance_fee(); } #[test] #[should_panic(expected = "Crystallize performance fee only works when yield hurdle is configured")] fn test_crystallization_fails_no_hurdle_configured() { // Create vault without hurdle (None) let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, None); // Configure fees contract.vault_config.performance_fee_bps = 2000; // Start vault start_vault(&mut contract); // Set up state set_total_shares(&mut contract, 1_000_000 * USDC_UNIT); set_share_price(&mut contract, RATE_PRECISION); // Try to crystallize without hurdle - should panic contract.internal_crystallize_performance_fee(); } #[test] #[should_panic(expected = "Performance fee crystallization too early")] fn test_crystallization_fails_too_early() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // 90 days // Configure fees contract.vault_config.performance_fee_bps = 2000; // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up state and last crystallization set_total_shares(&mut contract, 1_000_000 * USDC_UNIT); set_share_price(&mut contract, RATE_PRECISION); set_last_crystallization(&mut contract, start_time, RATE_PRECISION); // Fast forward only 30 days (less than 90 day interval) fast_forward_time(ONE_DAY_NS * 30); // Try to crystallize too early - should panic contract.internal_crystallize_performance_fee(); } #[test] #[should_panic(expected = "TVL is zero, cannot calculate performance fee")] fn test_crystallization_fails_zero_tvl() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // Configure fees contract.vault_config.performance_fee_bps = 2000; // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up state with ZERO total shares set_total_shares(&mut contract, 0); set_share_price(&mut contract, RATE_PRECISION); set_last_crystallization(&mut contract, start_time, RATE_PRECISION); // Fast forward past interval fast_forward_time(ONE_DAY_NS * 90); // Try to crystallize with zero TVL - should panic contract.internal_crystallize_performance_fee(); } #[test] fn test_crystallization_no_fees_hurdle_not_met() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // 7% annual hurdle // Configure fees contract.vault_config.performance_fee_bps = 2000; // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up state: 1M USDC TVL set_total_shares(&mut contract, 1_000_000 * USDC_UNIT); // Start at rate 1.0 set_share_price(&mut contract, RATE_PRECISION); set_last_crystallization(&mut contract, start_time, RATE_PRECISION); set_high_water_mark(&mut contract, RATE_PRECISION); // Fast forward 90 days fast_forward_time(ONE_DAY_NS * 90); // Current rate only 1.01 (1% gain over 90 days) // Annualized: ~4% which is below 7% hurdle set_share_price(&mut contract, RATE_PRECISION * 101 / 100); // Initial fees owed let asset = contract.base_asset.clone(); let initial_vault_fees = contract.get_fees_owed_for_asset(&asset).0; let initial_protocol_fees = contract.get_protocol_fees_owed_for_asset(&asset).0; // Crystallize - should update timestamp but not charge fees contract.internal_crystallize_performance_fee(); // Verify no fees were added assert_eq!( contract.get_fees_owed_for_asset(&asset).0, initial_vault_fees, "No vault fees should be added when hurdle not met" ); assert_eq!( contract.get_protocol_fees_owed_for_asset(&asset).0, initial_protocol_fees, "No protocol fees should be added when hurdle not met" ); // Verify crystallization timestamp was updated assert!( contract.state.last_crystallization_timestamp.0 > start_time, "Crystallization timestamp should be updated" ); } #[test] fn test_crystallization_no_fees_hwm_not_exceeded() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // 7% hurdle // Configure fees contract.vault_config.performance_fee_bps = 2000; // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up state set_total_shares(&mut contract, 1_000_000 * USDC_UNIT); // High water mark is ABOVE current rate set_high_water_mark(&mut contract, RATE_PRECISION * 12 / 10); // HWM at 1.2 set_last_crystallization(&mut contract, start_time, RATE_PRECISION); // Fast forward 90 days fast_forward_time(ONE_DAY_NS * 90); // Current rate is 1.10 (meets hurdle when annualized from last rate 1.0) // But still below HWM of 1.2 set_share_price(&mut contract, RATE_PRECISION * 110 / 100); let asset = contract.base_asset.clone(); let initial_vault_fees = contract.get_fees_owed_for_asset(&asset).0; // Crystallize contract.internal_crystallize_performance_fee(); // No fees should be charged (below HWM) assert_eq!( contract.get_fees_owed_for_asset(&asset).0, initial_vault_fees, "No fees when current rate below HWM" ); } #[test] fn test_crystallization_success_fees_calculated() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // 7% hurdle // Configure fees and protocol cut contract.vault_config.performance_fee_bps = 2000; // 20% contract.protocol_config.protocol_performance_fee_cut_bps = 2000; // 20% of fees to protocol // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up state: 1M USDC TVL let total_shares = 1_000_000 * USDC_UNIT; set_total_shares(&mut contract, total_shares); // Start at rate 1.0 set_share_price(&mut contract, RATE_PRECISION); set_last_crystallization(&mut contract, start_time, RATE_PRECISION); set_high_water_mark(&mut contract, RATE_PRECISION); // Fast forward 90 days fast_forward_time(ONE_DAY_NS * 90); // Set current rate to 1.10 (10% gain over 90 days) // Annualized: ~40% which exceeds 7% hurdle let new_rate = RATE_PRECISION * 110 / 100; set_share_price(&mut contract, new_rate); // Initial fees let asset = contract.base_asset.clone(); let initial_vault_fees = contract.get_fees_owed_for_asset(&asset).0; let initial_protocol_fees = contract.get_protocol_fees_owed_for_asset(&asset).0; // Crystallize contract.internal_crystallize_performance_fee(); // Verify fees were added assert!( contract.get_fees_owed_for_asset(&asset).0 > initial_vault_fees, "Vault fees should increase" ); assert!( contract.get_protocol_fees_owed_for_asset(&asset).0 > initial_protocol_fees, "Protocol fees should increase" ); // Verify HWM was updated (rate improved) // Note: HWM updates to adjusted rate after fees, so it might be slightly below new_rate assert!( contract.state.highwatermark_rate.0 > RATE_PRECISION, "HWM should be updated above initial" ); // Verify crystallization info was updated assert!( contract.state.last_crystallization_timestamp.0 > start_time, "Crystallization timestamp should update" ); assert!( contract.state.last_crystallization_share_price.0 > 0, "Crystallization rate should be recorded" ); } #[test] fn test_crystallization_updates_all_share_prices() { let mut contract = setup_vault_with_crystallization(ONE_DAY_NS * 90, Some(700)); // Configure fees contract.vault_config.performance_fee_bps = 2000; // 20% // Start vault let start_time = current_timestamp(); start_vault_at(&mut contract, start_time); // Set up base asset let total_shares = 1_000_000 * USDC_UNIT; set_total_shares(&mut contract, total_shares); let initial_base_rate = RATE_PRECISION; set_share_price(&mut contract, initial_base_rate); set_last_crystallization(&mut contract, start_time, initial_base_rate); set_high_water_mark(&mut contract, initial_base_rate); // Fast forward fast_forward_time(ONE_DAY_NS * 90); // Increase base rate let new_base_rate = RATE_PRECISION * 110 / 100; set_share_price(&mut contract, new_base_rate); // Crystallize contract.internal_crystallize_performance_fee(); // Base rate should have decreased from the new rate (fees taken) let base_rate_after = contract .state .share_prices .get(&contract.base_asset) .unwrap() .0; assert!( base_rate_after < new_base_rate, "Base rate should decrease due to fees" ); // Verify fees were collected let asset = contract.base_asset.clone(); assert!( contract.get_fees_owed_for_asset(&asset).0 > 0, "Vault should have collected fees" ); }