use crate::events::{AccountantPaused, AccountantUnpaused, SharePriceUpdated}; use crate::*; use common_data::constants::*; use common_data::math::{calculate_price_change_percent, mul_div, Rounding}; use near_sdk::{env, require}; #[near] impl Contract { /// Get share prices for all accepted assets pub fn get_all_share_prices(&self) -> Vec<(Asset, U128)> { let mut rates = Vec::new(); for asset in self.accepted_deposit_assets.iter() { if let Some(rate) = self.state.share_prices.get(&asset) { rates.push((asset.clone(), *rate)); } } rates } /// Get the timestamp of the previous share price update pub fn get_previous_share_price_update_timestamp(&self) -> U128 { self.state.previous_share_price_update_in_nano } /// Get total shares tracked for TVL and fee calculations pub fn get_total_shares(&self) -> U128 { self.state.total_shares } /// Get nanoseconds elapsed since the last share price update pub fn get_time_since_last_rate_update(&self) -> U128 { U128(self.internal_get_time_elapsed_nanos()) } /// Check if the accountant is currently paused pub fn is_accountant_paused(&self) -> bool { self.state.is_accountant_paused } } impl Contract { /// Update share prices and total shares /// Update total_shares LAST (so new shares don't pay fees on first report) pub fn internal_update_share_price(&mut self, share_prices: Vec<(Asset, U128)>) { // Get current total shares from token supply let total_shares = self.token.ft_total_supply(); // First, verify all provided assets are in the asset union let asset_union = self.get_asset_union(); for (asset, _) in share_prices.iter() { require!( asset_union.contains(asset), "Asset is not in the asset union. Please check supported assets." ); } // Ensure first element is the base asset require!( share_prices[0].0 == self.base_asset, "First share price must be for base asset. Please provide base asset share price first." ); // Verify all assets in union have share prices provided for union_asset in asset_union.iter() { require!( share_prices .iter() .any(|(asset, _)| asset == union_asset), "Share price missing for asset in union. Please provide share prices for all assets." ); } // Extract base token share price for validation and fee calculations let base_share_price = share_prices[0].1 .0; // Validate share price update constraints and pause if necessary self.internal_validate_share_price_update(base_share_price); // Calculate platform fees (time-based annualized fees) using old total_shares let (management_fee_amount, rate_after_management_fee) = self.internal_calculate_management_fee_bps(base_share_price); // Calculate performance fees (high water mark based fees) using old total_shares let (performance_fee_amount, final_adjusted_rate) = self.internal_calculate_performance_fee_bps(rate_after_management_fee.0); // Add fees to appropriate tracking with protocol cuts let management_protocol_cut = self.protocol_config.protocol_management_fee_cut_bps; self.internal_add_fees( self.base_asset.clone(), management_fee_amount, management_protocol_cut, ); let performance_protocol_cut = self.protocol_config.protocol_performance_fee_cut_bps; self.internal_add_fees( self.base_asset.clone(), performance_fee_amount, performance_protocol_cut, ); // Update high water mark if performance exceeded previous high // Only update if performance fees were actually calculated (not when yield hurdle is configured) if performance_fee_amount.0 > 0 && final_adjusted_rate.0 > self.state.highwatermark_rate.0 { self.state.highwatermark_rate = U128(base_share_price); } // Calculate the fee adjustment ratio to apply to all token rates let fee_adjustment_ratio = if base_share_price > 0 { mul_div( final_adjusted_rate.0, BASIS_POINTS_DIVISOR, base_share_price, Rounding::Down, ) } else { BASIS_POINTS_DIVISOR // No adjustment if base rate is zero }; // Apply proportional fee adjustments to all provided share prices let mut adjusted_share_prices = Vec::new(); for (asset, original_rate) in share_prices { let adjusted_rate = if asset == self.base_asset { final_adjusted_rate } else { // Apply the same proportional fee adjustment to other tokens U128(mul_div( original_rate.0, fee_adjustment_ratio, BASIS_POINTS_DIVISOR, Rounding::Down, )) }; adjusted_share_prices.push((asset, adjusted_rate)); } // Collect old rates before updating let mut assets = Vec::new(); let mut old_rates = Vec::new(); let mut new_rates = Vec::new(); // Update share prices for all provided assets with fee-adjusted values for (asset, new_rate) in &adjusted_share_prices { let old_rate = self .state .share_prices .get(asset) .cloned() .unwrap_or(U128(0)); self.state.share_prices.insert(asset.clone(), *new_rate); // Collect data for batch event emission assets.push(asset.clone()); old_rates.push(old_rate); new_rates.push(*new_rate); } // Emit share price updated event SharePriceUpdated { assets: &assets, old_rates: &old_rates, new_rates: &new_rates, } .emit(); // Update the timestamp for the share price update self.state.previous_share_price_update_in_nano = U128(env::block_timestamp() as u128); // This ensures new shares reported don't pay fees on their first report self.state.total_shares = total_shares; } pub fn internal_unpause_accountant(&mut self) { // Unpause the accountant self.state.is_accountant_paused = false; // Emit accountant unpaused event AccountantUnpaused {}.emit(); } pub fn internal_get_time_elapsed_nanos(&self) -> u128 { if !self.is_vault_live() { return 0; } let current_time = env::block_timestamp(); let last_update = self.state.previous_share_price_update_in_nano.0 as u64; current_time .checked_sub(last_update) .expect("Clock error: current time is before last share price update") as u128 } pub fn internal_pause_accountant(&mut self, reason: &str) { self.state.is_accountant_paused = true; // Emit accountant paused event AccountantPaused { reason }.emit(); } pub fn internal_validate_share_price_update(&mut self, base_share_price: u128) { // Validate that share price is not zero require!( base_share_price > 0, "Share price cannot be zero. Please provide a valid share price." ); // Check if the accountant is paused require!( !self.state.is_accountant_paused, "Accountant is currently paused. Share price updates are temporarily disabled." ); let time_elapsed_nanos = self.internal_get_time_elapsed_nanos(); // Get previous base token share price for price change calculation let previous_base_rate = self .state .share_prices .get(&self.base_asset) .map(|rate| rate.0) .unwrap_or(base_share_price); // Use current rate if no previous rate exists // Check time constraints - pause if violated but continue processing if time_elapsed_nanos < self.vault_config.new_price_min_interval_nanosec.0 { let reason = &format!( "Share price update too frequent: {} nanos elapsed, minimum {} nanos required", time_elapsed_nanos, self.vault_config.new_price_min_interval_nanosec.0 ); self.internal_pause_accountant(reason); } if time_elapsed_nanos > self.vault_config.new_price_max_interval_nanosec.0 { let reason = &format!( "Share price update too delayed: {} nanos elapsed, maximum {} nanos allowed", time_elapsed_nanos, self.vault_config.new_price_max_interval_nanosec.0 ); self.internal_pause_accountant(reason); } // Calculate price change percentage (in micro percent: 100,000,000,000 = 100%) if previous_base_rate > 0 { let price_change_percent = calculate_price_change_percent(base_share_price, previous_base_rate); // Check price change constraints - pause if violated but continue processing if price_change_percent < self.vault_config.new_price_min_deviation_nanopercent { let reason = &format!( "Price change too small: {}mc%, minimum {}mc% required", price_change_percent, self.vault_config.new_price_min_deviation_nanopercent ); self.internal_pause_accountant(reason); } if price_change_percent > self.vault_config.new_price_max_deviation_nanopercent { let reason = &format!( "Price change too large: {}mc%, maximum {}mc% allowed", price_change_percent, self.vault_config.new_price_max_deviation_nanopercent ); self.internal_pause_accountant(reason); } } } }