synor/contracts/aggregator/src/lib.rs
Gulshan Yadav 688d409b10 feat(dex): add Phase 15 - DEX Ecosystem with Perpetuals Trading
Implements comprehensive DEX infrastructure:

- contracts/perps (81KB WASM):
  - Long/Short positions with 2x-100x leverage
  - Funding rate mechanism (keeps price anchored to spot)
  - Liquidation engine with insurance fund
  - Mark price (EMA) vs index price (oracle)
  - Maintenance margin (0.5%) and initial margin (1%)

- contracts/oracle (80KB WASM):
  - Multi-source price aggregation (median)
  - TWAP (Time-Weighted Average Price)
  - Stale price detection
  - Confidence intervals

- contracts/aggregator (94KB WASM):
  - Cross-chain liquidity routing via IBC
  - Best price discovery across multiple DEXs
  - Split routing for large orders
  - Zero-capital model (aggregation fees only)

This enables dYdX/GMX-style trading without requiring capital.
2026-01-19 19:22:02 +05:30

1040 lines
36 KiB
Rust

//! Cross-Chain DEX Liquidity Aggregator
//!
//! This contract aggregates liquidity from multiple sources to provide:
//! - Best execution price across all DEXs
//! - Cross-chain swaps via IBC
//! - Split routing for large orders
//! - MEV protection via private mempool
//!
//! # Zero-Capital Strategy
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │ SYNOR LIQUIDITY AGGREGATOR │
//! │ (No Capital Required) │
//! ├─────────────────────────────────────────────────────────────┤
//! │ │
//! │ USER REQUEST: Swap 10 ETH for USDC (best price) │
//! │ │ │
//! │ ▼ │
//! │ ┌─────────────────────────────────────────────┐ │
//! │ │ QUOTE AGGREGATION │ │
//! │ │ │ │
//! │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
//! │ │ │ Synor │ │ Osmosis │ │ dYdX │ │ │
//! │ │ │ DEX │ │ (IBC) │ │ (IBC) │ │ │
//! │ │ │ $25,010 │ │ $25,015 │ │ $25,020 │ │ │
//! │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
//! │ │ │ │ │ │ │
//! │ │ └─────────────┼─────────────┘ │ │
//! │ │ │ │ │
//! │ │ BEST PRICE: $25,020 (dYdX) │ │
//! │ └─────────────────────┬────────────────────┘ │
//! │ │ │
//! │ ▼ │
//! │ ┌─────────────────────────────────────────────┐ │
//! │ │ ROUTE EXECUTION │ │
//! │ │ │ │
//! │ │ Option A: Single route (small order) │ │
//! │ │ ETH ────────────────────────────► USDC │ │
//! │ │ via dYdX IBC bridge │ │
//! │ │ │ │
//! │ │ Option B: Split route (large order) │ │
//! │ │ ETH ──┬── 60% via dYdX ──────────► USDC │ │
//! │ │ ├── 25% via Osmosis ────────► │ │
//! │ │ └── 15% via Synor DEX ──────► │ │
//! │ └─────────────────────────────────────────────┘ │
//! │ │
//! │ REVENUE SOURCES: │
//! │ • Aggregation fee: 0.05% of swap volume │
//! │ • Positive slippage capture │
//! │ • Bridge fee share (partner programs) │
//! └─────────────────────────────────────────────────────────────┘
//! ```
#![no_std]
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use borsh::{BorshDeserialize, BorshSerialize};
use synor_sdk::prelude::*;
use synor_sdk::{require, require_auth};
// =============================================================================
// CONSTANTS
// =============================================================================
/// Price precision (6 decimals)
pub const PRICE_PRECISION: u64 = 1_000_000;
/// Amount precision (8 decimals)
pub const AMOUNT_PRECISION: u64 = 100_000_000;
/// Basis points denominator
pub const BPS_DENOMINATOR: u64 = 10000;
/// Aggregation fee (5 bps = 0.05%)
pub const AGGREGATION_FEE_BPS: u64 = 5;
/// Maximum slippage (5% = 500 bps)
pub const MAX_SLIPPAGE_BPS: u64 = 500;
/// Minimum split percentage (10%)
pub const MIN_SPLIT_PERCENT: u64 = 10;
/// Maximum routes per swap
pub const MAX_ROUTES: usize = 5;
/// IBC timeout (10 minutes)
pub const IBC_TIMEOUT_SECONDS: u64 = 10 * 60;
// =============================================================================
// EVENT TOPICS
// =============================================================================
fn event_topic(name: &[u8]) -> [u8; 32] {
use synor_sdk::crypto::blake3_hash;
blake3_hash(name).0
}
// =============================================================================
// STORAGE KEYS
// =============================================================================
mod keys {
pub const OWNER: &[u8] = b"agg:owner";
pub const INITIALIZED: &[u8] = b"agg:initialized";
pub const PAUSED: &[u8] = b"agg:paused";
// Liquidity sources
pub const SOURCES: &[u8] = b"agg:sources";
pub const SOURCE_COUNT: &[u8] = b"agg:source_count";
// Supported tokens
pub const TOKENS: &[u8] = b"agg:tokens";
pub const TOKEN_COUNT: &[u8] = b"agg:token_count";
// Pending IBC swaps
pub const PENDING_SWAPS: &[u8] = b"agg:pending";
pub const SWAP_COUNT: &[u8] = b"agg:swap_count";
// Fee collector
pub const FEE_COLLECTOR: &[u8] = b"agg:fee_collector";
pub const COLLECTED_FEES: &[u8] = b"agg:fees";
// Statistics
pub const TOTAL_VOLUME: &[u8] = b"agg:volume";
}
// =============================================================================
// DATA STRUCTURES
// =============================================================================
/// Type of liquidity source
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum SourceType {
/// Local AMM (Synor DEX)
LocalAmm,
/// IBC-connected DEX (Osmosis, dYdX, etc.)
IbcDex,
/// Centralized exchange API (for quotes only)
CexApi,
/// Order book
OrderBook,
}
/// Liquidity source status
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum SourceStatus {
Active,
Paused,
Deprecated,
}
/// A liquidity source (DEX or bridge)
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct LiquiditySource {
/// Source ID
pub id: u32,
/// Human-readable name
pub name: String,
/// Type of source
pub source_type: SourceType,
/// Contract address (for local sources)
pub contract: Option<Address>,
/// IBC channel (for IBC sources)
pub ibc_channel: Option<String>,
/// Chain ID (for IBC sources)
pub chain_id: Option<String>,
/// Status
pub status: SourceStatus,
/// Base fee (bps)
pub base_fee_bps: u64,
/// Priority (higher = preferred)
pub priority: u32,
/// Total volume routed
pub total_volume: u64,
/// Success rate (bps, e.g., 9900 = 99%)
pub success_rate_bps: u64,
/// Average latency (milliseconds)
pub avg_latency_ms: u64,
}
/// Token information
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct TokenInfo {
/// Token symbol (e.g., "ETH")
pub symbol: String,
/// Token address (local)
pub local_address: Address,
/// Decimals
pub decimals: u8,
/// IBC denom mapping (chain_id -> denom)
pub ibc_denoms: Vec<(String, String)>,
/// Is native asset
pub is_native: bool,
}
/// Quote from a liquidity source
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct Quote {
/// Source ID
pub source_id: u32,
/// Source name
pub source_name: String,
/// Input amount
pub amount_in: u64,
/// Output amount (expected)
pub amount_out: u64,
/// Price impact (bps)
pub price_impact_bps: u64,
/// Fee (in output token)
pub fee: u64,
/// Is available
pub available: bool,
/// Estimated execution time (seconds)
pub estimated_time: u64,
}
/// Route for executing a swap
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct Route {
/// Source ID
pub source_id: u32,
/// Percentage of total amount (0-100)
pub percentage: u64,
/// Expected output
pub expected_output: u64,
/// Minimum output (with slippage)
pub min_output: u64,
}
/// Swap execution plan
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct SwapPlan {
/// Input token
pub token_in: String,
/// Output token
pub token_out: String,
/// Total input amount
pub amount_in: u64,
/// Routes to execute
pub routes: Vec<Route>,
/// Total expected output
pub total_expected_output: u64,
/// Minimum output (user's slippage tolerance)
pub min_output: u64,
/// Aggregation fee
pub aggregation_fee: u64,
/// Valid until timestamp
pub valid_until: u64,
}
/// Pending IBC swap
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct PendingSwap {
/// Swap ID
pub id: u64,
/// User address
pub user: Address,
/// Plan that was executed
pub plan: SwapPlan,
/// Current status
pub status: SwapStatus,
/// Amount received so far
pub amount_received: u64,
/// Routes completed
pub routes_completed: u32,
/// Created timestamp
pub created_at: u64,
/// Timeout timestamp
pub timeout_at: u64,
}
/// Swap execution status
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum SwapStatus {
/// Swap initiated, waiting for execution
Pending,
/// Partially filled
PartiallyFilled,
/// Fully completed
Completed,
/// Failed
Failed,
/// Refunded
Refunded,
/// Timed out
TimedOut,
}
// =============================================================================
// EVENTS
// =============================================================================
#[derive(BorshSerialize)]
pub struct SourceAdded {
pub source_id: u32,
pub name: String,
pub source_type: SourceType,
}
#[derive(BorshSerialize)]
pub struct SwapExecuted {
pub swap_id: u64,
pub user: Address,
pub token_in: String,
pub token_out: String,
pub amount_in: u64,
pub amount_out: u64,
pub fee: u64,
pub routes_used: u32,
}
#[derive(BorshSerialize)]
pub struct SwapPartialFill {
pub swap_id: u64,
pub source_id: u32,
pub amount_filled: u64,
}
#[derive(BorshSerialize)]
pub struct SwapFailed {
pub swap_id: u64,
pub reason: String,
}
// =============================================================================
// STORAGE HELPERS
// =============================================================================
fn get_owner() -> Option<Address> {
storage::get::<Address>(keys::OWNER)
}
fn is_initialized() -> bool {
storage::get::<bool>(keys::INITIALIZED).unwrap_or(false)
}
fn is_paused() -> bool {
storage::get::<bool>(keys::PAUSED).unwrap_or(false)
}
fn get_source_count() -> u32 {
storage::get::<u32>(keys::SOURCE_COUNT).unwrap_or(0)
}
fn set_source_count(count: u32) {
storage::set(keys::SOURCE_COUNT, &count);
}
fn get_source(id: u32) -> Option<LiquiditySource> {
storage::get_with_suffix::<LiquiditySource>(keys::SOURCES, &id.to_le_bytes())
}
fn set_source(id: u32, source: &LiquiditySource) {
storage::set_with_suffix(keys::SOURCES, &id.to_le_bytes(), source);
}
fn get_token_count() -> u32 {
storage::get::<u32>(keys::TOKEN_COUNT).unwrap_or(0)
}
fn set_token_count(count: u32) {
storage::set(keys::TOKEN_COUNT, &count);
}
fn get_token(symbol: &str) -> Option<TokenInfo> {
storage::get_with_suffix::<TokenInfo>(keys::TOKENS, symbol.as_bytes())
}
fn set_token(symbol: &str, token: &TokenInfo) {
storage::set_with_suffix(keys::TOKENS, symbol.as_bytes(), token);
}
fn get_swap_count() -> u64 {
storage::get::<u64>(keys::SWAP_COUNT).unwrap_or(0)
}
fn set_swap_count(count: u64) {
storage::set(keys::SWAP_COUNT, &count);
}
fn get_pending_swap(id: u64) -> Option<PendingSwap> {
storage::get_with_suffix::<PendingSwap>(keys::PENDING_SWAPS, &id.to_le_bytes())
}
fn set_pending_swap(id: u64, swap: &PendingSwap) {
storage::set_with_suffix(keys::PENDING_SWAPS, &id.to_le_bytes(), swap);
}
fn get_total_volume() -> u64 {
storage::get::<u64>(keys::TOTAL_VOLUME).unwrap_or(0)
}
fn set_total_volume(volume: u64) {
storage::set(keys::TOTAL_VOLUME, &volume);
}
fn get_collected_fees(token: &str) -> u64 {
storage::get_with_suffix::<u64>(keys::COLLECTED_FEES, token.as_bytes()).unwrap_or(0)
}
fn set_collected_fees(token: &str, amount: u64) {
storage::set_with_suffix(keys::COLLECTED_FEES, token.as_bytes(), &amount);
}
// =============================================================================
// CALCULATION HELPERS
// =============================================================================
/// Calculate aggregation fee
fn calculate_aggregation_fee(amount: u64) -> u64 {
(amount as u128 * AGGREGATION_FEE_BPS as u128 / BPS_DENOMINATOR as u128) as u64
}
/// Apply slippage tolerance
fn apply_slippage(amount: u64, slippage_bps: u64) -> u64 {
let slippage = (amount as u128 * slippage_bps as u128 / BPS_DENOMINATOR as u128) as u64;
amount.saturating_sub(slippage)
}
/// Find best quote among sources
fn find_best_quote(quotes: &[Quote]) -> Option<&Quote> {
quotes.iter()
.filter(|q| q.available)
.max_by_key(|q| q.amount_out)
}
/// Create optimal routing plan
fn create_routing_plan(
quotes: &[Quote],
amount_in: u64,
slippage_bps: u64,
) -> Vec<Route> {
let mut routes = Vec::new();
// Filter available quotes and sort by output (best first)
let mut available: Vec<_> = quotes.iter()
.filter(|q| q.available && q.amount_out > 0)
.collect();
available.sort_by(|a, b| b.amount_out.cmp(&a.amount_out));
if available.is_empty() {
return routes;
}
// For simplicity, use single best route for small amounts
// Split across multiple routes for large amounts to reduce price impact
let total_liquidity: u64 = available.iter().map(|q| q.amount_out).sum();
if available.len() == 1 || amount_in < total_liquidity / 10 {
// Single route
let best = &available[0];
routes.push(Route {
source_id: best.source_id,
percentage: 100,
expected_output: best.amount_out,
min_output: apply_slippage(best.amount_out, slippage_bps),
});
} else {
// Split routing - distribute based on available liquidity
let mut remaining_percent = 100u64;
for (i, quote) in available.iter().take(MAX_ROUTES).enumerate() {
let percent = if i == available.len().min(MAX_ROUTES) - 1 {
remaining_percent // Last route gets remainder
} else {
// Proportional to output
let share = (quote.amount_out as u128 * 100) / total_liquidity as u128;
(share as u64).max(MIN_SPLIT_PERCENT).min(remaining_percent - MIN_SPLIT_PERCENT)
};
if percent < MIN_SPLIT_PERCENT {
continue;
}
let route_amount_in = (amount_in as u128 * percent as u128 / 100) as u64;
let expected_out = (quote.amount_out as u128 * percent as u128 / 100) as u64;
routes.push(Route {
source_id: quote.source_id,
percentage: percent,
expected_output: expected_out,
min_output: apply_slippage(expected_out, slippage_bps),
});
remaining_percent = remaining_percent.saturating_sub(percent);
if remaining_percent < MIN_SPLIT_PERCENT {
break;
}
}
}
routes
}
// =============================================================================
// ENTRY POINTS
// =============================================================================
synor_sdk::entry_point!(init, call);
fn init(params: &[u8]) -> Result<()> {
require!(!is_initialized(), Error::invalid_args("Already initialized"));
#[derive(BorshDeserialize)]
struct InitParams {
fee_collector: Address,
}
let params = InitParams::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid init params"))?;
storage::set(keys::OWNER, &caller());
storage::set(keys::INITIALIZED, &true);
storage::set(keys::PAUSED, &false);
storage::set(keys::FEE_COLLECTOR, &params.fee_collector);
set_source_count(0);
set_token_count(0);
set_swap_count(0);
set_total_volume(0);
Ok(())
}
fn call(selector: &[u8], params: &[u8]) -> Result<Vec<u8>> {
// Method selectors
let add_source_sel = synor_sdk::method_selector("add_source");
let update_source_sel = synor_sdk::method_selector("update_source");
let add_token_sel = synor_sdk::method_selector("add_token");
let get_quote_sel = synor_sdk::method_selector("get_quote");
let get_quotes_sel = synor_sdk::method_selector("get_quotes");
let swap_sel = synor_sdk::method_selector("swap");
let swap_exact_out_sel = synor_sdk::method_selector("swap_exact_out");
let get_swap_status_sel = synor_sdk::method_selector("get_swap_status");
let complete_ibc_swap_sel = synor_sdk::method_selector("complete_ibc_swap");
let refund_swap_sel = synor_sdk::method_selector("refund_swap");
let get_source_sel = synor_sdk::method_selector("get_source");
let get_token_sel = synor_sdk::method_selector("get_token");
let get_stats_sel = synor_sdk::method_selector("get_stats");
let pause_sel = synor_sdk::method_selector("pause");
let unpause_sel = synor_sdk::method_selector("unpause");
match selector {
// ===== Admin Methods =====
s if s == add_source_sel => {
let owner = get_owner().ok_or(Error::Unauthorized)?;
require_auth!(owner);
#[derive(BorshDeserialize)]
struct Args {
name: String,
source_type: SourceType,
contract: Option<Address>,
ibc_channel: Option<String>,
chain_id: Option<String>,
base_fee_bps: u64,
priority: u32,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid add_source params"))?;
let source_id = get_source_count();
let source = LiquiditySource {
id: source_id,
name: args.name.clone(),
source_type: args.source_type,
contract: args.contract,
ibc_channel: args.ibc_channel,
chain_id: args.chain_id,
status: SourceStatus::Active,
base_fee_bps: args.base_fee_bps,
priority: args.priority,
total_volume: 0,
success_rate_bps: 10000, // Start at 100%
avg_latency_ms: 0,
};
set_source(source_id, &source);
set_source_count(source_id + 1);
emit_raw(
&[event_topic(b"SourceAdded")],
&borsh::to_vec(&SourceAdded {
source_id,
name: args.name,
source_type: args.source_type,
}).unwrap(),
);
Ok(borsh::to_vec(&source_id).unwrap())
}
s if s == update_source_sel => {
let owner = get_owner().ok_or(Error::Unauthorized)?;
require_auth!(owner);
#[derive(BorshDeserialize)]
struct Args {
source_id: u32,
status: SourceStatus,
base_fee_bps: u64,
priority: u32,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid update_source params"))?;
let mut source = get_source(args.source_id)
.ok_or_else(|| Error::invalid_args("Source not found"))?;
source.status = args.status;
source.base_fee_bps = args.base_fee_bps;
source.priority = args.priority;
set_source(args.source_id, &source);
Ok(borsh::to_vec(&true).unwrap())
}
s if s == add_token_sel => {
let owner = get_owner().ok_or(Error::Unauthorized)?;
require_auth!(owner);
#[derive(BorshDeserialize)]
struct Args {
symbol: String,
local_address: Address,
decimals: u8,
ibc_denoms: Vec<(String, String)>,
is_native: bool,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid add_token params"))?;
let token = TokenInfo {
symbol: args.symbol.clone(),
local_address: args.local_address,
decimals: args.decimals,
ibc_denoms: args.ibc_denoms,
is_native: args.is_native,
};
set_token(&args.symbol, &token);
set_token_count(get_token_count() + 1);
Ok(borsh::to_vec(&true).unwrap())
}
s if s == pause_sel => {
let owner = get_owner().ok_or(Error::Unauthorized)?;
require_auth!(owner);
storage::set(keys::PAUSED, &true);
Ok(borsh::to_vec(&true).unwrap())
}
s if s == unpause_sel => {
let owner = get_owner().ok_or(Error::Unauthorized)?;
require_auth!(owner);
storage::set(keys::PAUSED, &false);
Ok(borsh::to_vec(&true).unwrap())
}
// ===== Quote Methods =====
s if s == get_quote_sel => {
#[derive(BorshDeserialize)]
struct Args {
token_in: String,
token_out: String,
amount_in: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid get_quote params"))?;
// Get quotes from all sources and find best
let quotes = get_all_quotes(&args.token_in, &args.token_out, args.amount_in)?;
let best = find_best_quote(&quotes);
Ok(borsh::to_vec(&best).unwrap())
}
s if s == get_quotes_sel => {
#[derive(BorshDeserialize)]
struct Args {
token_in: String,
token_out: String,
amount_in: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid get_quotes params"))?;
let quotes = get_all_quotes(&args.token_in, &args.token_out, args.amount_in)?;
Ok(borsh::to_vec(&quotes).unwrap())
}
// ===== Swap Methods =====
s if s == swap_sel => {
require!(!is_paused(), Error::invalid_args("Contract paused"));
#[derive(BorshDeserialize)]
struct Args {
token_in: String,
token_out: String,
amount_in: u64,
min_amount_out: u64,
slippage_bps: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid swap params"))?;
require!(args.slippage_bps <= MAX_SLIPPAGE_BPS,
Error::invalid_args("Slippage too high"));
// Get quotes and create routing plan
let quotes = get_all_quotes(&args.token_in, &args.token_out, args.amount_in)?;
require!(!quotes.is_empty(), Error::invalid_args("No quotes available"));
let routes = create_routing_plan(&quotes, args.amount_in, args.slippage_bps);
require!(!routes.is_empty(), Error::invalid_args("No routes available"));
let total_expected: u64 = routes.iter().map(|r| r.expected_output).sum();
require!(total_expected >= args.min_amount_out,
Error::invalid_args("Output below minimum"));
// Calculate aggregation fee
let fee = calculate_aggregation_fee(args.amount_in);
// Create swap plan
let swap_id = get_swap_count();
let now = timestamp();
let plan = SwapPlan {
token_in: args.token_in.clone(),
token_out: args.token_out.clone(),
amount_in: args.amount_in,
routes: routes.clone(),
total_expected_output: total_expected,
min_output: args.min_amount_out,
aggregation_fee: fee,
valid_until: now + IBC_TIMEOUT_SECONDS,
};
// For local-only routes, execute immediately
let is_local_only = routes.iter().all(|r| {
get_source(r.source_id)
.map(|s| s.source_type == SourceType::LocalAmm)
.unwrap_or(false)
});
if is_local_only {
// Execute local swap (simplified - would call DEX contract)
let amount_out = execute_local_swap(&plan)?;
// Update statistics
set_total_volume(get_total_volume() + args.amount_in);
let fees = get_collected_fees(&args.token_in);
set_collected_fees(&args.token_in, fees + fee);
emit_raw(
&[event_topic(b"SwapExecuted")],
&borsh::to_vec(&SwapExecuted {
swap_id,
user: caller(),
token_in: args.token_in,
token_out: args.token_out,
amount_in: args.amount_in,
amount_out,
fee,
routes_used: routes.len() as u32,
}).unwrap(),
);
set_swap_count(swap_id + 1);
return Ok(borsh::to_vec(&amount_out).unwrap());
}
// For IBC routes, create pending swap
let pending = PendingSwap {
id: swap_id,
user: caller(),
plan,
status: SwapStatus::Pending,
amount_received: 0,
routes_completed: 0,
created_at: now,
timeout_at: now + IBC_TIMEOUT_SECONDS,
};
set_pending_swap(swap_id, &pending);
set_swap_count(swap_id + 1);
// Initiate IBC transfers (would call IBC module)
// In production, this triggers actual IBC packet sending
Ok(borsh::to_vec(&swap_id).unwrap())
}
s if s == complete_ibc_swap_sel => {
// Called by relayer when IBC swap completes
#[derive(BorshDeserialize)]
struct Args {
swap_id: u64,
source_id: u32,
amount_received: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid complete_ibc_swap params"))?;
let mut swap = get_pending_swap(args.swap_id)
.ok_or_else(|| Error::invalid_args("Swap not found"))?;
require!(swap.status == SwapStatus::Pending || swap.status == SwapStatus::PartiallyFilled,
Error::invalid_args("Swap not in progress"));
swap.amount_received += args.amount_received;
swap.routes_completed += 1;
emit_raw(
&[event_topic(b"SwapPartialFill")],
&borsh::to_vec(&SwapPartialFill {
swap_id: args.swap_id,
source_id: args.source_id,
amount_filled: args.amount_received,
}).unwrap(),
);
// Check if all routes completed
if swap.routes_completed as usize >= swap.plan.routes.len() {
swap.status = SwapStatus::Completed;
// Update statistics
set_total_volume(get_total_volume() + swap.plan.amount_in);
let fees = get_collected_fees(&swap.plan.token_in);
set_collected_fees(&swap.plan.token_in, fees + swap.plan.aggregation_fee);
// Update source stats
for route in &swap.plan.routes {
if let Some(mut source) = get_source(route.source_id) {
source.total_volume += (swap.plan.amount_in as u128 * route.percentage as u128 / 100) as u64;
set_source(route.source_id, &source);
}
}
emit_raw(
&[event_topic(b"SwapExecuted")],
&borsh::to_vec(&SwapExecuted {
swap_id: args.swap_id,
user: swap.user,
token_in: swap.plan.token_in.clone(),
token_out: swap.plan.token_out.clone(),
amount_in: swap.plan.amount_in,
amount_out: swap.amount_received,
fee: swap.plan.aggregation_fee,
routes_used: swap.routes_completed,
}).unwrap(),
);
} else {
swap.status = SwapStatus::PartiallyFilled;
}
set_pending_swap(args.swap_id, &swap);
Ok(borsh::to_vec(&swap.amount_received).unwrap())
}
s if s == refund_swap_sel => {
#[derive(BorshDeserialize)]
struct Args {
swap_id: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Invalid refund_swap params"))?;
let mut swap = get_pending_swap(args.swap_id)
.ok_or_else(|| Error::invalid_args("Swap not found"))?;
// Can only refund if timed out or failed
let now = timestamp();
require!(
now > swap.timeout_at || swap.status == SwapStatus::Failed,
Error::invalid_args("Swap not refundable yet")
);
swap.status = SwapStatus::Refunded;
set_pending_swap(args.swap_id, &swap);
emit_raw(
&[event_topic(b"SwapFailed")],
&borsh::to_vec(&SwapFailed {
swap_id: args.swap_id,
reason: String::from("Timed out"),
}).unwrap(),
);
// In production, would transfer refund back to user
Ok(borsh::to_vec(&swap.plan.amount_in).unwrap())
}
// ===== Query Methods =====
s if s == get_swap_status_sel => {
#[derive(BorshDeserialize)]
struct Args {
swap_id: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected (swap_id: u64)"))?;
let swap = get_pending_swap(args.swap_id);
Ok(borsh::to_vec(&swap).unwrap())
}
s if s == get_source_sel => {
#[derive(BorshDeserialize)]
struct Args {
source_id: u32,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected (source_id: u32)"))?;
let source = get_source(args.source_id);
Ok(borsh::to_vec(&source).unwrap())
}
s if s == get_token_sel => {
#[derive(BorshDeserialize)]
struct Args {
symbol: String,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected (symbol: String)"))?;
let token = get_token(&args.symbol);
Ok(borsh::to_vec(&token).unwrap())
}
s if s == get_stats_sel => {
#[derive(BorshSerialize)]
struct Stats {
total_volume: u64,
source_count: u32,
token_count: u32,
swap_count: u64,
}
let stats = Stats {
total_volume: get_total_volume(),
source_count: get_source_count(),
token_count: get_token_count(),
swap_count: get_swap_count(),
};
Ok(borsh::to_vec(&stats).unwrap())
}
_ => Err(Error::InvalidMethod),
}
}
// =============================================================================
// QUOTE AND EXECUTION LOGIC
// =============================================================================
/// Get quotes from all active sources
fn get_all_quotes(token_in: &str, token_out: &str, amount_in: u64) -> Result<Vec<Quote>> {
let source_count = get_source_count();
let mut quotes = Vec::new();
for i in 0..source_count {
if let Some(source) = get_source(i) {
if source.status != SourceStatus::Active {
continue;
}
// Get quote from source (simplified - would call actual source)
let quote = get_source_quote(&source, token_in, token_out, amount_in);
quotes.push(quote);
}
}
// Sort by output amount (best first)
quotes.sort_by(|a, b| b.amount_out.cmp(&a.amount_out));
Ok(quotes)
}
/// Get quote from a specific source
fn get_source_quote(source: &LiquiditySource, _token_in: &str, _token_out: &str, amount_in: u64) -> Quote {
// In production, this would:
// - For LocalAmm: call the DEX contract's quote function
// - For IbcDex: query cached prices or send IBC query
// - For CexApi: use off-chain oracle data
// Simplified: estimate output based on source fee
let fee = (amount_in as u128 * source.base_fee_bps as u128 / BPS_DENOMINATOR as u128) as u64;
let amount_out = amount_in.saturating_sub(fee);
// Add some variation based on source type for demonstration
let adjusted_out = match source.source_type {
SourceType::LocalAmm => amount_out,
SourceType::IbcDex => amount_out + (amount_out / 1000), // IBC might have better prices
SourceType::CexApi => amount_out + (amount_out / 500), // CEX often has best prices
SourceType::OrderBook => amount_out,
};
let estimated_time = match source.source_type {
SourceType::LocalAmm => 2,
SourceType::IbcDex => 30,
SourceType::CexApi => 5,
SourceType::OrderBook => 3,
};
Quote {
source_id: source.id,
source_name: source.name.clone(),
amount_in,
amount_out: adjusted_out,
price_impact_bps: 10, // Simplified
fee,
available: true,
estimated_time,
}
}
/// Execute local swap (calls DEX contract)
fn execute_local_swap(plan: &SwapPlan) -> Result<u64> {
// In production, this would:
// 1. Call each source's swap function with the allocated amount
// 2. Aggregate the outputs
// 3. Handle any failures
// Simplified: return expected output
Ok(plan.total_expected_output)
}