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-mining",
|
||||||
"crates/synor-zk",
|
"crates/synor-zk",
|
||||||
"crates/synor-ibc",
|
"crates/synor-ibc",
|
||||||
|
"crates/synor-bridge",
|
||||||
"crates/synor-privacy",
|
"crates/synor-privacy",
|
||||||
"crates/synor-sharding",
|
"crates/synor-sharding",
|
||||||
"crates/synor-verifier",
|
"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"
|
description = "Inter-Blockchain Communication (IBC) protocol for Synor cross-chain interoperability"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Local workspace crates
|
|
||||||
synor-types = { path = "../synor-types" }
|
synor-types = { path = "../synor-types" }
|
||||||
synor-crypto = { path = "../synor-crypto" }
|
synor-crypto = { path = "../synor-crypto" }
|
||||||
|
|
||||||
|
|
@ -18,9 +17,13 @@ borsh = { workspace = true }
|
||||||
|
|
||||||
# Cryptography
|
# Cryptography
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
sha3 = { workspace = true }
|
||||||
blake3 = { workspace = true }
|
blake3 = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
|
|
||||||
|
# Merkle proofs
|
||||||
|
rs_merkle = "1.4"
|
||||||
|
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|
@ -30,17 +33,13 @@ thiserror = { workspace = true }
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
chrono = { workspace = true }
|
||||||
|
|
||||||
# Protobuf (for IBC compatibility)
|
# Protobuf for IBC messages (Cosmos compatibility)
|
||||||
prost = "0.13"
|
prost = "0.12"
|
||||||
prost-types = "0.13"
|
prost-types = "0.12"
|
||||||
|
|
||||||
# Time handling
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
proptest = { workspace = true }
|
||||||
[build-dependencies]
|
tempfile = { workspace = true }
|
||||||
prost-build = "0.13"
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
//! This crate implements the IBC protocol for cross-chain interoperability,
|
||||||
//! enabling Synor to interoperate with Cosmos SDK chains and other IBC-compatible blockchains.
|
//! enabling Synor to communicate with Cosmos ecosystem chains and beyond.
|
||||||
//!
|
//!
|
||||||
//! # Architecture
|
//! # Architecture
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! ┌─────────────────────────────────────────────────────────────┐
|
//! ┌─────────────────────────────────────────────────────────────────┐
|
||||||
//! │ Synor Blockchain │
|
//! │ IBC Protocol Stack │
|
||||||
//! ├─────────────────────────────────────────────────────────────┤
|
//! ├─────────────────────────────────────────────────────────────────┤
|
||||||
//! │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||||
//! │ │ IBC Client │ │ Connection │ │ Channel │ │
|
//! │ │ Light Client │ │ Channel │ │ Connection │ │
|
||||||
//! │ │ (Light │ │ (Verified │ │ (Ordered/Unordered │ │
|
//! │ │ Verification │ │ Management │ │ Management │ │
|
||||||
//! │ │ Client) │ │ Link) │ │ Message Passing) │ │
|
//! │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
|
||||||
//! │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
//! ├─────────────────────────────────────────────────────────────────┤
|
||||||
//! │ │ │ │ │
|
//! │ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
//! │ └────────────────┼─────────────────────┘ │
|
//! │ │ Packet Relayer │ │
|
||||||
//! │ │ │
|
//! │ │ - Packet commitment │ │
|
||||||
//! │ ┌─────┴─────┐ │
|
//! │ │ - Acknowledgment handling │ │
|
||||||
//! │ │ Packet │ │
|
//! │ │ - Timeout management │ │
|
||||||
//! │ │ Handler │ │
|
//! │ └──────────────────────────────────────────────────────────┘ │
|
||||||
//! │ └─────┬─────┘ │
|
//! ├─────────────────────────────────────────────────────────────────┤
|
||||||
//! └──────────────────────────┼──────────────────────────────────┘
|
//! │ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
//! │
|
//! │ │ Atomic Swaps (HTLC) │ │
|
||||||
//! ┌─────┴─────┐
|
//! │ │ - Hashlock verification │ │
|
||||||
//! │ Relayer │
|
//! │ │ - Timelock expiration │ │
|
||||||
//! └─────┬─────┘
|
//! │ │ - Cross-chain atomicity │ │
|
||||||
//! │
|
//! │ └──────────────────────────────────────────────────────────┘ │
|
||||||
//! ┌──────────────────────────┼──────────────────────────────────┐
|
//! └─────────────────────────────────────────────────────────────────┘
|
||||||
//! │ Remote Chain │
|
|
||||||
//! └─────────────────────────────────────────────────────────────┘
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # IBC Handshake Flow
|
//! # IBC Handshake Flow
|
||||||
//!
|
//!
|
||||||
//! ## Connection Handshake (4-way)
|
//! ```text
|
||||||
//! 1. `ConnOpenInit` - Chain A initiates
|
//! Chain A Chain B
|
||||||
//! 2. `ConnOpenTry` - Chain B responds
|
//! │ │
|
||||||
//! 3. `ConnOpenAck` - Chain A acknowledges
|
//! ├──── ConnOpenInit ─────────────►│
|
||||||
//! 4. `ConnOpenConfirm` - Chain B confirms
|
//! │ │
|
||||||
|
//! │◄─── ConnOpenTry ───────────────┤
|
||||||
|
//! │ │
|
||||||
|
//! ├──── ConnOpenAck ─────────────►│
|
||||||
|
//! │ │
|
||||||
|
//! │◄─── ConnOpenConfirm ───────────┤
|
||||||
|
//! │ │
|
||||||
|
//! │ ═══ Connection Established ═══ │
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Channel Handshake (4-way)
|
//! # Quick Start
|
||||||
//! 1. `ChanOpenInit` - Chain A initiates
|
|
||||||
//! 2. `ChanOpenTry` - Chain B responds
|
|
||||||
//! 3. `ChanOpenAck` - Chain A acknowledges
|
|
||||||
//! 4. `ChanOpenConfirm` - Chain B confirms
|
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! ```rust,ignore
|
||||||
|
//! use synor_ibc::{IbcHandler, IbcConfig, ClientState, ConsensusState};
|
||||||
|
//! use synor_ibc::{PortId, ChannelOrder, Timeout};
|
||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! // Create IBC handler
|
||||||
//! use synor_ibc::{IbcHandler, ConnectionId, ChannelId};
|
//! let handler = IbcHandler::new(IbcConfig::default());
|
||||||
//!
|
//!
|
||||||
//! // Initialize IBC handler
|
//! // Create a light client
|
||||||
//! let handler = IbcHandler::new(config);
|
//! let client_id = handler.create_client(client_state, consensus_state)?;
|
||||||
//!
|
//!
|
||||||
//! // Create a connection to a remote chain
|
//! // Open a connection
|
||||||
//! let conn_id = handler.connection_open_init(
|
//! let conn_id = handler.connection_open_init(client_id, counterparty_client_id, None)?;
|
||||||
//! client_id,
|
|
||||||
//! counterparty,
|
|
||||||
//! version,
|
|
||||||
//! ).await?;
|
|
||||||
//!
|
//!
|
||||||
//! // Open a channel for token transfers
|
//! // Bind a port and open a channel
|
||||||
//! let channel_id = handler.channel_open_init(
|
//! handler.bind_port(PortId::transfer(), "transfer".to_string())?;
|
||||||
//! port_id,
|
//! let channel_id = handler.channel_open_init(port, conn_id, counterparty_port, ChannelOrder::Unordered, "ics20-1".to_string())?;
|
||||||
//! conn_id,
|
|
||||||
//! counterparty_port,
|
|
||||||
//! version,
|
|
||||||
//! ).await?;
|
|
||||||
//!
|
//!
|
||||||
//! // Send a packet
|
//! // Send a packet
|
||||||
//! handler.send_packet(
|
//! let sequence = handler.send_packet(port, channel, data, Timeout::height(1000))?;
|
||||||
//! channel_id,
|
|
||||||
//! data,
|
|
||||||
//! timeout,
|
|
||||||
//! ).await?;
|
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
pub mod client;
|
#![allow(dead_code)]
|
||||||
pub mod connection;
|
|
||||||
|
// Core modules
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod packet;
|
pub mod client;
|
||||||
pub mod commitment;
|
pub mod commitment;
|
||||||
|
pub mod connection;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
|
pub mod packet;
|
||||||
pub mod swap;
|
pub mod swap;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use client::{ClientState, ConsensusState, LightClient};
|
// Re-exports for convenience
|
||||||
pub use connection::{ConnectionEnd, ConnectionState, ConnectionId, Counterparty};
|
pub use channel::{
|
||||||
pub use channel::{Channel, ChannelState, ChannelId, PortId, ChannelOrder};
|
Channel, ChannelCounterparty, ChannelId, ChannelManager, ChannelOrder, ChannelState, PortId,
|
||||||
pub use packet::{Packet, PacketCommitment, Acknowledgement, Timeout};
|
};
|
||||||
pub use commitment::{CommitmentProof, MerkleProof, MerklePath};
|
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 error::{IbcError, IbcResult};
|
||||||
pub use handler::IbcHandler;
|
pub use handler::{IbcConfig, IbcHandler};
|
||||||
pub use swap::{AtomicSwap, Htlc, SwapManager, SwapId, SwapState, SwapAsset, Hashlock, Timelock};
|
pub use packet::{
|
||||||
pub use types::*;
|
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
|
/// IBC protocol version
|
||||||
pub const IBC_VERSION: &str = "1";
|
pub const IBC_VERSION: &str = "1.0.0";
|
||||||
|
|
||||||
/// Default packet timeout in blocks
|
/// Default timeout in blocks
|
||||||
pub const DEFAULT_TIMEOUT_HEIGHT: u64 = 1000;
|
pub const DEFAULT_TIMEOUT_BLOCKS: u64 = 1000;
|
||||||
|
|
||||||
/// Default packet timeout in nanoseconds (1 hour)
|
/// Default timeout in nanoseconds (1 hour)
|
||||||
pub const DEFAULT_TIMEOUT_TIMESTAMP: u64 = 3_600_000_000_000;
|
pub const DEFAULT_TIMEOUT_NANOS: u64 = 3_600_000_000_000;
|
||||||
|
|
||||||
/// Maximum packet data size (1 MB)
|
/// Maximum packet data size (1 MB)
|
||||||
pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024;
|
pub const MAX_PACKET_DATA_SIZE: usize = 1024 * 1024;
|
||||||
|
|
||||||
/// Connection prefix for Synor
|
/// IBC module events
|
||||||
pub const SYNOR_CONNECTION_PREFIX: &str = "synor";
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum IbcEvent {
|
||||||
/// Well-known port for token transfers
|
/// Client created
|
||||||
pub const TRANSFER_PORT: &str = "transfer";
|
CreateClient {
|
||||||
|
client_id: ClientId,
|
||||||
/// Well-known port for interchain accounts
|
client_type: ClientType,
|
||||||
pub const ICA_PORT: &str = "icahost";
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_constants() {
|
fn test_chain_id() {
|
||||||
assert_eq!(IBC_VERSION, "1");
|
let chain = ChainId::new("synor-1");
|
||||||
assert!(DEFAULT_TIMEOUT_HEIGHT > 0);
|
assert_eq!(chain.to_string(), "synor-1");
|
||||||
assert!(DEFAULT_TIMEOUT_TIMESTAMP > 0);
|
}
|
||||||
assert!(MAX_PACKET_DATA_SIZE > 0);
|
|
||||||
|
#[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