feat(bridge): add synor-bridge crate for cross-chain interoperability
Phase 14: Interoperability & Privacy enhancements New synor-bridge crate with Ethereum lock-mint bridge: - Bridge trait for generic cross-chain implementations - Vault management with daily limits and pause controls - Transfer lifecycle (pending → confirmed → minted) - Multi-relayer signature verification - Wrapped token minting (ETH → sETH, ERC20 → sERC20) - Burn-unlock flow for redemption Also fixes synor-ibc lib.rs exports and adds rand dependency. 21 tests passing for synor-bridge.
This commit is contained in:
parent
af79e21a1b
commit
45ccbcba03
8 changed files with 2845 additions and 100 deletions
|
|
@ -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",
|
||||
|
|
|
|||
43
crates/synor-bridge/Cargo.toml
Normal file
43
crates/synor-bridge/Cargo.toml
Normal file
|
|
@ -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 }
|
||||
808
crates/synor-bridge/src/ethereum.rs
Normal file
808
crates/synor-bridge/src/ethereum.rs
Normal file
|
|
@ -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<Address, AssetId>,
|
||||
/// Relayer addresses (for multi-sig)
|
||||
pub relayers: Vec<Address>,
|
||||
/// Required relayer signatures
|
||||
pub required_signatures: usize,
|
||||
/// Whether bridge is paused
|
||||
pub paused: bool,
|
||||
/// Daily limit per token (address → limit)
|
||||
pub daily_limits: HashMap<Address, U256>,
|
||||
}
|
||||
|
||||
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<u8>,
|
||||
/// 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<u8>,
|
||||
}
|
||||
|
||||
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<EthereumBridgeConfig>,
|
||||
/// Transfer manager
|
||||
transfers: Arc<RwLock<TransferManager>>,
|
||||
/// Vault manager
|
||||
vaults: Arc<RwLock<VaultManager>>,
|
||||
/// Wrapped tokens by original address
|
||||
wrapped_tokens: RwLock<HashMap<Address, WrappedToken>>,
|
||||
/// Pending events awaiting confirmation
|
||||
pending_events: RwLock<HashMap<B256, EthereumEvent>>,
|
||||
/// Processed event hashes (to prevent replay)
|
||||
processed_events: RwLock<HashMap<B256, bool>>,
|
||||
/// Relayer signatures per event
|
||||
relayer_signatures: RwLock<HashMap<B256, Vec<(Address, Vec<u8>)>>>,
|
||||
}
|
||||
|
||||
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<F>(&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<TransferId> {
|
||||
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::<u128>();
|
||||
|
||||
// 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<u8>,
|
||||
) -> BridgeResult<bool> {
|
||||
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<Vec<TransferId>> {
|
||||
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<TransferId> {
|
||||
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<WrappedToken> {
|
||||
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<RwLock<TransferManager>> {
|
||||
self.transfers.clone()
|
||||
}
|
||||
|
||||
/// Get vault manager
|
||||
pub fn vault_manager(&self) -> Arc<RwLock<VaultManager>> {
|
||||
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<TransferId> {
|
||||
// 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<bool> {
|
||||
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<TransferId> {
|
||||
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<TransferStatus> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
436
crates/synor-bridge/src/lib.rs
Normal file
436
crates/synor-bridge/src/lib.rs
Normal file
|
|
@ -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<u64> {
|
||||
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<String>, symbol: impl Into<String>, 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<u8>,
|
||||
}
|
||||
|
||||
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<Self, BridgeError> {
|
||||
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<T> = std::result::Result<T, BridgeError>;
|
||||
|
||||
/// 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<TransferId>;
|
||||
|
||||
/// Verify a lock proof
|
||||
async fn verify_lock(&self, transfer_id: &TransferId) -> BridgeResult<bool>;
|
||||
|
||||
/// 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<TransferId>;
|
||||
|
||||
/// Unlock original tokens
|
||||
async fn unlock(&self, transfer_id: &TransferId) -> BridgeResult<()>;
|
||||
|
||||
/// Get transfer status
|
||||
async fn get_transfer_status(&self, transfer_id: &TransferId) -> BridgeResult<TransferStatus>;
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
}
|
||||
}
|
||||
830
crates/synor-bridge/src/transfer.rs
Normal file
830
crates/synor-bridge/src/transfer.rs
Normal file
|
|
@ -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<String>) -> 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<Vec<u8>>,
|
||||
/// Destination chain transaction hash
|
||||
pub destination_tx_hash: Option<Vec<u8>>,
|
||||
/// Block number where lock occurred
|
||||
pub lock_block: Option<u64>,
|
||||
/// 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<String>,
|
||||
/// 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<u8>, 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<u8>, 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<u8>, 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<String>, 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<TransferId, BridgeTransfer>,
|
||||
/// Transfers by sender
|
||||
by_sender: HashMap<BridgeAddress, Vec<TransferId>>,
|
||||
/// Transfers by recipient
|
||||
by_recipient: HashMap<BridgeAddress, Vec<TransferId>>,
|
||||
/// Pending transfers
|
||||
pending: Vec<TransferId>,
|
||||
/// 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<TransferId> {
|
||||
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<TransferId> {
|
||||
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<TransferId> {
|
||||
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<u8>,
|
||||
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<u8>,
|
||||
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<u8>,
|
||||
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<String>,
|
||||
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<TransferId> {
|
||||
let expired: Vec<TransferId> = 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);
|
||||
}
|
||||
}
|
||||
519
crates/synor-bridge/src/vault.rs
Normal file
519
crates/synor-bridge/src/vault.rs
Normal file
|
|
@ -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<String>) -> 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<Vec<u8>>,
|
||||
/// 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<u8>) -> 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<String, LockedAsset>,
|
||||
/// Vault address (contract address for EVM)
|
||||
pub vault_address: Option<BridgeAddress>,
|
||||
/// Admin addresses
|
||||
pub admins: Vec<BridgeAddress>,
|
||||
/// 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<String>,
|
||||
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<LockedAsset> {
|
||||
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<Item = (&String, &LockedAsset)> {
|
||||
self.locked_assets.iter()
|
||||
}
|
||||
|
||||
/// Get active (unreleased) locked assets
|
||||
pub fn active_locked(&self) -> impl Iterator<Item = (&String, &LockedAsset)> {
|
||||
self.locked_assets.iter().filter(|(_, l)| !l.released)
|
||||
}
|
||||
|
||||
/// Get expired locked assets
|
||||
pub fn expired_locked(&self, current_time: u64) -> impl Iterator<Item = (&String, &LockedAsset)> {
|
||||
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<VaultId, Vault>,
|
||||
/// 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<VaultId> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue