- Replace manual modulo checks with .is_multiple_of() - Use enumerate() instead of manual loop counters - Use iterator .take() instead of index-based loops - Use slice literals instead of unnecessary vec![] - Allow too_many_arguments in IBC and bridge crates (protocol requirements) - Allow assertions on constants in integration tests
447 lines
15 KiB
Rust
447 lines
15 KiB
Rust
// Allow many arguments since bridge functions require many parameters
|
|
#![allow(clippy::too_many_arguments)]
|
|
// Allow complex types for multi-signature collections
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
//! 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]);
|
|
}
|
|
}
|