use crate::Contract; use common_data::asset::Asset; use common_data::constants::*; use common_data::protocol_config::ProtocolConfig; use near_sdk::json_types::U128; use near_sdk::test_utils::{accounts, VMContextBuilder}; use near_sdk::testing_env; use near_sdk::AccountId; // ============================================================================ // Test Constants // ============================================================================ pub const ONE_HOUR_NS: u128 = NANOSECONDS_PER_HOUR; pub const ONE_DAY_NS: u128 = NANOSECONDS_PER_DAY; pub const ONE_YEAR_NS: u128 = NANOSECONDS_PER_YEAR_LEAP; pub const USDC_DECIMALS: u8 = 6; pub const USDC_UNIT: u128 = 1_000_000; // 1 USDC with 6 decimals pub const RATE_DECIMALS: u8 = 8; pub const RATE_PRECISION: u128 = 100_000_000; // 10^8 // ============================================================================ // Test Setup Functions // ============================================================================ /// Create a test contract with default configuration pub fn create_test_contract() -> Contract { let context = VMContextBuilder::new() .predecessor_account_id(accounts(0)) .block_timestamp(1_000_000_000_000_000_000) // Start at 1 second .is_view(false) .build(); testing_env!(context); let base_asset = Asset::new_fungible_token(accounts(1)); let accepted_assets = vec![base_asset.clone()]; let available_assets = vec![base_asset.clone()]; Contract::new( accounts(0), // owner_account_id base_asset, accepted_assets, available_assets, "vUSDC".to_string(), "Vault USDC Shares".to_string(), USDC_DECIMALS, None, accounts(2), // fee_recipient accounts(3), // protocol_account ProtocolConfig::default(), ) } /// Create a vault with specific fee configuration pub fn setup_vault_with_fees(management_fee_bps: u16, performance_fee_bps: u16) -> Contract { let mut contract = create_test_contract(); // Update vault config with fee rates contract.vault_config.management_fee_bps = management_fee_bps; contract.vault_config.performance_fee_bps = performance_fee_bps; contract } /// Create a vault with specific protocol fee cuts pub fn setup_vault_with_protocol_cuts( management_cut_bps: u16, performance_cut_bps: u16, ) -> Contract { let mut contract = create_test_contract(); // Update protocol config contract.protocol_config.protocol_management_fee_cut_bps = management_cut_bps; contract.protocol_config.protocol_performance_fee_cut_bps = performance_cut_bps; contract } /// Create a vault with crystallization interval and hurdle pub fn setup_vault_with_crystallization(interval_ns: u128, hurdle_bps: Option) -> Contract { let mut contract = create_test_contract(); contract.vault_config.crystallization_interval_nanosec = U128(interval_ns); contract.vault_config.performance_hurdle_bps = hurdle_bps; contract } // ============================================================================ // Time Manipulation // ============================================================================ /// Fast forward time by specified nanoseconds pub fn fast_forward_time(nanoseconds: u128) { let current = near_sdk::env::block_timestamp(); let new_timestamp = current + nanoseconds as u64; let mut builder = VMContextBuilder::new(); builder .predecessor_account_id(accounts(0)) .block_timestamp(new_timestamp) .is_view(false); testing_env!(builder.build()); } /// Get current block timestamp pub fn current_timestamp() -> u128 { near_sdk::env::block_timestamp() as u128 } /// Set block timestamp to specific value pub fn set_timestamp(timestamp: u128) { let mut builder = VMContextBuilder::new(); builder .predecessor_account_id(accounts(0)) .block_timestamp(timestamp as u64) .is_view(false); testing_env!(builder.build()); } /// Set predecessor account for testing authorization pub fn set_predecessor(account_id: AccountId) { let current = near_sdk::env::block_timestamp(); let mut builder = VMContextBuilder::new(); builder .predecessor_account_id(account_id) .block_timestamp(current) .is_view(false); testing_env!(builder.build()); } // ============================================================================ // Vault State Helpers // ============================================================================ /// Initialize vault as live with a starting timestamp pub fn start_vault(contract: &mut Contract) { contract.live_at = U128(current_timestamp()); } /// Start vault at specific timestamp pub fn start_vault_at(contract: &mut Contract, timestamp: u128) { contract.live_at = U128(timestamp); } /// Set total shares for TVL calculations /// Note: This function expects shares in the same denomination as asset amounts (e.g., USDC base units) /// and automatically applies the extra_decimals scaling factor to convert to actual share amounts. /// /// Example: For 1M USDC (1_000_000 * 10^6), call set_total_shares(contract, 1_000_000 * USDC_UNIT) /// This will be scaled to the correct share amount accounting for extra_decimals. pub fn set_total_shares(contract: &mut Contract, asset_amount: u128) { // Convert asset amount to shares using the extra_decimals scaling let extra_decimal_scale = 10u128.pow(contract.vault_config.extra_decimals as u32); let shares = asset_amount * extra_decimal_scale; contract.state.total_shares = U128(shares); } /// Set share price for base asset pub fn set_share_price(contract: &mut Contract, rate: u128) { contract .state .share_prices .insert(contract.base_asset.clone(), U128(rate)); } /// Set high water mark pub fn set_high_water_mark(contract: &mut Contract, rate: u128) { contract.state.highwatermark_rate = U128(rate); } /// Set last crystallization info pub fn set_last_crystallization(contract: &mut Contract, timestamp: u128, share_price: u128) { contract.state.last_crystallization_timestamp = U128(timestamp); contract.state.last_crystallization_share_price = U128(share_price); } /// Set previous share price update timestamp pub fn set_previous_update_time(contract: &mut Contract, timestamp: u128) { contract.state.previous_share_price_update_in_nano = U128(timestamp); } // ============================================================================ // Assertion Helpers // ============================================================================ /// Assert fee amount is within tolerance (in basis points) /// Example: assert_fee_within_tolerance(1000, 1001, 10) // Allow 0.1% difference pub fn assert_fee_within_tolerance(expected: u128, actual: u128, tolerance_bps: u128) { let diff = if expected > actual { expected - actual } else { actual - expected }; let max_diff = expected * tolerance_bps / BASIS_POINTS_DIVISOR; assert!( diff <= max_diff, "Fee mismatch: expected {}, got {}, diff {} exceeds tolerance {} bps", expected, actual, diff, tolerance_bps ); } /// Assert U128 values are equal pub fn assert_u128_eq(expected: U128, actual: U128, context: &str) { assert_eq!( expected.0, actual.0, "{}: expected {}, got {}", context, expected.0, actual.0 ); } /// Assert value is within percentage range pub fn assert_within_percent(expected: u128, actual: u128, percent: u128) { let tolerance = expected * percent / 100; let diff = if expected > actual { expected - actual } else { actual - expected }; assert!( diff <= tolerance, "Value mismatch: expected {}, got {}, diff {} exceeds {}%", expected, actual, diff, percent ); } // ============================================================================ // Test Account Helpers // ============================================================================ /// Get owner account pub fn owner() -> AccountId { accounts(0) } /// Get base asset token account pub fn token_account() -> AccountId { accounts(1) } /// Get fee recipient account pub fn fee_recipient() -> AccountId { accounts(2) } /// Get protocol account pub fn protocol_account() -> AccountId { accounts(3) } /// Get user account pub fn user() -> AccountId { accounts(4) } // ============================================================================ // Calculation Helpers // ============================================================================ /// Calculate expected management fee /// fee = TVL * fee_bps * time_elapsed / (BASIS_POINTS * YEAR) pub fn calculate_management_fee(tvl: u128, fee_bps: u16, time_elapsed: u128) -> u128 { if tvl == 0 || fee_bps == 0 || time_elapsed == 0 { return 0; } // Use the same formula as the contract let annual_fee = tvl * fee_bps as u128 / BASIS_POINTS_DIVISOR; let fee = annual_fee * time_elapsed / ONE_YEAR_NS; // Round up conservatively if annual_fee * time_elapsed % ONE_YEAR_NS > 0 { fee + 1 } else { fee } } /// Calculate expected performance fee /// fee = TVL * performance_gain * fee_bps / (rate * BASIS_POINTS) pub fn calculate_performance_fee( tvl: u128, current_rate: u128, high_water_mark: u128, fee_bps: u16, rate_decimals: u128, ) -> u128 { if tvl == 0 || fee_bps == 0 || current_rate <= high_water_mark { return 0; } let performance_gain = current_rate - high_water_mark; let gain_asset_amount = tvl * performance_gain / rate_decimals; let fee = gain_asset_amount * fee_bps as u128 / BASIS_POINTS_DIVISOR; // Round up conservatively if (gain_asset_amount * fee_bps as u128) % BASIS_POINTS_DIVISOR > 0 { fee + 1 } else { fee } } /// Calculate protocol fee cut pub fn calculate_protocol_cut(total_fee: u128, protocol_cut_bps: u16) -> u128 { if protocol_cut_bps == 0 { return 0; } let protocol_fee = total_fee * protocol_cut_bps as u128 / BASIS_POINTS_DIVISOR; // Round up conservatively if (total_fee * protocol_cut_bps as u128) % BASIS_POINTS_DIVISOR > 0 { protocol_fee + 1 } else { protocol_fee } } /// Calculate TVL from total shares and share price /// Note: This matches the contract's internal calculation which accounts for both /// share_price_decimals and extra_decimals in the denominator pub fn calculate_tvl(total_shares: u128, share_price: u128, rate_decimals: u128) -> u128 { if total_shares == 0 { return 0; } // The contract uses: assets = (shares * share_price) / (rate_scale * extra_scale) // For default config: rate_scale = 10^8, extra_scale = 10^18 // So we need to divide by both to match the contract's calculation let extra_decimal_scale = 10u128.pow(DEFAULT_EXTRA_DECIMALS as u32); let scale_product = rate_decimals * extra_decimal_scale; total_shares * share_price / scale_product } // ============================================================================ // Price Validation Helpers // ============================================================================ /// Setup vault with price deviation constraints pub fn setup_vault_with_price_validation( min_interval_ns: u128, max_interval_ns: u128, min_deviation_nanopercent: u64, max_deviation_nanopercent: u64, ) -> Contract { let mut contract = create_test_contract(); contract.vault_config.new_price_min_interval_nanosec = U128(min_interval_ns); contract.vault_config.new_price_max_interval_nanosec = U128(max_interval_ns); contract.vault_config.new_price_min_deviation_nanopercent = min_deviation_nanopercent; contract.vault_config.new_price_max_deviation_nanopercent = max_deviation_nanopercent; contract }