use crate::events::{ ConfigUpdated, EmergencyPauseChanged, FeesClaimed, MetadataUpdated, ProtocolFeesClaimed, RedeemConfirmed, VaultDeposit, }; use crate::*; use common_data::constants::*; use common_data::dew_vault_interface::DewVaultInterface; use near_contract_standards::fungible_token::events::FtMint; use near_sdk::{env, json_types::U128, log, near, require, AccountId, Promise}; #[near] impl DewVaultInterface for Contract { /// Update share prices for accepted assets (Owner only) fn dew_vault_update_share_prices(&mut self, rates: Vec<(Asset, U128)>) { self.internal_validate_is_owner(); self.internal_update_share_price(rates); } /// Update vault configuration (Owner only) fn dew_vault_update_config(&mut self, new_config: VaultConfig) { self.internal_validate_is_owner(); new_config.validate(); self.vault_config.validate_config_change(&new_config); self.vault_config = new_config; // Emit config updated event ConfigUpdated { updater: &self.owner_account_id, } .emit(); log!("Updated vault configuration"); } /// Confirm pending redeem requests with locked-in share prices (Owner only) fn dew_vault_confirm_pending_redeems(&mut self, requests: Vec<(u64, U128)>) { self.internal_validate_is_owner(); let current_timestamp = env::block_timestamp() as u128; for (operation_id, locked_share_price) in requests { // Get the withdraw request let withdraw = self .state .teller_state .get_withdraw(operation_id) .unwrap_or_else(|| { env::panic_str(&format!("Redeem request {} not found", operation_id)) }); // Validate that the withdrawal is not already confirmed require!( !withdraw.confirmed, format!("Redeem request {} is already confirmed", operation_id) ); // Validate that the share price is positive require!( locked_share_price.0 > 0, format!( "Share price must be positive for operation {}", operation_id ) ); // Validate that the locked share price provides at least min_asset_amount (slippage protection) let gross_asset_amount = self .internal_convert_to_asset_amount_with_rate(withdraw.shares, locked_share_price.0); let (total_fee, _, _) = self.internal_calculate_withdrawal_fee(gross_asset_amount, &withdraw.asset); let expected_asset_amount = U128(gross_asset_amount.0 - total_fee.0); require!( expected_asset_amount.0 >= withdraw.min_asset_amount.0, format!( "Locked share price {} for operation {} would provide {} assets, less than required minimum {}", locked_share_price.0, operation_id, expected_asset_amount.0, withdraw.min_asset_amount.0 ) ); // Clone the withdraw to modify it let mut updated_withdraw = withdraw.clone(); updated_withdraw.confirmed = true; updated_withdraw.confirmed_share_price = Some(locked_share_price); updated_withdraw.confirmed_at_timestamp = Some(U128(current_timestamp)); // Update the withdraw in storage self.state .teller_state .pending_withdraws .insert(operation_id, updated_withdraw); // Emit redeem confirmed event RedeemConfirmed { operation_id, owner_id: &withdraw.owner_id, locked_share_price, } .emit(); log!( "Confirmed redeem request {} for {} shares at share price {} at timestamp {}", operation_id, withdraw.shares.0, locked_share_price.0, current_timestamp ); } } /// Process pending deposits with provided share prices (Owner only) fn dew_vault_process_pending_deposits(&mut self, requests: Vec<(u64, U128)>) { self.internal_validate_is_owner(); self.internal_validate_not_emergency_paused(); for (operation_id, share_price) in requests { // Get the deposit request let deposit = self .state .teller_state .get_deposit(operation_id) .unwrap_or_else(|| { env::panic_str(&format!("Deposit request {} not found", operation_id)) }); // Validate share price require!( share_price.0 > 0, format!( "Share price must be positive for operation {}", operation_id ) ); // Re-validate TVL cap at processing time (in case vault state changed since request) self.internal_validate_tvl_cap(deposit.deposit_amount, &deposit.asset); // Calculate deposit fee let (total_fee, vault_fee, protocol_fee) = self.internal_calculate_deposit_fee(deposit.deposit_amount, &deposit.asset); let net_deposit_amount = U128(deposit.deposit_amount.0 - total_fee.0); // Calculate shares to mint with provided share price (using net assets after fee) let shares_to_mint = self.internal_convert_to_shares_with_rate(net_deposit_amount, share_price.0); // Validate slippage protection require!( shares_to_mint.0 >= deposit.min_shares.0, format!( "Deposit request {} failed: share price {} would provide {} shares, less than required minimum {}", operation_id, share_price.0, shares_to_mint.0, deposit.min_shares.0 ) ); // Remove the deposit from pending list 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: move from pending to available (single storage operation) let mut balance = self.get_asset_balance(&deposit.asset); // Validate accounting invariant: pending_deposit must be >= deposit amount assert!( balance.pending_deposit.0 >= deposit.deposit_amount.0, "Accounting error: VaultBalance pending_deposit < deposit amount", ); balance.available_amount.0 = balance .available_amount .0 .checked_add(deposit.deposit_amount.0) .expect("deposit vault balance oveflow"); balance.pending_deposit.0 = balance.pending_deposit.0 - deposit.deposit_amount.0; self.state .asset_balances .insert(deposit.asset.clone(), balance); // Accumulate fees self.internal_accumulate_fees(&deposit.asset, vault_fee, protocol_fee); // Mint shares to receiver self.token .internal_deposit(&deposit.receiver_id, shares_to_mint.0); // Emit FT mint event FtMint { owner_id: &deposit.receiver_id, amount: shares_to_mint, memo: deposit.memo.as_deref(), } .emit(); // Emit vault deposit event VaultDeposit { sender_id: deposit.owner_id.as_ref(), owner_id: deposit.receiver_id.as_ref(), asset: &deposit.asset, deposit_amount: deposit.deposit_amount, shares: shares_to_mint, memo: deposit.memo.as_deref(), } .emit(); log!( "Processed deposit request {} for {} assets (fee: {}), minted {} shares at share price {}", operation_id, deposit.deposit_amount.0, total_fee.0, shares_to_mint.0, share_price.0 ); } } fn dew_vault_asset_transfer( &mut self, asset: Asset, amount: U128, receiver_id: AccountId, memo: Option, ) -> Promise { self.internal_validate_is_owner(); require!(amount.0 > 0, "Amount must be positive"); // Check sufficient available assets let vault_balance = self.get_vault_balance(&asset); require!( vault_balance.available_amount.0 >= amount.0, "Insufficient available assets" ); // Decrease available asset balance self.internal_update_asset_balance(&asset, amount, false); // Create transfer promise let promise = asset.create_transfer_promise(receiver_id.clone(), amount, memo.clone()); log!( "Dew Kernel transferred {} {} assets to {} with memo: {:?}", amount.0, asset, receiver_id, memo, ); // Chain callback to handle transfer success/failure promise.then( crate::types::ext_self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_CALLBACK) .asset_transfer_callback( None, // No operation_id for direct transfers asset.clone(), amount, receiver_id.clone(), memo.clone(), ), ) } /// Update the fungible token metadata (Owner only) fn dew_vault_update_metadata(&mut self, new_metadata: FungibleTokenMetadata) { self.internal_validate_is_owner(); // Validate metadata fields require!(!new_metadata.name.is_empty(), "Token name cannot be empty"); require!( !new_metadata.symbol.is_empty(), "Token symbol cannot be empty" ); require!( new_metadata.spec == "ft-1.0.0", "Invalid NEP-141 spec version" ); self.metadata = new_metadata.clone(); // Emit metadata updated event MetadataUpdated { updater: &self.owner_account_id, name: &new_metadata.name, symbol: &new_metadata.symbol, } .emit(); log!("Dew Kernel updated fungible token metadata"); } /// Add account to whitelist (internal function called by Dew Kernel) fn dew_vault_add_to_whitelist(&mut self, account_id: AccountId) { self.internal_validate_is_owner(); require!(self.vault_config.is_private, "vault not private"); // Initialize whitelist if it doesn't exist if self.state.whitelist_registry.is_none() { self.state.whitelist_registry = Some(LookupSet::new(StorageKey::WhitelistRegistry)); } if let Some(ref mut whitelist) = self.state.whitelist_registry { whitelist.insert(account_id); } } /// Remove account from whitelist (internal function called by Dew Kernel) fn dew_vault_remove_from_whitelist(&mut self, account_id: AccountId) { self.internal_validate_is_owner(); require!(self.vault_config.is_private, "vault not private"); if let Some(ref mut whitelist) = self.state.whitelist_registry { whitelist.remove(&account_id); } } /// Add account to blacklist (internal function called by Dew Kernel) fn dew_vault_add_to_blacklist(&mut self, account_id: AccountId) { self.internal_validate_is_owner(); require!( self.vault_config.blacklist_enabled, "vault does not enable blacklist" ); // Initialize blacklist if it doesn't exist if self.state.blacklist_registry.is_none() { self.state.blacklist_registry = Some(LookupSet::new(StorageKey::BlacklistRegistry)); } if let Some(ref mut blacklist) = self.state.blacklist_registry { blacklist.insert(account_id); } } /// Remove account from blacklist (internal function called by Dew Kernel) fn dew_vault_remove_from_blacklist(&mut self, account_id: AccountId) { self.internal_validate_is_owner(); require!( self.vault_config.blacklist_enabled, "vault does not enable blacklist" ); if let Some(ref mut blacklist) = self.state.blacklist_registry { blacklist.remove(&account_id); } } /// Emergency pause all vault operations (Owner only) fn dew_vault_emergency_pause(&mut self) { self.internal_validate_is_owner(); require!( self.vault_config.emergency_pause_enabled, "Emergency pause functionality is disabled" ); require!( !self.state.emergency_paused, "Vault is already emergency paused" ); self.state.emergency_paused = true; // Emit emergency pause changed event EmergencyPauseChanged { emergency_paused: true, changed_by: &self.owner_account_id, } .emit(); log!("Emergency pause activated by Dew Kernel"); } /// Emergency unpause all vault operations (Owner only) fn dew_vault_emergency_unpause(&mut self) { self.internal_validate_is_owner(); require!( self.vault_config.emergency_pause_enabled, "Emergency pause functionality is disabled" ); require!(self.state.emergency_paused, "Vault is not emergency paused"); self.state.emergency_paused = false; // Emit emergency pause changed event EmergencyPauseChanged { emergency_paused: false, changed_by: &self.owner_account_id, } .emit(); log!("Emergency pause deactivated by Dew Kernel"); } /// Reject pending deposit requests (Owner only) fn dew_vault_reject_pending_deposits(&mut self, request_ids: Vec, reason: String) { self.internal_validate_is_owner(); require!(!request_ids.is_empty(), "Request IDs cannot be empty"); require!(!reason.is_empty(), "Rejection reason cannot be empty"); for &request_id in &request_ids { let deposit = self .state .teller_state .get_deposit(request_id) .unwrap_or_else(|| { env::panic_str(&format!("Deposit request {} not found", request_id)) }); // Remove deposit from teller state (includes user tracking cleanup) let deposit = self .state .teller_state .remove_deposit(request_id, &deposit.owner_id) .unwrap_or_else(|| { env::panic_str(&format!("Failed to remove deposit request {}", request_id)) }); // Move assets from pending_deposit directly to claimable balance self.internal_move_pending_to_claimable( &deposit.owner_id, &deposit.asset, deposit.deposit_amount, ); log!( "Rejected deposit {} for {}: {}. {} {} assets moved to claimable balance.", request_id, deposit.owner_id, reason, deposit.deposit_amount.0, deposit.asset ); } log!( "Rejected {} deposit requests. Reason: {}", request_ids.len(), reason ); } /// Reject pending redeem requests (Owner only) fn dew_vault_reject_pending_redeems(&mut self, request_ids: Vec, reason: String) { self.internal_validate_is_owner(); require!(!request_ids.is_empty(), "Request IDs cannot be empty"); require!(!reason.is_empty(), "Rejection reason cannot be empty"); for &request_id in &request_ids { let withdraw = self .state .teller_state .get_withdraw(request_id) .unwrap_or_else(|| { env::panic_str(&format!("Redeem request {} not found", request_id)) }); // Re-mint shares back to the original owner since they were burned when request was created self.token .internal_deposit(&withdraw.owner_id, withdraw.shares.0); // Emit FT mint event for the re-minted shares FtMint { owner_id: &withdraw.owner_id, amount: withdraw.shares, memo: Some("Shares re-minted due to rejected redeem request"), } .emit(); // Remove withdraw from teller state (includes user tracking cleanup) self.state .teller_state .remove_withdraw(request_id, &withdraw.owner_id); } log!( "Rejected {} redeem requests. Reason: {}", request_ids.len(), reason ); } /// Force reset flow control caps (Owner only) fn dew_vault_force_reset_flow_cap(&mut self, is_deposit: bool) { self.internal_validate_is_owner(); // Call the internal implementation from flow_control.rs self.internal_force_reset_flow_cap(is_deposit); log!( "Flow control cap reset by Dew Kernel. Type: {}", if is_deposit { "deposit" } else { "withdrawal" } ); } /// Set per-asset deposit and withdrawal fees (Owner only) fn dew_vault_set_asset_fees( &mut self, asset: Asset, deposit_fee_bps: Option, withdrawal_fee_bps: Option, ) { self.internal_validate_is_owner(); self.internal_validate_asset(&asset); if let Some(fee) = deposit_fee_bps { require!(fee <= MAX_DEPOSIT_FEE_BPS, "Deposit fee cannot exceed 20%"); if fee == 0 { self.state.deposit_fees.remove(&asset); } else { self.state.deposit_fees.insert(asset.clone(), fee); } log!("Set deposit fee for {} to {} bps", asset, fee); } if let Some(fee) = withdrawal_fee_bps { require!( fee <= MAX_WITHDRAWAL_FEE_BPS, "Withdrawal fee cannot exceed 20%" ); if fee == 0 { self.state.withdrawal_fees.remove(&asset); } else { self.state.withdrawal_fees.insert(asset.clone(), fee); } log!("Set withdrawal fee for {} to {} bps", asset, fee); } } /// Set protocol fee cuts for per-asset fees (Owner only) fn dew_vault_set_protocol_fee_cuts( &mut self, asset: Asset, deposit_cut_bps: Option, withdrawal_cut_bps: Option, ) { self.internal_validate_is_owner(); self.internal_validate_asset(&asset); if let Some(cut) = deposit_cut_bps { require!( cut <= MAX_PROTOCOL_CUT_BPS, "Protocol cut cannot exceed 100%" ); if cut == 0 { self.state.protocol_deposit_fee_cuts.remove(&asset); } else { self.state .protocol_deposit_fee_cuts .insert(asset.clone(), cut); } log!("Set protocol deposit fee cut for {} to {} bps", asset, cut); } if let Some(cut) = withdrawal_cut_bps { require!( cut <= MAX_PROTOCOL_CUT_BPS, "Protocol cut cannot exceed 100%" ); if cut == 0 { self.state.protocol_withdrawal_fee_cuts.remove(&asset); } else { self.state .protocol_withdrawal_fee_cuts .insert(asset.clone(), cut); } log!( "Set protocol withdrawal fee cut for {} to {} bps", asset, cut ); } } fn dew_vault_set_fee_recipient(&mut self, fee_recipient: AccountId) { self.internal_validate_is_owner(); require!( env::is_valid_account_id(fee_recipient.as_bytes()), "Invalid fee recipient account ID" ); self.fee_recipient = fee_recipient.clone(); log!("Fee recipient updated to {}", fee_recipient); } /// Claim accumulated vault fees for a specific asset (Owner only) fn dew_vault_claim_fees(&mut self, asset: Asset) -> Promise { self.internal_validate_is_owner(); self.internal_validate_asset(&asset); let fees_owed = self.state.fees_owed.get(&asset).unwrap_or(&U128(0)).clone(); require!(fees_owed.0 > 0, "No fees to claim for this asset"); // Check sufficient available assets let vault_balance = self.get_vault_balance(&asset); require!( vault_balance.available_amount.0 >= fees_owed.0, "Insufficient available assets to claim fees" ); // Clear fees owed self.state.fees_owed.insert(asset.clone(), U128(0)); // Decrease available asset balance self.internal_update_asset_balance(&asset, fees_owed, false); // Emit fees claimed event FeesClaimed { asset: &asset, amount: fees_owed, recipient: &self.fee_recipient, } .emit(); log!( "Claiming {} {} in vault fees for recipient {}", fees_owed.0, asset, self.fee_recipient ); // Transfer fees to fee recipient asset.create_transfer_promise( self.fee_recipient.clone(), fees_owed, Some("Vault fees claim".to_string()), ) } /// Claim accumulated protocol fees for a specific asset (Owner only) fn dew_vault_claim_protocol_fees(&mut self, asset: Asset) -> Promise { self.internal_validate_is_owner(); self.internal_validate_asset(&asset); let protocol_fees_owed = self .state .protocol_fees_owed .get(&asset) .unwrap_or(&U128(0)) .clone(); require!( protocol_fees_owed.0 > 0, "No protocol fees to claim for this asset" ); // Check sufficient available assets let vault_balance = self.get_vault_balance(&asset); require!( vault_balance.available_amount.0 >= protocol_fees_owed.0, "Insufficient available assets to claim protocol fees" ); // Clear protocol fees owed self.state.protocol_fees_owed.insert(asset.clone(), U128(0)); // Decrease available asset balance self.internal_update_asset_balance(&asset, protocol_fees_owed, false); // Emit protocol fees claimed event ProtocolFeesClaimed { asset: &asset, amount: protocol_fees_owed, recipient: &self.protocol_config.protocol_fee_recipient, } .emit(); log!( "Claiming {} {} in protocol fees for recipient {}", protocol_fees_owed.0, asset, self.protocol_config.protocol_fee_recipient ); // Transfer protocol fees to protocol fee recipient asset.create_transfer_promise( self.protocol_config.protocol_fee_recipient.clone(), protocol_fees_owed, Some("Protocol fees claim".to_string()), ) } /// Unpause the accountant module (Owner only) fn dew_vault_unpause_accountant(&mut self) { self.internal_validate_is_owner(); require!( self.state.is_accountant_paused, "Accountant is not currently paused" ); self.internal_unpause_accountant(); log!("Accountant unpaused by Dew Kernel"); } /// Crystallize performance fees with hurdle-based calculation (Owner only) fn dew_vault_crystallize_performance_fee(&mut self) { self.internal_validate_is_owner(); self.internal_crystallize_performance_fee(); log!("Performance fees crystallized by Dew Kernel"); } /// Start the vault and enable fee calculations (Owner only) fn dew_vault_start_vault(&mut self) { self.internal_validate_is_owner(); self.internal_start_vault(); log!("Vault started by Dew Kernel"); } /// Transfer vault ownership to a new owner (Owner only) fn dew_vault_transfer_ownership(&mut self, new_owner: AccountId) { self.internal_validate_is_owner(); require!( self.vault_config.ownership_transfer_enabled, "Ownership transfer functionality is disabled" ); require!( new_owner != self.owner_account_id, "New owner must be different from current owner" ); let old_owner = self.owner_account_id.clone(); self.owner_account_id = new_owner.clone(); log!( "Vault ownership transferred from {} to {}", old_owner, new_owner ); } /// Set code hash to be uploaded (Owner only) fn dew_vault_set_code_hash(&mut self, code_hash: String) { self.internal_validate_is_owner(); require!( self.staged_code_hash.is_none(), "Previous contract code hash is not deleted yet" ); self.staged_code_hash = Some(code_hash); } /// Delete staged contract code (Owner only) fn dew_vault_delete_code_hash(&mut self, code_hash: String) { self.internal_validate_is_owner(); require!( self.staged_code_hash.as_deref() == Some(&code_hash), "Uploaded contract code hash does not match" ); self.staged_wasm.set(None); self.staged_code_hash = None; } /// Deploy staged contract code and run migrate (Owner only) fn dew_vault_deploy_uploaded_contract(&mut self, code_hash: String) -> Promise { self.internal_validate_is_owner(); require!( self.staged_code_hash.as_deref() == Some(&code_hash), "Uploaded contract code hash does not match" ); let code = self.staged_wasm.get().clone().unwrap(); Promise::new(env::current_account_id()) .deploy_contract(code) .function_call( "migrate".to_string(), Vec::new(), NearToken::from_near(0), Gas::from_tgas(100), ) .as_return() } }