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.
1118 lines
40 KiB
Rust
1118 lines
40 KiB
Rust
//! Perpetual Futures Trading Contract
|
|
//!
|
|
//! This contract implements perpetual futures trading with:
|
|
//! - Long and Short positions
|
|
//! - Leverage from 2x to 100x
|
|
//! - Funding rate mechanism (keeps price anchored to spot)
|
|
//! - Mark price for liquidations (manipulation resistant)
|
|
//! - Index price from oracles
|
|
//! - Liquidation engine with insurance fund
|
|
//! - Cross-margin and isolated margin modes
|
|
//!
|
|
//! # Architecture (Similar to dYdX/GMX)
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────────┐
|
|
//! │ PERPETUAL FUTURES │
|
|
//! ├─────────────────────────────────────────────────────────────┤
|
|
//! │ PRICE FEEDS: │
|
|
//! │ ┌─────────┐ ┌───────────┐ ┌─────────────┐ │
|
|
//! │ │ Oracle │────►│Index Price│────►│ Mark Price │ │
|
|
//! │ │ (Pyth) │ │ (Spot) │ │ (EMA-based) │ │
|
|
//! │ └─────────┘ └───────────┘ └─────────────┘ │
|
|
//! │ │ │
|
|
//! │ POSITIONS: ▼ │
|
|
//! │ ┌─────────────────────────────────────────────────────┐ │
|
|
//! │ │ Position { size, collateral, entry_price, leverage }│ │
|
|
//! │ │ PnL = size * (mark_price - entry_price) │ │
|
|
//! │ │ Liquidation when: margin_ratio < maintenance_margin │ │
|
|
//! │ └─────────────────────────────────────────────────────┘ │
|
|
//! │ │
|
|
//! │ FUNDING: │
|
|
//! │ ┌─────────────────────────────────────────────────────┐ │
|
|
//! │ │ funding_rate = (mark_price - index_price) / 8h │ │
|
|
//! │ │ if mark > index: longs pay shorts │ │
|
|
//! │ │ if mark < index: shorts pay longs │ │
|
|
//! │ └─────────────────────────────────────────────────────┘ │
|
|
//! └─────────────────────────────────────────────────────────────┘
|
|
//! ```
|
|
|
|
#![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
|
|
// =============================================================================
|
|
|
|
/// Minimum leverage (2x)
|
|
pub const MIN_LEVERAGE: u64 = 2;
|
|
|
|
/// Maximum leverage (100x)
|
|
pub const MAX_LEVERAGE: u64 = 100;
|
|
|
|
/// Basis points denominator (10000 = 100%)
|
|
pub const BPS_DENOMINATOR: u64 = 10000;
|
|
|
|
/// Initial margin requirement (1% at 100x leverage = 100 bps)
|
|
pub const INITIAL_MARGIN_BPS: u64 = 100;
|
|
|
|
/// Maintenance margin (0.5% = 50 bps) - below this, liquidation
|
|
pub const MAINTENANCE_MARGIN_BPS: u64 = 50;
|
|
|
|
/// Liquidation fee (5% = 500 bps) - goes to liquidator + insurance
|
|
pub const LIQUIDATION_FEE_BPS: u64 = 500;
|
|
|
|
/// Insurance fund share of liquidation fee (50%)
|
|
pub const INSURANCE_SHARE_BPS: u64 = 5000;
|
|
|
|
/// Maximum funding rate (0.1% per 8h = 10 bps)
|
|
pub const MAX_FUNDING_RATE_BPS: u64 = 10;
|
|
|
|
/// Funding interval (8 hours in seconds)
|
|
pub const FUNDING_INTERVAL: u64 = 8 * 60 * 60;
|
|
|
|
/// Price precision (6 decimals, like USDC)
|
|
pub const PRICE_PRECISION: u64 = 1_000_000;
|
|
|
|
/// Size precision (8 decimals)
|
|
pub const SIZE_PRECISION: u64 = 100_000_000;
|
|
|
|
// =============================================================================
|
|
// 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"perps:owner";
|
|
pub const INITIALIZED: &[u8] = b"perps:initialized";
|
|
pub const PAUSED: &[u8] = b"perps:paused";
|
|
|
|
// Market configuration
|
|
pub const MARKETS: &[u8] = b"perps:markets";
|
|
pub const MARKET_COUNT: &[u8] = b"perps:market_count";
|
|
|
|
// Positions
|
|
pub const POSITIONS: &[u8] = b"perps:positions";
|
|
pub const POSITION_COUNT: &[u8] = b"perps:position_count";
|
|
pub const USER_POSITIONS: &[u8] = b"perps:user_positions";
|
|
|
|
// Funding
|
|
pub const LAST_FUNDING_TIME: &[u8] = b"perps:last_funding";
|
|
pub const CUMULATIVE_FUNDING: &[u8] = b"perps:cum_funding";
|
|
|
|
// Insurance fund
|
|
pub const INSURANCE_FUND: &[u8] = b"perps:insurance";
|
|
|
|
// Oracle
|
|
pub const ORACLE: &[u8] = b"perps:oracle";
|
|
pub const PRICES: &[u8] = b"perps:prices";
|
|
}
|
|
|
|
// =============================================================================
|
|
// DATA STRUCTURES
|
|
// =============================================================================
|
|
|
|
/// Position direction
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
|
pub enum Direction {
|
|
Long,
|
|
Short,
|
|
}
|
|
|
|
/// Margin mode
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
|
pub enum MarginMode {
|
|
/// Each position has isolated margin
|
|
Isolated,
|
|
/// All positions share margin
|
|
Cross,
|
|
}
|
|
|
|
/// Market status
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
|
pub enum MarketStatus {
|
|
Active,
|
|
Paused,
|
|
SettleOnly,
|
|
Delisted,
|
|
}
|
|
|
|
/// Market configuration
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
|
pub struct Market {
|
|
/// Market ID
|
|
pub id: u32,
|
|
/// Base asset symbol (e.g., "BTC")
|
|
pub base_asset: String,
|
|
/// Quote asset symbol (e.g., "USD")
|
|
pub quote_asset: String,
|
|
/// Market status
|
|
pub status: MarketStatus,
|
|
/// Maximum leverage allowed
|
|
pub max_leverage: u64,
|
|
/// Maintenance margin requirement (bps)
|
|
pub maintenance_margin_bps: u64,
|
|
/// Taker fee (bps)
|
|
pub taker_fee_bps: u64,
|
|
/// Maker fee (bps, can be negative for rebates)
|
|
pub maker_fee_bps: i64,
|
|
/// Minimum position size
|
|
pub min_size: u64,
|
|
/// Maximum position size per user
|
|
pub max_size: u64,
|
|
/// Total open interest (long)
|
|
pub open_interest_long: u64,
|
|
/// Total open interest (short)
|
|
pub open_interest_short: u64,
|
|
/// Maximum open interest
|
|
pub max_open_interest: u64,
|
|
}
|
|
|
|
/// A trading position
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
|
pub struct Position {
|
|
/// Position ID
|
|
pub id: u64,
|
|
/// Owner address
|
|
pub owner: Address,
|
|
/// Market ID
|
|
pub market_id: u32,
|
|
/// Position direction
|
|
pub direction: Direction,
|
|
/// Position size (in SIZE_PRECISION)
|
|
pub size: u64,
|
|
/// Collateral amount (in PRICE_PRECISION)
|
|
pub collateral: u64,
|
|
/// Entry price (in PRICE_PRECISION)
|
|
pub entry_price: u64,
|
|
/// Leverage (2x to 100x)
|
|
pub leverage: u64,
|
|
/// Margin mode
|
|
pub margin_mode: MarginMode,
|
|
/// Cumulative funding at entry
|
|
pub entry_funding: i64,
|
|
/// Timestamp when opened
|
|
pub opened_at: u64,
|
|
/// Last update timestamp
|
|
pub updated_at: u64,
|
|
/// Whether position is active
|
|
pub is_open: bool,
|
|
}
|
|
|
|
/// Price data from oracle
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
|
pub struct PriceData {
|
|
/// Index price (spot price from oracles)
|
|
pub index_price: u64,
|
|
/// Mark price (EMA-smoothed, used for liquidations)
|
|
pub mark_price: u64,
|
|
/// Last update timestamp
|
|
pub timestamp: u64,
|
|
/// Confidence interval
|
|
pub confidence: u64,
|
|
}
|
|
|
|
/// Funding rate data
|
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
|
pub struct FundingData {
|
|
/// Current funding rate (can be negative, in bps)
|
|
pub rate_bps: i64,
|
|
/// Cumulative funding for longs
|
|
pub cumulative_long: i64,
|
|
/// Cumulative funding for shorts
|
|
pub cumulative_short: i64,
|
|
/// Last funding timestamp
|
|
pub last_update: u64,
|
|
}
|
|
|
|
// =============================================================================
|
|
// EVENTS
|
|
// =============================================================================
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct MarketCreated {
|
|
pub market_id: u32,
|
|
pub base_asset: String,
|
|
pub quote_asset: String,
|
|
pub max_leverage: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct PositionOpened {
|
|
pub position_id: u64,
|
|
pub owner: Address,
|
|
pub market_id: u32,
|
|
pub direction: Direction,
|
|
pub size: u64,
|
|
pub collateral: u64,
|
|
pub entry_price: u64,
|
|
pub leverage: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct PositionClosed {
|
|
pub position_id: u64,
|
|
pub owner: Address,
|
|
pub exit_price: u64,
|
|
pub realized_pnl: i64,
|
|
pub fee_paid: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct PositionLiquidated {
|
|
pub position_id: u64,
|
|
pub owner: Address,
|
|
pub liquidator: Address,
|
|
pub size: u64,
|
|
pub collateral_seized: u64,
|
|
pub liquidation_price: u64,
|
|
pub insurance_fund_share: u64,
|
|
pub liquidator_reward: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct FundingPaid {
|
|
pub market_id: u32,
|
|
pub funding_rate_bps: i64,
|
|
pub total_long_payment: i64,
|
|
pub total_short_payment: i64,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize)]
|
|
pub struct CollateralModified {
|
|
pub position_id: u64,
|
|
pub old_collateral: u64,
|
|
pub new_collateral: u64,
|
|
pub new_leverage: u64,
|
|
}
|
|
|
|
// =============================================================================
|
|
// STORAGE HELPERS
|
|
// =============================================================================
|
|
|
|
fn get_owner() -> Option<Address> {
|
|
storage::get::<Address>(keys::OWNER)
|
|
}
|
|
|
|
fn is_owner(addr: &Address) -> bool {
|
|
get_owner().map(|o| o == *addr).unwrap_or(false)
|
|
}
|
|
|
|
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_market_count() -> u32 {
|
|
storage::get::<u32>(keys::MARKET_COUNT).unwrap_or(0)
|
|
}
|
|
|
|
fn set_market_count(count: u32) {
|
|
storage::set(keys::MARKET_COUNT, &count);
|
|
}
|
|
|
|
fn get_market(id: u32) -> Option<Market> {
|
|
storage::get_with_suffix::<Market>(keys::MARKETS, &id.to_le_bytes())
|
|
}
|
|
|
|
fn set_market(id: u32, market: &Market) {
|
|
storage::set_with_suffix(keys::MARKETS, &id.to_le_bytes(), market);
|
|
}
|
|
|
|
fn get_position_count() -> u64 {
|
|
storage::get::<u64>(keys::POSITION_COUNT).unwrap_or(0)
|
|
}
|
|
|
|
fn set_position_count(count: u64) {
|
|
storage::set(keys::POSITION_COUNT, &count);
|
|
}
|
|
|
|
fn get_position(id: u64) -> Option<Position> {
|
|
storage::get_with_suffix::<Position>(keys::POSITIONS, &id.to_le_bytes())
|
|
}
|
|
|
|
fn set_position(id: u64, position: &Position) {
|
|
storage::set_with_suffix(keys::POSITIONS, &id.to_le_bytes(), position);
|
|
}
|
|
|
|
fn get_price(market_id: u32) -> Option<PriceData> {
|
|
storage::get_with_suffix::<PriceData>(keys::PRICES, &market_id.to_le_bytes())
|
|
}
|
|
|
|
fn set_price(market_id: u32, price: &PriceData) {
|
|
storage::set_with_suffix(keys::PRICES, &market_id.to_le_bytes(), price);
|
|
}
|
|
|
|
fn get_funding(market_id: u32) -> Option<FundingData> {
|
|
storage::get_with_suffix::<FundingData>(keys::CUMULATIVE_FUNDING, &market_id.to_le_bytes())
|
|
}
|
|
|
|
fn set_funding(market_id: u32, funding: &FundingData) {
|
|
storage::set_with_suffix(keys::CUMULATIVE_FUNDING, &market_id.to_le_bytes(), funding);
|
|
}
|
|
|
|
fn get_insurance_fund() -> u64 {
|
|
storage::get::<u64>(keys::INSURANCE_FUND).unwrap_or(0)
|
|
}
|
|
|
|
fn set_insurance_fund(amount: u64) {
|
|
storage::set(keys::INSURANCE_FUND, &amount);
|
|
}
|
|
|
|
fn get_oracle() -> Option<Address> {
|
|
storage::get::<Address>(keys::ORACLE)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CALCULATION HELPERS
|
|
// =============================================================================
|
|
|
|
/// Calculate unrealized PnL for a position
|
|
fn calculate_pnl(position: &Position, mark_price: u64) -> i64 {
|
|
let size_value = (position.size as u128 * mark_price as u128) / SIZE_PRECISION as u128;
|
|
let entry_value = (position.size as u128 * position.entry_price as u128) / SIZE_PRECISION as u128;
|
|
|
|
match position.direction {
|
|
Direction::Long => (size_value as i64) - (entry_value as i64),
|
|
Direction::Short => (entry_value as i64) - (size_value as i64),
|
|
}
|
|
}
|
|
|
|
/// Calculate margin ratio (collateral + pnl) / position_value
|
|
fn calculate_margin_ratio(position: &Position, mark_price: u64) -> u64 {
|
|
let pnl = calculate_pnl(position, mark_price);
|
|
let effective_collateral = if pnl >= 0 {
|
|
position.collateral + (pnl as u64)
|
|
} else {
|
|
position.collateral.saturating_sub((-pnl) as u64)
|
|
};
|
|
|
|
let position_value = (position.size as u128 * mark_price as u128) / SIZE_PRECISION as u128;
|
|
if position_value == 0 {
|
|
return BPS_DENOMINATOR;
|
|
}
|
|
|
|
((effective_collateral as u128 * BPS_DENOMINATOR as u128) / position_value) as u64
|
|
}
|
|
|
|
/// Check if position should be liquidated
|
|
fn is_liquidatable(position: &Position, mark_price: u64) -> bool {
|
|
let margin_ratio = calculate_margin_ratio(position, mark_price);
|
|
margin_ratio < MAINTENANCE_MARGIN_BPS
|
|
}
|
|
|
|
/// Calculate liquidation price
|
|
fn calculate_liquidation_price(position: &Position) -> u64 {
|
|
// For long: liq_price = entry_price * (1 - maintenance_margin / leverage)
|
|
// For short: liq_price = entry_price * (1 + maintenance_margin / leverage)
|
|
|
|
let margin_factor = (MAINTENANCE_MARGIN_BPS as u128 * PRICE_PRECISION as u128)
|
|
/ (position.leverage as u128 * 100);
|
|
|
|
match position.direction {
|
|
Direction::Long => {
|
|
position.entry_price.saturating_sub(margin_factor as u64)
|
|
}
|
|
Direction::Short => {
|
|
position.entry_price + margin_factor as u64
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Calculate funding payment for a position
|
|
fn calculate_funding_payment(position: &Position, current_funding: &FundingData) -> i64 {
|
|
let funding_delta = match position.direction {
|
|
Direction::Long => current_funding.cumulative_long - position.entry_funding,
|
|
Direction::Short => current_funding.cumulative_short - position.entry_funding,
|
|
};
|
|
|
|
// funding_payment = position_size * funding_delta / SIZE_PRECISION
|
|
((position.size as i128 * funding_delta as i128) / SIZE_PRECISION as i128) as i64
|
|
}
|
|
|
|
/// Calculate new funding rate based on mark vs index price
|
|
fn calculate_funding_rate(mark_price: u64, index_price: u64) -> i64 {
|
|
if index_price == 0 {
|
|
return 0;
|
|
}
|
|
|
|
// funding_rate = (mark_price - index_price) / index_price
|
|
// Scaled to bps
|
|
let price_diff = mark_price as i64 - index_price as i64;
|
|
let rate = (price_diff as i128 * BPS_DENOMINATOR as i128) / index_price as i128;
|
|
|
|
// Clamp to max funding rate
|
|
rate.max(-(MAX_FUNDING_RATE_BPS as i128)).min(MAX_FUNDING_RATE_BPS as i128) as i64
|
|
}
|
|
|
|
/// Calculate trading fee
|
|
fn calculate_fee(size: u64, price: u64, fee_bps: u64) -> u64 {
|
|
let notional = (size as u128 * price as u128) / SIZE_PRECISION as u128;
|
|
((notional * fee_bps as u128) / BPS_DENOMINATOR as u128) as u64
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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 {
|
|
oracle: 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::ORACLE, ¶ms.oracle);
|
|
set_market_count(0);
|
|
set_position_count(0);
|
|
set_insurance_fund(0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn call(selector: &[u8], params: &[u8]) -> Result<Vec<u8>> {
|
|
// Method selectors
|
|
let create_market_sel = synor_sdk::method_selector("create_market");
|
|
let open_position_sel = synor_sdk::method_selector("open_position");
|
|
let close_position_sel = synor_sdk::method_selector("close_position");
|
|
let add_collateral_sel = synor_sdk::method_selector("add_collateral");
|
|
let remove_collateral_sel = synor_sdk::method_selector("remove_collateral");
|
|
let liquidate_sel = synor_sdk::method_selector("liquidate");
|
|
let update_price_sel = synor_sdk::method_selector("update_price");
|
|
let settle_funding_sel = synor_sdk::method_selector("settle_funding");
|
|
let get_position_sel = synor_sdk::method_selector("get_position");
|
|
let get_market_sel = synor_sdk::method_selector("get_market");
|
|
let get_price_sel = synor_sdk::method_selector("get_price");
|
|
let get_pnl_sel = synor_sdk::method_selector("get_pnl");
|
|
let pause_sel = synor_sdk::method_selector("pause");
|
|
let unpause_sel = synor_sdk::method_selector("unpause");
|
|
|
|
match selector {
|
|
// ===== Admin Methods =====
|
|
|
|
s if s == create_market_sel => {
|
|
let owner = get_owner().ok_or(Error::Unauthorized)?;
|
|
require_auth!(owner);
|
|
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
base_asset: String,
|
|
quote_asset: String,
|
|
max_leverage: u64,
|
|
taker_fee_bps: u64,
|
|
maker_fee_bps: i64,
|
|
min_size: u64,
|
|
max_size: u64,
|
|
max_open_interest: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid create_market params"))?;
|
|
|
|
require!(args.max_leverage >= MIN_LEVERAGE && args.max_leverage <= MAX_LEVERAGE,
|
|
Error::invalid_args("Leverage must be 2-100x"));
|
|
|
|
let market_id = get_market_count();
|
|
let market = Market {
|
|
id: market_id,
|
|
base_asset: args.base_asset.clone(),
|
|
quote_asset: args.quote_asset.clone(),
|
|
status: MarketStatus::Active,
|
|
max_leverage: args.max_leverage,
|
|
maintenance_margin_bps: MAINTENANCE_MARGIN_BPS,
|
|
taker_fee_bps: args.taker_fee_bps,
|
|
maker_fee_bps: args.maker_fee_bps,
|
|
min_size: args.min_size,
|
|
max_size: args.max_size,
|
|
open_interest_long: 0,
|
|
open_interest_short: 0,
|
|
max_open_interest: args.max_open_interest,
|
|
};
|
|
|
|
set_market(market_id, &market);
|
|
set_market_count(market_id + 1);
|
|
|
|
// Initialize funding data
|
|
let funding = FundingData {
|
|
rate_bps: 0,
|
|
cumulative_long: 0,
|
|
cumulative_short: 0,
|
|
last_update: timestamp(),
|
|
};
|
|
set_funding(market_id, &funding);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"MarketCreated")],
|
|
&borsh::to_vec(&MarketCreated {
|
|
market_id,
|
|
base_asset: args.base_asset,
|
|
quote_asset: args.quote_asset,
|
|
max_leverage: args.max_leverage,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&market_id).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())
|
|
}
|
|
|
|
// ===== Oracle Methods =====
|
|
|
|
s if s == update_price_sel => {
|
|
// Only oracle can update prices
|
|
let oracle = get_oracle().ok_or(Error::Unauthorized)?;
|
|
require_auth!(oracle);
|
|
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
market_id: u32,
|
|
index_price: u64,
|
|
confidence: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid update_price params"))?;
|
|
|
|
let _market = get_market(args.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
|
|
// Get previous mark price for EMA calculation
|
|
let prev_price = get_price(args.market_id);
|
|
let prev_mark = prev_price.map(|p| p.mark_price).unwrap_or(args.index_price);
|
|
|
|
// Mark price is EMA of index price (smooths out manipulation)
|
|
// mark_price = 0.9 * prev_mark + 0.1 * index_price
|
|
let mark_price = (prev_mark * 9 + args.index_price) / 10;
|
|
|
|
let price_data = PriceData {
|
|
index_price: args.index_price,
|
|
mark_price,
|
|
timestamp: timestamp(),
|
|
confidence: args.confidence,
|
|
};
|
|
|
|
set_price(args.market_id, &price_data);
|
|
|
|
Ok(borsh::to_vec(&price_data).unwrap())
|
|
}
|
|
|
|
// ===== Trading Methods =====
|
|
|
|
s if s == open_position_sel => {
|
|
require!(!is_paused(), Error::invalid_args("Contract paused"));
|
|
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
market_id: u32,
|
|
direction: Direction,
|
|
size: u64,
|
|
collateral: u64,
|
|
leverage: u64,
|
|
margin_mode: MarginMode,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid open_position params"))?;
|
|
|
|
let mut market = get_market(args.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
require!(market.status == MarketStatus::Active,
|
|
Error::invalid_args("Market not active"));
|
|
|
|
let price = get_price(args.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
// Validate leverage
|
|
require!(args.leverage >= MIN_LEVERAGE && args.leverage <= market.max_leverage,
|
|
Error::invalid_args("Invalid leverage"));
|
|
|
|
// Validate size
|
|
require!(args.size >= market.min_size && args.size <= market.max_size,
|
|
Error::invalid_args("Invalid position size"));
|
|
|
|
// Validate collateral matches leverage requirement
|
|
let required_collateral = (args.size as u128 * price.mark_price as u128)
|
|
/ (SIZE_PRECISION as u128 * args.leverage as u128);
|
|
require!(args.collateral >= required_collateral as u64,
|
|
Error::invalid_args("Insufficient collateral for leverage"));
|
|
|
|
// Check open interest limits
|
|
let new_oi = match args.direction {
|
|
Direction::Long => market.open_interest_long + args.size,
|
|
Direction::Short => market.open_interest_short + args.size,
|
|
};
|
|
require!(new_oi <= market.max_open_interest,
|
|
Error::invalid_args("Exceeds max open interest"));
|
|
|
|
// Get current funding
|
|
let funding = get_funding(args.market_id).unwrap_or(FundingData {
|
|
rate_bps: 0,
|
|
cumulative_long: 0,
|
|
cumulative_short: 0,
|
|
last_update: timestamp(),
|
|
});
|
|
|
|
let entry_funding = match args.direction {
|
|
Direction::Long => funding.cumulative_long,
|
|
Direction::Short => funding.cumulative_short,
|
|
};
|
|
|
|
// Calculate and deduct fee
|
|
let fee = calculate_fee(args.size, price.mark_price, market.taker_fee_bps);
|
|
let net_collateral = args.collateral.saturating_sub(fee);
|
|
|
|
// Create position
|
|
let position_id = get_position_count();
|
|
let position = Position {
|
|
id: position_id,
|
|
owner: caller(),
|
|
market_id: args.market_id,
|
|
direction: args.direction,
|
|
size: args.size,
|
|
collateral: net_collateral,
|
|
entry_price: price.mark_price,
|
|
leverage: args.leverage,
|
|
margin_mode: args.margin_mode,
|
|
entry_funding,
|
|
opened_at: timestamp(),
|
|
updated_at: timestamp(),
|
|
is_open: true,
|
|
};
|
|
|
|
// Update open interest
|
|
match args.direction {
|
|
Direction::Long => market.open_interest_long += args.size,
|
|
Direction::Short => market.open_interest_short += args.size,
|
|
}
|
|
|
|
set_position(position_id, &position);
|
|
set_position_count(position_id + 1);
|
|
set_market(args.market_id, &market);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"PositionOpened")],
|
|
&borsh::to_vec(&PositionOpened {
|
|
position_id,
|
|
owner: caller(),
|
|
market_id: args.market_id,
|
|
direction: args.direction,
|
|
size: args.size,
|
|
collateral: net_collateral,
|
|
entry_price: price.mark_price,
|
|
leverage: args.leverage,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&position_id).unwrap())
|
|
}
|
|
|
|
s if s == close_position_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid close_position params"))?;
|
|
|
|
let mut position = get_position(args.position_id)
|
|
.ok_or_else(|| Error::invalid_args("Position not found"))?;
|
|
require!(position.is_open, Error::invalid_args("Position already closed"));
|
|
require!(position.owner == caller(), Error::Unauthorized);
|
|
|
|
let mut market = get_market(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
|
|
let price = get_price(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
// Calculate PnL
|
|
let pnl = calculate_pnl(&position, price.mark_price);
|
|
|
|
// Calculate funding owed
|
|
let funding = get_funding(position.market_id).unwrap_or(FundingData {
|
|
rate_bps: 0,
|
|
cumulative_long: 0,
|
|
cumulative_short: 0,
|
|
last_update: timestamp(),
|
|
});
|
|
let funding_payment = calculate_funding_payment(&position, &funding);
|
|
|
|
// Calculate fee
|
|
let fee = calculate_fee(position.size, price.mark_price, market.taker_fee_bps);
|
|
|
|
// Calculate final payout
|
|
let total_pnl = pnl - funding_payment - fee as i64;
|
|
let payout = if total_pnl >= 0 {
|
|
position.collateral + total_pnl as u64
|
|
} else {
|
|
position.collateral.saturating_sub((-total_pnl) as u64)
|
|
};
|
|
|
|
// Update open interest
|
|
match position.direction {
|
|
Direction::Long => market.open_interest_long = market.open_interest_long.saturating_sub(position.size),
|
|
Direction::Short => market.open_interest_short = market.open_interest_short.saturating_sub(position.size),
|
|
}
|
|
|
|
// Close position
|
|
position.is_open = false;
|
|
position.updated_at = timestamp();
|
|
|
|
set_position(args.position_id, &position);
|
|
set_market(position.market_id, &market);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"PositionClosed")],
|
|
&borsh::to_vec(&PositionClosed {
|
|
position_id: args.position_id,
|
|
owner: position.owner,
|
|
exit_price: price.mark_price,
|
|
realized_pnl: total_pnl,
|
|
fee_paid: fee,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&payout).unwrap())
|
|
}
|
|
|
|
s if s == add_collateral_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
amount: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid add_collateral params"))?;
|
|
|
|
let mut position = get_position(args.position_id)
|
|
.ok_or_else(|| Error::invalid_args("Position not found"))?;
|
|
require!(position.is_open, Error::invalid_args("Position closed"));
|
|
require!(position.owner == caller(), Error::Unauthorized);
|
|
|
|
let old_collateral = position.collateral;
|
|
position.collateral += args.amount;
|
|
position.updated_at = timestamp();
|
|
|
|
// Recalculate effective leverage
|
|
let price = get_price(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
let notional = (position.size as u128 * price.mark_price as u128) / SIZE_PRECISION as u128;
|
|
let new_leverage = if position.collateral > 0 {
|
|
(notional / position.collateral as u128) as u64
|
|
} else {
|
|
MAX_LEVERAGE
|
|
};
|
|
position.leverage = new_leverage.max(MIN_LEVERAGE);
|
|
|
|
set_position(args.position_id, &position);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"CollateralModified")],
|
|
&borsh::to_vec(&CollateralModified {
|
|
position_id: args.position_id,
|
|
old_collateral,
|
|
new_collateral: position.collateral,
|
|
new_leverage: position.leverage,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&position.collateral).unwrap())
|
|
}
|
|
|
|
s if s == remove_collateral_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
amount: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid remove_collateral params"))?;
|
|
|
|
let mut position = get_position(args.position_id)
|
|
.ok_or_else(|| Error::invalid_args("Position not found"))?;
|
|
require!(position.is_open, Error::invalid_args("Position closed"));
|
|
require!(position.owner == caller(), Error::Unauthorized);
|
|
|
|
let market = get_market(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
|
|
let price = get_price(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
let old_collateral = position.collateral;
|
|
let new_collateral = position.collateral.saturating_sub(args.amount);
|
|
|
|
// Check that new collateral maintains margin requirement
|
|
let notional = (position.size as u128 * price.mark_price as u128) / SIZE_PRECISION as u128;
|
|
let new_leverage = if new_collateral > 0 {
|
|
(notional / new_collateral as u128) as u64
|
|
} else {
|
|
MAX_LEVERAGE + 1
|
|
};
|
|
|
|
require!(new_leverage <= market.max_leverage,
|
|
Error::invalid_args("Would exceed max leverage"));
|
|
|
|
// Check margin ratio after removal
|
|
position.collateral = new_collateral;
|
|
require!(!is_liquidatable(&position, price.mark_price),
|
|
Error::invalid_args("Would become liquidatable"));
|
|
|
|
position.leverage = new_leverage;
|
|
position.updated_at = timestamp();
|
|
|
|
set_position(args.position_id, &position);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"CollateralModified")],
|
|
&borsh::to_vec(&CollateralModified {
|
|
position_id: args.position_id,
|
|
old_collateral,
|
|
new_collateral,
|
|
new_leverage,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&args.amount).unwrap())
|
|
}
|
|
|
|
s if s == liquidate_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid liquidate params"))?;
|
|
|
|
let mut position = get_position(args.position_id)
|
|
.ok_or_else(|| Error::invalid_args("Position not found"))?;
|
|
require!(position.is_open, Error::invalid_args("Position closed"));
|
|
|
|
let mut market = get_market(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
|
|
let price = get_price(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
// Verify position is liquidatable
|
|
require!(is_liquidatable(&position, price.mark_price),
|
|
Error::invalid_args("Position not liquidatable"));
|
|
|
|
// Calculate liquidation amounts
|
|
let collateral_seized = position.collateral;
|
|
let liquidation_fee = (collateral_seized as u128 * LIQUIDATION_FEE_BPS as u128
|
|
/ BPS_DENOMINATOR as u128) as u64;
|
|
|
|
let insurance_share = (liquidation_fee as u128 * INSURANCE_SHARE_BPS as u128
|
|
/ BPS_DENOMINATOR as u128) as u64;
|
|
let liquidator_reward = liquidation_fee - insurance_share;
|
|
|
|
// Update insurance fund
|
|
let insurance = get_insurance_fund();
|
|
set_insurance_fund(insurance + insurance_share);
|
|
|
|
// Update open interest
|
|
match position.direction {
|
|
Direction::Long => market.open_interest_long = market.open_interest_long.saturating_sub(position.size),
|
|
Direction::Short => market.open_interest_short = market.open_interest_short.saturating_sub(position.size),
|
|
}
|
|
|
|
// Close position
|
|
position.is_open = false;
|
|
position.collateral = 0;
|
|
position.updated_at = timestamp();
|
|
|
|
set_position(args.position_id, &position);
|
|
set_market(position.market_id, &market);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"PositionLiquidated")],
|
|
&borsh::to_vec(&PositionLiquidated {
|
|
position_id: args.position_id,
|
|
owner: position.owner,
|
|
liquidator: caller(),
|
|
size: position.size,
|
|
collateral_seized,
|
|
liquidation_price: price.mark_price,
|
|
insurance_fund_share: insurance_share,
|
|
liquidator_reward,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&liquidator_reward).unwrap())
|
|
}
|
|
|
|
s if s == settle_funding_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
market_id: u32,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Invalid settle_funding params"))?;
|
|
|
|
let market = get_market(args.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Market not found"))?;
|
|
|
|
let price = get_price(args.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
let mut funding = get_funding(args.market_id).unwrap_or(FundingData {
|
|
rate_bps: 0,
|
|
cumulative_long: 0,
|
|
cumulative_short: 0,
|
|
last_update: timestamp(),
|
|
});
|
|
|
|
let now = timestamp();
|
|
let time_elapsed = now - funding.last_update;
|
|
|
|
// Only settle if enough time has passed
|
|
require!(time_elapsed >= FUNDING_INTERVAL,
|
|
Error::invalid_args("Funding interval not elapsed"));
|
|
|
|
// Calculate new funding rate
|
|
let new_rate = calculate_funding_rate(price.mark_price, price.index_price);
|
|
|
|
// Calculate funding payments
|
|
// Positive rate = longs pay shorts
|
|
// Negative rate = shorts pay longs
|
|
let long_payment = (market.open_interest_long as i128 * new_rate as i128
|
|
/ BPS_DENOMINATOR as i128) as i64;
|
|
let short_payment = -long_payment;
|
|
|
|
// Update cumulative funding
|
|
funding.rate_bps = new_rate;
|
|
funding.cumulative_long += long_payment;
|
|
funding.cumulative_short += short_payment;
|
|
funding.last_update = now;
|
|
|
|
set_funding(args.market_id, &funding);
|
|
|
|
emit_raw(
|
|
&[event_topic(b"FundingPaid")],
|
|
&borsh::to_vec(&FundingPaid {
|
|
market_id: args.market_id,
|
|
funding_rate_bps: new_rate,
|
|
total_long_payment: long_payment,
|
|
total_short_payment: short_payment,
|
|
timestamp: now,
|
|
}).unwrap(),
|
|
);
|
|
|
|
Ok(borsh::to_vec(&new_rate).unwrap())
|
|
}
|
|
|
|
// ===== Query Methods =====
|
|
|
|
s if s == get_position_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Expected (position_id: u64)"))?;
|
|
|
|
let position = get_position(args.position_id);
|
|
Ok(borsh::to_vec(&position).unwrap())
|
|
}
|
|
|
|
s if s == get_market_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
market_id: u32,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Expected (market_id: u32)"))?;
|
|
|
|
let market = get_market(args.market_id);
|
|
Ok(borsh::to_vec(&market).unwrap())
|
|
}
|
|
|
|
s if s == get_price_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
market_id: u32,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Expected (market_id: u32)"))?;
|
|
|
|
let price = get_price(args.market_id);
|
|
Ok(borsh::to_vec(&price).unwrap())
|
|
}
|
|
|
|
s if s == get_pnl_sel => {
|
|
#[derive(BorshDeserialize)]
|
|
struct Args {
|
|
position_id: u64,
|
|
}
|
|
let args = Args::try_from_slice(params)
|
|
.map_err(|_| Error::invalid_args("Expected (position_id: u64)"))?;
|
|
|
|
let position = get_position(args.position_id)
|
|
.ok_or_else(|| Error::invalid_args("Position not found"))?;
|
|
|
|
let price = get_price(position.market_id)
|
|
.ok_or_else(|| Error::invalid_args("Price not available"))?;
|
|
|
|
let pnl = calculate_pnl(&position, price.mark_price);
|
|
let liq_price = calculate_liquidation_price(&position);
|
|
let margin_ratio = calculate_margin_ratio(&position, price.mark_price);
|
|
|
|
#[derive(BorshSerialize)]
|
|
struct PnlResult {
|
|
unrealized_pnl: i64,
|
|
liquidation_price: u64,
|
|
margin_ratio_bps: u64,
|
|
is_liquidatable: bool,
|
|
}
|
|
|
|
let result = PnlResult {
|
|
unrealized_pnl: pnl,
|
|
liquidation_price: liq_price,
|
|
margin_ratio_bps: margin_ratio,
|
|
is_liquidatable: is_liquidatable(&position, price.mark_price),
|
|
};
|
|
|
|
Ok(borsh::to_vec(&result).unwrap())
|
|
}
|
|
|
|
_ => Err(Error::InvalidMethod),
|
|
}
|
|
}
|