mod access_control; mod accountant; mod callback; mod dew_vault; mod events; mod flow_control; mod internal; mod teller; mod traits; mod types; use crate::events::AssetsClaimed; use crate::teller::TellerState; use crate::types::{ Deposit, FlowTracker, TellerOperation, TellerOperationWithId, VaultBalance, Withdraw, }; use common_data::asset::Asset; use common_data::basis_points::BasisPoints; use common_data::constants::GAS_FOR_CALLBACK; use common_data::math::{mul_div, Rounding}; use common_data::protocol_config::ProtocolConfig; use common_data::vault_config::*; use near_contract_standards::fungible_token::events::FtMint; use near_contract_standards::fungible_token::{ core::FungibleTokenCore, metadata::FungibleTokenMetadata, FungibleToken, }; use near_sdk::store::IterableMap; use near_sdk::{assert_one_yocto, bs58}; use std::collections::{HashMap, HashSet}; use near_sdk::{ env, json_types::U128, log, near, require, store::{IterableSet, LazyOption, LookupMap, LookupSet}, AccountId, Gas, NearToken, Promise, PromiseOrValue, }; /// Storage keys for collections to prevent storage collisions #[derive(near_sdk::BorshStorageKey, borsh::BorshSerialize)] pub enum StorageKey { FungibleToken, AssetBalances, SharePrices, AcceptedDepositAssets, AvailableRedeemAssets, StagedWasm, TellerState, WhitelistRegistry, BlacklistRegistry, DepositFees, WithdrawalFees, ProtocolDepositFeeCuts, ProtocolWithdrawalFeeCuts, FeesOwed, ProtocolFeesOwed, UserClaimableAssets, UserClaimableAssetMap { account_hash: Vec }, HighWaterMarkRate, FeesOwedVault, ProtocolFeesOwedVault, LastCrystallizationTimestamp, LastCrystallizationSharePrice, TotalShares, } /// Vault state structure containing all contract data #[near(serializers=[borsh])] pub struct VaultState { // -------------------- Accountant State -------------------- /// Asset -> VaultBalance pub asset_balances: LookupMap, /// Asset -> share price pub share_prices: LookupMap, // -------------------- Teller State -------------------- /// Teller State (Pending deposit and withdraws) pub teller_state: TellerState, // -------------------- Access Control -------------------- /// Optional allowlist registry contract (e.g., KYCed or partners addresses). pub whitelist_registry: Option>, /// Optional denylist registry contract. pub blacklist_registry: Option>, // -------------------- Flow Cap State -------------------- /// Deposit flow tracker for rate limiting (in base token units) pub deposit_flow_tracker: FlowTracker, /// Withdrawal flow tracker for rate limiting (in base token units) pub withdrawal_flow_tracker: FlowTracker, // -------------------- Emergency Controls -------------------- /// Emergency pause flag - when true, all deposits and redeems are disabled pub emergency_paused: bool, // -------------------- Fee State -------------------- /// Per-asset deposit fees in bps (defaults to 0 if not set) deposit_fees: LookupMap, /// Per-asset withdrawal fees in bps (defaults to 0 if not set) withdrawal_fees: LookupMap, /// Per-asset protocol cut for deposit fees in bps (defaults to 0 if not set) protocol_deposit_fee_cuts: LookupMap, /// Per-asset protocol cut for withdrawal fees in bps (defaults to 0 if not set) protocol_withdrawal_fee_cuts: LookupMap, /// Vault fees owed per asset (goes to fee_recipient) fees_owed: IterableMap, /// Protocol fees owed per asset (goes to protocol_fee_recipient) protocol_fees_owed: IterableMap, // -------------------- Claimable Assets State -------------------- /// User claimable assets: AccountId -> Asset -> claimable amount user_claimable_assets: LookupMap>, // -------------------- Fee Tracking State -------------------- /// High water mark share price for performance fee calculations pub highwatermark_rate: U128, /// Last crystallization timestamp for hurdle-based performance fees pub last_crystallization_timestamp: U128, /// Last crystallization share price for hurdle-based performance fees pub last_crystallization_share_price: U128, /// Accountant paused flag for safety during anomalies pub is_accountant_paused: bool, /// Previous share price update timestamp for time-based fee calculations pub previous_share_price_update_in_nano: U128, /// Total shares tracked from kernel for TVL calculations pub total_shares: U128, } impl VaultState { pub fn new() -> Self { Self { asset_balances: LookupMap::new(StorageKey::AssetBalances), share_prices: LookupMap::new(StorageKey::SharePrices), teller_state: TellerState::new(), whitelist_registry: None, blacklist_registry: None, deposit_flow_tracker: FlowTracker::default(), withdrawal_flow_tracker: FlowTracker::default(), emergency_paused: false, deposit_fees: LookupMap::new(StorageKey::DepositFees), withdrawal_fees: LookupMap::new(StorageKey::WithdrawalFees), protocol_deposit_fee_cuts: LookupMap::new(StorageKey::ProtocolDepositFeeCuts), protocol_withdrawal_fee_cuts: LookupMap::new(StorageKey::ProtocolWithdrawalFeeCuts), fees_owed: IterableMap::new(StorageKey::FeesOwed), protocol_fees_owed: IterableMap::new(StorageKey::ProtocolFeesOwed), user_claimable_assets: LookupMap::new(StorageKey::UserClaimableAssets), // Fee tracking state - initialized with defaults highwatermark_rate: U128(0), last_crystallization_timestamp: U128(0), last_crystallization_share_price: U128(0), is_accountant_paused: false, previous_share_price_update_in_nano: U128(0), total_shares: U128(0), } } } // Main vault contract implementing Dew Kernel integration #[near(contract_state)] pub struct Contract { /// NEP-141 fungible token for vault shares pub token: FungibleToken, /// Metadata for the vault shares token pub metadata: FungibleTokenMetadata, /// Authorized Dew Kernel contract that manages this vault pub owner_account_id: AccountId, /// Base asset, for calculations pub base_asset: Asset, /// Set of accepted assets for deposits pub accepted_deposit_assets: IterableSet, /// Set of assets available to receive from redeem pub available_redeem_assets: IterableSet, /// Vault configuration pub vault_config: VaultConfig, /// Vault state containing all contract data pub state: VaultState, /// fee recipient pub fee_recipient: AccountId, /// protocol account (for access control) pub protocol_account: AccountId, /// Protocol configuration for fee cuts pub protocol_config: ProtocolConfig, /// Timestamp when vault went live (0 = not live yet) pub live_at: U128, /// Staged contract code for upgrades pub staged_wasm: LazyOption>, /// Base58 hash of staged contract code pub staged_code_hash: Option, } impl Default for Contract { fn default() -> Self { Self::new( "placeholder.near".parse().unwrap(), Asset::new_fungible_token("usdc.near".parse().unwrap()), vec![Asset::new_fungible_token("usdc.near".parse().unwrap())], vec![Asset::new_fungible_token("usdc.near".parse().unwrap())], "DVS".to_string(), "Default Vault Shares".to_string(), 6, None, "placeholder.near".parse().unwrap(), "placeholder.near".parse().unwrap(), // protocol_account ProtocolConfig::default(), ) } } #[near] impl Contract { /// Initialize the vault contract with Dew Kernel integration #[init] pub fn new( owner_account_id: AccountId, base_asset: Asset, accepted_deposit_assets: Vec, available_redeem_assets: Vec, share_token_symbol: String, share_token_name: String, share_token_decimals: u8, share_token_icon: Option, fee_recipient: AccountId, protocol_account: AccountId, protocol_config: ProtocolConfig, ) -> Self { Self::new_with_config( owner_account_id, base_asset, accepted_deposit_assets, available_redeem_assets, share_token_symbol, share_token_name, share_token_decimals, share_token_icon, VaultConfig::default(), fee_recipient, protocol_account, protocol_config, ) } /// Initialize the vault contract with custom configuration #[init] pub fn new_with_config( owner_account_id: AccountId, base_asset: Asset, accepted_deposit_assets: Vec, available_redeem_assets: Vec, share_token_symbol: String, share_token_name: String, share_token_decimals: u8, share_token_icon: Option, vault_config: VaultConfig, fee_recipient: AccountId, protocol_account: AccountId, protocol_config: ProtocolConfig, ) -> Self { // Validate that base_asset is in deposit_assets require!( accepted_deposit_assets.contains(&base_asset), "Base asset must be in accepted deposit assets" ); // Not enforced for available_redeem_assets vault_config.validate(); let mut contract = Self { token: FungibleToken::new(StorageKey::FungibleToken), metadata: FungibleTokenMetadata { spec: "ft-1.0.0".to_string(), name: share_token_name, symbol: share_token_symbol, icon: share_token_icon, reference: None, reference_hash: None, decimals: share_token_decimals, }, owner_account_id, base_asset, accepted_deposit_assets: IterableSet::new(StorageKey::AcceptedDepositAssets), available_redeem_assets: IterableSet::new(StorageKey::AvailableRedeemAssets), vault_config, state: VaultState::new(), fee_recipient, protocol_account, protocol_config, live_at: U128(0), // Vault created but not live yet staged_wasm: LazyOption::new(StorageKey::StagedWasm, None), staged_code_hash: None, }; // Initialize assets and default share prices contract.initialize_assets(accepted_deposit_assets, available_redeem_assets); contract } #[init(ignore_state)] #[payable] pub fn reset_with_config( owner_account_id: AccountId, base_asset: Asset, accepted_deposit_assets: Vec, available_redeem_assets: Vec, share_token_symbol: String, share_token_name: String, share_token_icon: Option, share_token_decimals: u8, vault_config: VaultConfig, fee_recipient: AccountId, protocol_account: AccountId, ) -> Self { let old_contract: Contract = env::state_read().unwrap(); let mut old_contract_mut: Contract = env::state_read().unwrap(); if env::predecessor_account_id() == env::current_account_id() { // This is not typo // signer_account_id is used to ensure that only the key holder can reset the state // we also need to ensure that caller pay at least 1 yoctoNEAR to ensure that they key is actually a full access key assert!( env::signer_account_id() == env::current_account_id(), "This method must be called directly by the contract account, delegation is not allowed" ); assert_one_yocto(); } else { panic!("Only owner or contract account can reset the state"); } require!( accepted_deposit_assets.contains(&base_asset), "Base asset must be in accepted deposit assets" ); vault_config.validate(); old_contract_mut.metadata = FungibleTokenMetadata { spec: "ft-1.0.0".to_string(), name: share_token_name, symbol: share_token_symbol, icon: share_token_icon, reference: None, reference_hash: None, decimals: share_token_decimals, }; old_contract_mut.owner_account_id = owner_account_id; old_contract_mut.base_asset = base_asset.clone(); old_contract .accepted_deposit_assets .iter() .cloned() .for_each(|asset| { if !accepted_deposit_assets.contains(&asset) { old_contract_mut.accepted_deposit_assets.remove(&asset); } }); old_contract .available_redeem_assets .iter() .cloned() .for_each(|asset| { if !available_redeem_assets.contains(&asset) { old_contract_mut.available_redeem_assets.remove(&asset); } }); old_contract_mut.vault_config = vault_config; old_contract_mut.fee_recipient = fee_recipient; old_contract_mut.protocol_account = protocol_account; // state should not be reset old_contract_mut.live_at = U128(0); // Reset to not live old_contract_mut.staged_wasm.set(None); old_contract_mut.staged_code_hash = None; old_contract_mut.initialize_assets(accepted_deposit_assets, available_redeem_assets); old_contract_mut } /// Caller can be anyone but uploaded code mush match codehash set by owner pub fn upload_contract(&mut self) { let staged_hash = self .staged_code_hash .clone() .expect("No staged contract code hash set"); let code = env::input().expect("Error: No input"); require!(!code.is_empty(), "Error: Empty contract code"); let hash = bs58::encode(env::sha256(&code)).into_string(); require!(staged_hash == hash, "Error: Contract code hash mismatch"); self.staged_wasm.set(Some(code)); } #[private] #[init(ignore_state)] pub fn migrate() -> Self { let mut old_contract: Contract = env::state_read().expect("read state failed"); old_contract.staged_wasm.set(None); old_contract.staged_code_hash = None; old_contract } /// Initialize accepted assets with default share prices fn initialize_assets( &mut self, accepted_deposit_assets: Vec, available_redeem_assets: Vec, ) { let default_rate = U128(10u128.pow(self.vault_config.share_price_decimals as u32)); // Insert deposit assets for asset in &accepted_deposit_assets { self.accepted_deposit_assets.insert(asset.clone()); } // Insert redeem assets for asset in &available_redeem_assets { self.available_redeem_assets.insert(asset.clone()); } // Initialize share prices and balances for all assets in the union let asset_union = self.get_asset_union(); for asset in asset_union { self.state .asset_balances .insert(asset.clone(), VaultBalance::default()); self.state.share_prices.insert(asset, default_rate); } } /// Get the vault metadata pub fn get_metadata(&self) -> &FungibleTokenMetadata { &self.metadata } /// Get the authorized Dew Kernel contract pub fn get_owner_account_id(&self) -> &AccountId { &self.owner_account_id } /// Check if the vault is live (operational) pub fn is_vault_live(&self) -> bool { self.live_at.0 > 0 } /// Get vault live timestamp pub fn get_live_at(&self) -> U128 { self.live_at } /// Get all accepted deposit assets pub fn get_accepted_deposit_assets(&self) -> Vec { self.accepted_deposit_assets.iter().cloned().collect() } /// Get all available redeem assets pub fn get_available_redeem_assets(&self) -> Vec { self.available_redeem_assets.iter().cloned().collect() } /// Get union of all assets (accepted deposits + available redeems) pub fn get_asset_union(&self) -> Vec { let mut union = HashSet::new(); // Add all deposit assets for asset in self.accepted_deposit_assets.iter() { union.insert(asset.clone()); } // Add all redeem assets for asset in self.available_redeem_assets.iter() { union.insert(asset.clone()); } union.into_iter().collect() } /// Get vault configuration pub fn get_vault_config(&self) -> &VaultConfig { &self.vault_config } /// Get the base asset used for calculations pub fn get_base_asset(&self) -> &Asset { &self.base_asset } /// Get protocol configuration pub fn get_protocol_config(&self) -> &ProtocolConfig { &self.protocol_config } /// Get all pending deposits pub fn get_all_pending_deposits(&self) -> Vec<(u64, Deposit)> { self.state.teller_state.get_all_pending_deposits() } /// Get all pending redeems pub fn get_all_pending_redeems(&self) -> Vec<(u64, Withdraw)> { self.state.teller_state.get_all_pending_withdraws() } pub fn get_account_pending_redeems(&self, account_id: AccountId) -> Vec { self.state .teller_state .get_user_withdraw_ids(&account_id) .iter() .filter_map(|operation_id| { self.state .teller_state .get_withdraw(*operation_id) .map(|withdraw| TellerOperationWithId { operation_id: *operation_id, operation: TellerOperation::Withdraw(withdraw), }) }) .collect() } pub fn get_accountant_data(&self, assets: Vec) -> serde_json::Value { serde_json::json!({ "total_supply": self.ft_total_supply(), "pending_redeems": self.get_all_pending_redeems(), "vault_balances": assets .iter() .map(|asset| serde_json::json!({ "asset": asset, "balance": self.get_asset_balance(asset), })) .collect::>(), }) } pub fn get_total_confirmed_pending_redeem_assets(&self) -> HashMap { let pending_redeems = self.state.teller_state.get_all_pending_withdraws(); pending_redeems .iter() .filter(|(_, withdraw)| withdraw.confirmed) .map(|(_, withdraw)| { 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, _, _) = self.internal_calculate_withdrawal_fee(gross_asset_amount, &withdraw.asset); ( withdraw.asset.clone(), gross_asset_amount.0.checked_sub(total_fee.0), ) }) // filter out all invalid confirmed pending redeems that admin are forced to reject .filter_map(|(withdraw_asset, net_asset_amount)| { if net_asset_amount.is_some() { Some((withdraw_asset, net_asset_amount.unwrap())) } else { None } }) .fold( HashMap::new(), |mut acc, (withdraw_asset, net_asset_amount)| { let original_amount = acc.get(&withdraw_asset).cloned().unwrap_or(0u128); acc.insert(withdraw_asset, original_amount + net_asset_amount); acc }, ) } pub fn get_total_unconfirmed_pending_redeem_shares(&self) -> U128 { let pending_redeems = self.state.teller_state.get_all_pending_withdraws(); let total_shares: u128 = pending_redeems .iter() .map(|(_, withdraw)| { if !withdraw.confirmed { withdraw.shares.0 } else { 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, _, _) = self.internal_calculate_withdrawal_fee(gross_asset_amount, &withdraw.asset); let opt_net_asset_amount = gross_asset_amount.0.checked_sub(total_fee.0); // if net asset is none, it means admin are forced to reject this pending redeem if opt_net_asset_amount.is_none() { withdraw.shares.0 } else { 0 } } }) .sum(); U128(total_shares) } /// Get share price for an asset pub fn get_share_price_in_asset(&self, asset: &Asset) -> U128 { *self.state.share_prices.get(asset).unwrap_or(&U128(0)) } /// Get asset balance for an asset pub fn get_asset_balance(&self, asset: &Asset) -> VaultBalance { self.state .asset_balances .get(asset) .cloned() .unwrap_or_default() } /// Check if an asset is accepted for deposits by this vault pub fn is_asset_accepted_for_deposit(&self, asset: &Asset) -> bool { self.accepted_deposit_assets.contains(asset) } /// Check if an asset is available for redemption by this vault pub fn is_asset_available_for_redeem(&self, asset: &Asset) -> bool { self.available_redeem_assets.contains(asset) } /// Get the scale factor for share price calculations pub fn get_share_price_scale(&self) -> u128 { 10u128.pow(self.vault_config.share_price_decimals as u32) } /// Get the scale factor for extra decimals pub fn get_extra_decimal_scale(&self) -> u128 { 10u128.pow(self.vault_config.extra_decimals as u32) } /// Get current total value locked (TVL) in base asset terms pub fn get_tvl_in_base_asset(&self) -> U128 { let base_asset_share_price = self.get_share_price_in_asset(&self.base_asset); if base_asset_share_price.0 == 0 { return U128(0); } self.internal_calculate_total_asset_amount(base_asset_share_price.0) } /// Get remaining capacity before TVL cap is reached (in base asset terms) pub fn get_tvl_capacity_remaining(&self) -> Option { self.vault_config.tvl_cap.map(|cap| { let current_tvl = self.get_tvl_in_base_asset(); U128(cap.0.saturating_sub(current_tvl.0)) }) } #[payable] pub fn redeem( &mut self, asset: Asset, shares: U128, min_asset_amount: U128, receiver_id: Option, memo: Option, ) -> PromiseOrValue { assert_one_yocto(); require!(shares.0 > 0, "Shares must be positive"); self.internal_validate_asset_for_redeem(&asset); self.internal_validate_not_emergency_paused(); self.internal_validate_share_price_freshness(); self.internal_validate_sync_redeem_size(shares); let redeemer = env::predecessor_account_id(); let receiver = receiver_id.unwrap_or(redeemer.clone()); self.internal_validate_transfer_permissions(&redeemer, &receiver); let promise = self.internal_redeem_immediate( shares, asset, min_asset_amount, redeemer, receiver, memo, ); PromiseOrValue::Promise(promise) } #[payable] pub fn request_redeem( &mut self, asset: Asset, shares: U128, min_asset_amount: U128, receiver_id: Option, memo: Option, ) -> u64 { assert_one_yocto(); require!(shares.0 > 0, "Shares must be positive"); self.internal_validate_asset_for_redeem(&asset); self.internal_validate_not_emergency_paused(); self.internal_validate_share_price_freshness(); let redeemer = env::predecessor_account_id(); let receiver = receiver_id.unwrap_or(redeemer.clone()); self.internal_validate_transfer_permissions(&redeemer, &receiver); // Accept any size redeem for async processing self.internal_request_redeem_async( shares, asset, min_asset_amount, redeemer, receiver, memo, ) } /// Cancel a pending deposit request /// Only the deposit owner can cancel their own request #[payable] pub fn cancel_request_deposit(&mut self, operation_id: u64) -> Promise { assert_one_yocto(); let caller = env::predecessor_account_id(); let deposit = self .state .teller_state .get_deposit(operation_id) .unwrap_or_else(|| { env::panic_str(&format!("Deposit request {} not found", operation_id)) }); require!( caller == deposit.owner_id, "Only the deposit owner can cancel this request" ); // Remove deposit from teller state (includes user tracking cleanup) let deposit = self .state .teller_state .remove_deposit(operation_id, &deposit.owner_id) .unwrap_or_else(|| { env::panic_str(&format!( "Failed to remove deposit request {}", operation_id )) }); // Update vault balance: decrease pending_deposit (assets will be returned to user) let mut balance = self.get_asset_balance(&deposit.asset); balance.pending_deposit.0 = balance .pending_deposit .0 .checked_sub(deposit.deposit_amount.0) .expect("Accounting error: VaultBalance pending_deposit < deposit amount"); self.state .asset_balances .insert(deposit.asset.clone(), balance); log!( "User {} cancelled deposit request {}: {} {} assets", deposit.owner_id, operation_id, deposit.deposit_amount.0, deposit.asset ); // Transfer assets back to the owner with callback for error handling self.internal_transfer_asset_with_deposit_rejection_callback( operation_id, deposit.owner_id.clone(), deposit.asset.clone(), deposit.deposit_amount, deposit.min_shares, deposit.receiver_id.clone(), deposit.memo.clone(), "User-initiated cancellation".to_string(), ) } /// Cancel a pending redeem request /// Only unconfirmed withdrawals can be cancelled /// Only the redeem shares owner can cancel their own request #[payable] pub fn cancel_request_redeem(&mut self, operation_id: u64) { assert_one_yocto(); let caller = env::predecessor_account_id(); // Get withdraw from teller state let withdraw = self .state .teller_state .get_withdraw(operation_id) .unwrap_or_else(|| { env::panic_str(&format!("Redeem request {} not found", operation_id)) }); require!( caller == withdraw.owner_id, "Only the redeem owner can cancel this request" ); require!( !withdraw.confirmed, "Cannot cancel confirmed withdrawal. Withdrawal must be unconfirmed." ); // Re-mint shares back to the original owner self.token .internal_deposit(&withdraw.owner_id, withdraw.shares.0); FtMint { owner_id: &withdraw.owner_id, amount: withdraw.shares, memo: Some("Shares re-minted due to user-cancelled redeem request"), } .emit(); log!( "User {} cancelled redeem request {}: {} shares for {} {}", withdraw.owner_id, operation_id, withdraw.shares.0, withdraw.asset, withdraw.asset ); self.state .teller_state .remove_withdraw(operation_id, &withdraw.owner_id); } /// Claim all processed redeem assets for a specific asset type pub fn claim_assets(&mut self, asset: Asset) -> Promise { let claimer = env::predecessor_account_id(); // Get claimable amount let claim_amount = self.internal_get_claimable_asset_amount(&claimer, &asset); require!(claim_amount.0 > 0, "No claimable assets for this asset"); // Subtract from claimable balance self.internal_subtract_claimable_asset_amount(&claimer, &asset, claim_amount); // Emit AssetsClaimed event AssetsClaimed { claimer: &claimer, asset: &asset, amount: claim_amount, } .emit(); // Create transfer promise with callback for error handling let transfer_promise = asset.create_transfer_promise(claimer.clone(), claim_amount, None); // Chain callback to handle transfer success/failure transfer_promise.then( crate::types::ext_self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_CALLBACK) .claim_assets_callback(claimer, asset, claim_amount), ) } pub fn convert_to_shares(&self, asset: &Asset, asset_amount: U128) -> U128 { let rate = self.get_share_price_in_asset(asset); if rate.0 == 0 { return U128(0); } self.internal_convert_to_shares_with_rate(asset_amount, rate.0) } pub fn convert_to_asset_amount(&self, asset: &Asset, shares: U128) -> U128 { let rate = self.get_share_price_in_asset(asset); if rate.0 == 0 { return U128(0); } self.internal_convert_to_asset_amount_with_rate(shares, rate.0) } pub fn get_vault_balance(&self, asset: &Asset) -> VaultBalance { self.get_asset_balance(asset) } pub fn preview_deposit_shares(&self, asset: &Asset, deposit_amount: U128) -> U128 { // 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.saturating_sub(total_fee.0)); // Convert net assets (after fee) to shares self.convert_to_shares(asset, net_deposit_amount) } pub fn preview_redeem_asset_amount(&self, asset: &Asset, shares: U128) -> U128 { // Convert shares to gross assets let gross_asset_amount = self.convert_to_asset_amount(asset, shares); // Calculate withdrawal fee let (total_fee, _vault_fee, _protocol_fee) = self.internal_calculate_withdrawal_fee(gross_asset_amount, asset); // Return net assets (after fee) U128(gross_asset_amount.0.saturating_sub(total_fee.0)) } pub fn max_redeem_shares(&self, owner_id: &AccountId) -> U128 { self.token.ft_balance_of(owner_id.clone()) } pub fn max_deposit_amount(&self, asset: &Asset) -> U128 { // If no TVL cap is set, deposits are unlimited let Some(remaining_capacity) = self.get_tvl_capacity_remaining() else { return U128(u128::MAX); }; // If cap is reached, no more deposits allowed if remaining_capacity.0 == 0 { return U128(0); } // If the requested asset is the base asset, return remaining capacity directly if asset == &self.base_asset { return remaining_capacity; } // Convert remaining capacity (base asset) → shares → requested asset let shares = self.convert_to_shares(&self.base_asset, remaining_capacity); self.convert_to_asset_amount(asset, shares) } // === Teller Query Methods === /// Get all unconfirmed withdrawal requests (ready for confirmation) pub fn get_unconfirmed_withdraws(&self) -> Vec<(u64, Withdraw)> { self.state.teller_state.get_unconfirmed_withdraws() } /// Get all confirmed withdrawal requests (ready for processing) pub fn get_confirmed_withdraws(&self) -> Vec<(u64, Withdraw)> { self.state.teller_state.get_confirmed_withdraws() } /// Check if a withdrawal is confirmed pub fn is_withdraw_confirmed(&self, operation_id: u64) -> bool { self.state.teller_state.is_withdraw_confirmed(operation_id) } /// Get the locked share price for a confirmed withdrawal pub fn get_confirmed_share_price(&self, operation_id: u64) -> Option { self.state .teller_state .get_confirmed_share_price(operation_id) } /// Get withdrawal details for a specific operation ID pub fn get_withdraw_info(&self, operation_id: u64) -> Option { self.state.teller_state.get_withdraw(operation_id) } /// Get deposit details for a specific operation ID pub fn get_deposit_info(&self, operation_id: u64) -> Option { self.state.teller_state.get_deposit(operation_id) } /// Get emergency pause status - true if vault is emergency paused pub fn is_emergency_paused(&self) -> bool { self.state.emergency_paused } /// Get deposit fee for a specific asset (returns 0 if not set) pub fn get_deposit_fee_by_asset(&self, asset: &Asset) -> BasisPoints { *self.state.deposit_fees.get(asset).unwrap_or(&0) } /// Get withdrawal fee for a specific asset (returns 0 if not set) pub fn get_withdrawal_fee_by_asset(&self, asset: &Asset) -> BasisPoints { *self.state.withdrawal_fees.get(asset).unwrap_or(&0) } /// Get protocol deposit fee cut for a specific asset (returns 0 if not set) pub fn get_protocol_deposit_fee_cut(&self, asset: &Asset) -> BasisPoints { *self .state .protocol_deposit_fee_cuts .get(asset) .unwrap_or(&0) } /// Get protocol withdrawal fee cut for a specific asset (returns 0 if not set) pub fn get_protocol_withdrawal_fee_cut(&self, asset: &Asset) -> BasisPoints { *self .state .protocol_withdrawal_fee_cuts .get(asset) .unwrap_or(&0) } /// Get accumulated vault fees owed for a specific asset (returns 0 if not set) pub fn get_fees_owed_for_asset(&self, asset: &Asset) -> U128 { *self.state.fees_owed.get(asset).unwrap_or(&U128(0)) } /// Get accumulated vault fees owed for all assets pub fn get_all_fees_owed(&self) -> Vec<(Asset, U128)> { self.state .fees_owed .iter() .map(|(asset, amount)| (asset.clone(), *amount)) .collect() } /// Get accumulated protocol fees owed for a specific asset (returns 0 if not set) pub fn get_protocol_fees_owed_for_asset(&self, asset: &Asset) -> U128 { *self.state.protocol_fees_owed.get(asset).unwrap_or(&U128(0)) } /// Get accumulated protocol fees owed for all assets pub fn get_all_protocol_fees_owed(&self) -> Vec<(Asset, U128)> { self.state .protocol_fees_owed .iter() .map(|(asset, amount)| (asset.clone(), *amount)) .collect() } /// Get fee recipient address pub fn get_fee_recipient(&self) -> &AccountId { &self.fee_recipient } /// Get protocol account address pub fn get_protocol_account(&self) -> &AccountId { &self.protocol_account } /// Get last share price update timestamp pub fn get_last_share_price_update(&self) -> U128 { self.state.previous_share_price_update_in_nano } /// Get whitelist status for private vault mode pub fn is_whitelist_enabled(&self) -> bool { self.vault_config.is_private } /// Get blacklist status pub fn is_blacklist_enabled(&self) -> bool { self.vault_config.blacklist_enabled } /// Check if an account is whitelisted (returns true if no whitelist exists) pub fn is_whitelisted(&self, account_id: &AccountId) -> bool { match &self.state.whitelist_registry { None => true, // No whitelist means everyone is allowed Some(registry) => registry.contains(account_id), } } /// Check if an account is blacklisted (returns false if no blacklist exists) pub fn is_blacklisted(&self, account_id: &AccountId) -> bool { match &self.state.blacklist_registry { None => false, // No blacklist means no one is blocked Some(registry) => registry.contains(account_id), } } /// Get deposit flow tracker accumulated amount pub fn get_deposit_flow_accumulated(&self) -> U128 { self.state.deposit_flow_tracker.accumulated_amount } /// Get deposit flow tracker window start timestamp pub fn get_deposit_flow_window_start(&self) -> u64 { self.state.deposit_flow_tracker.window_start_timestamp } /// Get withdrawal flow tracker accumulated amount pub fn get_withdrawal_flow_accumulated(&self) -> U128 { self.state.withdrawal_flow_tracker.accumulated_amount } /// Get withdrawal flow tracker window start timestamp pub fn get_withdrawal_flow_window_start(&self) -> u64 { self.state.withdrawal_flow_tracker.window_start_timestamp } /// Get claimable assets for a user and asset /// /// # Arguments /// * `account_id` - Account to check /// * `asset` - Asset to check /// /// # Returns /// Amount of claimable assets pub fn get_claimable_asset_amount(&self, account_id: AccountId, asset: Asset) -> U128 { self.internal_get_claimable_asset_amount(&account_id, &asset) } /// Get all claimable assets for a user /// /// # Arguments /// * `account_id` - Account to check /// /// # Returns /// Vector of (Asset, amount) tuples for all claimable assets pub fn get_all_claimable_asset_amounts(&self, account_id: AccountId) -> Vec<(Asset, U128)> { self.available_redeem_assets .iter() .map(|asset| { let amount = self.internal_get_claimable_asset_amount(&account_id, &asset); (asset.clone(), amount) }) .filter(|(_, amount)| amount.0 > 0) .collect() } }