//! 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
{
storage::get::(keys::OWNER)
}
fn is_owner(addr: &Address) -> bool {
get_owner().map(|o| o == *addr).unwrap_or(false)
}
fn is_initialized() -> bool {
storage::get::(keys::INITIALIZED).unwrap_or(false)
}
fn is_paused() -> bool {
storage::get::(keys::PAUSED).unwrap_or(false)
}
fn get_market_count() -> u32 {
storage::get::(keys::MARKET_COUNT).unwrap_or(0)
}
fn set_market_count(count: u32) {
storage::set(keys::MARKET_COUNT, &count);
}
fn get_market(id: u32) -> Option {
storage::get_with_suffix::(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::(keys::POSITION_COUNT).unwrap_or(0)
}
fn set_position_count(count: u64) {
storage::set(keys::POSITION_COUNT, &count);
}
fn get_position(id: u64) -> Option {
storage::get_with_suffix::(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 {
storage::get_with_suffix::(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 {
storage::get_with_suffix::(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::(keys::INSURANCE_FUND).unwrap_or(0)
}
fn set_insurance_fund(amount: u64) {
storage::set(keys::INSURANCE_FUND, &amount);
}
fn get_oracle() -> Option {
storage::get::(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> {
// 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),
}
}