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.
1040 lines
36 KiB
Rust
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, ¶ms.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("es);
|
|
|
|
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("es).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("es, 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)
|
|
}
|