//! Bridge Vault //! //! Manages locked assets for cross-chain transfers. //! Assets are locked in vaults on the source chain and //! wrapped tokens are minted on the destination chain. use crate::{AssetId, BridgeAddress, BridgeError, BridgeResult, ChainType}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::fmt; /// Unique vault identifier #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct VaultId(pub String); impl VaultId { /// Create a new vault ID pub fn new(id: impl Into) -> Self { Self(id.into()) } /// Generate vault ID from asset pub fn from_asset(asset: &AssetId, chain: &ChainType) -> Self { let mut hasher = Sha256::new(); hasher.update(chain.to_string().as_bytes()); hasher.update(asset.identifier.as_bytes()); Self(hex::encode(&hasher.finalize()[..16])) } } impl fmt::Display for VaultId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// Locked asset record #[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct LockedAsset { /// Asset being locked pub asset: AssetId, /// Amount locked pub amount: u128, /// Who locked the asset pub owner: BridgeAddress, /// Recipient on destination chain pub recipient: BridgeAddress, /// Lock timestamp (Unix seconds) pub locked_at: u64, /// Lock expiry (for timelock, 0 = no expiry) pub expires_at: u64, /// Transaction hash on source chain pub lock_tx_hash: Option>, /// Whether the asset has been released pub released: bool, } impl LockedAsset { /// Create a new locked asset record pub fn new( asset: AssetId, amount: u128, owner: BridgeAddress, recipient: BridgeAddress, locked_at: u64, ) -> Self { Self { asset, amount, owner, recipient, locked_at, expires_at: 0, lock_tx_hash: None, released: false, } } /// Set expiry time pub fn with_expiry(mut self, expires_at: u64) -> Self { self.expires_at = expires_at; self } /// Set lock transaction hash pub fn with_tx_hash(mut self, tx_hash: Vec) -> Self { self.lock_tx_hash = Some(tx_hash); self } /// Check if the lock has expired pub fn is_expired(&self, current_time: u64) -> bool { self.expires_at > 0 && current_time >= self.expires_at } /// Mark as released pub fn release(&mut self) { self.released = true; } } /// Vault state #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum VaultState { /// Vault is active and accepting deposits Active, /// Vault is paused (no new deposits) Paused, /// Vault is deprecated (migration needed) Deprecated, } /// Asset vault for a specific chain #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Vault { /// Vault identifier pub id: VaultId, /// Chain this vault is on pub chain: ChainType, /// Asset managed by this vault pub asset: AssetId, /// Current vault state pub state: VaultState, /// Total locked amount pub total_locked: u128, /// Individual locked assets by lock ID locked_assets: HashMap, /// Vault address (contract address for EVM) pub vault_address: Option, /// Admin addresses pub admins: Vec, /// Daily limit (0 = unlimited) pub daily_limit: u128, /// Today's usage daily_usage: u128, /// Last reset timestamp last_reset: u64, } impl Vault { /// Create a new vault pub fn new(id: VaultId, chain: ChainType, asset: AssetId) -> Self { Self { id, chain, asset, state: VaultState::Active, total_locked: 0, locked_assets: HashMap::new(), vault_address: None, admins: Vec::new(), daily_limit: 0, daily_usage: 0, last_reset: 0, } } /// Set vault address pub fn with_address(mut self, address: BridgeAddress) -> Self { self.vault_address = Some(address); self } /// Set daily limit pub fn with_daily_limit(mut self, limit: u128) -> Self { self.daily_limit = limit; self } /// Add admin pub fn add_admin(&mut self, admin: BridgeAddress) { if !self.admins.contains(&admin) { self.admins.push(admin); } } /// Lock assets in the vault pub fn lock( &mut self, lock_id: impl Into, amount: u128, owner: BridgeAddress, recipient: BridgeAddress, current_time: u64, ) -> BridgeResult<()> { // Check vault state if self.state != VaultState::Active { return Err(BridgeError::BridgePaused); } // Check daily limit self.check_daily_limit(amount, current_time)?; let lock_id = lock_id.into(); if self.locked_assets.contains_key(&lock_id) { return Err(BridgeError::TransferAlreadyExists(lock_id)); } let locked = LockedAsset::new( self.asset.clone(), amount, owner, recipient, current_time, ); self.locked_assets.insert(lock_id, locked); self.total_locked += amount; self.daily_usage += amount; Ok(()) } /// Unlock assets from the vault pub fn unlock(&mut self, lock_id: &str) -> BridgeResult { let locked = self .locked_assets .get_mut(lock_id) .ok_or_else(|| BridgeError::TransferNotFound(lock_id.to_string()))?; if locked.released { return Err(BridgeError::TransferAlreadyCompleted(lock_id.to_string())); } locked.release(); self.total_locked = self.total_locked.saturating_sub(locked.amount); Ok(locked.clone()) } /// Get locked asset pub fn get_locked(&self, lock_id: &str) -> Option<&LockedAsset> { self.locked_assets.get(lock_id) } /// Check and update daily limit fn check_daily_limit(&mut self, amount: u128, current_time: u64) -> BridgeResult<()> { if self.daily_limit == 0 { return Ok(()); } // Reset daily usage if new day let day = current_time / 86400; let last_day = self.last_reset / 86400; if day > last_day { self.daily_usage = 0; self.last_reset = current_time; } // Check limit if self.daily_usage + amount > self.daily_limit { return Err(BridgeError::RateLimitExceeded); } Ok(()) } /// Pause the vault pub fn pause(&mut self) { self.state = VaultState::Paused; } /// Resume the vault pub fn resume(&mut self) { self.state = VaultState::Active; } /// Deprecate the vault pub fn deprecate(&mut self) { self.state = VaultState::Deprecated; } /// Get all locked assets pub fn all_locked(&self) -> impl Iterator { self.locked_assets.iter() } /// Get active (unreleased) locked assets pub fn active_locked(&self) -> impl Iterator { self.locked_assets.iter().filter(|(_, l)| !l.released) } /// Get expired locked assets pub fn expired_locked(&self, current_time: u64) -> impl Iterator { self.locked_assets .iter() .filter(move |(_, l)| !l.released && l.is_expired(current_time)) } } /// Vault manager for multiple vaults pub struct VaultManager { /// Vaults by ID vaults: HashMap, /// Vault lookup by (chain, asset) by_chain_asset: HashMap<(ChainType, String), VaultId>, } impl VaultManager { /// Create a new vault manager pub fn new() -> Self { Self { vaults: HashMap::new(), by_chain_asset: HashMap::new(), } } /// Create and register a new vault pub fn create_vault(&mut self, chain: ChainType, asset: AssetId) -> VaultId { let vault_id = VaultId::from_asset(&asset, &chain); let vault = Vault::new(vault_id.clone(), chain.clone(), asset.clone()); self.by_chain_asset .insert((chain, asset.identifier.clone()), vault_id.clone()); self.vaults.insert(vault_id.clone(), vault); vault_id } /// Get vault by ID pub fn get_vault(&self, vault_id: &VaultId) -> Option<&Vault> { self.vaults.get(vault_id) } /// Get mutable vault by ID pub fn get_vault_mut(&mut self, vault_id: &VaultId) -> Option<&mut Vault> { self.vaults.get_mut(vault_id) } /// Find vault by chain and asset pub fn find_vault(&self, chain: &ChainType, asset: &AssetId) -> Option<&Vault> { self.by_chain_asset .get(&(chain.clone(), asset.identifier.clone())) .and_then(|id| self.vaults.get(id)) } /// Find mutable vault by chain and asset pub fn find_vault_mut(&mut self, chain: &ChainType, asset: &AssetId) -> Option<&mut Vault> { let vault_id = self .by_chain_asset .get(&(chain.clone(), asset.identifier.clone()))? .clone(); self.vaults.get_mut(&vault_id) } /// Get or create vault for asset pub fn get_or_create_vault(&mut self, chain: ChainType, asset: AssetId) -> &mut Vault { let key = (chain.clone(), asset.identifier.clone()); if !self.by_chain_asset.contains_key(&key) { self.create_vault(chain.clone(), asset.clone()); } let vault_id = self.by_chain_asset.get(&key).unwrap().clone(); self.vaults.get_mut(&vault_id).unwrap() } /// Get total locked value across all vaults pub fn total_locked(&self) -> u128 { self.vaults.values().map(|v| v.total_locked).sum() } /// Get total locked value for an asset pub fn total_locked_for_asset(&self, asset: &AssetId) -> u128 { self.vaults .values() .filter(|v| v.asset.identifier == asset.identifier) .map(|v| v.total_locked) .sum() } /// List all vault IDs pub fn vault_ids(&self) -> Vec { self.vaults.keys().cloned().collect() } } impl Default for VaultManager { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; // ==================== Helper Functions ==================== fn test_owner() -> BridgeAddress { BridgeAddress::from_eth([0xaa; 20]) } fn test_recipient() -> BridgeAddress { BridgeAddress::from_synor([0xbb; 32]) } fn test_owner_alt() -> BridgeAddress { BridgeAddress::from_eth([0xcc; 20]) } fn test_erc20_asset() -> AssetId { AssetId::erc20("0x1234567890123456789012345678901234567890", "USDC", 6) } // ==================== VaultId Tests ==================== #[test] fn test_vault_id_new() { let id = VaultId::new("my-vault"); assert_eq!(id.0, "my-vault"); } #[test] fn test_vault_id_display() { let id = VaultId::new("vault-123"); assert_eq!(format!("{}", id), "vault-123"); } #[test] fn test_vault_id_from_asset_deterministic() { let asset = AssetId::eth(); let id1 = VaultId::from_asset(&asset, &ChainType::Ethereum); let id2 = VaultId::from_asset(&asset, &ChainType::Ethereum); assert_eq!(id1, id2); assert!(!id1.0.is_empty()); } #[test] fn test_vault_id_from_asset_different_chains() { let asset = AssetId::eth(); let id1 = VaultId::from_asset(&asset, &ChainType::Ethereum); let id2 = VaultId::from_asset(&asset, &ChainType::EthereumSepolia); assert_ne!(id1, id2); } #[test] fn test_vault_id_from_asset_different_assets() { let eth = AssetId::eth(); let usdc = test_erc20_asset(); let id1 = VaultId::from_asset(ð, &ChainType::Ethereum); let id2 = VaultId::from_asset(&usdc, &ChainType::Ethereum); assert_ne!(id1, id2); } // ==================== LockedAsset Tests ==================== #[test] fn test_locked_asset_new() { let locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1700000000, ); assert_eq!(locked.amount, 1000); assert_eq!(locked.locked_at, 1700000000); assert_eq!(locked.expires_at, 0); assert!(locked.lock_tx_hash.is_none()); assert!(!locked.released); } #[test] fn test_locked_asset_with_expiry() { let locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1700000000, ) .with_expiry(1700003600); assert_eq!(locked.expires_at, 1700003600); } #[test] fn test_locked_asset_with_tx_hash() { let tx_hash = vec![0x11; 32]; let locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1700000000, ) .with_tx_hash(tx_hash.clone()); assert_eq!(locked.lock_tx_hash, Some(tx_hash)); } #[test] fn test_locked_asset_is_expired_no_expiry() { let locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1700000000, ); assert!(!locked.is_expired(1800000000)); } #[test] fn test_locked_asset_expiry() { let locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1000, ) .with_expiry(2000); assert!(!locked.is_expired(1500)); assert!(locked.is_expired(2000)); assert!(locked.is_expired(3000)); } #[test] fn test_locked_asset_release() { let mut locked = LockedAsset::new( AssetId::eth(), 1000, test_owner(), test_recipient(), 1700000000, ); assert!(!locked.released); locked.release(); assert!(locked.released); } // ==================== VaultState Tests ==================== #[test] fn test_vault_state_equality() { assert_eq!(VaultState::Active, VaultState::Active); assert_eq!(VaultState::Paused, VaultState::Paused); assert_eq!(VaultState::Deprecated, VaultState::Deprecated); assert_ne!(VaultState::Active, VaultState::Paused); } // ==================== Vault Tests ==================== #[test] fn test_vault_new() { let vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); assert_eq!(vault.id.0, "test-vault"); assert_eq!(vault.chain, ChainType::Ethereum); assert_eq!(vault.state, VaultState::Active); assert_eq!(vault.total_locked, 0); assert_eq!(vault.daily_limit, 0); } #[test] fn test_vault_with_address() { let address = test_owner(); let vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ) .with_address(address.clone()); assert_eq!(vault.vault_address, Some(address)); } #[test] fn test_vault_with_daily_limit() { let vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ) .with_daily_limit(1000000); assert_eq!(vault.daily_limit, 1000000); } #[test] fn test_vault_add_admin() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); let admin = test_owner(); vault.add_admin(admin.clone()); assert_eq!(vault.admins.len(), 1); assert_eq!(vault.admins[0], admin); } #[test] fn test_vault_add_admin_duplicate() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); let admin = test_owner(); vault.add_admin(admin.clone()); vault.add_admin(admin.clone()); assert_eq!(vault.admins.len(), 1); } #[test] fn test_lock_unlock() { let mut vault = Vault::new( VaultId::new("test"), ChainType::Ethereum, AssetId::eth(), ); let current_time = 1700000000; vault .lock("lock1", 1000, test_owner(), test_recipient(), current_time) .unwrap(); assert_eq!(vault.total_locked, 1000); assert!(vault.get_locked("lock1").is_some()); let released = vault.unlock("lock1").unwrap(); assert_eq!(released.amount, 1000); assert!(released.released); assert_eq!(vault.total_locked, 0); } #[test] fn test_vault_lock_multiple() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); let current_time = 1700000000; vault.lock("lock-1", 1000, test_owner(), test_recipient(), current_time).unwrap(); vault.lock("lock-2", 2000, test_owner(), test_recipient(), current_time).unwrap(); vault.lock("lock-3", 500, test_owner_alt(), test_recipient(), current_time).unwrap(); assert_eq!(vault.total_locked, 3500); } #[test] fn test_duplicate_lock() { let mut vault = Vault::new( VaultId::new("test"), ChainType::Ethereum, AssetId::eth(), ); vault .lock("lock1", 1000, test_owner(), test_recipient(), 0) .unwrap(); let result = vault.lock("lock1", 500, test_owner(), test_recipient(), 0); assert!(result.is_err()); } #[test] fn test_vault_unlock_nonexistent() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); let result = vault.unlock("nonexistent"); assert!(matches!(result, Err(BridgeError::TransferNotFound(_)))); } #[test] fn test_vault_unlock_already_released() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); vault.unlock("lock-1").unwrap(); let result = vault.unlock("lock-1"); assert!(matches!(result, Err(BridgeError::TransferAlreadyCompleted(_)))); } #[test] fn test_vault_pause() { let mut vault = Vault::new( VaultId::new("test"), ChainType::Ethereum, AssetId::eth(), ); vault.pause(); let result = vault.lock("lock1", 1000, test_owner(), test_recipient(), 0); assert!(matches!(result, Err(BridgeError::BridgePaused))); } #[test] fn test_vault_resume() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.pause(); vault.resume(); assert_eq!(vault.state, VaultState::Active); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); } #[test] fn test_vault_deprecate() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.deprecate(); assert_eq!(vault.state, VaultState::Deprecated); let result = vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0); assert!(matches!(result, Err(BridgeError::BridgePaused))); } #[test] fn test_daily_limit() { let mut vault = Vault::new( VaultId::new("test"), ChainType::Ethereum, AssetId::eth(), ) .with_daily_limit(1000); let current_time = 86400 * 100; vault .lock("lock1", 500, test_owner(), test_recipient(), current_time) .unwrap(); let result = vault.lock("lock2", 600, test_owner(), test_recipient(), current_time); assert!(matches!(result, Err(BridgeError::RateLimitExceeded))); let next_day = current_time + 86400; vault .lock("lock2", 600, test_owner(), test_recipient(), next_day) .unwrap(); } #[test] fn test_vault_no_daily_limit() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); let current_time = 0; vault.lock("lock-1", 1000000000, test_owner(), test_recipient(), current_time).unwrap(); vault.lock("lock-2", 1000000000, test_owner(), test_recipient(), current_time).unwrap(); assert_eq!(vault.total_locked, 2000000000); } #[test] fn test_vault_get_locked() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); assert!(vault.get_locked("lock-1").is_some()); assert!(vault.get_locked("nonexistent").is_none()); } #[test] fn test_vault_all_locked() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); vault.lock("lock-2", 2000, test_owner(), test_recipient(), 0).unwrap(); let all: Vec<_> = vault.all_locked().collect(); assert_eq!(all.len(), 2); } #[test] fn test_vault_active_locked() { let mut vault = Vault::new( VaultId::new("test-vault"), ChainType::Ethereum, AssetId::eth(), ); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); vault.lock("lock-2", 2000, test_owner(), test_recipient(), 0).unwrap(); vault.unlock("lock-1").unwrap(); let active: Vec<_> = vault.active_locked().collect(); assert_eq!(active.len(), 1); } // ==================== VaultManager Tests ==================== #[test] fn test_vault_manager_new() { let manager = VaultManager::new(); assert_eq!(manager.total_locked(), 0); assert!(manager.vault_ids().is_empty()); } #[test] fn test_vault_manager_default() { let manager = VaultManager::default(); assert_eq!(manager.total_locked(), 0); } #[test] fn test_vault_manager() { let mut manager = VaultManager::new(); let eth = AssetId::eth(); let vault_id = manager.create_vault(ChainType::Ethereum, eth.clone()); assert!(manager.get_vault(&vault_id).is_some()); assert!(manager.find_vault(&ChainType::Ethereum, ð).is_some()); let vault = manager.get_or_create_vault(ChainType::Ethereum, eth.clone()); vault.lock("lock1", 100, test_owner(), test_recipient(), 0).unwrap(); assert_eq!(manager.total_locked(), 100); } #[test] fn test_vault_manager_create_multiple() { let mut manager = VaultManager::new(); manager.create_vault(ChainType::Ethereum, AssetId::eth()); manager.create_vault(ChainType::Ethereum, test_erc20_asset()); manager.create_vault(ChainType::EthereumSepolia, AssetId::eth()); assert_eq!(manager.vault_ids().len(), 3); } #[test] fn test_vault_manager_get_vault_mut() { let mut manager = VaultManager::new(); let vault_id = manager.create_vault(ChainType::Ethereum, AssetId::eth()); { let vault = manager.get_vault_mut(&vault_id).unwrap(); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); } let vault = manager.get_vault(&vault_id).unwrap(); assert_eq!(vault.total_locked, 1000); } #[test] fn test_vault_manager_find_vault_not_found() { let manager = VaultManager::new(); let vault = manager.find_vault(&ChainType::Ethereum, &AssetId::eth()); assert!(vault.is_none()); } #[test] fn test_vault_manager_find_vault_mut() { let mut manager = VaultManager::new(); let eth = AssetId::eth(); manager.create_vault(ChainType::Ethereum, eth.clone()); let vault = manager.find_vault_mut(&ChainType::Ethereum, ð).unwrap(); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); assert_eq!(manager.total_locked(), 1000); } #[test] fn test_vault_manager_get_or_create_new() { let mut manager = VaultManager::new(); let eth = AssetId::eth(); let vault = manager.get_or_create_vault(ChainType::Ethereum, eth.clone()); vault.lock("lock-1", 1000, test_owner(), test_recipient(), 0).unwrap(); assert_eq!(manager.vault_ids().len(), 1); assert_eq!(manager.total_locked(), 1000); } #[test] fn test_vault_manager_total_locked() { let mut manager = VaultManager::new(); let eth = AssetId::eth(); let usdc = test_erc20_asset(); let eth_vault_id = manager.create_vault(ChainType::Ethereum, eth); let usdc_vault_id = manager.create_vault(ChainType::Ethereum, usdc); manager .get_vault_mut(ð_vault_id) .unwrap() .lock("lock-1", 1000, test_owner(), test_recipient(), 0) .unwrap(); manager .get_vault_mut(&usdc_vault_id) .unwrap() .lock("lock-2", 2000, test_owner(), test_recipient(), 0) .unwrap(); assert_eq!(manager.total_locked(), 3000); } #[test] fn test_vault_manager_total_locked_for_asset() { let mut manager = VaultManager::new(); let eth = AssetId::eth(); let usdc = test_erc20_asset(); let eth_vault_id = manager.create_vault(ChainType::Ethereum, eth.clone()); let usdc_vault_id = manager.create_vault(ChainType::Ethereum, usdc.clone()); manager .get_vault_mut(ð_vault_id) .unwrap() .lock("lock-1", 1000, test_owner(), test_recipient(), 0) .unwrap(); manager .get_vault_mut(&usdc_vault_id) .unwrap() .lock("lock-2", 2000, test_owner(), test_recipient(), 0) .unwrap(); assert_eq!(manager.total_locked_for_asset(ð), 1000); assert_eq!(manager.total_locked_for_asset(&usdc), 2000); } #[test] fn test_vault_manager_vault_ids() { let mut manager = VaultManager::new(); let id1 = manager.create_vault(ChainType::Ethereum, AssetId::eth()); let id2 = manager.create_vault(ChainType::Ethereum, test_erc20_asset()); let ids = manager.vault_ids(); assert_eq!(ids.len(), 2); assert!(ids.contains(&id1)); assert!(ids.contains(&id2)); } }