use crate::events::{DepositQueued, RedeemProcessed, RedeemQueued, VaultDeposit, VaultWithdraw}; use crate::types::DepositMessage; use crate::*; use common_data::constants::*; use near_contract_standards::fungible_token::events::{FtBurn, FtMint}; use near_sdk::{env, Promise}; impl Contract { pub fn internal_update_asset_balance(&mut self, asset: &Asset, amount: U128, is_deposit: bool) { if let Some(balance) = self.state.asset_balances.get_mut(asset) { // Asset exists - update balance if is_deposit { balance.available_amount.0 = balance .available_amount .0 .checked_add(amount.0) .expect("Asset balance overflow on deposit"); } else { balance.available_amount.0 = balance .available_amount .0 .checked_sub(amount.0) .expect("Asset balance underflow on withdrawal"); } } else { // Asset doesn't exist require!( is_deposit, "Cannot withdraw from non-existent asset balance" ); // Create new balance entry for deposit let mut balance = VaultBalance::default(); balance.available_amount = amount; self.state.asset_balances.insert(asset.clone(), balance); } } pub fn internal_validate_tvl_cap(&self, deposit_amount: U128, deposit_asset: &Asset) { if let Some(cap) = self.vault_config.tvl_cap { // Get base asset share price for TVL calculation let base_asset_share_price = self.get_share_price_in_asset(&self.base_asset); require!( base_asset_share_price.0 > 0, "Base asset share price not available for TVL calculation" ); // Calculate current total assets in base asset terms let current_total_asset_amount = self.internal_calculate_total_asset_amount(base_asset_share_price.0); // Convert deposit amount to base asset equivalent let deposit_amount_in_base = self.internal_convert_amount_to_base_asset(deposit_amount, deposit_asset); // Calculate total assets after deposit (must not overflow) let total_assets_after_deposit = current_total_asset_amount .0 .checked_add(deposit_amount_in_base.0) .expect("TVL calculation overflow"); require!(total_assets_after_deposit <= cap.0, "TVL cap exceeded"); } } pub fn internal_calculate_total_asset_amount(&self, share_price: u128) -> U128 { let total_supply = self.token.ft_total_supply(); self.internal_convert_to_asset_amount_with_rate(total_supply, share_price) } pub fn internal_validate_asset_for_deposit(&self, asset: &Asset) { require!( self.is_asset_accepted_for_deposit(asset), "Asset not accepted for deposits" ); } pub fn internal_validate_asset_for_redeem(&self, asset: &Asset) { require!( self.is_asset_available_for_redeem(asset), "Asset not available for redemption" ); } pub fn internal_validate_asset(&self, asset: &Asset) { require!( self.is_asset_accepted_for_deposit(asset) || self.is_asset_available_for_redeem(asset), "Asset not accepted" ); } pub fn internal_validate_sync_redeem_size(&self, shares: U128) { // Validate sync redeem size limit // Calculate using base asset terms: shares_value and total_assets are both in base asset let base_asset_share_price = self.get_share_price_in_asset(&self.base_asset); let shares_value_in_base = self.internal_convert_to_asset_amount_with_rate(shares, base_asset_share_price.0); let total_assets_in_base = self.internal_calculate_total_asset_amount(base_asset_share_price.0); if total_assets_in_base.0 > 0 { let redeem_ratio_bps = mul_div( shares_value_in_base.0, BASIS_POINTS_DIVISOR, total_assets_in_base.0, Rounding::Down, ); require!( redeem_ratio_bps <= self.vault_config.max_sync_redeem_ratio as u128, "Redeem amount too large, use request redeem" ); } } pub fn internal_validate_is_owner(&self) { let predecessor = env::predecessor_account_id(); require!( predecessor == self.owner_account_id, "Only owner can call this function" ); } pub fn internal_validate_protocol_caller(&self) { let predecessor = env::predecessor_account_id(); require!( predecessor == self.protocol_account, "Only protocol account can call this function" ); } pub fn internal_validate_basis_points(&self, bps: BasisPoints) { require!( bps <= BASIS_POINTS_DIVISOR_U16, "Basis points cannot exceed 10000 (100%)" ); } pub fn internal_validate_account_id(&self, account_id: &AccountId) { require!( env::is_valid_account_id(account_id.as_bytes()), "Invalid account ID" ); } pub fn internal_validate_not_emergency_paused(&self) { require!( !self.state.emergency_paused, "Vault is emergency paused - all operations are disabled" ); } pub fn internal_validate_share_price_freshness(&self) { let current_time = env::block_timestamp() as u128; let last_update = self.state.previous_share_price_update_in_nano.0; let max_staleness = self.vault_config.max_share_price_staleness_ns.0; let time_since_update = current_time .checked_sub(last_update) .expect("Clock error: current time is before last share price update"); require!( time_since_update <= max_staleness, format!( "Share price is stale (last updated {} ns ago, max allowed {} ns)", time_since_update, max_staleness ) ); } pub fn internal_validate_deposit_slippage( &self, asset_amount: U128, asset: &Asset, min_shares: U128, ) { // Validate slippage protection for deposits let expected_shares = self.convert_to_shares(asset, asset_amount); require!(expected_shares.0 >= min_shares.0, "Slippage exceeded"); } pub fn internal_validate_redeem_slippage( &self, shares: U128, asset: &Asset, min_asset_amount: U128, ) { // Validate slippage protection for withdrawals let gross_assets = self.convert_to_asset_amount(asset, shares); // Calculate withdrawal fee let (total_fee, _vault_fee, _protocol_fee) = self.internal_calculate_withdrawal_fee(gross_assets, asset); let net_assets = U128(gross_assets.0 - total_fee.0); // Validate slippage using net assets (after fee) - what user actually receives require!(net_assets.0 >= min_asset_amount.0, "Slippage exceeded"); } pub fn internal_validate_min_deposit(&self, deposit_amount: U128, asset: &Asset) { // Convert deposit amount to base asset equivalent let deposit_amount_in_base = self.internal_convert_amount_to_base_asset(deposit_amount, asset); require!( deposit_amount_in_base.0 >= self.vault_config.min_deposit.0, "Deposit amount below minimum" ); } pub fn internal_validate_min_withdraw(&self, shares: U128) { if self.vault_config.min_withdraw.0 > 0 { let assets_in_base = self.convert_to_asset_amount(&self.base_asset, shares); require!( assets_in_base.0 >= self.vault_config.min_withdraw.0, "Withdrawal amount below minimum" ); } } pub fn internal_deposit_immediate( &mut self, asset: Asset, deposit_amount: U128, min_shares: U128, sender_id: AccountId, receiver_id: AccountId, memo: Option, ) -> U128 { self.internal_validate_min_deposit(deposit_amount, &asset); // Calculate deposit fee let (total_fee, vault_fee, protocol_fee) = self.internal_calculate_deposit_fee(deposit_amount, &asset); let net_deposit_amount = U128(deposit_amount.0 - total_fee.0); // Validate slippage using net assets (after fee) self.internal_validate_deposit_slippage(net_deposit_amount, &asset, min_shares); // Validate TVL cap before processing deposit (using gross assets) self.internal_validate_tvl_cap(deposit_amount, &asset); let receiver = &receiver_id; // Validate flow control limits (using gross assets) self.internal_validate_deposit_flow_cap(deposit_amount, &asset); // Convert net assets (after fee) to shares let shares = self.convert_to_shares(&asset, net_deposit_amount); // Update asset balance (gross assets go into vault, fees stay in vault) self.internal_update_asset_balance(&asset, deposit_amount, true); // Accumulate fees self.internal_accumulate_fees(&asset, vault_fee, protocol_fee); // Update flow tracker after successful deposit self.internal_update_deposit_flow(deposit_amount, &asset); // Mint shares (no lock in simplified version) self.token.internal_deposit(receiver, shares.0); // Emit FT mint event FtMint { owner_id: receiver, amount: shares, memo: memo.as_deref(), } .emit(); // Emit vault deposit event VaultDeposit { sender_id: sender_id.as_ref(), owner_id: receiver.as_ref(), asset: &asset, deposit_amount, shares, memo: memo.as_deref(), } .emit(); log!( "Deposited {} {} assets (fee: {}), minted {} shares for {}", deposit_amount.0, asset, total_fee.0, shares.0, receiver ); shares } pub fn internal_redeem_immediate( &mut self, shares: U128, asset: Asset, min_asset_amount: U128, owner_id: AccountId, receiver_id: AccountId, memo: Option, ) -> Promise { self.internal_validate_min_withdraw(shares); self.internal_validate_withdrawal_flow_cap(shares); // Convert shares to gross assets let gross_assets = self.convert_to_asset_amount(&asset, shares); // Calculate withdrawal fee let (total_fee, vault_fee, protocol_fee) = self.internal_calculate_withdrawal_fee(gross_assets, &asset); require!( gross_assets.0 >= total_fee.0, "Fee exceeds gross assets - invalid fee configuration" ); let net_assets = U128(gross_assets.0 - total_fee.0); // Validate slippage protection with net assets (after fee) require!( net_assets.0 >= min_asset_amount.0, format!( "Slippage exceeded: net assets {} less than minimum {}", net_assets.0, min_asset_amount.0 ) ); // Check sufficient balance (need gross assets in vault) let vault_balance = self.get_vault_balance(&asset); require!( vault_balance.available_amount.0 >= net_assets.0, "Insufficient liquidity, wait for liquidity or use request redeem" ); // Burn shares self.token.internal_withdraw(&owner_id, shares.0); // Emit FT burn event FtBurn { owner_id: &owner_id, amount: shares, memo: memo.as_deref(), } .emit(); // Update asset balance (remove net assets) self.internal_update_asset_balance(&asset, net_assets, false); self.internal_update_withdrawal_flow(shares); // Emit vault withdrawal event VaultWithdraw { owner_id: owner_id.as_ref(), receiver_id: receiver_id.as_ref(), asset: &asset, shares, withdraw_amount: net_assets, // Event shows net assets memo: memo.as_deref(), } .emit(); log!( "Redeemed {} shares for {} {} assets (fee: {}, net: {}) for {}", shares.0, gross_assets.0, asset, total_fee.0, net_assets.0, receiver_id ); // Create transfer promise with callback (transfer net assets to user) let promise = asset.create_transfer_promise(receiver_id.clone(), net_assets, memo.clone()); // Chain callback to handle transfer success/failure promise.then( crate::types::ext_self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_CALLBACK) .redeem_transfer_callback( None, // No operation_id for immediate redeems owner_id.clone(), receiver_id.clone(), asset.clone(), shares, net_assets, // Callback tracks net assets transferred vault_fee, protocol_fee, memo.clone(), ), ) } pub fn internal_request_deposit_async( &mut self, asset: Asset, deposit_amount: U128, min_shares: U128, sender_id: AccountId, receiver_id: AccountId, memo: Option, ) -> u64 { self.internal_validate_min_deposit(deposit_amount, &asset); self.internal_validate_deposit_slippage(deposit_amount, &asset, min_shares); self.internal_validate_tvl_cap(deposit_amount, &asset); let deposit = Deposit::new( asset.clone(), deposit_amount, min_shares, sender_id.clone(), receiver_id, memo, ); let operation_id = self.state.teller_state.add_deposit(deposit); // Update pending balance self.state .asset_balances .entry(asset.clone()) .and_modify(|balance| { balance.pending_deposit.0 = balance .pending_deposit .0 .checked_add(deposit_amount.0) .expect("Pending deposit balance overflow"); }) .or_insert_with(|| { let mut balance = VaultBalance::default(); balance.pending_deposit = deposit_amount; balance }); // Emit deposit queued event DepositQueued { operation_id, owner_id: &sender_id, asset: &asset, queued_amount: deposit_amount, } .emit(); log!( "Created deposit request {} for {} assets", operation_id, deposit_amount.0 ); operation_id } pub fn internal_request_redeem_async( &mut self, shares: U128, asset: Asset, min_asset_amount: U128, owner_id: AccountId, receiver_id: AccountId, memo: Option, ) -> u64 { self.internal_validate_min_withdraw(shares); self.internal_validate_redeem_slippage(shares, &asset, min_asset_amount); // Burn shares immediately self.token.internal_withdraw(&owner_id, shares.0); // Emit FT burn event FtBurn { owner_id: &owner_id, amount: shares, memo: memo.as_deref(), } .emit(); let current_time = env::block_timestamp() as u128; let activation_time = U128(current_time + self.vault_config.get_redeem_delay()); let mut withdraw = Withdraw::new( asset.clone(), shares, min_asset_amount, owner_id.clone(), receiver_id, memo, activation_time, ); // Auto-confirm if redeem_confirmation == false if !self.vault_config.redeem_confirmation { let current_share_price = self.get_share_price_in_asset(&asset); withdraw.confirmed = true; withdraw.confirmed_share_price = Some(current_share_price); withdraw.confirmed_at_timestamp = Some(U128(current_time)); } let operation_id = self.state.teller_state.add_withdraw(withdraw); // Emit redeem queued event RedeemQueued { operation_id, owner_id: &owner_id, asset: &asset, shares, } .emit(); log!( "Created redeem request {} for {} shares", operation_id, shares.0 ); operation_id } /// Create a transfer promise with async redeem callback pub fn internal_transfer_asset_with_async_redeem_callback( &self, operation_id: u64, owner_id: AccountId, receiver_id: AccountId, asset: Asset, shares: U128, asset_amount: U128, vault_fee: U128, protocol_fee: U128, min_asset_amount: U128, memo: Option, activation_timestamp_nanosec: U128, confirmed_share_price: U128, ) -> Promise { let transfer_promise = asset.create_transfer_promise(receiver_id.clone(), asset_amount, memo.clone()); // Chain with callback to handle success/failure transfer_promise.then( crate::types::ext_self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_CALLBACK) .async_redeem_transfer_callback( operation_id, owner_id, receiver_id, asset, shares, asset_amount, vault_fee, protocol_fee, min_asset_amount, memo, activation_timestamp_nanosec, confirmed_share_price, ), ) } /// Create a transfer promise with deposit rejection callback pub fn internal_transfer_asset_with_deposit_rejection_callback( &self, operation_id: u64, owner_id: AccountId, asset: Asset, asset_amount: U128, min_shares: U128, receiver_id: AccountId, memo: Option, reason: String, ) -> Promise { let transfer_promise = asset.create_transfer_promise(owner_id.clone(), asset_amount, memo.clone()); // Chain with callback to handle success/failure transfer_promise.then( crate::types::ext_self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_CALLBACK) .deposit_rejection_transfer_callback( operation_id, owner_id, asset, asset_amount, min_shares, receiver_id, memo, reason, ), ) } pub fn internal_convert_amount_to_base_asset(&self, amount: U128, asset: &Asset) -> U128 { // If the asset is already the base asset, no conversion needed if asset == &self.base_asset { return amount; } // Get share prices for both assets let asset_rate = self.get_share_price_in_asset(asset); let base_asset_rate = self.get_share_price_in_asset(&self.base_asset); // If either rate is 0, we can't convert if asset_rate.0 == 0 || base_asset_rate.0 == 0 { return U128(0); } // Convert to base asset equivalent // Formula: base_amount = (amount * asset_rate) / base_asset_rate let result = mul_div(amount.0, base_asset_rate.0, asset_rate.0, Rounding::Down); U128(result) } /// Process deposit from xx_on_transfer (both FT and MT) pub fn internal_process_deposit( &mut self, sender_id: AccountId, asset: Asset, amount: U128, msg: DepositMessage, ) { let min_shares = msg.min_shares; let receiver_id = msg.receiver_id.unwrap_or_else(|| sender_id.clone()); if msg.is_request { // Process as async request let operation_id = self.internal_request_deposit_async( asset, amount, min_shares, sender_id, receiver_id, msg.memo, ); log!( "Deposit processed as async request. Operation ID: {}", operation_id ); return; } // Validate sync deposit size limit require!( amount.0 <= self.vault_config.max_sync_deposit.0, "Deposit amount exceeds max sync deposit limit" ); // Process immediately (sync deposit) let shares = self.internal_deposit_immediate( asset, amount, min_shares, sender_id, receiver_id, msg.memo, ); log!( "Processed immediate deposit: {} tokens -> {} shares", amount.0, shares.0 ); } /// Internal utility to convert assets to shares with specific share price pub fn internal_convert_to_shares_with_rate( &self, asset_amount: U128, share_price: u128, ) -> U128 { require!(share_price != 0, "Invalid share price"); if asset_amount.0 == 0 { return U128(0); } let share_price_scale = 10u128.pow(self.vault_config.share_price_decimals as u32); let extra_decimal_scale = 10u128.pow(self.vault_config.extra_decimals as u32); // Formula: shares = (assets * extra_decimal_scale * share_price_scale) / share_price let scale_product = extra_decimal_scale .checked_mul(share_price_scale) .expect("Decimal scale overflow: share_price_decimals + extra_decimals too large"); let result = mul_div(asset_amount.0, scale_product, share_price, Rounding::Down); U128(result) } /// Internal utility to convert shares to assets with specific share price pub fn internal_convert_to_asset_amount_with_rate( &self, shares: U128, share_price: u128, ) -> U128 { require!(share_price != 0, "Invalid share price"); if shares.0 == 0 { return U128(0); } let share_price_scale = 10u128.pow(self.vault_config.share_price_decimals as u32); let extra_decimal_scale = 10u128.pow(self.vault_config.extra_decimals as u32); // Formula: assets = (shares * share_price) / (share_price_scale * extra_decimal_scale) let scale_product = share_price_scale .checked_mul(extra_decimal_scale) .expect("Decimal scale overflow: share_price_decimals + extra_decimals too large"); let result = mul_div(shares.0, share_price, scale_product, Rounding::Down); U128(result) } /// Calculate deposit fee and split between vault and protocol /// Returns: (total_fee, vault_fee, protocol_fee) pub fn internal_calculate_deposit_fee( &self, deposit_amount: U128, asset: &Asset, ) -> (U128, U128, U128) { let fee_bps = *self.state.deposit_fees.get(asset).unwrap_or(&0); if fee_bps == 0 || deposit_amount.0 == 0 { return (U128(0), U128(0), U128(0)); } // Calculate total fee: fee = (assets * fee_bps) / 10000 let total_fee = mul_div( deposit_amount.0, fee_bps as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Get protocol cut (defaults to 0 if not set) let protocol_cut_bps = *self .state .protocol_deposit_fee_cuts .get(asset) .unwrap_or(&0); // Calculate protocol fee: protocol_fee = (total_fee * protocol_cut_bps) / 10000 let protocol_fee = mul_div( total_fee, protocol_cut_bps as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Vault gets the remainder let vault_fee = total_fee - protocol_fee; (U128(total_fee), U128(vault_fee), U128(protocol_fee)) } /// Calculate withdrawal fee and split between vault and protocol /// Returns: (total_fee, vault_fee, protocol_fee) pub fn internal_calculate_withdrawal_fee( &self, withdraw_amount: U128, asset: &Asset, ) -> (U128, U128, U128) { let fee_bps = *self.state.withdrawal_fees.get(asset).unwrap_or(&0); if fee_bps == 0 || withdraw_amount.0 == 0 { return (U128(0), U128(0), U128(0)); } // Calculate total fee: fee = (assets * fee_bps) / 10000 let total_fee = mul_div( withdraw_amount.0, fee_bps as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Get protocol cut (defaults to 0 if not set) let protocol_cut_bps = *self .state .protocol_withdrawal_fee_cuts .get(asset) .unwrap_or(&0); // Calculate protocol fee: protocol_fee = (total_fee * protocol_cut_bps) / 10000 let protocol_fee = mul_div( total_fee, protocol_cut_bps as u128, BASIS_POINTS_DIVISOR, Rounding::Up, ); // Vault gets the remainder let vault_fee = total_fee - protocol_fee; (U128(total_fee), U128(vault_fee), U128(protocol_fee)) } /// Accumulate fees in state pub fn internal_accumulate_fees(&mut self, asset: &Asset, vault_fee: U128, protocol_fee: U128) { if vault_fee.0 > 0 { self.state .fees_owed .entry(asset.clone()) .and_modify(|fees| fees.0 += vault_fee.0) .or_insert(vault_fee); } if protocol_fee.0 > 0 { self.state .protocol_fees_owed .entry(asset.clone()) .and_modify(|fees| fees.0 += protocol_fee.0) .or_insert(protocol_fee); } } /// Add claimable assets to a user's balance pub fn internal_add_claimable_asset_amount( &mut self, account_id: &AccountId, asset: &Asset, amount: U128, ) { if amount.0 == 0 { return; } // decrease available_balance (amount is now reserved to processed redeem for claim) self.internal_update_asset_balance(asset, amount, false); // Get or create the user's asset map using entry API let user_assets = self .state .user_claimable_assets .entry(account_id.clone()) .or_insert_with(|| { let account_hash = env::sha256(account_id.as_bytes()); LookupMap::new(crate::StorageKey::UserClaimableAssetMap { account_hash }) }); // Update balance using entry API user_assets .entry(asset.clone()) .and_modify(|current| current.0 += amount.0) .or_insert(amount); } /// Subtract claimable assets from a user's balance pub fn internal_subtract_claimable_asset_amount( &mut self, account_id: &AccountId, asset: &Asset, amount: U128, ) { if amount.0 == 0 { return; } // Get mutable access to the user's asset map (must exist) if let Some(user_assets) = self.state.user_claimable_assets.get_mut(account_id) { // Check balance and calculate new value let should_remove = if let Some(current) = user_assets.get_mut(asset) { require!(current.0 >= amount.0, "Insufficient claimable balance"); let new_balance = current.0 - amount.0; if new_balance == 0 { true // Will remove after reference is dropped } else { *current = U128(new_balance); false } } else { env::panic_str("Asset not found in claimable balance"); }; // Remove asset entry if balance reached zero if should_remove { user_assets.remove(asset); } } else { env::panic_str("User has no claimable assets"); } } /// Get claimable assets for a user and asset pub fn internal_get_claimable_asset_amount( &self, account_id: &AccountId, asset: &Asset, ) -> U128 { self.state .user_claimable_assets .get(account_id) .and_then(|user_assets| user_assets.get(asset)) .unwrap_or(&U128(0)) .clone() } /// Move assets directly from pending_deposit to claimable balance /// Used for deposit rejections where assets move: pending → claimable pub fn internal_move_pending_to_claimable( &mut self, account_id: &AccountId, asset: &Asset, amount: U128, ) { if amount.0 == 0 { return; } // Update asset balance: decrease pending_deposit only if let Some(balance) = self.state.asset_balances.get_mut(asset) { balance.pending_deposit.0 = balance .pending_deposit .0 .checked_sub(amount.0) .expect("Accounting error: pending_deposit underflow"); } else { panic!("Accounting error: attempting to move pending deposits for non-existent asset balance"); } // Add to user's claimable balance using entry API let user_assets = self .state .user_claimable_assets .entry(account_id.clone()) .or_insert_with(|| { let account_hash = env::sha256(account_id.as_bytes()); LookupMap::new(crate::StorageKey::UserClaimableAssetMap { account_hash }) }); // Update balance using entry API user_assets .entry(asset.clone()) .and_modify(|current| current.0 += amount.0) .or_insert(amount); } /// Validate redeems and calculate total assets needed /// Returns total net assets required fn internal_validate_and_calculate_redeems( &self, asset: &Asset, operation_ids: &[u64], current_time: u128, ) -> u128 { let mut total_needed = 0u128; for &operation_id in operation_ids { let withdraw = self .state .teller_state .get_withdraw(operation_id) .unwrap_or_else(|| { env::panic_str(&format!("Redeem request {} not found", operation_id)) }); // Validate confirmed require!( withdraw.confirmed, format!("Redeem request {} is not confirmed", operation_id) ); // Validate asset matches require!( &withdraw.asset == asset, format!( "Redeem request {} asset mismatch: expected {}, got {}", operation_id, asset, withdraw.asset ) ); // Validate activation timestamp require!( current_time >= withdraw.activation_timestamp_nanosec.0, format!( "Redeem request {} activation timestamp not yet reached", operation_id ) ); // Calculate net assets needed for this redeem let share_price = withdraw.confirmed_share_price.unwrap_or_else(|| { env::panic_str(&format!( "Confirmed redeem request {} missing share price", operation_id )) }); let gross_assets = self.internal_convert_to_asset_amount_with_rate(withdraw.shares, share_price.0); let (total_fee, _, _) = self.internal_calculate_withdrawal_fee(gross_assets, asset); let net_assets = gross_assets.0 - total_fee.0; // Validate slippage protection require!( net_assets >= withdraw.min_asset_amount.0, format!( "Redeem request {} failed: net assets {} less than minimum {}", operation_id, net_assets, withdraw.min_asset_amount.0 ) ); total_needed = total_needed .checked_add(net_assets) .expect("Total needed overflow"); } total_needed } /// Process pending redeems using transferred funds /// Returns the leftover amount after processing all redeems pub fn internal_process_redeems_with_funds( &mut self, asset: &Asset, transferred_amount: U128, operation_ids: Vec, ) { require!(!operation_ids.is_empty(), "No operation IDs provided"); self.internal_validate_not_emergency_paused(); // First, add transferred funds to available_assets self.internal_update_asset_balance(asset, transferred_amount, true); let current_time = env::block_timestamp() as u128; // Validate all redeems and calculate total needed let total_needed = self.internal_validate_and_calculate_redeems(asset, &operation_ids, current_time); // Validate sufficient available assets (after adding transferred funds) let vault_balance = self.get_asset_balance(asset); require!( vault_balance.available_amount.0 >= total_needed, format!( "Insufficient available assets: have {}, needed {}", vault_balance.available_amount.0, total_needed ) ); // process all redeems for operation_id in operation_ids { let withdraw = self.state.teller_state.get_withdraw(operation_id).unwrap(); let share_price = withdraw.confirmed_share_price.unwrap(); let gross_asset_amount = self.internal_convert_to_asset_amount_with_rate(withdraw.shares, share_price.0); let (total_fee, vault_fee, protocol_fee) = self.internal_calculate_withdrawal_fee(gross_asset_amount, asset); let net_asset_amount = U128(gross_asset_amount.0 - total_fee.0); // Remove from pending let withdraw = self .state .teller_state .remove_withdraw(operation_id, &withdraw.owner_id) .unwrap_or_else(|| { env::panic_str(&format!("Failed to remove redeem request {}", operation_id)) }); // Accumulate fees self.internal_accumulate_fees(asset, vault_fee, protocol_fee); // Add to claimable (this decreases available_amount internally) self.internal_add_claimable_asset_amount(&withdraw.owner_id, asset, net_asset_amount); // Emit event RedeemProcessed { operation_id, owner_id: &withdraw.owner_id, receiver_id: &withdraw.receiver_id, asset, shares: withdraw.shares, gross_asset_amount, net_asset_amount, vault_fee, protocol_fee, share_price, } .emit(); log!( "Processed redeem request {} - {} shares for {} assets (fee: {}, net: {}) - ready for claiming", operation_id, withdraw.shares.0, gross_asset_amount.0, total_fee.0, net_asset_amount.0 ); } } /// Start the vault - transition from created to live state /// This method should be called by the owner (kernel) after initial setup pub fn internal_start_vault(&mut self) { // Validate vault can be started require!( !self.is_vault_live(), "Vault is already live and operational" ); // Ensure all accepted assets have share prices > 0 let all_assets = self.get_asset_union(); for asset in all_assets { let rate = self.state.share_prices.get(&asset).unwrap_or(&U128(0)); require!( rate.0 > 0, format!( "Share price not set or zero for asset {}. Please set share prices before starting the vault.", asset ) ); } let current_time = env::block_timestamp() as u128; // Set vault as live self.live_at = U128(current_time); // Initialize fee-related state with current base asset share price let base_share_price = self .state .share_prices .get(&self.base_asset) .expect("Base asset share price must be set") .0; // Set high water mark to initial share price self.state.highwatermark_rate = U128(base_share_price); // Initialize crystallization state self.state.last_crystallization_timestamp = U128(current_time); self.state.last_crystallization_share_price = U128(base_share_price); // Set initial share price update timestamp self.state.previous_share_price_update_in_nano = U128(current_time); log!( "Vault started successfully at timestamp {}. Base share price: {}, high water mark: {}", current_time, base_share_price, base_share_price ); } }