mod helpers; use helpers::*; use near_sdk::json_types::U128; /// Test 3: Performance fee on gains (high water mark) #[tokio::test] async fn test_performance_fee_on_gains() -> TestResult<()> { println!("\nTest: Performance Fee on Gains"); // Deploy vault with 20% performance fee and 8 decimals for share price // Note: Fees are now managed by vault, not kernel let mut vault_config = common_data::vault_config::VaultConfig::default(); vault_config.performance_fee_bps = 2000; // 20% vault_config.management_fee_bps = 0; // simplify vault_config.share_price_decimals = 8; let ctx = setup_full_integration_test_with_vault_config(2, false, 1, vault_config).await?; let vault = ctx.vault(); let ft_contract = ctx.mock_ft_primary.as_ref().expect("FT should exist"); let ft_asset = ctx.accepted_assets.first().unwrap().clone(); // Start the vault (enables fee accrual) owner_start_vault(&vault, &ctx.owner).await?; vault_deposit_ft( ft_contract, &vault, &ctx.users[0], U128(100_000_000), false, U128(1), ) .await?; // Simulate gains by increasing share price to 1.5 * 10^8 let rate1 = U128(SCALE_8); // no gain, declaring shares let rate2 = U128(RATE_1_5); // 50% gain owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate1)]).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; // HWM should be 1.0e8 after initial declaration let hwm_after_1_0: U128 = vault.call("get_highwatermark_rate").view().await?.json()?; assert_eq!( hwm_after_1_0.0, SCALE_8, "HWM should be 1.0e8 after initial declaration" ); owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate2)]).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; // HWM should update to 1.5e8 after gain let hwm_after_1_5: U128 = vault.call("get_highwatermark_rate").view().await?.json()?; assert_eq!( hwm_after_1_5.0, RATE_1_5, "HWM should update to 1.5e8 after gain" ); // Get actual fee-adjusted share price from vault let actual_rate: U128 = vault .call("get_share_price_in_asset") .args_json(serde_json::json!({ "asset": ft_asset })) .view() .await? .json()?; println!( " Rate moved to {}, vault adjusted to {}", rate2.0, actual_rate.0 ); let expected: u128 = 140_000_000; let tolerance: u128 = 10_000; // allow small rounding differences let diff = if actual_rate.0 > expected { actual_rate.0 - expected } else { expected - actual_rate.0 }; assert!( diff <= tolerance, "Vault should apply ~1.4e8 after fees (got {}, diff {} > tol {})", actual_rate.0, diff, tolerance ); // Additional HWM scenarios: drop to 1.2 (no HWM decrease), then rise to 2.0 (HWM increase) let rate3 = U128(rate_8_frac(12, 10)); // 1.2 owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate3)]).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; let hwm_after_1_2: U128 = vault.call("get_highwatermark_rate").view().await?.json()?; assert_eq!( hwm_after_1_2.0, RATE_1_5, "HWM should remain 1.5e8 after drop to 1.2" ); let rate4 = U128(RATE_2_0); // 2.0 owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate4)]).await?; let hwm_after_2_0: U128 = vault.call("get_highwatermark_rate").view().await?.json()?; assert_eq!( hwm_after_2_0.0, RATE_2_0, "HWM should update to 2.0e8 after new high" ); println!("Test passed: Performance fee on gains applies correctly"); Ok(()) } /// Test 5: No performance fee on losses #[tokio::test] async fn test_no_performance_fee_on_losses() -> TestResult<()> { println!("\nTest: No Performance Fee on Losses"); let ctx = setup_full_integration_test(2, false, 1).await?; let vault = ctx.vault(); let ft_contract = ctx.mock_ft_primary.as_ref().expect("FT should exist"); let ft_asset = ctx.accepted_assets.first().unwrap().clone(); // Start at higher rate (direct owner call) let rate1 = U128(2 * SCALE_6); let rates1 = vec![(ft_asset.clone(), rate1)]; owner_update_vault_share_prices(&vault, &ctx.owner, rates1).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; vault_deposit_ft( ft_contract, &vault, &ctx.users[0], U128(SCALE_8), false, U128(1), ) .await?; // Decrease rate (simulate losses) let rate2 = U128(rate_8_frac(15, 10)); // 1.5 with 6 decimals (25% loss) let rates2 = vec![(ft_asset.clone(), rate2)]; owner_update_vault_share_prices(&vault, &ctx.owner, rates2).await?; println!( " Share price decreased from {} to {} (losses)", rate1.0, rate2.0 ); // No performance fee should be charged on losses // High water mark should not decrease println!("Test passed: No performance fee on losses verified"); Ok(()) } /// Test 6: Protocol fee distribution #[tokio::test] async fn test_protocol_fee_distribution() -> TestResult<()> { println!("\nTest: Protocol Fee Distribution"); let ctx = setup_minimal_vault().await?; let vault = ctx.vault(); // Query fee distribution (now tracked in vault, not kernel) let fees_owed: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset) .await .unwrap_or(U128(0)); let protocol_fees_owed: U128 = vault_get_protocol_fees_owed_for_asset(&vault, &ctx.base_asset) .await .unwrap_or(U128(0)); println!(" Vault creator fees owed: {}", fees_owed.0); println!(" Protocol fees owed: {}", protocol_fees_owed.0); // Protocol fees are a percentage of total fees // This test verifies the split mechanism exists println!("Test passed: Protocol fee distribution mechanism verified"); Ok(()) } /// Test 7: Vault creator claims fees #[tokio::test] async fn test_vault_creator_claims_fees() -> TestResult<()> { println!("\nTest: Vault Creator Claims Fees"); let ctx = setup_minimal_vault().await?; let vault = ctx.vault(); // Query current fees owed (now in vault) let fees_owed: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset) .await .unwrap_or(U128(0)); println!(" Fees owed before claim: {}", fees_owed.0); // Attempt to claim fees directly (no proposal needed) // Note: May not have fees to claim in sandbox environment if fees_owed.0 > 0 { let result = owner_claim_vault_fees(&vault, &ctx.owner, ctx.base_asset.clone(), fees_owed).await; if result.is_ok() { println!(" Fee claim executed"); } } println!("Test passed: Fee claim mechanism verified"); Ok(()) } /// Test 9: Fee crystallization via owner call #[tokio::test] async fn test_fee_crystallization_via_proposal() -> TestResult<()> { println!("\nTest: Fee Crystallization via Owner Call"); // Configure vault with hurdle and fees to enable crystallization let mut vault_config = common_data::vault_config::VaultConfig::default(); vault_config.performance_fee_bps = 2000; // 20% vault_config.management_fee_bps = 0; // simplify vault_config.share_price_decimals = 8; // Require hurdle for crystallization path; 0bp simplifies vault_config.performance_hurdle_bps = Some(0); // Allow immediate crystallization in tests vault_config.crystallization_interval_nanosec = U128(0); let ctx = setup_full_integration_test_with_vault_config(2, false, 1, vault_config).await?; let vault = ctx.vault(); let ft_contract = ctx.mock_ft_primary.as_ref().expect("FT should exist"); let ft_asset = ctx.accepted_assets.first().unwrap().clone(); // Start vault owner_start_vault(&vault, &ctx.owner).await?; // 1) deposit vault_deposit_ft( ft_contract, &vault, &ctx.users[0], U128(SCALE_8), false, U128(1), ) .await?; // Record fees and rate before gains let fees_before: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset).await?; let rate_before: U128 = vault .call("get_share_price_in_asset") .args_json(serde_json::json!({ "asset": ft_asset.clone() })) .view() .await? .json()?; // 2) update share price to 1.5*10^8, declaring shares first let rate1 = U128(SCALE_8); // declare shares let rate2 = U128(RATE_1_5); // 50% gain owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate1)]).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate2)]).await?; // 3) execute "crystallize_performance_fee" as owner owner_crystallize_performance_fee(&vault, &ctx.owner).await?; // 4) check that the fees_owed increased, check share_price has decreased let fees_after: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset).await?; let rate_after: U128 = vault .call("get_share_price_in_asset") .args_json(serde_json::json!({ "asset": ft_asset })) .view() .await? .json()?; println!( " Fees before: {}, after: {}; Rate before: {}, target: {}, after: {}", fees_before.0, fees_after.0, rate_before.0, rate2.0, rate_after.0 ); assert!( fees_after.0 > fees_before.0, "fees_owed should increase after crystallization" ); assert!( rate_after.0 < rate2.0, "share_price should decrease due to fee deduction" ); println!("Test passed: Fee crystallization via owner call charges fees and adjusts rate"); Ok(()) } /// Test 11: Performance fee with 50% protocol cut #[tokio::test] async fn test_performance_fee_with_protocol_cut() -> TestResult<()> { println!("\nTest: Performance Fee with 50% Protocol Cut"); // Configure vault with 20% performance fee let mut vault_config = common_data::vault_config::VaultConfig::default(); vault_config.performance_fee_bps = 2000; // 20% performance fee vault_config.management_fee_bps = 0; // simplify - no management fee vault_config.share_price_decimals = 8; // Configure protocol to take 50% of performance fees let mut protocol_config = common_data::protocol_config::ProtocolConfig::default(); protocol_config.protocol_performance_fee_cut_bps = 5000; // 50% cut let ctx = setup_full_integration_test_with_protocol_config( 2, false, 1, vault_config, protocol_config, ) .await?; let vault = ctx.vault(); let ft_contract = ctx.mock_ft_primary.as_ref().expect("FT should exist"); let ft_asset = ctx.accepted_assets.first().unwrap().clone(); // Start the vault owner_start_vault(&vault, &ctx.owner).await?; // User deposits 100M tokens vault_deposit_ft( ft_contract, &vault, &ctx.users[0], U128(100_000_000), false, U128(1), ) .await?; // Record fees before gains let vault_fees_before: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset).await?; let protocol_fees_before: U128 = vault_get_protocol_fees_owed_for_asset(&vault, &ctx.base_asset).await?; println!( " Fees before: vault={}, protocol={}", vault_fees_before.0, protocol_fees_before.0 ); // Simulate 50% gains by updating share price let rate1 = U128(SCALE_8); // 1.0 - declare shares let rate2 = U128(RATE_1_5); // 1.5 - 50% gain owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate1)]).await?; owner_unpause_accountant(&vault, &ctx.owner).await?; owner_update_vault_share_prices(&vault, &ctx.owner, vec![(ft_asset.clone(), rate2)]).await?; // Record fees after gains let vault_fees_after: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset).await?; let protocol_fees_after: U128 = vault_get_protocol_fees_owed_for_asset(&vault, &ctx.base_asset).await?; println!( " Fees after: vault={}, protocol={}", vault_fees_after.0, protocol_fees_after.0 ); // Calculate fee increases let vault_fee_increase = vault_fees_after.0 - vault_fees_before.0; let protocol_fee_increase = protocol_fees_after.0 - protocol_fees_before.0; println!( " Fee increases: vault={}, protocol={}", vault_fee_increase, protocol_fee_increase ); // Verify fees increased assert!( vault_fee_increase > 0, "Vault fees should increase after gains" ); assert!( protocol_fee_increase > 0, "Protocol fees should increase after gains" ); // Verify protocol gets approximately 50% of total performance fees // Total performance fee = vault_fee + protocol_fee let total_performance_fee = vault_fee_increase + protocol_fee_increase; let expected_protocol_fee = total_performance_fee / 2; // 50% // Allow 1% tolerance for rounding let tolerance = expected_protocol_fee / 100; let diff = if protocol_fee_increase > expected_protocol_fee { protocol_fee_increase - expected_protocol_fee } else { expected_protocol_fee - protocol_fee_increase }; assert!( diff <= tolerance, "Protocol should receive ~50% of performance fees. Expected: {}, Got: {}, Diff: {} > Tolerance: {}", expected_protocol_fee, protocol_fee_increase, diff, tolerance ); // Verify vault gets the remaining ~50% let expected_vault_fee = total_performance_fee / 2; // 50% let vault_diff = if vault_fee_increase > expected_vault_fee { vault_fee_increase - expected_vault_fee } else { expected_vault_fee - vault_fee_increase }; assert!( vault_diff <= tolerance, "Vault should receive ~50% of performance fees. Expected: {}, Got: {}, Diff: {} > Tolerance: {}", expected_vault_fee, vault_fee_increase, vault_diff, tolerance ); println!("Test passed: Protocol correctly receives 50% cut of performance fees"); // Both should claim fees // Ensure that the fees are claimed correctly // Tokens should be transferred to the fee recipient // fees_owed should be 0 // Capture owner's balance before claiming (fee_recipient and protocol_fee_recipient are both owner.id()) let owner_balance_before = get_asset_balance(&ctx.base_asset, &ctx.owner).await?; println!( " Owner balance before claiming: {}", owner_balance_before.0 ); // increase more available assets to vault via deposit vault_deposit_ft( ft_contract, &vault, &ctx.users[0], U128(500_000_000), false, U128(1), ) .await?; // Debug: Check vault balance before claiming let vault_balance = vault_get_asset_balance(&vault, &ctx.base_asset).await?; let available_before_claim = vault_balance["available_amount"] .as_str() .unwrap() .parse::() .unwrap(); println!( " Vault available balance before claiming: {}", available_before_claim ); println!( " Fees to claim - vault: {}, protocol: {}", vault_fees_after.0, protocol_fees_after.0 ); // Verify fees don't exceed available balance (required for claiming to succeed) // If fees are larger than available balance, claiming will fail // This could indicate a scaling bug in fee calculation assert!( vault_fees_after.0 <= available_before_claim, "Vault fees ({}) exceed available balance ({}) - cannot claim fees. This may indicate a scaling issue in fee calculation.", vault_fees_after.0, available_before_claim ); assert!( protocol_fees_after.0 <= available_before_claim, "Protocol fees ({}) exceed available balance ({}) - cannot claim fees. This may indicate a scaling issue in fee calculation.", protocol_fees_after.0, available_before_claim ); // Claim vault fees println!(" Claiming vault fees: {}", vault_fees_after.0); owner_claim_vault_fees(&vault, &ctx.owner, ctx.base_asset.clone(), vault_fees_after).await?; // Claim protocol fees println!(" Claiming protocol fees: {}", protocol_fees_after.0); owner_claim_protocol_fees( &vault, &ctx.owner, ctx.base_asset.clone(), protocol_fees_after, ) .await?; // Verify fees_owed are zero after claiming let vault_fees_after_claim: U128 = vault_get_fees_owed_for_asset(&vault, &ctx.base_asset).await?; let protocol_fees_after_claim: U128 = vault_get_protocol_fees_owed_for_asset(&vault, &ctx.base_asset).await?; assert_eq!( vault_fees_after_claim.0, 0, "Vault fees_owed should be 0 after claiming, got {}", vault_fees_after_claim.0 ); assert_eq!( protocol_fees_after_claim.0, 0, "Protocol fees_owed should be 0 after claiming, got {}", protocol_fees_after_claim.0 ); println!(" ✓ Fees_owed are zero after claiming"); // Verify tokens were transferred to fee recipient (owner) let owner_balance_after = get_asset_balance(&ctx.base_asset, &ctx.owner).await?; let expected_balance_increase = vault_fees_after.0 + protocol_fees_after.0; let actual_balance_increase = owner_balance_after.0 - owner_balance_before.0; println!( " Owner balance after claiming: {} (increase: {})", owner_balance_after.0, actual_balance_increase ); assert_eq!( actual_balance_increase, expected_balance_increase, "Owner balance should increase by sum of claimed fees. Expected: {}, Got: {}", expected_balance_increase, actual_balance_increase ); println!(" ✓ Tokens correctly transferred to fee recipient"); println!("Test passed: Fee claiming works correctly for both vault and protocol"); Ok(()) }