use crate::*; use common_data::constants::*; use common_data::math::{apply_fee_ratio_to_rate, calculate_fee_ratio, mul_div, Rounding}; use near_sdk::{env, require}; #[near] impl Contract { /// Get crystallization information: (timestamp, share_price, time_elapsed) pub fn get_crystallization_info(&self) -> (U128, U128, U128) { let current_time = env::block_timestamp() as u128; let last_crystallization = self.state.last_crystallization_timestamp.0; let time_elapsed = U128(current_time - last_crystallization); ( self.state.last_crystallization_timestamp, self.state.last_crystallization_share_price, time_elapsed, ) } /// Check if crystallization is due based on the crystallization interval pub fn is_crystallization_due(&self) -> bool { let (_, _, time_elapsed) = self.get_crystallization_info(); let crystallization_interval = self.vault_config.crystallization_interval_nanosec.0; time_elapsed.0 >= crystallization_interval } /// Get the current high water mark rate pub fn get_highwatermark_rate(&self) -> U128 { self.state.highwatermark_rate } /// Preview management fees that would be charged at current rates and time elapsed pub fn get_current_management_fee_preview(&self) -> U128 { let current_base_rate = self .state .share_prices .get(&self.base_asset) .map(|rate| rate.0) .unwrap_or(0); let time_elapsed_nanos = self.internal_get_time_elapsed_nanos(); let total_base_asset_amount = self.internal_calculate_tvl(current_base_rate); let management_fee_bps_rate = self.vault_config.management_fee_bps; // If any of the values are zero, return zero if current_base_rate == 0 || time_elapsed_nanos == 0 || total_base_asset_amount == 0 || management_fee_bps_rate == 0 { return U128(0); } // Calculate the annual fee rate first (this is safe from overflow) let annual_fee_rate = mul_div( total_base_asset_amount, management_fee_bps_rate as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Then apply the time factor (this is also safe from overflow) let fee_amount = mul_div( annual_fee_rate, time_elapsed_nanos, NANOSECONDS_PER_YEAR_LEAP, Rounding::Up, ); U128(fee_amount) } /// Preview performance fees that would be charged at current rates pub fn get_current_performance_fee_preview(&self) -> U128 { let current_base_rate = self .state .share_prices .get(&self.base_asset) .map(|rate| rate.0) .unwrap_or(0); let (fee_amount, _) = self.internal_calculate_performance_fee_bps(current_base_rate); fee_amount } /// Check if any fees are owed (vault creator or protocol) pub fn has_any_fees_owed(&self) -> bool { self.state.fees_owed.iter().any(|(_, amount)| amount.0 > 0) || self .state .protocol_fees_owed .iter() .any(|(_, amount)| amount.0 > 0) } } impl Contract { /// Calculate total asset amount (TVL) from total_shares and share price /// Returns 0 if total_shares is zero (no TVL) /// /// Formula: total_asset_amount = total_shares * share_price / rate_decimals fn internal_calculate_tvl(&self, share_price: u128) -> u128 { let total_shares = self.state.total_shares; // If no shares exist, return 0 (zero TVL) if total_shares.0 == 0 { return 0; } let total_asset_amount = self .internal_convert_to_asset_amount_with_rate(total_shares, share_price) .0; log!( "Calculated TVL: total_shares={} share_price={} => total_asset_amount={}", total_shares.0, share_price, total_asset_amount ); total_asset_amount } /// Calculate management fees based on time elapsed and total value locked. /// /// # Parameters /// * `base_share_price` - The current share price for the base asset /// /// # Returns /// * `(fee_amount, adjusted_share_price)` - Tuple containing the calculated fee amount and the share price after fee deduction pub fn internal_calculate_management_fee_bps(&self, base_share_price: u128) -> (U128, U128) { // No fees if vault is not live if !self.is_vault_live() { return (U128(0), U128(base_share_price)); } let time_elapsed_nanos = self.internal_get_time_elapsed_nanos(); let total_base_asset_amount = self.internal_calculate_tvl(base_share_price); let management_fee_bps_rate = self.vault_config.management_fee_bps; // No fees for zero values if time_elapsed_nanos == 0 || total_base_asset_amount == 0 || management_fee_bps_rate == 0 { return (U128(0), U128(base_share_price)); } // Formula: (total_base_asset_amount * management_fee_bps * time_elapsed) / (BASIS_POINTS_DIVISOR * NANOSECONDS_PER_YEAR_LEAP) // Calculate annual fee rate let annual_fee_rate = mul_div( total_base_asset_amount, management_fee_bps_rate as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Apply time factor to get actual fee let fee_amount = mul_div( annual_fee_rate, time_elapsed_nanos, NANOSECONDS_PER_YEAR_LEAP, Rounding::Up, ); // Calculate adjusted share price after fee deduction let fee_ratio = calculate_fee_ratio(fee_amount, total_base_asset_amount); let adjusted_rate = apply_fee_ratio_to_rate(base_share_price, fee_ratio); (U128(fee_amount), U128(adjusted_rate)) } /// Calculate performance fees based on total asset amount and share price pub fn internal_calculate_performance_fee_bps(&self, base_share_price: u128) -> (U128, U128) { // No fees if vault is not live // No performance fees if hurdle is configured, performance fees will be applied during crystallization instead if !self.is_vault_live() || self.vault_config.performance_hurdle_bps.is_some() { return (U128(0), U128(base_share_price)); } let current_highwatermark = self.state.highwatermark_rate.0; let performance_fee_bps_rate = self.vault_config.performance_fee_bps; let total_base_asset_amount = self.internal_calculate_tvl(base_share_price); // No fees if zero values or high water mark not reached if total_base_asset_amount == 0 || performance_fee_bps_rate == 0 || base_share_price <= current_highwatermark { return (U128(0), U128(base_share_price)); } // High water mark exceeded - calculate performance fee let performance_gain = base_share_price - current_highwatermark; // Formula: (total_base_asset_amount * performance_gain * performance_fee_bps) / (base_share_price * BASIS_POINTS_DIVISOR) // Calculate gain portion of asset amount let gain_asset_amount = mul_div( total_base_asset_amount, performance_gain, base_share_price, Rounding::Up, ); // Apply performance fee rate to gain let fee_amount = mul_div( gain_asset_amount, performance_fee_bps_rate as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Calculate adjusted share price after fee deduction let fee_ratio = calculate_fee_ratio(fee_amount, total_base_asset_amount); let adjusted_rate = apply_fee_ratio_to_rate(base_share_price, fee_ratio); (U128(fee_amount), U128(adjusted_rate)) } /// Crystallize performance fees with yield hurdle pub fn internal_crystallize_performance_fee(&mut self) { require!( self.is_vault_live(), "Cannot crystallize fees before vault is live" ); let yield_hurdle_bps = self .vault_config .performance_hurdle_bps .expect("Crystallize performance fee only works when yield hurdle is configured"); let current_time = env::block_timestamp() as u128; let last_crystallization = self.state.last_crystallization_timestamp.0; let time_elapsed = current_time .checked_sub(last_crystallization) .expect("Clock error: current time is before last crystallization timestamp"); let crystallization_interval = self.vault_config.crystallization_interval_nanosec.0; let last_crystallization_rate = self.state.last_crystallization_share_price.0; require!( time_elapsed >= crystallization_interval, "Performance fee crystallization too early" ); require!( last_crystallization_rate > 0, "Invalid last crystallization rate" ); let current_share_price = *self .state .share_prices .get(&self.base_asset) .expect("Base share price not found"); let total_base_asset_amount = self.internal_calculate_tvl(current_share_price.0); require!( total_base_asset_amount > 0, "TVL is zero, cannot calculate performance fee" ); let annualized_return_bps = self.internal_calculate_annualized_return_bps( current_share_price.0, last_crystallization_rate, time_elapsed, ); // Check if annualized return meets the yield hurdle if annualized_return_bps < yield_hurdle_bps as u128 { env::log_str(&format!( "Yield hurdle not met: {}bp annualized return < {}bp hurdle - no fees calculated", annualized_return_bps, yield_hurdle_bps )); // Update timestamp even if no fees calculated self.state.last_crystallization_timestamp = U128(current_time); self.state.last_crystallization_share_price = current_share_price; return; } // Check if current share price exceeds high water mark let current_highwatermark = self.state.highwatermark_rate.0; if current_share_price.0 <= current_highwatermark { env::log_str(&format!( "High water mark not exceeded: current rate {} <= highwatermark {} - no fees calculated", current_share_price.0, current_highwatermark )); // Update timestamp even if no fees calculated self.state.last_crystallization_timestamp = U128(current_time); self.state.last_crystallization_share_price = current_share_price; return; } let hurdle_adjusted_rate = self.internal_calculate_hurdle_adjusted_rate( last_crystallization_rate, yield_hurdle_bps as u128, time_elapsed, ); // Performance fee only applies to gain above both hurdle and high water mark let performance_gain = current_share_price .0 .checked_sub(hurdle_adjusted_rate.max(current_highwatermark)) .expect("Performance gain calculation underflow hurdle_adjusted_rate or current_highwatermark > current_share_price"); let performance_fee_bps_rate = self.vault_config.performance_fee_bps; // Fee amount = (total_base_asset_amount * performance_gain / current_share_price) * performance_fee_bps / 10000 let fee_amount = mul_div( mul_div( total_base_asset_amount, performance_gain, current_share_price.0, Rounding::Up, ), performance_fee_bps_rate as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); if fee_amount > 0 { // Add fees with protocol cut let performance_protocol_cut = self.protocol_config.protocol_performance_fee_cut_bps; self.internal_add_fees( self.base_asset.clone(), U128(fee_amount), performance_protocol_cut, ); let fee_ratio = calculate_fee_ratio(fee_amount, total_base_asset_amount); let adjusted_base_rate = apply_fee_ratio_to_rate(current_share_price.0, fee_ratio); // Calculate the fee adjustment ratio to apply to all token rates let fee_adjustment_ratio = if current_share_price.0 > 0 { mul_div( adjusted_base_rate, BASIS_POINTS_DIVISOR, current_share_price.0, Rounding::Down, ) } else { BASIS_POINTS_DIVISOR // No adjustment if base rate is zero }; // Apply proportional fee adjustments to all asset share prices let assets_to_update: Vec = self.get_asset_union(); for asset in assets_to_update { if let Some(current_rate) = self.state.share_prices.get_mut(&asset) { *current_rate = if asset == self.base_asset { U128(adjusted_base_rate) } else { // Apply the same proportional fee adjustment to other tokens U128(mul_div( current_rate.0, fee_adjustment_ratio, BASIS_POINTS_DIVISOR, Rounding::Down, )) }; } else { panic!("Share price not found for asset: {}", asset); } } if adjusted_base_rate > self.state.highwatermark_rate.0 { self.state.highwatermark_rate = current_share_price; } self.state.last_crystallization_timestamp = U128(current_time); self.state.last_crystallization_share_price = U128(adjusted_base_rate); env::log_str(&format!( "Crystallized performance fee: {} assets at rate {} -> {} (annualized return: {}bp > {}bp hurdle)", fee_amount, current_share_price.0, adjusted_base_rate, annualized_return_bps, yield_hurdle_bps )); } else { env::log_str(&format!( "Yield hurdle exceeded but no fees to claim: annualized return {}bp > {}bp hurdle", annualized_return_bps, yield_hurdle_bps )); self.state.last_crystallization_timestamp = U128(current_time); self.state.last_crystallization_share_price = current_share_price; } } /// Calculate annualized return in basis points /// Example: If rate increased 5% over 6 months: /// - annualized = 1000 bps (5%) fn internal_calculate_annualized_return_bps( &self, current_rate: u128, previous_rate: u128, time_elapsed_nanos: u128, ) -> u128 { // Use high precision (1e8 = 100 million) to capture small returns accurately // This prevents precision loss for small returns over short periods let return_ratio = mul_div(current_rate, HIGH_PRECISION, previous_rate, Rounding::Down); if return_ratio <= HIGH_PRECISION { // Negative or zero return - no fees 0 } else { let excess_return = return_ratio - HIGH_PRECISION; // Annualize the excess return based on time elapsed let annualized_high_precision = mul_div( excess_return, NANOSECONDS_PER_YEAR_LEAP, time_elapsed_nanos, Rounding::Down, ); // Convert from high precision to basis points (10000 bps = 100%) mul_div( annualized_high_precision, BASIS_POINTS_DIVISOR, HIGH_PRECISION, Rounding::Down, ) } } /// Calculate what the share price would be at exactly the hurdle rate /// Formula: hurdle_adjusted_rate = last_rate * (1 + (hurdle_bps * time_elapsed / nanoseconds_per_year)) fn internal_calculate_hurdle_adjusted_rate( &self, last_crystallization_rate: u128, yield_hurdle_bps: u128, time_elapsed_nanos: u128, ) -> u128 { mul_div( last_crystallization_rate, BASIS_POINTS_DIVISOR + mul_div( yield_hurdle_bps, time_elapsed_nanos, NANOSECONDS_PER_YEAR_LEAP, Rounding::Up, ), BASIS_POINTS_DIVISOR, Rounding::Up, ) } }