diff --git a/Cargo.toml b/Cargo.toml
index 10592a8..9b84b06 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,7 @@ members = [
"crates/synor-mining",
"crates/synor-zk",
"crates/synor-ibc",
+ "crates/synor-bridge",
"crates/synor-privacy",
"crates/synor-sharding",
"crates/synor-verifier",
diff --git a/crates/synor-bridge/Cargo.toml b/crates/synor-bridge/Cargo.toml
new file mode 100644
index 0000000..dc0ce50
--- /dev/null
+++ b/crates/synor-bridge/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "synor-bridge"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+description = "Cross-chain bridge infrastructure for Synor blockchain"
+
+[dependencies]
+synor-types = { path = "../synor-types" }
+synor-crypto = { path = "../synor-crypto" }
+synor-ibc = { path = "../synor-ibc" }
+
+# Serialization
+serde = { workspace = true }
+serde_json = { workspace = true }
+borsh = { workspace = true }
+
+# Cryptography
+sha2 = "0.10"
+sha3 = { workspace = true }
+blake3 = { workspace = true }
+rand = { workspace = true }
+
+# Ethereum compatibility
+alloy-primitives = { version = "0.8", features = ["serde"] }
+alloy-sol-types = "0.8"
+
+# Async runtime
+tokio = { version = "1", features = ["full"] }
+async-trait = "0.1"
+
+# Utilities
+thiserror = { workspace = true }
+hex = { workspace = true }
+parking_lot = { workspace = true }
+tracing = "0.1"
+chrono = { workspace = true }
+
+[dev-dependencies]
+tokio-test = "0.4"
+proptest = { workspace = true }
+tempfile = { workspace = true }
diff --git a/crates/synor-bridge/src/ethereum.rs b/crates/synor-bridge/src/ethereum.rs
new file mode 100644
index 0000000..6299ab4
--- /dev/null
+++ b/crates/synor-bridge/src/ethereum.rs
@@ -0,0 +1,808 @@
+//! Ethereum Bridge Implementation
+//!
+//! Lock-Mint bridge for Ethereum assets:
+//! - ETH → sETH (wrapped ETH on Synor)
+//! - ERC-20 → sERC20 (wrapped tokens)
+//!
+//! # Lock-Mint Flow
+//!
+//! 1. User locks ETH/ERC-20 in vault contract on Ethereum
+//! 2. Relayer detects lock event and submits proof to Synor
+//! 3. Synor verifies proof and mints wrapped tokens
+//!
+//! # Burn-Unlock Flow
+//!
+//! 1. User burns wrapped tokens on Synor
+//! 2. Relayer detects burn event and submits proof to Ethereum
+//! 3. Vault contract verifies proof and unlocks original tokens
+
+use crate::{
+ AssetId, Bridge, BridgeAddress, BridgeError, BridgeResult, BridgeTransfer, ChainType,
+ TransferDirection, TransferId, TransferManager, TransferStatus, VaultManager,
+ ETH_MIN_CONFIRMATIONS,
+};
+use alloy_primitives::{Address, B256, U256};
+use alloy_sol_types::sol;
+use async_trait::async_trait;
+use borsh::{BorshDeserialize, BorshSerialize};
+use parking_lot::RwLock;
+use serde::{Deserialize, Serialize};
+use sha3::{Digest, Keccak256};
+use std::collections::HashMap;
+use std::sync::Arc;
+
+// Solidity event signatures
+sol! {
+ event TokenLocked(
+ address indexed token,
+ address indexed sender,
+ bytes32 recipient,
+ uint256 amount,
+ uint256 nonce
+ );
+
+ event TokenUnlocked(
+ address indexed token,
+ address indexed recipient,
+ uint256 amount,
+ bytes32 transferId
+ );
+}
+
+/// Ethereum bridge configuration
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct EthereumBridgeConfig {
+ /// Ethereum chain ID
+ pub chain_id: u64,
+ /// Vault contract address
+ pub vault_address: Address,
+ /// Required confirmations
+ pub required_confirmations: u64,
+ /// Supported tokens (address → AssetId)
+ pub supported_tokens: HashMap
,
+ /// Relayer addresses (for multi-sig)
+ pub relayers: Vec,
+ /// Required relayer signatures
+ pub required_signatures: usize,
+ /// Whether bridge is paused
+ pub paused: bool,
+ /// Daily limit per token (address → limit)
+ pub daily_limits: HashMap,
+}
+
+impl Default for EthereumBridgeConfig {
+ fn default() -> Self {
+ let mut supported_tokens = HashMap::new();
+ supported_tokens.insert(Address::ZERO, AssetId::eth()); // Native ETH
+
+ Self {
+ chain_id: 1, // Mainnet
+ vault_address: Address::ZERO,
+ required_confirmations: ETH_MIN_CONFIRMATIONS,
+ supported_tokens,
+ relayers: Vec::new(),
+ required_signatures: 1,
+ paused: false,
+ daily_limits: HashMap::new(),
+ }
+ }
+}
+
+impl EthereumBridgeConfig {
+ /// Create testnet config
+ pub fn sepolia() -> Self {
+ Self {
+ chain_id: 11155111,
+ required_confirmations: 3, // Lower for testnet
+ ..Default::default()
+ }
+ }
+
+ /// Add supported token
+ pub fn add_token(&mut self, address: Address, asset: AssetId) {
+ self.supported_tokens.insert(address, asset);
+ }
+
+ /// Add relayer
+ pub fn add_relayer(&mut self, address: Address) {
+ if !self.relayers.contains(&address) {
+ self.relayers.push(address);
+ }
+ }
+}
+
+/// Ethereum event types
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum EthereumEventType {
+ /// Token locked for bridging to Synor
+ TokenLocked,
+ /// Token unlocked after bridging from Synor
+ TokenUnlocked,
+}
+
+/// Ethereum event
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct EthereumEvent {
+ /// Event type
+ pub event_type: EthereumEventType,
+ /// Transaction hash
+ pub tx_hash: B256,
+ /// Block number
+ pub block_number: u64,
+ /// Log index
+ pub log_index: u64,
+ /// Token address (zero for native ETH)
+ pub token: Address,
+ /// Sender address
+ pub sender: Address,
+ /// Amount
+ pub amount: U256,
+ /// Recipient (Synor address for lock, Ethereum address for unlock)
+ pub recipient: Vec,
+ /// Nonce
+ pub nonce: u64,
+}
+
+impl EthereumEvent {
+ /// Compute event hash for verification
+ pub fn hash(&self) -> B256 {
+ let mut hasher = Keccak256::new();
+ hasher.update(self.tx_hash.as_slice());
+ hasher.update(&self.block_number.to_le_bytes());
+ hasher.update(&self.log_index.to_le_bytes());
+ hasher.update(self.token.as_slice());
+ hasher.update(self.sender.as_slice());
+ hasher.update(&self.amount.to_le_bytes::<32>());
+ hasher.update(&self.recipient);
+ hasher.update(&self.nonce.to_le_bytes());
+
+ let result = hasher.finalize();
+ B256::from_slice(&result)
+ }
+}
+
+/// Wrapped token on Synor
+#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub struct WrappedToken {
+ /// Original asset
+ pub original: AssetId,
+ /// Wrapped asset on Synor
+ pub wrapped: AssetId,
+ /// Total supply minted
+ pub total_supply: u128,
+ /// Original token contract on Ethereum
+ pub ethereum_address: Vec,
+}
+
+impl WrappedToken {
+ /// Create a wrapped token
+ pub fn new(ethereum_address: Address, original: AssetId) -> Self {
+ let wrapped = AssetId::wrapped(&original);
+ Self {
+ original,
+ wrapped,
+ total_supply: 0,
+ ethereum_address: ethereum_address.to_vec(),
+ }
+ }
+
+ /// Mint wrapped tokens
+ pub fn mint(&mut self, amount: u128) {
+ self.total_supply += amount;
+ }
+
+ /// Burn wrapped tokens
+ pub fn burn(&mut self, amount: u128) -> BridgeResult<()> {
+ if amount > self.total_supply {
+ return Err(BridgeError::InsufficientBalance {
+ required: amount,
+ available: self.total_supply,
+ });
+ }
+ self.total_supply -= amount;
+ Ok(())
+ }
+}
+
+/// Ethereum bridge
+pub struct EthereumBridge {
+ /// Configuration
+ config: RwLock,
+ /// Transfer manager
+ transfers: Arc>,
+ /// Vault manager
+ vaults: Arc>,
+ /// Wrapped tokens by original address
+ wrapped_tokens: RwLock>,
+ /// Pending events awaiting confirmation
+ pending_events: RwLock>,
+ /// Processed event hashes (to prevent replay)
+ processed_events: RwLock>,
+ /// Relayer signatures per event
+ relayer_signatures: RwLock)>>>,
+}
+
+impl EthereumBridge {
+ /// Create a new Ethereum bridge
+ pub fn new(config: EthereumBridgeConfig) -> Self {
+ Self {
+ config: RwLock::new(config),
+ transfers: Arc::new(RwLock::new(TransferManager::new())),
+ vaults: Arc::new(RwLock::new(VaultManager::new())),
+ wrapped_tokens: RwLock::new(HashMap::new()),
+ pending_events: RwLock::new(HashMap::new()),
+ processed_events: RwLock::new(HashMap::new()),
+ relayer_signatures: RwLock::new(HashMap::new()),
+ }
+ }
+
+ /// Create for Sepolia testnet
+ pub fn sepolia() -> Self {
+ Self::new(EthereumBridgeConfig::sepolia())
+ }
+
+ /// Get configuration
+ pub fn config(&self) -> EthereumBridgeConfig {
+ self.config.read().clone()
+ }
+
+ /// Update configuration
+ pub fn update_config(&self, f: F)
+ where
+ F: FnOnce(&mut EthereumBridgeConfig),
+ {
+ f(&mut self.config.write());
+ }
+
+ /// Pause bridge
+ pub fn pause(&self) {
+ self.config.write().paused = true;
+ }
+
+ /// Resume bridge
+ pub fn resume(&self) {
+ self.config.write().paused = false;
+ }
+
+ /// Check if bridge is paused
+ pub fn is_paused(&self) -> bool {
+ self.config.read().paused
+ }
+
+ /// Process a lock event from Ethereum
+ pub fn process_lock_event(
+ &self,
+ event: EthereumEvent,
+ current_time: u64,
+ ) -> BridgeResult {
+ if self.is_paused() {
+ return Err(BridgeError::BridgePaused);
+ }
+
+ // Check for replay
+ let event_hash = event.hash();
+ if self.processed_events.read().contains_key(&event_hash) {
+ return Err(BridgeError::TransferAlreadyExists(
+ hex::encode(event_hash.as_slice()),
+ ));
+ }
+
+ // Verify token is supported
+ let config = self.config.read();
+ let asset = config
+ .supported_tokens
+ .get(&event.token)
+ .cloned()
+ .ok_or_else(|| BridgeError::AssetNotSupported(format!("{:?}", event.token)))?;
+
+ // Create bridge address from recipient bytes
+ let recipient = if event.recipient.len() == 32 {
+ let mut arr = [0u8; 32];
+ arr.copy_from_slice(&event.recipient);
+ BridgeAddress::from_synor(arr)
+ } else {
+ return Err(BridgeError::InvalidAddress("invalid recipient".to_string()));
+ };
+
+ let sender = BridgeAddress::from_eth(event.sender.into());
+ let amount = event.amount.to::();
+
+ // Create transfer
+ let transfer_id = self.transfers.write().create_inbound(
+ ChainType::Ethereum,
+ asset,
+ amount,
+ sender,
+ recipient,
+ config.required_confirmations,
+ current_time,
+ )?;
+
+ // Store pending event
+ self.pending_events.write().insert(event_hash, event);
+
+ // Record lock in transfer
+ self.transfers.write().confirm_lock(
+ &transfer_id,
+ event_hash.to_vec(),
+ 0, // Will be updated with actual block number
+ current_time,
+ )?;
+
+ Ok(transfer_id)
+ }
+
+ /// Submit relayer signature for an event
+ pub fn submit_relayer_signature(
+ &self,
+ event_hash: B256,
+ relayer: Address,
+ signature: Vec,
+ ) -> BridgeResult {
+ let config = self.config.read();
+
+ // Verify relayer is authorized
+ if !config.relayers.contains(&relayer) {
+ return Err(BridgeError::SignatureVerificationFailed(
+ "unauthorized relayer".to_string(),
+ ));
+ }
+
+ // Add signature
+ let mut signatures = self.relayer_signatures.write();
+ let sigs = signatures.entry(event_hash).or_default();
+
+ // Check for duplicate
+ if sigs.iter().any(|(r, _)| r == &relayer) {
+ return Ok(false);
+ }
+
+ sigs.push((relayer, signature));
+
+ // Check if we have enough signatures
+ Ok(sigs.len() >= config.required_signatures)
+ }
+
+ /// Update confirmations for pending events
+ pub fn update_confirmations(
+ &self,
+ current_block: u64,
+ current_time: u64,
+ ) -> BridgeResult> {
+ let config = self.config.read();
+ let required_confirmations = config.required_confirmations;
+ drop(config);
+
+ let mut confirmed = Vec::new();
+
+ // Collect pending events and their confirmations
+ let pending = self.pending_events.read();
+ let events_to_process: Vec<_> = pending
+ .iter()
+ .map(|(hash, event)| {
+ let confirmations = current_block.saturating_sub(event.block_number);
+ (*hash, confirmations)
+ })
+ .collect();
+ drop(pending);
+
+ // Find and update matching transfers
+ for (event_hash, confirmations) in events_to_process {
+ // Collect matching transfer IDs first
+ let matching_transfer_id = {
+ let transfers = self.transfers.read();
+ transfers
+ .pending_transfers()
+ .iter()
+ .find_map(|transfer| {
+ transfer.source_tx_hash.as_ref().and_then(|tx_hash| {
+ if tx_hash.as_slice() == event_hash.as_slice() {
+ Some(transfer.id.clone())
+ } else {
+ None
+ }
+ })
+ })
+ };
+
+ // Now update the transfer if found
+ if let Some(transfer_id) = matching_transfer_id {
+ self.transfers.write().update_confirmations(
+ &transfer_id,
+ confirmations,
+ current_time,
+ )?;
+
+ if confirmations >= required_confirmations {
+ confirmed.push(transfer_id);
+ }
+ }
+ }
+
+ Ok(confirmed)
+ }
+
+ /// Mint wrapped tokens after confirmation
+ pub fn mint_wrapped_tokens(
+ &self,
+ transfer_id: &TransferId,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let mut transfers = self.transfers.write();
+ let transfer = transfers
+ .get(transfer_id)
+ .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?;
+
+ // Verify transfer is confirmed
+ if transfer.status != TransferStatus::Confirmed {
+ return Err(BridgeError::InvalidProof(format!(
+ "transfer not confirmed: {}",
+ transfer.status
+ )));
+ }
+
+ // Get original token address
+ let token_address = if transfer.asset.identifier == "native" {
+ Address::ZERO
+ } else {
+ let bytes = hex::decode(
+ transfer
+ .asset
+ .identifier
+ .strip_prefix("0x")
+ .unwrap_or(&transfer.asset.identifier),
+ )
+ .map_err(|e| BridgeError::InvalidAddress(e.to_string()))?;
+
+ if bytes.len() != 20 {
+ return Err(BridgeError::InvalidAddress("invalid address length".to_string()));
+ }
+ Address::from_slice(&bytes)
+ };
+
+ // Get or create wrapped token
+ let mut wrapped_tokens = self.wrapped_tokens.write();
+ let wrapped = wrapped_tokens
+ .entry(token_address)
+ .or_insert_with(|| WrappedToken::new(token_address, transfer.asset.clone()));
+
+ // Mint tokens
+ wrapped.mint(transfer.amount);
+
+ // Mark transfer as completed
+ drop(wrapped_tokens);
+ transfers.confirm_mint(transfer_id, vec![], current_time)?;
+
+ Ok(())
+ }
+
+ /// Initiate burn for outbound transfer
+ pub fn initiate_burn(
+ &self,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ current_time: u64,
+ ) -> BridgeResult {
+ if self.is_paused() {
+ return Err(BridgeError::BridgePaused);
+ }
+
+ // Find wrapped token
+ let token_address = if let Some(id) = asset.unwrap_identifier() {
+ if id == "native" {
+ Address::ZERO
+ } else {
+ let bytes = hex::decode(id.strip_prefix("0x").unwrap_or(id))
+ .map_err(|e| BridgeError::InvalidAddress(e.to_string()))?;
+ Address::from_slice(&bytes)
+ }
+ } else {
+ return Err(BridgeError::AssetNotSupported(asset.to_string()));
+ };
+
+ // Verify we have enough supply
+ let mut wrapped_tokens = self.wrapped_tokens.write();
+ let wrapped = wrapped_tokens
+ .get_mut(&token_address)
+ .ok_or_else(|| BridgeError::AssetNotSupported(format!("{:?}", token_address)))?;
+
+ // Burn tokens
+ wrapped.burn(amount)?;
+
+ // Get original asset
+ let original_asset = wrapped.original.clone();
+
+ // Create outbound transfer
+ let config = self.config.read();
+ let transfer_id = self.transfers.write().create_outbound(
+ ChainType::Ethereum,
+ original_asset,
+ amount,
+ sender,
+ recipient,
+ config.required_confirmations,
+ current_time,
+ )?;
+
+ Ok(transfer_id)
+ }
+
+ /// Get wrapped token info
+ pub fn get_wrapped_token(&self, address: Address) -> Option {
+ self.wrapped_tokens.read().get(&address).cloned()
+ }
+
+ /// Get total wrapped supply
+ pub fn total_wrapped_supply(&self) -> u128 {
+ self.wrapped_tokens
+ .read()
+ .values()
+ .map(|t| t.total_supply)
+ .sum()
+ }
+
+ /// Get transfer manager
+ pub fn transfer_manager(&self) -> Arc> {
+ self.transfers.clone()
+ }
+
+ /// Get vault manager
+ pub fn vault_manager(&self) -> Arc> {
+ self.vaults.clone()
+ }
+}
+
+#[async_trait]
+impl Bridge for EthereumBridge {
+ fn source_chain(&self) -> ChainType {
+ ChainType::Ethereum
+ }
+
+ fn destination_chain(&self) -> ChainType {
+ ChainType::Synor
+ }
+
+ fn supports_asset(&self, asset: &AssetId) -> bool {
+ let config = self.config.read();
+ config
+ .supported_tokens
+ .values()
+ .any(|a| a.identifier == asset.identifier)
+ }
+
+ fn min_confirmations(&self) -> u64 {
+ self.config.read().required_confirmations
+ }
+
+ async fn lock(&self, transfer: &BridgeTransfer) -> BridgeResult {
+ // In production, this would interact with Ethereum contract
+ // For now, we simulate the lock
+ Ok(transfer.id.clone())
+ }
+
+ async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult {
+ let transfers = self.transfers.read();
+ let transfer = transfers
+ .get(transfer_id)
+ .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?;
+
+ Ok(transfer.has_sufficient_confirmations())
+ }
+
+ async fn mint(&self, transfer: &BridgeTransfer) -> BridgeResult<()> {
+ let current_time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ self.mint_wrapped_tokens(&transfer.id, current_time)
+ }
+
+ async fn burn(&self, transfer: &BridgeTransfer) -> BridgeResult {
+ let current_time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ self.initiate_burn(
+ transfer.asset.clone(),
+ transfer.amount,
+ transfer.sender.clone(),
+ transfer.recipient.clone(),
+ current_time,
+ )
+ }
+
+ async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()> {
+ // In production, this would interact with Ethereum contract
+ let current_time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ self.transfers
+ .write()
+ .confirm_unlock(transfer_id, vec![], current_time)
+ }
+
+ async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult {
+ let transfers = self.transfers.read();
+ let transfer = transfers
+ .get(transfer_id)
+ .ok_or_else(|| BridgeError::TransferNotFound(transfer_id.to_string()))?;
+
+ Ok(transfer.status)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_sender() -> BridgeAddress {
+ BridgeAddress::from_eth([0xaa; 20])
+ }
+
+ fn test_recipient() -> BridgeAddress {
+ BridgeAddress::from_synor([0xbb; 32])
+ }
+
+ #[test]
+ fn test_bridge_creation() {
+ let bridge = EthereumBridge::new(EthereumBridgeConfig::default());
+ assert!(!bridge.is_paused());
+ assert!(bridge.supports_asset(&AssetId::eth()));
+ }
+
+ #[test]
+ fn test_lock_event_processing() {
+ let bridge = EthereumBridge::new(EthereumBridgeConfig::default());
+ let current_time = 1700000000;
+
+ let event = EthereumEvent {
+ event_type: EthereumEventType::TokenLocked,
+ tx_hash: B256::from([0x11; 32]),
+ block_number: 100,
+ log_index: 0,
+ token: Address::ZERO, // Native ETH
+ sender: Address::from([0xaa; 20]),
+ amount: U256::from(1000u64),
+ recipient: vec![0xbb; 32],
+ nonce: 0,
+ };
+
+ let transfer_id = bridge.process_lock_event(event.clone(), current_time).unwrap();
+
+ // Verify transfer was created
+ let transfers = bridge.transfers.read();
+ let transfer = transfers.get(&transfer_id).unwrap();
+ assert_eq!(transfer.direction, TransferDirection::Inbound);
+ assert_eq!(transfer.amount, 1000);
+ }
+
+ #[test]
+ fn test_wrapped_token_minting() {
+ let bridge = EthereumBridge::new(EthereumBridgeConfig::default());
+ let current_time = 1700000000;
+
+ // Process lock event
+ let event = EthereumEvent {
+ event_type: EthereumEventType::TokenLocked,
+ tx_hash: B256::from([0x11; 32]),
+ block_number: 100,
+ log_index: 0,
+ token: Address::ZERO,
+ sender: Address::from([0xaa; 20]),
+ amount: U256::from(1000u64),
+ recipient: vec![0xbb; 32],
+ nonce: 0,
+ };
+
+ let transfer_id = bridge.process_lock_event(event, current_time).unwrap();
+
+ // Simulate confirmations
+ bridge
+ .transfers
+ .write()
+ .update_confirmations(&transfer_id, 12, current_time + 100)
+ .unwrap();
+
+ // Mint wrapped tokens
+ bridge.mint_wrapped_tokens(&transfer_id, current_time + 200).unwrap();
+
+ // Verify wrapped tokens were minted
+ let wrapped = bridge.get_wrapped_token(Address::ZERO).unwrap();
+ assert_eq!(wrapped.total_supply, 1000);
+ }
+
+ #[test]
+ fn test_burn_initiation() {
+ let bridge = EthereumBridge::new(EthereumBridgeConfig::default());
+ let current_time = 1700000000;
+
+ // First mint some wrapped tokens
+ let mut wrapped_tokens = bridge.wrapped_tokens.write();
+ let mut wrapped = WrappedToken::new(Address::ZERO, AssetId::eth());
+ wrapped.mint(5000);
+ wrapped_tokens.insert(Address::ZERO, wrapped);
+ drop(wrapped_tokens);
+
+ // Initiate burn
+ let asset = AssetId::wrapped(&AssetId::eth());
+ let transfer_id = bridge
+ .initiate_burn(
+ asset,
+ 1000,
+ test_recipient(),
+ test_sender(),
+ current_time,
+ )
+ .unwrap();
+
+ // Verify transfer was created
+ let transfers = bridge.transfers.read();
+ let transfer = transfers.get(&transfer_id).unwrap();
+ assert_eq!(transfer.direction, TransferDirection::Outbound);
+ assert_eq!(transfer.amount, 1000);
+
+ // Verify wrapped supply decreased
+ drop(transfers);
+ let wrapped = bridge.get_wrapped_token(Address::ZERO).unwrap();
+ assert_eq!(wrapped.total_supply, 4000);
+ }
+
+ #[test]
+ fn test_bridge_pause() {
+ let bridge = EthereumBridge::new(EthereumBridgeConfig::default());
+
+ bridge.pause();
+ assert!(bridge.is_paused());
+
+ let event = EthereumEvent {
+ event_type: EthereumEventType::TokenLocked,
+ tx_hash: B256::from([0x11; 32]),
+ block_number: 100,
+ log_index: 0,
+ token: Address::ZERO,
+ sender: Address::from([0xaa; 20]),
+ amount: U256::from(1000u64),
+ recipient: vec![0xbb; 32],
+ nonce: 0,
+ };
+
+ let result = bridge.process_lock_event(event, 0);
+ assert!(matches!(result, Err(BridgeError::BridgePaused)));
+
+ bridge.resume();
+ assert!(!bridge.is_paused());
+ }
+
+ #[test]
+ fn test_event_hash() {
+ let event1 = EthereumEvent {
+ event_type: EthereumEventType::TokenLocked,
+ tx_hash: B256::from([0x11; 32]),
+ block_number: 100,
+ log_index: 0,
+ token: Address::ZERO,
+ sender: Address::from([0xaa; 20]),
+ amount: U256::from(1000u64),
+ recipient: vec![0xbb; 32],
+ nonce: 0,
+ };
+
+ let event2 = EthereumEvent {
+ nonce: 1,
+ ..event1.clone()
+ };
+
+ // Different nonce should produce different hash
+ assert_ne!(event1.hash(), event2.hash());
+
+ // Same event should produce same hash
+ let event3 = event1.clone();
+ assert_eq!(event1.hash(), event3.hash());
+ }
+}
diff --git a/crates/synor-bridge/src/lib.rs b/crates/synor-bridge/src/lib.rs
new file mode 100644
index 0000000..6d1b0c2
--- /dev/null
+++ b/crates/synor-bridge/src/lib.rs
@@ -0,0 +1,436 @@
+//! Cross-Chain Bridge Infrastructure for Synor
+//!
+//! This crate provides bridge infrastructure for cross-chain asset transfers,
+//! enabling Synor to interoperate with external blockchains like Ethereum.
+//!
+//! # Architecture
+//!
+//! ```text
+//! ┌────────────────────────────────────────────────────────────────────┐
+//! │ Synor Bridge Architecture │
+//! ├────────────────────────────────────────────────────────────────────┤
+//! │ │
+//! │ ┌──────────────┐ Lock-Mint ┌──────────────────────────┐ │
+//! │ │ External │ ──────────────► │ Synor Chain │ │
+//! │ │ Chain │ │ │ │
+//! │ │ (Ethereum) │ ◄────────────── │ Wrapped Tokens (sETH) │ │
+//! │ │ │ Burn-Unlock │ │ │
+//! │ └──────────────┘ └──────────────────────────┘ │
+//! │ │
+//! │ ┌─────────────────────────────────────────────────────────────┐ │
+//! │ │ Bridge Components │ │
+//! │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐ │ │
+//! │ │ │ Vault │ │ Relayer │ │ Validator │ │ Oracle │ │ │
+//! │ │ │ (Locks) │ │ (Events) │ │ (Proofs) │ │ (Price) │ │ │
+//! │ │ └───────────┘ └───────────┘ └───────────┘ └──────────┘ │ │
+//! │ └─────────────────────────────────────────────────────────────┘ │
+//! │ │
+//! └────────────────────────────────────────────────────────────────────┘
+//! ```
+//!
+//! # Bridge Flow (Lock-Mint)
+//!
+//! ```text
+//! User Ethereum Relayer Synor
+//! │ │ │ │
+//! ├── Lock ETH ─────────►│ │ │
+//! │ ├── LockEvent ─────►│ │
+//! │ │ ├── SubmitProof ───►│
+//! │ │ │ ├─ Verify
+//! │ │ │ ├─ Mint sETH
+//! │◄─────────────────────────────────────────────────── sETH ──────┤
+//! ```
+//!
+//! # Supported Bridges
+//!
+//! - **Ethereum**: Lock-Mint bridge for ETH and ERC-20 tokens
+//! - **Bitcoin**: HTLC-based atomic swaps (via synor-ibc)
+//! - **IBC Chains**: Native IBC transfers (via synor-ibc)
+
+#![allow(dead_code)]
+
+pub mod ethereum;
+pub mod transfer;
+pub mod vault;
+
+pub use ethereum::{
+ EthereumBridge, EthereumBridgeConfig, EthereumEvent, EthereumEventType, WrappedToken,
+};
+pub use transfer::{
+ BridgeTransfer, TransferDirection, TransferId, TransferManager, TransferStatus,
+};
+pub use vault::{LockedAsset, Vault, VaultId, VaultManager};
+
+use borsh::{BorshDeserialize, BorshSerialize};
+use serde::{Deserialize, Serialize};
+use std::fmt;
+use thiserror::Error;
+
+/// Bridge protocol version
+pub const BRIDGE_VERSION: &str = "1.0.0";
+
+/// Maximum transfer amount for safety (in smallest units)
+pub const MAX_TRANSFER_AMOUNT: u128 = 1_000_000_000_000_000_000_000_000; // 1M tokens
+
+/// Minimum confirmations for Ethereum deposits
+pub const ETH_MIN_CONFIRMATIONS: u64 = 12;
+
+/// Minimum confirmations for Bitcoin deposits
+pub const BTC_MIN_CONFIRMATIONS: u64 = 6;
+
+/// Bridge chain identifier
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub enum ChainType {
+ /// Synor native chain
+ Synor,
+ /// Ethereum mainnet
+ Ethereum,
+ /// Ethereum Sepolia testnet
+ EthereumSepolia,
+ /// Bitcoin mainnet
+ Bitcoin,
+ /// Bitcoin testnet
+ BitcoinTestnet,
+ /// Cosmos SDK chain (IBC)
+ Cosmos(String),
+ /// Custom chain
+ Custom(String),
+}
+
+impl ChainType {
+ /// Get chain ID for Ethereum networks
+ pub fn eth_chain_id(&self) -> Option {
+ match self {
+ ChainType::Ethereum => Some(1),
+ ChainType::EthereumSepolia => Some(11155111),
+ _ => None,
+ }
+ }
+
+ /// Check if this is an EVM-compatible chain
+ pub fn is_evm(&self) -> bool {
+ matches!(self, ChainType::Ethereum | ChainType::EthereumSepolia)
+ }
+}
+
+impl fmt::Display for ChainType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ChainType::Synor => write!(f, "synor"),
+ ChainType::Ethereum => write!(f, "ethereum"),
+ ChainType::EthereumSepolia => write!(f, "ethereum-sepolia"),
+ ChainType::Bitcoin => write!(f, "bitcoin"),
+ ChainType::BitcoinTestnet => write!(f, "bitcoin-testnet"),
+ ChainType::Cosmos(id) => write!(f, "cosmos:{}", id),
+ ChainType::Custom(id) => write!(f, "custom:{}", id),
+ }
+ }
+}
+
+/// Asset identifier across chains
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub struct AssetId {
+ /// Chain where the asset originates
+ pub chain: ChainType,
+ /// Asset identifier (contract address for ERC-20, "native" for ETH)
+ pub identifier: String,
+ /// Asset symbol
+ pub symbol: String,
+ /// Decimal places
+ pub decimals: u8,
+}
+
+impl AssetId {
+ /// Create native Ethereum asset
+ pub fn eth() -> Self {
+ Self {
+ chain: ChainType::Ethereum,
+ identifier: "native".to_string(),
+ symbol: "ETH".to_string(),
+ decimals: 18,
+ }
+ }
+
+ /// Create ERC-20 asset
+ pub fn erc20(address: impl Into, symbol: impl Into, decimals: u8) -> Self {
+ Self {
+ chain: ChainType::Ethereum,
+ identifier: address.into(),
+ symbol: symbol.into(),
+ decimals,
+ }
+ }
+
+ /// Create Synor native asset
+ pub fn synor() -> Self {
+ Self {
+ chain: ChainType::Synor,
+ identifier: "native".to_string(),
+ symbol: "SYNOR".to_string(),
+ decimals: 18,
+ }
+ }
+
+ /// Create wrapped asset on Synor
+ pub fn wrapped(original: &AssetId) -> Self {
+ Self {
+ chain: ChainType::Synor,
+ identifier: format!("wrapped:{}", original.identifier),
+ symbol: format!("s{}", original.symbol),
+ decimals: original.decimals,
+ }
+ }
+
+ /// Check if this is a wrapped asset
+ pub fn is_wrapped(&self) -> bool {
+ self.identifier.starts_with("wrapped:")
+ }
+
+ /// Get the original asset if this is wrapped
+ pub fn unwrap_identifier(&self) -> Option<&str> {
+ self.identifier.strip_prefix("wrapped:")
+ }
+}
+
+impl fmt::Display for AssetId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}:{}", self.chain, self.symbol)
+ }
+}
+
+/// Bridge address (unified format for cross-chain addresses)
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub struct BridgeAddress {
+ /// Chain type
+ pub chain: ChainType,
+ /// Address bytes
+ pub address: Vec,
+}
+
+impl BridgeAddress {
+ /// Create from Ethereum address (20 bytes)
+ pub fn from_eth(address: [u8; 20]) -> Self {
+ Self {
+ chain: ChainType::Ethereum,
+ address: address.to_vec(),
+ }
+ }
+
+ /// Create from Synor address (32 bytes)
+ pub fn from_synor(address: [u8; 32]) -> Self {
+ Self {
+ chain: ChainType::Synor,
+ address: address.to_vec(),
+ }
+ }
+
+ /// Create from hex string
+ pub fn from_hex(chain: ChainType, hex: &str) -> Result {
+ let hex = hex.strip_prefix("0x").unwrap_or(hex);
+ let address = hex::decode(hex)
+ .map_err(|e| BridgeError::InvalidAddress(format!("invalid hex: {}", e)))?;
+ Ok(Self { chain, address })
+ }
+
+ /// Get as Ethereum address
+ pub fn as_eth(&self) -> Option<[u8; 20]> {
+ if self.address.len() == 20 {
+ let mut arr = [0u8; 20];
+ arr.copy_from_slice(&self.address);
+ Some(arr)
+ } else {
+ None
+ }
+ }
+
+ /// Get as Synor address
+ pub fn as_synor(&self) -> Option<[u8; 32]> {
+ if self.address.len() == 32 {
+ let mut arr = [0u8; 32];
+ arr.copy_from_slice(&self.address);
+ Some(arr)
+ } else {
+ None
+ }
+ }
+
+ /// Get as hex string
+ pub fn to_hex(&self) -> String {
+ format!("0x{}", hex::encode(&self.address))
+ }
+}
+
+impl fmt::Display for BridgeAddress {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}:{}", self.chain, self.to_hex())
+ }
+}
+
+/// Bridge error types
+#[derive(Debug, Error)]
+pub enum BridgeError {
+ #[error("Invalid address: {0}")]
+ InvalidAddress(String),
+
+ #[error("Invalid amount: {0}")]
+ InvalidAmount(String),
+
+ #[error("Transfer not found: {0}")]
+ TransferNotFound(String),
+
+ #[error("Vault not found: {0}")]
+ VaultNotFound(String),
+
+ #[error("Insufficient balance: need {required}, have {available}")]
+ InsufficientBalance { required: u128, available: u128 },
+
+ #[error("Asset not supported: {0}")]
+ AssetNotSupported(String),
+
+ #[error("Chain not supported: {0}")]
+ ChainNotSupported(String),
+
+ #[error("Transfer already exists: {0}")]
+ TransferAlreadyExists(String),
+
+ #[error("Transfer already completed: {0}")]
+ TransferAlreadyCompleted(String),
+
+ #[error("Invalid proof: {0}")]
+ InvalidProof(String),
+
+ #[error("Insufficient confirmations: need {required}, have {actual}")]
+ InsufficientConfirmations { required: u64, actual: u64 },
+
+ #[error("Bridge paused")]
+ BridgePaused,
+
+ #[error("Rate limit exceeded")]
+ RateLimitExceeded,
+
+ #[error("Signature verification failed: {0}")]
+ SignatureVerificationFailed(String),
+
+ #[error("Oracle error: {0}")]
+ OracleError(String),
+
+ #[error("Relayer error: {0}")]
+ RelayerError(String),
+
+ #[error("Internal error: {0}")]
+ Internal(String),
+}
+
+/// Result type for bridge operations
+pub type BridgeResult = std::result::Result;
+
+/// Bridge trait for implementing cross-chain bridges
+#[async_trait::async_trait]
+pub trait Bridge: Send + Sync {
+ /// Get the source chain type
+ fn source_chain(&self) -> ChainType;
+
+ /// Get the destination chain type
+ fn destination_chain(&self) -> ChainType;
+
+ /// Check if an asset is supported
+ fn supports_asset(&self, asset: &AssetId) -> bool;
+
+ /// Get minimum confirmations required
+ fn min_confirmations(&self) -> u64;
+
+ /// Lock assets on the source chain
+ async fn lock(&self, transfer: &BridgeTransfer) -> BridgeResult;
+
+ /// Verify a lock proof
+ async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult;
+
+ /// Mint wrapped tokens on the destination chain
+ async fn mint(&self, transfer: &BridgeTransfer) -> BridgeResult<()>;
+
+ /// Burn wrapped tokens (for redemption)
+ async fn burn(&self, transfer: &BridgeTransfer) -> BridgeResult;
+
+ /// Unlock original tokens
+ async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()>;
+
+ /// Get transfer status
+ async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult;
+}
+
+/// Bridge event for tracking cross-chain activity
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum BridgeEvent {
+ /// Asset locked on source chain
+ AssetLocked {
+ transfer_id: TransferId,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ },
+ /// Lock verified
+ LockVerified {
+ transfer_id: TransferId,
+ confirmations: u64,
+ },
+ /// Wrapped token minted
+ TokenMinted {
+ transfer_id: TransferId,
+ asset: AssetId,
+ amount: u128,
+ recipient: BridgeAddress,
+ },
+ /// Token burned for redemption
+ TokenBurned {
+ transfer_id: TransferId,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ },
+ /// Original asset unlocked
+ AssetUnlocked {
+ transfer_id: TransferId,
+ recipient: BridgeAddress,
+ },
+ /// Transfer completed
+ TransferCompleted { transfer_id: TransferId },
+ /// Transfer failed
+ TransferFailed {
+ transfer_id: TransferId,
+ reason: String,
+ },
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_chain_type() {
+ assert!(ChainType::Ethereum.is_evm());
+ assert!(!ChainType::Bitcoin.is_evm());
+ assert_eq!(ChainType::Ethereum.eth_chain_id(), Some(1));
+ }
+
+ #[test]
+ fn test_asset_id() {
+ let eth = AssetId::eth();
+ assert_eq!(eth.symbol, "ETH");
+ assert_eq!(eth.decimals, 18);
+
+ let wrapped = AssetId::wrapped(ð);
+ assert!(wrapped.is_wrapped());
+ assert_eq!(wrapped.symbol, "sETH");
+ }
+
+ #[test]
+ fn test_bridge_address() {
+ let eth_addr = BridgeAddress::from_eth([0xde; 20]);
+ assert_eq!(eth_addr.address.len(), 20);
+ assert!(eth_addr.as_eth().is_some());
+
+ let hex_addr = BridgeAddress::from_hex(ChainType::Ethereum, "0xdeadbeef").unwrap();
+ assert_eq!(hex_addr.address, vec![0xde, 0xad, 0xbe, 0xef]);
+ }
+}
diff --git a/crates/synor-bridge/src/transfer.rs b/crates/synor-bridge/src/transfer.rs
new file mode 100644
index 0000000..b05d148
--- /dev/null
+++ b/crates/synor-bridge/src/transfer.rs
@@ -0,0 +1,830 @@
+//! Bridge Transfer Management
+//!
+//! Manages cross-chain transfer lifecycle including:
+//! - Transfer initiation
+//! - Proof verification
+//! - Status tracking
+//! - Completion and failure handling
+
+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 transfer identifier
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub struct TransferId(pub String);
+
+impl TransferId {
+ /// Create a new transfer ID
+ pub fn new(id: impl Into) -> Self {
+ Self(id.into())
+ }
+
+ /// Generate transfer ID from parameters
+ pub fn generate(
+ sender: &BridgeAddress,
+ recipient: &BridgeAddress,
+ asset: &AssetId,
+ amount: u128,
+ nonce: u64,
+ ) -> Self {
+ let mut hasher = Sha256::new();
+ hasher.update(&sender.address);
+ hasher.update(&recipient.address);
+ hasher.update(asset.identifier.as_bytes());
+ hasher.update(&amount.to_le_bytes());
+ hasher.update(&nonce.to_le_bytes());
+ Self(hex::encode(&hasher.finalize()[..16]))
+ }
+}
+
+impl fmt::Display for TransferId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+/// Transfer direction
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub enum TransferDirection {
+ /// From external chain to Synor (Lock → Mint)
+ Inbound,
+ /// From Synor to external chain (Burn → Unlock)
+ Outbound,
+}
+
+impl fmt::Display for TransferDirection {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ TransferDirection::Inbound => write!(f, "inbound"),
+ TransferDirection::Outbound => write!(f, "outbound"),
+ }
+ }
+}
+
+/// Transfer status
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub enum TransferStatus {
+ /// Transfer initiated, awaiting lock confirmation
+ Pending,
+ /// Lock confirmed, awaiting sufficient confirmations
+ Locked,
+ /// Sufficient confirmations, ready for minting/unlocking
+ Confirmed,
+ /// Tokens minted on destination (inbound) or burned (outbound)
+ Minted,
+ /// Original tokens unlocked (outbound only)
+ Unlocked,
+ /// Transfer completed successfully
+ Completed,
+ /// Transfer failed
+ Failed,
+ /// Transfer expired
+ Expired,
+ /// Transfer refunded
+ Refunded,
+}
+
+impl TransferStatus {
+ /// Check if transfer is finalized
+ pub fn is_finalized(&self) -> bool {
+ matches!(
+ self,
+ TransferStatus::Completed
+ | TransferStatus::Failed
+ | TransferStatus::Expired
+ | TransferStatus::Refunded
+ )
+ }
+
+ /// Check if transfer can be retried
+ pub fn can_retry(&self) -> bool {
+ matches!(
+ self,
+ TransferStatus::Pending | TransferStatus::Failed | TransferStatus::Expired
+ )
+ }
+}
+
+impl fmt::Display for TransferStatus {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ TransferStatus::Pending => write!(f, "pending"),
+ TransferStatus::Locked => write!(f, "locked"),
+ TransferStatus::Confirmed => write!(f, "confirmed"),
+ TransferStatus::Minted => write!(f, "minted"),
+ TransferStatus::Unlocked => write!(f, "unlocked"),
+ TransferStatus::Completed => write!(f, "completed"),
+ TransferStatus::Failed => write!(f, "failed"),
+ TransferStatus::Expired => write!(f, "expired"),
+ TransferStatus::Refunded => write!(f, "refunded"),
+ }
+ }
+}
+
+/// Cross-chain bridge transfer
+#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
+pub struct BridgeTransfer {
+ /// Unique transfer ID
+ pub id: TransferId,
+ /// Transfer direction
+ pub direction: TransferDirection,
+ /// Source chain
+ pub source_chain: ChainType,
+ /// Destination chain
+ pub destination_chain: ChainType,
+ /// Asset being transferred
+ pub asset: AssetId,
+ /// Amount in smallest unit
+ pub amount: u128,
+ /// Sender address on source chain
+ pub sender: BridgeAddress,
+ /// Recipient address on destination chain
+ pub recipient: BridgeAddress,
+ /// Current status
+ pub status: TransferStatus,
+ /// Source chain transaction hash
+ pub source_tx_hash: Option>,
+ /// Destination chain transaction hash
+ pub destination_tx_hash: Option>,
+ /// Block number where lock occurred
+ pub lock_block: Option,
+ /// Current confirmations
+ pub confirmations: u64,
+ /// Required confirmations
+ pub required_confirmations: u64,
+ /// Transfer initiation timestamp
+ pub created_at: u64,
+ /// Last update timestamp
+ pub updated_at: u64,
+ /// Expiry timestamp (0 = no expiry)
+ pub expires_at: u64,
+ /// Error message if failed
+ pub error: Option,
+ /// Nonce for uniqueness
+ pub nonce: u64,
+}
+
+impl BridgeTransfer {
+ /// Create a new transfer
+ pub fn new(
+ direction: TransferDirection,
+ source_chain: ChainType,
+ destination_chain: ChainType,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ required_confirmations: u64,
+ nonce: u64,
+ current_time: u64,
+ ) -> Self {
+ let id = TransferId::generate(&sender, &recipient, &asset, amount, nonce);
+
+ Self {
+ id,
+ direction,
+ source_chain,
+ destination_chain,
+ asset,
+ amount,
+ sender,
+ recipient,
+ status: TransferStatus::Pending,
+ source_tx_hash: None,
+ destination_tx_hash: None,
+ lock_block: None,
+ confirmations: 0,
+ required_confirmations,
+ created_at: current_time,
+ updated_at: current_time,
+ expires_at: 0,
+ error: None,
+ nonce,
+ }
+ }
+
+ /// Create inbound transfer (external → Synor)
+ pub fn inbound(
+ source_chain: ChainType,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ required_confirmations: u64,
+ nonce: u64,
+ current_time: u64,
+ ) -> Self {
+ Self::new(
+ TransferDirection::Inbound,
+ source_chain,
+ ChainType::Synor,
+ asset,
+ amount,
+ sender,
+ recipient,
+ required_confirmations,
+ nonce,
+ current_time,
+ )
+ }
+
+ /// Create outbound transfer (Synor → external)
+ pub fn outbound(
+ destination_chain: ChainType,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ required_confirmations: u64,
+ nonce: u64,
+ current_time: u64,
+ ) -> Self {
+ Self::new(
+ TransferDirection::Outbound,
+ ChainType::Synor,
+ destination_chain,
+ asset,
+ amount,
+ sender,
+ recipient,
+ required_confirmations,
+ nonce,
+ current_time,
+ )
+ }
+
+ /// Set expiry
+ pub fn with_expiry(mut self, expires_at: u64) -> Self {
+ self.expires_at = expires_at;
+ self
+ }
+
+ /// Update status
+ pub fn set_status(&mut self, status: TransferStatus, current_time: u64) {
+ self.status = status;
+ self.updated_at = current_time;
+ }
+
+ /// Set lock confirmed
+ pub fn confirm_lock(&mut self, tx_hash: Vec, block_number: u64, current_time: u64) {
+ self.source_tx_hash = Some(tx_hash);
+ self.lock_block = Some(block_number);
+ self.status = TransferStatus::Locked;
+ self.updated_at = current_time;
+ }
+
+ /// Update confirmations
+ pub fn update_confirmations(&mut self, confirmations: u64, current_time: u64) {
+ self.confirmations = confirmations;
+ self.updated_at = current_time;
+
+ if confirmations >= self.required_confirmations && self.status == TransferStatus::Locked {
+ self.status = TransferStatus::Confirmed;
+ }
+ }
+
+ /// Mark as minted
+ pub fn confirm_mint(&mut self, tx_hash: Vec, current_time: u64) {
+ self.destination_tx_hash = Some(tx_hash);
+ self.status = TransferStatus::Minted;
+ self.updated_at = current_time;
+
+ // For inbound, minting is the final step
+ if self.direction == TransferDirection::Inbound {
+ self.status = TransferStatus::Completed;
+ }
+ }
+
+ /// Mark as unlocked
+ pub fn confirm_unlock(&mut self, tx_hash: Vec, current_time: u64) {
+ self.destination_tx_hash = Some(tx_hash);
+ self.status = TransferStatus::Unlocked;
+ self.updated_at = current_time;
+
+ // For outbound, unlocking is the final step
+ if self.direction == TransferDirection::Outbound {
+ self.status = TransferStatus::Completed;
+ }
+ }
+
+ /// Mark as failed
+ pub fn fail(&mut self, error: impl Into, current_time: u64) {
+ self.error = Some(error.into());
+ self.status = TransferStatus::Failed;
+ self.updated_at = current_time;
+ }
+
+ /// Check if expired
+ pub fn is_expired(&self, current_time: u64) -> bool {
+ self.expires_at > 0 && current_time >= self.expires_at
+ }
+
+ /// Check if transfer has sufficient confirmations
+ pub fn has_sufficient_confirmations(&self) -> bool {
+ self.confirmations >= self.required_confirmations
+ }
+
+ /// Get completion percentage (0-100)
+ pub fn completion_percentage(&self) -> u8 {
+ match self.status {
+ TransferStatus::Pending => 0,
+ TransferStatus::Locked => 25,
+ TransferStatus::Confirmed => 50,
+ TransferStatus::Minted => 75,
+ TransferStatus::Unlocked => 90,
+ TransferStatus::Completed => 100,
+ TransferStatus::Failed | TransferStatus::Expired | TransferStatus::Refunded => 0,
+ }
+ }
+}
+
+/// Transfer manager
+pub struct TransferManager {
+ /// Transfers by ID
+ transfers: HashMap,
+ /// Transfers by sender
+ by_sender: HashMap>,
+ /// Transfers by recipient
+ by_recipient: HashMap>,
+ /// Pending transfers
+ pending: Vec,
+ /// Next nonce
+ next_nonce: u64,
+}
+
+impl TransferManager {
+ /// Create a new transfer manager
+ pub fn new() -> Self {
+ Self {
+ transfers: HashMap::new(),
+ by_sender: HashMap::new(),
+ by_recipient: HashMap::new(),
+ pending: Vec::new(),
+ next_nonce: 0,
+ }
+ }
+
+ /// Get next nonce
+ pub fn next_nonce(&mut self) -> u64 {
+ let nonce = self.next_nonce;
+ self.next_nonce += 1;
+ nonce
+ }
+
+ /// Create a new inbound transfer
+ pub fn create_inbound(
+ &mut self,
+ source_chain: ChainType,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ required_confirmations: u64,
+ current_time: u64,
+ ) -> BridgeResult {
+ let nonce = self.next_nonce();
+ let transfer = BridgeTransfer::inbound(
+ source_chain,
+ asset,
+ amount,
+ sender.clone(),
+ recipient.clone(),
+ required_confirmations,
+ nonce,
+ current_time,
+ );
+
+ self.register_transfer(transfer)
+ }
+
+ /// Create a new outbound transfer
+ pub fn create_outbound(
+ &mut self,
+ destination_chain: ChainType,
+ asset: AssetId,
+ amount: u128,
+ sender: BridgeAddress,
+ recipient: BridgeAddress,
+ required_confirmations: u64,
+ current_time: u64,
+ ) -> BridgeResult {
+ let nonce = self.next_nonce();
+ let transfer = BridgeTransfer::outbound(
+ destination_chain,
+ asset,
+ amount,
+ sender.clone(),
+ recipient.clone(),
+ required_confirmations,
+ nonce,
+ current_time,
+ );
+
+ self.register_transfer(transfer)
+ }
+
+ /// Register a transfer
+ fn register_transfer(&mut self, transfer: BridgeTransfer) -> BridgeResult {
+ let id = transfer.id.clone();
+
+ if self.transfers.contains_key(&id) {
+ return Err(BridgeError::TransferAlreadyExists(id.to_string()));
+ }
+
+ self.by_sender
+ .entry(transfer.sender.clone())
+ .or_default()
+ .push(id.clone());
+
+ self.by_recipient
+ .entry(transfer.recipient.clone())
+ .or_default()
+ .push(id.clone());
+
+ self.pending.push(id.clone());
+ self.transfers.insert(id.clone(), transfer);
+
+ Ok(id)
+ }
+
+ /// Get transfer by ID
+ pub fn get(&self, id: &TransferId) -> Option<&BridgeTransfer> {
+ self.transfers.get(id)
+ }
+
+ /// Get mutable transfer by ID
+ pub fn get_mut(&mut self, id: &TransferId) -> Option<&mut BridgeTransfer> {
+ self.transfers.get_mut(id)
+ }
+
+ /// Confirm lock
+ pub fn confirm_lock(
+ &mut self,
+ id: &TransferId,
+ tx_hash: Vec,
+ block_number: u64,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let transfer = self
+ .transfers
+ .get_mut(id)
+ .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?;
+
+ transfer.confirm_lock(tx_hash, block_number, current_time);
+ Ok(())
+ }
+
+ /// Update confirmations
+ pub fn update_confirmations(
+ &mut self,
+ id: &TransferId,
+ confirmations: u64,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let transfer = self
+ .transfers
+ .get_mut(id)
+ .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?;
+
+ transfer.update_confirmations(confirmations, current_time);
+
+ // Remove from pending if confirmed
+ if transfer.status == TransferStatus::Confirmed {
+ self.pending.retain(|pid| pid != id);
+ }
+
+ Ok(())
+ }
+
+ /// Confirm mint
+ pub fn confirm_mint(
+ &mut self,
+ id: &TransferId,
+ tx_hash: Vec,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let transfer = self
+ .transfers
+ .get_mut(id)
+ .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?;
+
+ transfer.confirm_mint(tx_hash, current_time);
+ Ok(())
+ }
+
+ /// Confirm unlock
+ pub fn confirm_unlock(
+ &mut self,
+ id: &TransferId,
+ tx_hash: Vec,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let transfer = self
+ .transfers
+ .get_mut(id)
+ .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?;
+
+ transfer.confirm_unlock(tx_hash, current_time);
+ Ok(())
+ }
+
+ /// Mark transfer as failed
+ pub fn fail_transfer(
+ &mut self,
+ id: &TransferId,
+ error: impl Into,
+ current_time: u64,
+ ) -> BridgeResult<()> {
+ let transfer = self
+ .transfers
+ .get_mut(id)
+ .ok_or_else(|| BridgeError::TransferNotFound(id.to_string()))?;
+
+ transfer.fail(error, current_time);
+ self.pending.retain(|pid| pid != id);
+
+ Ok(())
+ }
+
+ /// Get transfers by sender
+ pub fn by_sender(&self, sender: &BridgeAddress) -> Vec<&BridgeTransfer> {
+ self.by_sender
+ .get(sender)
+ .map(|ids| ids.iter().filter_map(|id| self.transfers.get(id)).collect())
+ .unwrap_or_default()
+ }
+
+ /// Get transfers by recipient
+ pub fn by_recipient(&self, recipient: &BridgeAddress) -> Vec<&BridgeTransfer> {
+ self.by_recipient
+ .get(recipient)
+ .map(|ids| ids.iter().filter_map(|id| self.transfers.get(id)).collect())
+ .unwrap_or_default()
+ }
+
+ /// Get pending transfers
+ pub fn pending_transfers(&self) -> Vec<&BridgeTransfer> {
+ self.pending
+ .iter()
+ .filter_map(|id| self.transfers.get(id))
+ .collect()
+ }
+
+ /// Get transfers ready for confirmation
+ pub fn ready_for_confirmation(&self) -> Vec<&BridgeTransfer> {
+ self.transfers
+ .values()
+ .filter(|t| t.status == TransferStatus::Confirmed)
+ .collect()
+ }
+
+ /// Check and expire old transfers
+ pub fn expire_old_transfers(&mut self, current_time: u64) -> Vec {
+ let expired: Vec = self
+ .transfers
+ .iter()
+ .filter(|(_, t)| !t.status.is_finalized() && t.is_expired(current_time))
+ .map(|(id, _)| id.clone())
+ .collect();
+
+ for id in &expired {
+ if let Some(transfer) = self.transfers.get_mut(id) {
+ transfer.set_status(TransferStatus::Expired, current_time);
+ }
+ self.pending.retain(|pid| pid != id);
+ }
+
+ expired
+ }
+
+ /// Get transfer statistics
+ pub fn stats(&self) -> TransferStats {
+ let mut stats = TransferStats::default();
+
+ for transfer in self.transfers.values() {
+ stats.total_count += 1;
+ stats.total_volume += transfer.amount;
+
+ match transfer.status {
+ TransferStatus::Pending | TransferStatus::Locked => stats.pending_count += 1,
+ TransferStatus::Confirmed | TransferStatus::Minted | TransferStatus::Unlocked => {
+ stats.in_progress_count += 1
+ }
+ TransferStatus::Completed => {
+ stats.completed_count += 1;
+ stats.completed_volume += transfer.amount;
+ }
+ TransferStatus::Failed => stats.failed_count += 1,
+ TransferStatus::Expired | TransferStatus::Refunded => stats.expired_count += 1,
+ }
+
+ match transfer.direction {
+ TransferDirection::Inbound => stats.inbound_count += 1,
+ TransferDirection::Outbound => stats.outbound_count += 1,
+ }
+ }
+
+ stats
+ }
+}
+
+impl Default for TransferManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Transfer statistics
+#[derive(Debug, Clone, Default)]
+pub struct TransferStats {
+ pub total_count: u64,
+ pub total_volume: u128,
+ pub pending_count: u64,
+ pub in_progress_count: u64,
+ pub completed_count: u64,
+ pub completed_volume: u128,
+ pub failed_count: u64,
+ pub expired_count: u64,
+ pub inbound_count: u64,
+ pub outbound_count: u64,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn test_sender() -> BridgeAddress {
+ BridgeAddress::from_eth([0xaa; 20])
+ }
+
+ fn test_recipient() -> BridgeAddress {
+ BridgeAddress::from_synor([0xbb; 32])
+ }
+
+ #[test]
+ fn test_transfer_id() {
+ let sender = test_sender();
+ let recipient = test_recipient();
+ let asset = AssetId::eth();
+
+ let id1 = TransferId::generate(&sender, &recipient, &asset, 1000, 0);
+ let id2 = TransferId::generate(&sender, &recipient, &asset, 1000, 0);
+ let id3 = TransferId::generate(&sender, &recipient, &asset, 1000, 1);
+
+ assert_eq!(id1, id2);
+ assert_ne!(id1, id3);
+ }
+
+ #[test]
+ fn test_transfer_lifecycle() {
+ let current_time = 1700000000;
+ let mut transfer = BridgeTransfer::inbound(
+ ChainType::Ethereum,
+ AssetId::eth(),
+ 1000,
+ test_sender(),
+ test_recipient(),
+ 12,
+ 0,
+ current_time,
+ );
+
+ assert_eq!(transfer.status, TransferStatus::Pending);
+ assert_eq!(transfer.completion_percentage(), 0);
+
+ // Lock confirmed
+ transfer.confirm_lock(vec![0x11; 32], 100, current_time + 10);
+ assert_eq!(transfer.status, TransferStatus::Locked);
+ assert_eq!(transfer.completion_percentage(), 25);
+
+ // Update confirmations
+ transfer.update_confirmations(6, current_time + 100);
+ assert_eq!(transfer.status, TransferStatus::Locked);
+
+ transfer.update_confirmations(12, current_time + 200);
+ assert_eq!(transfer.status, TransferStatus::Confirmed);
+ assert_eq!(transfer.completion_percentage(), 50);
+
+ // Mint confirmed
+ transfer.confirm_mint(vec![0x22; 32], current_time + 300);
+ assert_eq!(transfer.status, TransferStatus::Completed);
+ assert_eq!(transfer.completion_percentage(), 100);
+ }
+
+ #[test]
+ fn test_transfer_manager() {
+ let mut manager = TransferManager::new();
+ let current_time = 1700000000;
+
+ let id = manager
+ .create_inbound(
+ ChainType::Ethereum,
+ AssetId::eth(),
+ 1000,
+ test_sender(),
+ test_recipient(),
+ 12,
+ current_time,
+ )
+ .unwrap();
+
+ assert!(manager.get(&id).is_some());
+ assert_eq!(manager.pending_transfers().len(), 1);
+
+ // Confirm lock
+ manager.confirm_lock(&id, vec![0x11; 32], 100, current_time + 10).unwrap();
+
+ // Update confirmations
+ manager.update_confirmations(&id, 12, current_time + 100).unwrap();
+
+ // Should be ready for confirmation
+ assert_eq!(manager.ready_for_confirmation().len(), 1);
+
+ // Confirm mint
+ manager.confirm_mint(&id, vec![0x22; 32], current_time + 200).unwrap();
+
+ let transfer = manager.get(&id).unwrap();
+ assert_eq!(transfer.status, TransferStatus::Completed);
+ }
+
+ #[test]
+ fn test_transfer_expiry() {
+ let mut manager = TransferManager::new();
+ let current_time = 1700000000;
+
+ let id = manager
+ .create_inbound(
+ ChainType::Ethereum,
+ AssetId::eth(),
+ 1000,
+ test_sender(),
+ test_recipient(),
+ 12,
+ current_time,
+ )
+ .unwrap();
+
+ // Set expiry
+ if let Some(transfer) = manager.get_mut(&id) {
+ transfer.expires_at = current_time + 1000;
+ }
+
+ // Not expired yet
+ let expired = manager.expire_old_transfers(current_time + 500);
+ assert!(expired.is_empty());
+
+ // Expired
+ let expired = manager.expire_old_transfers(current_time + 1500);
+ assert_eq!(expired.len(), 1);
+
+ let transfer = manager.get(&id).unwrap();
+ assert_eq!(transfer.status, TransferStatus::Expired);
+ }
+
+ #[test]
+ fn test_transfer_stats() {
+ let mut manager = TransferManager::new();
+ let current_time = 1700000000;
+
+ // Create transfers
+ let id1 = manager
+ .create_inbound(
+ ChainType::Ethereum,
+ AssetId::eth(),
+ 1000,
+ test_sender(),
+ test_recipient(),
+ 12,
+ current_time,
+ )
+ .unwrap();
+
+ let _id2 = manager
+ .create_outbound(
+ ChainType::Ethereum,
+ AssetId::eth(),
+ 500,
+ test_recipient(),
+ test_sender(),
+ 12,
+ current_time,
+ )
+ .unwrap();
+
+ // Complete one
+ manager.confirm_lock(&id1, vec![0x11; 32], 100, current_time).unwrap();
+ manager.update_confirmations(&id1, 12, current_time).unwrap();
+ manager.confirm_mint(&id1, vec![0x22; 32], current_time).unwrap();
+
+ let stats = manager.stats();
+ assert_eq!(stats.total_count, 2);
+ assert_eq!(stats.completed_count, 1);
+ assert_eq!(stats.pending_count, 1);
+ assert_eq!(stats.inbound_count, 1);
+ assert_eq!(stats.outbound_count, 1);
+ }
+}
diff --git a/crates/synor-bridge/src/vault.rs b/crates/synor-bridge/src/vault.rs
new file mode 100644
index 0000000..128b915
--- /dev/null
+++ b/crates/synor-bridge/src/vault.rs
@@ -0,0 +1,519 @@
+//! 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::*;
+
+ fn test_owner() -> BridgeAddress {
+ BridgeAddress::from_eth([0xaa; 20])
+ }
+
+ fn test_recipient() -> BridgeAddress {
+ BridgeAddress::from_synor([0xbb; 32])
+ }
+
+ #[test]
+ fn test_vault_id() {
+ let asset = AssetId::eth();
+ let id = VaultId::from_asset(&asset, &ChainType::Ethereum);
+ assert!(!id.0.is_empty());
+ }
+
+ #[test]
+ fn test_lock_unlock() {
+ let mut vault = Vault::new(
+ VaultId::new("test"),
+ ChainType::Ethereum,
+ AssetId::eth(),
+ );
+
+ let current_time = 1700000000;
+
+ // Lock
+ vault
+ .lock("lock1", 1000, test_owner(), test_recipient(), current_time)
+ .unwrap();
+
+ assert_eq!(vault.total_locked, 1000);
+ assert!(vault.get_locked("lock1").is_some());
+
+ // Unlock
+ let released = vault.unlock("lock1").unwrap();
+ assert_eq!(released.amount, 1000);
+ assert!(released.released);
+ assert_eq!(vault.total_locked, 0);
+ }
+
+ #[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();
+
+ // Duplicate should fail
+ let result = vault.lock("lock1", 500, test_owner(), test_recipient(), 0);
+ assert!(result.is_err());
+ }
+
+ #[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_daily_limit() {
+ let mut vault = Vault::new(
+ VaultId::new("test"),
+ ChainType::Ethereum,
+ AssetId::eth(),
+ )
+ .with_daily_limit(1000);
+
+ let current_time = 86400 * 100; // Day 100
+
+ // Under limit - OK
+ vault
+ .lock("lock1", 500, test_owner(), test_recipient(), current_time)
+ .unwrap();
+
+ // Exceed limit - fail
+ let result = vault.lock("lock2", 600, test_owner(), test_recipient(), current_time);
+ assert!(matches!(result, Err(BridgeError::RateLimitExceeded)));
+
+ // Next day - reset
+ let next_day = current_time + 86400;
+ vault
+ .lock("lock2", 600, test_owner(), test_recipient(), next_day)
+ .unwrap();
+ }
+
+ #[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());
+
+ // Get or create existing
+ 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_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));
+ }
+}
diff --git a/crates/synor-ibc/Cargo.toml b/crates/synor-ibc/Cargo.toml
index f83185e..a9e0f5c 100644
--- a/crates/synor-ibc/Cargo.toml
+++ b/crates/synor-ibc/Cargo.toml
@@ -7,7 +7,6 @@ license.workspace = true
description = "Inter-Blockchain Communication (IBC) protocol for Synor cross-chain interoperability"
[dependencies]
-# Local workspace crates
synor-types = { path = "../synor-types" }
synor-crypto = { path = "../synor-crypto" }
@@ -18,9 +17,13 @@ borsh = { workspace = true }
# Cryptography
sha2 = "0.10"
+sha3 = { workspace = true }
blake3 = { workspace = true }
rand = { workspace = true }
+# Merkle proofs
+rs_merkle = "1.4"
+
# Async runtime
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
@@ -30,17 +33,13 @@ thiserror = { workspace = true }
hex = { workspace = true }
parking_lot = { workspace = true }
tracing = "0.1"
+chrono = { workspace = true }
-# Protobuf (for IBC compatibility)
-prost = "0.13"
-prost-types = "0.13"
-
-# Time handling
-chrono = { version = "0.4", features = ["serde"] }
+# Protobuf for IBC messages (Cosmos compatibility)
+prost = "0.12"
+prost-types = "0.12"
[dev-dependencies]
-tempfile = { workspace = true }
tokio-test = "0.4"
-
-[build-dependencies]
-prost-build = "0.13"
+proptest = { workspace = true }
+tempfile = { workspace = true }
diff --git a/crates/synor-ibc/src/lib.rs b/crates/synor-ibc/src/lib.rs
index 425cd7a..9b623dd 100644
--- a/crates/synor-ibc/src/lib.rs
+++ b/crates/synor-ibc/src/lib.rs
@@ -1,132 +1,241 @@
-//! Synor IBC - Inter-Blockchain Communication Protocol
+//! Inter-Blockchain Communication (IBC) Protocol for Synor
//!
-//! This crate implements the IBC protocol for cross-chain communication,
-//! enabling Synor to interoperate with Cosmos SDK chains and other IBC-compatible blockchains.
+//! This crate implements the IBC protocol for cross-chain interoperability,
+//! enabling Synor to communicate with Cosmos ecosystem chains and beyond.
//!
//! # Architecture
//!
//! ```text
-//! ┌─────────────────────────────────────────────────────────────┐
-//! │ Synor Blockchain │
-//! ├─────────────────────────────────────────────────────────────┤
-//! │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
-//! │ │ IBC Client │ │ Connection │ │ Channel │ │
-//! │ │ (Light │ │ (Verified │ │ (Ordered/Unordered │ │
-//! │ │ Client) │ │ Link) │ │ Message Passing) │ │
-//! │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
-//! │ │ │ │ │
-//! │ └────────────────┼─────────────────────┘ │
-//! │ │ │
-//! │ ┌─────┴─────┐ │
-//! │ │ Packet │ │
-//! │ │ Handler │ │
-//! │ └─────┬─────┘ │
-//! └──────────────────────────┼──────────────────────────────────┘
-//! │
-//! ┌─────┴─────┐
-//! │ Relayer │
-//! └─────┬─────┘
-//! │
-//! ┌──────────────────────────┼──────────────────────────────────┐
-//! │ Remote Chain │
-//! └─────────────────────────────────────────────────────────────┘
+//! ┌─────────────────────────────────────────────────────────────────┐
+//! │ IBC Protocol Stack │
+//! ├─────────────────────────────────────────────────────────────────┤
+//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
+//! │ │ Light Client │ │ Channel │ │ Connection │ │
+//! │ │ Verification │ │ Management │ │ Management │ │
+//! │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
+//! ├─────────────────────────────────────────────────────────────────┤
+//! │ ┌──────────────────────────────────────────────────────────┐ │
+//! │ │ Packet Relayer │ │
+//! │ │ - Packet commitment │ │
+//! │ │ - Acknowledgment handling │ │
+//! │ │ - Timeout management │ │
+//! │ └──────────────────────────────────────────────────────────┘ │
+//! ├─────────────────────────────────────────────────────────────────┤
+//! │ ┌──────────────────────────────────────────────────────────┐ │
+//! │ │ Atomic Swaps (HTLC) │ │
+//! │ │ - Hashlock verification │ │
+//! │ │ - Timelock expiration │ │
+//! │ │ - Cross-chain atomicity │ │
+//! │ └──────────────────────────────────────────────────────────┘ │
+//! └─────────────────────────────────────────────────────────────────┘
//! ```
//!
//! # IBC Handshake Flow
//!
-//! ## Connection Handshake (4-way)
-//! 1. `ConnOpenInit` - Chain A initiates
-//! 2. `ConnOpenTry` - Chain B responds
-//! 3. `ConnOpenAck` - Chain A acknowledges
-//! 4. `ConnOpenConfirm` - Chain B confirms
+//! ```text
+//! Chain A Chain B
+//! │ │
+//! ├──── ConnOpenInit ─────────────►│
+//! │ │
+//! │◄─── ConnOpenTry ───────────────┤
+//! │ │
+//! ├──── ConnOpenAck ─────────────►│
+//! │ │
+//! │◄─── ConnOpenConfirm ───────────┤
+//! │ │
+//! │ ═══ Connection Established ═══ │
+//! ```
//!
-//! ## Channel Handshake (4-way)
-//! 1. `ChanOpenInit` - Chain A initiates
-//! 2. `ChanOpenTry` - Chain B responds
-//! 3. `ChanOpenAck` - Chain A acknowledges
-//! 4. `ChanOpenConfirm` - Chain B confirms
+//! # Quick Start
//!
-//! # Example
+//! ```rust,ignore
+//! use synor_ibc::{IbcHandler, IbcConfig, ClientState, ConsensusState};
+//! use synor_ibc::{PortId, ChannelOrder, Timeout};
//!
-//! ```ignore
-//! use synor_ibc::{IbcHandler, ConnectionId, ChannelId};
+//! // Create IBC handler
+//! let handler = IbcHandler::new(IbcConfig::default());
//!
-//! // Initialize IBC handler
-//! let handler = IbcHandler::new(config);
+//! // Create a light client
+//! let client_id = handler.create_client(client_state, consensus_state)?;
//!
-//! // Create a connection to a remote chain
-//! let conn_id = handler.connection_open_init(
-//! client_id,
-//! counterparty,
-//! version,
-//! ).await?;
+//! // Open a connection
+//! let conn_id = handler.connection_open_init(client_id, counterparty_client_id, None)?;
//!
-//! // Open a channel for token transfers
-//! let channel_id = handler.channel_open_init(
-//! port_id,
-//! conn_id,
-//! counterparty_port,
-//! version,
-//! ).await?;
+//! // Bind a port and open a channel
+//! handler.bind_port(PortId::transfer(), "transfer".to_string())?;
+//! let channel_id = handler.channel_open_init(port, conn_id, counterparty_port, ChannelOrder::Unordered, "ics20-1".to_string())?;
//!
//! // Send a packet
-//! handler.send_packet(
-//! channel_id,
-//! data,
-//! timeout,
-//! ).await?;
+//! let sequence = handler.send_packet(port, channel, data, Timeout::height(1000))?;
//! ```
-pub mod client;
-pub mod connection;
+#![allow(dead_code)]
+
+// Core modules
pub mod channel;
-pub mod packet;
+pub mod client;
pub mod commitment;
+pub mod connection;
pub mod error;
pub mod handler;
+pub mod packet;
pub mod swap;
pub mod types;
-pub use client::{ClientState, ConsensusState, LightClient};
-pub use connection::{ConnectionEnd, ConnectionState, ConnectionId, Counterparty};
-pub use channel::{Channel, ChannelState, ChannelId, PortId, ChannelOrder};
-pub use packet::{Packet, PacketCommitment, Acknowledgement, Timeout};
-pub use commitment::{CommitmentProof, MerkleProof, MerklePath};
+// Re-exports for convenience
+pub use channel::{
+ Channel, ChannelCounterparty, ChannelId, ChannelManager, ChannelOrder, ChannelState, PortId,
+};
+pub use client::{
+ ClientId, ClientState, ClientType, Commit, CommitSig, ConsensusState, Header, LightClient,
+ SignedHeader, TrustLevel, Validator, ValidatorSet,
+};
+pub use commitment::{
+ channel_path, client_state_path, connection_path, consensus_state_path, next_sequence_ack_path,
+ next_sequence_recv_path, next_sequence_send_path, packet_acknowledgement_path,
+ packet_commitment_path, packet_receipt_path, CommitmentProof, MerklePath, MerkleProof, ProofOp,
+ ProofOpType,
+};
+pub use connection::{
+ ConnectionEnd, ConnectionId, ConnectionManager, ConnectionState,
+ Counterparty as ConnectionCounterparty,
+};
pub use error::{IbcError, IbcResult};
-pub use handler::IbcHandler;
-pub use swap::{AtomicSwap, Htlc, SwapManager, SwapId, SwapState, SwapAsset, Hashlock, Timelock};
-pub use types::*;
+pub use handler::{IbcConfig, IbcHandler};
+pub use packet::{
+ Acknowledgement, FungibleTokenPacketData, Packet, PacketCommitment, PacketHandler,
+ PacketReceipt, Timeout,
+};
+pub use swap::{
+ AtomicSwap, Hashlock, Htlc, SwapAction, SwapAsset, SwapId, SwapManager, SwapPacketData,
+ SwapState, Timelock,
+};
+pub use types::{ChainId, CommitmentPrefix, Height, Signer, Timestamp, Version};
/// IBC protocol version
-pub const IBC_VERSION: &str = "1";
+pub const IBC_VERSION: &str = "1.0.0";
-/// Default packet timeout in blocks
-pub const DEFAULT_TIMEOUT_HEIGHT: u64 = 1000;
+/// Default timeout in blocks
+pub const DEFAULT_TIMEOUT_BLOCKS: u64 = 1000;
-/// Default packet timeout in nanoseconds (1 hour)
-pub const DEFAULT_TIMEOUT_TIMESTAMP: u64 = 3_600_000_000_000;
+/// Default timeout in nanoseconds (1 hour)
+pub const DEFAULT_TIMEOUT_NANOS: u64 = 3_600_000_000_000;
/// Maximum packet data size (1 MB)
pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024;
-/// Connection prefix for Synor
-pub const SYNOR_CONNECTION_PREFIX: &str = "synor";
-
-/// Well-known port for token transfers
-pub const TRANSFER_PORT: &str = "transfer";
-
-/// Well-known port for interchain accounts
-pub const ICA_PORT: &str = "icahost";
+/// IBC module events
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub enum IbcEvent {
+ /// Client created
+ CreateClient {
+ client_id: ClientId,
+ client_type: ClientType,
+ consensus_height: Height,
+ },
+ /// Client updated
+ UpdateClient {
+ client_id: ClientId,
+ consensus_height: Height,
+ },
+ /// Connection opened (init)
+ OpenInitConnection {
+ connection_id: ConnectionId,
+ client_id: ClientId,
+ counterparty_client_id: ClientId,
+ },
+ /// Connection opened (try)
+ OpenTryConnection {
+ connection_id: ConnectionId,
+ client_id: ClientId,
+ counterparty_connection_id: ConnectionId,
+ },
+ /// Connection opened (ack)
+ OpenAckConnection { connection_id: ConnectionId },
+ /// Connection opened (confirm)
+ OpenConfirmConnection { connection_id: ConnectionId },
+ /// Channel opened (init)
+ OpenInitChannel {
+ port_id: PortId,
+ channel_id: ChannelId,
+ connection_id: ConnectionId,
+ },
+ /// Channel opened (try)
+ OpenTryChannel {
+ port_id: PortId,
+ channel_id: ChannelId,
+ counterparty_channel_id: ChannelId,
+ },
+ /// Channel opened (ack)
+ OpenAckChannel {
+ port_id: PortId,
+ channel_id: ChannelId,
+ },
+ /// Channel opened (confirm)
+ OpenConfirmChannel {
+ port_id: PortId,
+ channel_id: ChannelId,
+ },
+ /// Channel closed
+ CloseChannel {
+ port_id: PortId,
+ channel_id: ChannelId,
+ },
+ /// Packet sent
+ SendPacket { packet: Packet },
+ /// Packet received
+ ReceivePacket { packet: Packet },
+ /// Packet acknowledged
+ AcknowledgePacket {
+ packet: Packet,
+ acknowledgement: Acknowledgement,
+ },
+ /// Packet timed out
+ TimeoutPacket { packet: Packet },
+ /// Swap initiated
+ SwapInitiated {
+ swap_id: SwapId,
+ initiator: String,
+ responder: String,
+ },
+ /// Swap completed
+ SwapCompleted { swap_id: SwapId },
+ /// Swap refunded
+ SwapRefunded { swap_id: SwapId },
+}
#[cfg(test)]
mod tests {
use super::*;
#[test]
- fn test_constants() {
- assert_eq!(IBC_VERSION, "1");
- assert!(DEFAULT_TIMEOUT_HEIGHT > 0);
- assert!(DEFAULT_TIMEOUT_TIMESTAMP > 0);
- assert!(MAX_PACKET_DATA_SIZE > 0);
+ fn test_chain_id() {
+ let chain = ChainId::new("synor-1");
+ assert_eq!(chain.to_string(), "synor-1");
+ }
+
+ #[test]
+ fn test_commitment_prefix() {
+ let prefix = CommitmentPrefix::default();
+ assert_eq!(prefix.key_prefix, b"ibc");
+ }
+
+ #[test]
+ fn test_height() {
+ let h = Height::new(1, 100);
+ assert_eq!(h.revision_number, 1);
+ assert_eq!(h.revision_height, 100);
+ }
+
+ #[test]
+ fn test_ibc_handler_creation() {
+ let handler = IbcHandler::default();
+ assert_eq!(handler.chain_id(), "synor-1");
+ }
+
+ #[test]
+ fn test_swap_manager_creation() {
+ let manager = SwapManager::new();
+ assert!(manager.get_active_swaps().is_empty());
}
}