diff --git a/contracts/waterx_perp/ARCHITECTURE.md b/contracts/waterx_perp/ARCHITECTURE.md index 9bf82dd..b33f200 100644 --- a/contracts/waterx_perp/ARCHITECTURE.md +++ b/contracts/waterx_perp/ARCHITECTURE.md @@ -142,7 +142,7 @@ Position has key, store { - Borrow fee: continuous, `collateral_amount * rate_delta / 1e9` (u64 sufficient) - Funding rate: periodic (1h intervals), OI imbalance, Double precision (u256/1e18) - Protocol fee share: configurable % split to protocol address -- Liquidator fee + insurance fee on liquidation +- Liquidation: insurance fee = configurable bps of liquidation notional; phase 1 keeper fee defaults to 0; remaining collateral returns to WLP ### 10. Funding Rate - `funding_rate = basic_rate * (long_oi - short_oi) / total_oi` diff --git a/contracts/waterx_perp/sources/global_config.move b/contracts/waterx_perp/sources/global_config.move index 322ef93..b440ce1 100644 --- a/contracts/waterx_perp/sources/global_config.move +++ b/contracts/waterx_perp/sources/global_config.move @@ -30,9 +30,9 @@ public struct GlobalConfig has key { protocol_fee_share_bps: u64, /// Insurance fund recipient. insurance_address: address, - /// Liquidator reward fee in bps. + /// Optional keeper reward fee in bps of position notional. liquidator_fee_bps: u64, - /// Insurance fund fee in bps (of remaining collateral after liquidator fee). + /// Insurance fund fee in bps of position notional. insurance_fee_bps: u64, /// Maximum orders per price level. max_orders_per_price: u64, @@ -57,7 +57,7 @@ fun init(ctx: &mut TxContext) { fee_address: ctx.sender(), protocol_fee_share_bps: 3000, // 30% default insurance_address: ctx.sender(), - liquidator_fee_bps: 100, + liquidator_fee_bps: 0, insurance_fee_bps: 100, max_orders_per_price: 100, oi_cap_bps: 0, // no cap by default diff --git a/contracts/waterx_perp/sources/market_config.move b/contracts/waterx_perp/sources/market_config.move index 2d03a1c..c5802af 100644 --- a/contracts/waterx_perp/sources/market_config.move +++ b/contracts/waterx_perp/sources/market_config.move @@ -14,6 +14,15 @@ use waterx_perp::error; use waterx_perp::math; use waterx_perp::events; +// === Constants === + +/// Typus-compatible default share of LP TVL allocated to net exposure before impact hits max. +const DEFAULT_ALLOCATED_LP_EXPOSURE_BPS: u64 = 2_000; // 20% +/// Typus-compatible default curvature for the impact fee curve. +const DEFAULT_IMPACT_FEE_CURVATURE: u64 = 2; +/// Typus-compatible default scale divisor applied before curvature. +const DEFAULT_IMPACT_FEE_SCALE: u64 = 1; + // === Structs === /// Configuration and state for a single trading symbol. @@ -32,6 +41,14 @@ public struct MarketConfig has key, store { lot_size: u64, /// Base trading fee in bps. trading_fee_bps: u64, + /// Maximum additional impact fee in bps. + max_impact_fee_bps: u64, + /// Share of LP TVL allocated to net exposure before impact hits max, in bps. + allocated_lp_exposure_bps: u64, + /// Curvature exponent for the impact fee curve. + impact_fee_curvature: u64, + /// Scale divisor applied to the exposure ratio before curvature. + impact_fee_scale: u64, /// Size decimal (matches base token). size_decimal: u8, /// Maintenance margin rate in bps (default 150). @@ -94,6 +111,16 @@ public(package) fun new_market_config( ): MarketConfig { let now = clock.timestamp_ms(); let last_funding = now / funding_interval_ms * funding_interval_ms; + let max_impact_fee_bps = trading_fee_bps; + let allocated_lp_exposure_bps = DEFAULT_ALLOCATED_LP_EXPOSURE_BPS; + let impact_fee_curvature = DEFAULT_IMPACT_FEE_CURVATURE; + let impact_fee_scale = DEFAULT_IMPACT_FEE_SCALE; + assert_valid_impact_fee_config( + max_impact_fee_bps, + allocated_lp_exposure_bps, + impact_fee_curvature, + impact_fee_scale, + ); MarketConfig { id: object::new(ctx), @@ -102,6 +129,10 @@ public(package) fun new_market_config( min_size, lot_size, trading_fee_bps, + max_impact_fee_bps, + allocated_lp_exposure_bps, + impact_fee_curvature, + impact_fee_scale, size_decimal, maintenance_margin_bps, max_long_oi, @@ -130,6 +161,10 @@ public fun update_market_config( min_size: Option, lot_size: Option, trading_fee_bps: Option, + max_impact_fee_bps: Option, + allocated_lp_exposure_bps: Option, + impact_fee_curvature: Option, + impact_fee_scale: Option, maintenance_margin_bps: Option, max_long_oi: Option, max_short_oi: Option, @@ -141,6 +176,16 @@ public fun update_market_config( if (min_size.is_some()) { market_config.min_size = min_size.destroy_some(); }; if (lot_size.is_some()) { market_config.lot_size = lot_size.destroy_some(); }; if (trading_fee_bps.is_some()) { market_config.trading_fee_bps = trading_fee_bps.destroy_some(); }; + if (max_impact_fee_bps.is_some()) { market_config.max_impact_fee_bps = max_impact_fee_bps.destroy_some(); }; + if (allocated_lp_exposure_bps.is_some()) { market_config.allocated_lp_exposure_bps = allocated_lp_exposure_bps.destroy_some(); }; + if (impact_fee_curvature.is_some()) { market_config.impact_fee_curvature = impact_fee_curvature.destroy_some(); }; + if (impact_fee_scale.is_some()) { market_config.impact_fee_scale = impact_fee_scale.destroy_some(); }; + assert_valid_impact_fee_config( + market_config.max_impact_fee_bps, + market_config.allocated_lp_exposure_bps, + market_config.impact_fee_curvature, + market_config.impact_fee_scale, + ); if (maintenance_margin_bps.is_some()) { market_config.maintenance_margin_bps = maintenance_margin_bps.destroy_some(); }; if (max_long_oi.is_some()) { market_config.max_long_oi = max_long_oi.destroy_some(); }; if (max_short_oi.is_some()) { market_config.max_short_oi = max_short_oi.destroy_some(); }; @@ -248,6 +293,18 @@ public fun market_config_lot_size(m: &MarketConfig): u64 public use fun market_config_trading_fee_bps as MarketConfig.trading_fee_bps; public fun market_config_trading_fee_bps(m: &MarketConfig): u64 { m.trading_fee_bps } +public use fun market_config_max_impact_fee_bps as MarketConfig.max_impact_fee_bps; +public fun market_config_max_impact_fee_bps(m: &MarketConfig): u64 { m.max_impact_fee_bps } + +public use fun market_config_allocated_lp_exposure_bps as MarketConfig.allocated_lp_exposure_bps; +public fun market_config_allocated_lp_exposure_bps(m: &MarketConfig): u64 { m.allocated_lp_exposure_bps } + +public use fun market_config_impact_fee_curvature as MarketConfig.impact_fee_curvature; +public fun market_config_impact_fee_curvature(m: &MarketConfig): u64 { m.impact_fee_curvature } + +public use fun market_config_impact_fee_scale as MarketConfig.impact_fee_scale; +public fun market_config_impact_fee_scale(m: &MarketConfig): u64 { m.impact_fee_scale } + public use fun market_config_size_decimal as MarketConfig.size_decimal; public fun market_config_size_decimal(m: &MarketConfig): u8 { m.size_decimal } @@ -396,6 +453,20 @@ public fun calculate_funding_rate( } } +fun assert_valid_impact_fee_config( + max_impact_fee_bps: u64, + allocated_lp_exposure_bps: u64, + impact_fee_curvature: u64, + impact_fee_scale: u64, +) { + if ( + max_impact_fee_bps > math::bp_scale() + || allocated_lp_exposure_bps > math::bp_scale() + || impact_fee_curvature == 0 + || impact_fee_scale == 0 + ) error::err_invalid_config(); +} + // === Tests === #[test] diff --git a/contracts/waterx_perp/sources/position.move b/contracts/waterx_perp/sources/position.move index 1879a3a..5b3d94b 100644 --- a/contracts/waterx_perp/sources/position.move +++ b/contracts/waterx_perp/sources/position.move @@ -416,7 +416,7 @@ public fun is_liquidatable( position: &Position, current_price: Float, collateral_price: Float, - trading_fee_bps: u64, + closing_fee: Float, maintenance_margin_bps: u64, cumulative_borrow_rate: u64, cumulative_funding_sign: bool, @@ -441,7 +441,6 @@ public fun is_liquidatable( let borrow_fee_delta = calculate_borrow_fee(position, cumulative_borrow_rate); let total_borrow_fee = position.unrealized_borrow_fee + borrow_fee_delta; let total_trading_fee = position.unrealized_trading_fee; - let closing_fee = math::fee_from_bps(notional, trading_fee_bps); let total_fees_usd = math::amount_to_usd( total_borrow_fee + total_trading_fee, position.collateral_decimal, collateral_price, diff --git a/contracts/waterx_perp/sources/trading.move b/contracts/waterx_perp/sources/trading.move index 081ac3a..377808e 100644 --- a/contracts/waterx_perp/sources/trading.move +++ b/contracts/waterx_perp/sources/trading.move @@ -8,31 +8,27 @@ /// 4. trading::destroy_response(global_config, market, response); module waterx_perp::trading; +use bucket_v2_framework::account::AccountRequest; +use bucket_v2_framework::float::{Self, Float}; +use bucket_v2_oracle::result::PriceResult; use std::type_name::with_defining_ids; - use sui::balance::{Self, Balance}; use sui::clock::Clock; use sui::coin::{Self, Coin}; -use waterx_perp::keyed_big_vector::{Self, KeyedBigVector}; +use sui::transfer::Receiving; use sui::vec_map::{Self, VecMap}; - -use bucket_v2_framework::float::{Self, Float}; -use bucket_v2_framework::account::{AccountRequest}; -use bucket_v2_oracle::result::PriceResult; - +use waterx_perp::admin::AdminCap; use waterx_perp::error; -use waterx_perp::math; use waterx_perp::events; -use waterx_perp::admin::AdminCap; use waterx_perp::global_config::GlobalConfig; +use waterx_perp::keyed_big_vector::{Self, KeyedBigVector}; +use waterx_perp::lp_pool::WlpPool; use waterx_perp::market_config::{Self, MarketConfig}; +use waterx_perp::math; use waterx_perp::position::{Self, Position, Order}; -use waterx_perp::lp_pool::WlpPool; -use sui::transfer::Receiving; - -use waterx_perp::user_account::{Self, AccountRegistry}; use waterx_perp::request::{Self, TradingRequest}; use waterx_perp::response::{Self, TradingResponse}; +use waterx_perp::user_account::{Self, AccountRegistry}; // === Action Constants === @@ -51,65 +47,65 @@ const ACTION_DECREASE_POSITION: u8 = 8; /// Shared trading vault per market, storing positions and orders. /// Embeds MarketConfig for per-market configuration and state. public struct Market has key, store { - id: UID, - /// Per-market configuration and state. - config: MarketConfig, - /// Positions indexed by position_id. - positions: KeyedBigVector, - /// Limit buy orders: price_key → vector - limit_buys: VecMap>, - /// Limit sell orders: price_key → vector - limit_sells: VecMap>, - /// Stop buy orders: price_key → vector - stop_buys: VecMap>, - /// Stop sell orders: price_key → vector - stop_sells: VecMap>, + id: UID, + /// Per-market configuration and state. + config: MarketConfig, + /// Positions indexed by position_id. + positions: KeyedBigVector, + /// Limit buy orders: price_key → vector + limit_buys: VecMap>, + /// Limit sell orders: price_key → vector + limit_sells: VecMap>, + /// Stop buy orders: price_key → vector + stop_buys: VecMap>, + /// Stop sell orders: price_key → vector + stop_sells: VecMap>, } // === Init === /// Creates a trading market with full configuration. public fun create_market( - _cap: &AdminCap, - max_leverage_bps: u64, - min_size: u64, - lot_size: u64, - trading_fee_bps: u64, - size_decimal: u8, - maintenance_margin_bps: u64, - max_long_oi: u64, - max_short_oi: u64, - cooldown_ms: u64, - basic_funding_rate_bps: u64, - funding_interval_ms: u64, - clock: &Clock, - ctx: &mut TxContext, + _cap: &AdminCap, + max_leverage_bps: u64, + min_size: u64, + lot_size: u64, + trading_fee_bps: u64, + size_decimal: u8, + maintenance_margin_bps: u64, + max_long_oi: u64, + max_short_oi: u64, + cooldown_ms: u64, + basic_funding_rate_bps: u64, + funding_interval_ms: u64, + clock: &Clock, + ctx: &mut TxContext, ): Market { - let config = market_config::new_market_config( - max_leverage_bps, - min_size, - lot_size, - trading_fee_bps, - size_decimal, - maintenance_margin_bps, - max_long_oi, - max_short_oi, - cooldown_ms, - basic_funding_rate_bps, - funding_interval_ms, - clock, - ctx, - ); - - Market { - id: object::new(ctx), - config, - positions: keyed_big_vector::new(256, ctx), - limit_buys: vec_map::empty(), - limit_sells: vec_map::empty(), - stop_buys: vec_map::empty(), - stop_sells: vec_map::empty(), - } + let config = market_config::new_market_config( + max_leverage_bps, + min_size, + lot_size, + trading_fee_bps, + size_decimal, + maintenance_margin_bps, + max_long_oi, + max_short_oi, + cooldown_ms, + basic_funding_rate_bps, + funding_interval_ms, + clock, + ctx, + ); + + Market { + id: object::new(ctx), + config, + positions: keyed_big_vector::new(256, ctx), + limit_buys: vec_map::empty(), + limit_sells: vec_map::empty(), + stop_buys: vec_map::empty(), + stop_sells: vec_map::empty(), + } } // ================================================================ @@ -118,268 +114,361 @@ public fun create_market( /// Creates a request to open a new position. public fun open_position_request( - global_config: &GlobalConfig, - account_registry: &mut AccountRegistry, - market: &Market, - sender_request: &AccountRequest, - account_object_address: address, - receivings: vector>>, - collateral_amount: u64, - is_long: bool, - size: u64, - acceptable_price: u64, - ctx: &mut TxContext, + global_config: &GlobalConfig, + account_registry: &mut AccountRegistry, + market: &Market, + sender_request: &AccountRequest, + account_object_address: address, + receivings: vector>>, + collateral_amount: u64, + is_long: bool, + size: u64, + acceptable_price: u64, + ctx: &mut TxContext, ): TradingRequest { - global_config.assert_version(); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_open_position())) error::err_unauthorized(); - let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( - account_id, receivings, ctx, - ); - if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); - let collateral = total_coin.split(collateral_amount, ctx); - if (total_coin.value() > 0) { - transfer::public_transfer(total_coin, account_obj_addr); - } else { - total_coin.destroy_zero(); - }; - request::new( - market_id, account_object_address, ACTION_OPEN_POSITION, sender, - is_long, size, collateral.into_balance(), - option::none(), option::none(), false, false, option::none(), 0, - acceptable_price, - ) + global_config.assert_version(); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_open_position())) + error::err_unauthorized(); + let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( + account_id, + receivings, + ctx, + ); + if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); + let collateral = total_coin.split(collateral_amount, ctx); + if (total_coin.value() > 0) { + transfer::public_transfer(total_coin, account_obj_addr); + } else { + total_coin.destroy_zero(); + }; + request::new( + market_id, + account_object_address, + ACTION_OPEN_POSITION, + sender, + is_long, + size, + collateral.into_balance(), + option::none(), + option::none(), + false, + false, + option::none(), + 0, + acceptable_price, + ) } /// Creates a request to close a position. public fun close_position_request( - global_config: &GlobalConfig, - account_registry: &AccountRegistry, - market: &mut Market, - sender_request: &AccountRequest, - account_object_address: address, - position_id: u64, - acceptable_price: u64, + global_config: &GlobalConfig, + account_registry: &AccountRegistry, + market: &mut Market, + sender_request: &AccountRequest, + account_object_address: address, + position_id: u64, + acceptable_price: u64, ): TradingRequest { - global_config.assert_version(); - market.config.lock_position(position_id); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_close_position())) error::err_unauthorized(); - request::new_no_collateral( - market_id, account_object_address, ACTION_CLOSE_POSITION, sender, - option::some(position_id), 0, acceptable_price, - ) + global_config.assert_version(); + market.config.lock_position(position_id); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_close_position())) + error::err_unauthorized(); + request::new_no_collateral( + market_id, + account_object_address, + ACTION_CLOSE_POSITION, + sender, + option::some(position_id), + 0, + acceptable_price, + ) } /// Creates a request to increase an existing position at market. public fun increase_position_request( - global_config: &GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - sender_request: &AccountRequest, - account_object_address: address, - position_id: u64, - receivings: vector>>, - collateral_amount: u64, - size: u64, - acceptable_price: u64, - ctx: &mut TxContext, + global_config: &GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + sender_request: &AccountRequest, + account_object_address: address, + position_id: u64, + receivings: vector>>, + collateral_amount: u64, + size: u64, + acceptable_price: u64, + ctx: &mut TxContext, ): TradingRequest { - global_config.assert_version(); - market.config.lock_position(position_id); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_open_position())) error::err_unauthorized(); - let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( -account_id, receivings, ctx, - ); - if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); - let collateral = total_coin.split(collateral_amount, ctx); - if (total_coin.value() > 0) { - transfer::public_transfer(total_coin, account_obj_addr); - } else { - total_coin.destroy_zero(); - }; - request::new( - market_id, account_object_address, ACTION_INCREASE_POSITION, sender, - false, size, collateral.into_balance(), - option::some(position_id), option::none(), false, false, option::none(), 0, - acceptable_price, - ) + global_config.assert_version(); + market.config.lock_position(position_id); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_open_position())) + error::err_unauthorized(); + let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( + account_id, + receivings, + ctx, + ); + if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); + let collateral = total_coin.split(collateral_amount, ctx); + if (total_coin.value() > 0) { + transfer::public_transfer(total_coin, account_obj_addr); + } else { + total_coin.destroy_zero(); + }; + request::new( + market_id, + account_object_address, + ACTION_INCREASE_POSITION, + sender, + false, + size, + collateral.into_balance(), + option::some(position_id), + option::none(), + false, + false, + option::none(), + 0, + acceptable_price, + ) } /// Creates a request to partially reduce an existing position at market. public fun decrease_position_request( - global_config: &GlobalConfig, - account_registry: &AccountRegistry, - market: &mut Market, - sender_request: &AccountRequest, - account_object_address: address, - position_id: u64, - size: u64, - acceptable_price: u64, + global_config: &GlobalConfig, + account_registry: &AccountRegistry, + market: &mut Market, + sender_request: &AccountRequest, + account_object_address: address, + position_id: u64, + size: u64, + acceptable_price: u64, ): TradingRequest { - global_config.assert_version(); - market.config.lock_position(position_id); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_close_position())) error::err_unauthorized(); - request::new( - market_id, account_object_address, ACTION_DECREASE_POSITION, sender, - false, size, balance::zero(), - option::some(position_id), option::none(), false, false, option::none(), 0, - acceptable_price, - ) + global_config.assert_version(); + market.config.lock_position(position_id); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_close_position())) + error::err_unauthorized(); + request::new( + market_id, + account_object_address, + ACTION_DECREASE_POSITION, + sender, + false, + size, + balance::zero(), + option::some(position_id), + option::none(), + false, + false, + option::none(), + 0, + acceptable_price, + ) } /// Creates a request to place a limit or stop order. public fun place_order_request( - global_config: &GlobalConfig, - account_registry: &mut AccountRegistry, - market: &Market, - sender_request: &AccountRequest, - account_object_address: address, - receivings: vector>>, - collateral_amount: u64, - is_long: bool, - is_stop_order: bool, - reduce_only: bool, - size: u64, - trigger_price: Float, - linked_position_id: Option, - ctx: &mut TxContext, + global_config: &GlobalConfig, + account_registry: &mut AccountRegistry, + market: &Market, + sender_request: &AccountRequest, + account_object_address: address, + receivings: vector>>, + collateral_amount: u64, + is_long: bool, + is_stop_order: bool, + reduce_only: bool, + size: u64, + trigger_price: Float, + linked_position_id: Option, + ctx: &mut TxContext, ): TradingRequest { - global_config.assert_version(); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_place_order())) error::err_unauthorized(); - let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( -account_id, receivings, ctx, - ); - if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); - let collateral = total_coin.split(collateral_amount, ctx); - if (total_coin.value() > 0) { - transfer::public_transfer(total_coin, account_obj_addr); - } else { - total_coin.destroy_zero(); - }; - request::new( - market_id, account_object_address, ACTION_PLACE_ORDER, sender, - is_long, size, collateral.into_balance(), - option::none(), option::some(trigger_price), - reduce_only, is_stop_order, linked_position_id, 0, 0, - ) + global_config.assert_version(); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_place_order())) + error::err_unauthorized(); + let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( + account_id, + receivings, + ctx, + ); + if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); + let collateral = total_coin.split(collateral_amount, ctx); + if (total_coin.value() > 0) { + transfer::public_transfer(total_coin, account_obj_addr); + } else { + total_coin.destroy_zero(); + }; + request::new( + market_id, + account_object_address, + ACTION_PLACE_ORDER, + sender, + is_long, + size, + collateral.into_balance(), + option::none(), + option::some(trigger_price), + reduce_only, + is_stop_order, + linked_position_id, + 0, + 0, + ) } /// Creates a request to cancel an order. public fun cancel_order_request( - global_config: &GlobalConfig, - account_registry: &AccountRegistry, - market: &Market, - sender_request: &AccountRequest, - account_object_address: address, - order_id: u64, - trigger_price_key: u64, - order_type_tag: u8, + global_config: &GlobalConfig, + account_registry: &AccountRegistry, + market: &Market, + sender_request: &AccountRequest, + account_object_address: address, + order_id: u64, + trigger_price_key: u64, + order_type_tag: u8, ): TradingRequest { - global_config.assert_version(); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_cancel_order())) error::err_unauthorized(); - let is_long = (order_type_tag == 0 || order_type_tag == 2); - let is_stop = (order_type_tag == 2 || order_type_tag == 3); - request::new( - market_id, account_object_address, ACTION_CANCEL_ORDER, sender, - is_long, 0, sui::balance::zero(), - option::some(order_id), option::none(), - false, is_stop, option::none(), trigger_price_key, 0, - ) + global_config.assert_version(); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_cancel_order())) + error::err_unauthorized(); + let is_long = (order_type_tag == 0 || order_type_tag == 2); + let is_stop = (order_type_tag == 2 || order_type_tag == 3); + request::new( + market_id, + account_object_address, + ACTION_CANCEL_ORDER, + sender, + is_long, + 0, + sui::balance::zero(), + option::some(order_id), + option::none(), + false, + is_stop, + option::none(), + trigger_price_key, + 0, + ) } /// Creates a request to deposit collateral on a position. public fun deposit_collateral_request( - global_config: &GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - sender_request: &AccountRequest, - account_object_address: address, - position_id: u64, - receivings: vector>>, - collateral_amount: u64, - ctx: &mut TxContext, + global_config: &GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + sender_request: &AccountRequest, + account_object_address: address, + position_id: u64, + receivings: vector>>, + collateral_amount: u64, + ctx: &mut TxContext, ): TradingRequest { - global_config.assert_version(); - market.config.lock_position(position_id); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_deposit_collateral())) error::err_unauthorized(); - let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( -account_id, receivings, ctx, - ); - if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); - let collateral = total_coin.split(collateral_amount, ctx); - if (total_coin.value() > 0) { - transfer::public_transfer(total_coin, account_obj_addr); - } else { - total_coin.destroy_zero(); - }; - request::new( - market_id, account_object_address, ACTION_DEPOSIT_COLLATERAL, sender, - false, 0, collateral.into_balance(), - option::some(position_id), option::none(), false, false, option::none(), 0, 0, - ) + global_config.assert_version(); + market.config.lock_position(position_id); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if (!account_registry.has_permission(account_id, sender, user_account::perm_deposit_collateral())) + error::err_unauthorized(); + let (mut total_coin, account_obj_addr) = account_registry.receive_and_merge_internal( + account_id, + receivings, + ctx, + ); + if (total_coin.value() < collateral_amount) error::err_insufficient_collateral(); + let collateral = total_coin.split(collateral_amount, ctx); + if (total_coin.value() > 0) { + transfer::public_transfer(total_coin, account_obj_addr); + } else { + total_coin.destroy_zero(); + }; + request::new( + market_id, + account_object_address, + ACTION_DEPOSIT_COLLATERAL, + sender, + false, + 0, + collateral.into_balance(), + option::some(position_id), + option::none(), + false, + false, + option::none(), + 0, + 0, + ) } /// Creates a request to withdraw collateral from a position. public fun withdraw_collateral_request( - global_config: &GlobalConfig, - account_registry: &AccountRegistry, - market: &mut Market, - sender_request: &AccountRequest, - account_object_address: address, - position_id: u64, - amount: u64, + global_config: &GlobalConfig, + account_registry: &AccountRegistry, + market: &mut Market, + sender_request: &AccountRequest, + account_object_address: address, + position_id: u64, + amount: u64, ): TradingRequest { - global_config.assert_version(); - market.config.lock_position(position_id); - let market_id = object::id(market); - let account_id = account_object_address.to_id(); - let sender = sender_request.address(); - if (!account_registry.has_permission(account_id, sender, user_account::perm_withdraw_collateral())) error::err_unauthorized(); - request::new_no_collateral( - market_id, account_object_address, ACTION_WITHDRAW_COLLATERAL, sender, - option::some(position_id), amount, 0, - ) + global_config.assert_version(); + market.config.lock_position(position_id); + let market_id = object::id(market); + let account_id = account_object_address.to_id(); + let sender = sender_request.address(); + if ( + !account_registry.has_permission(account_id, sender, user_account::perm_withdraw_collateral()) + ) error::err_unauthorized(); + request::new_no_collateral( + market_id, + account_object_address, + ACTION_WITHDRAW_COLLATERAL, + sender, + option::some(position_id), + amount, + 0, + ) } /// Creates a request to liquidate a position (keeper only). /// Reads account_object_address from the position directly. public fun liquidate_request( - global_config: &GlobalConfig, - market: &mut Market, - sender_request: &AccountRequest, - position_id: u64, + global_config: &GlobalConfig, + market: &mut Market, + sender_request: &AccountRequest, + position_id: u64, ): TradingRequest { - let sender = sender_request.address(); - global_config.verify_keeper(sender); - market.config.lock_position(position_id); - let market_id = object::id(market); - if (!market.positions.contains(position_id)) error::err_position_not_found(); - let pos_ref: &Position = &market.positions[position_id]; - let account_object_address = pos_ref.account_object_address(); - request::new_no_collateral( - market_id, account_object_address, ACTION_LIQUIDATE, sender, - option::some(position_id), 0, 0, - ) + let sender = sender_request.address(); + global_config.verify_keeper(sender); + market.config.lock_position(position_id); + let market_id = object::id(market); + if (!market.positions.contains(position_id)) error::err_position_not_found(); + let pos_ref: &Position = &market.positions[position_id]; + let account_object_address = pos_ref.account_object_address(); + request::new_no_collateral( + market_id, + account_object_address, + ACTION_LIQUIDATE, + sender, + option::some(position_id), + 0, + 0, + ) } // ================================================================ @@ -388,140 +477,256 @@ public fun liquidate_request( /// Executes a trading request. public fun execute( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - req: TradingRequest, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + req: TradingRequest, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let market_id = object::id(market); - - let ( - req_market_id, account_object_address, action, sender, - is_long, size, collateral, position_id_opt, - trigger_price_opt, reduce_only, is_stop_order, - linked_position_id, withdraw_amount, acceptable_price, witnesses, - ) = req.destroy(); - - if (req_market_id != market_id) error::err_invalid_config(); - - // Validate request_checklist - let checklist = market.config.request_checklist(); - if (!checklist.all!(|rule| witnesses.contains(rule))) error::err_missing_request_witness(); - - let account_id = account_object_address.to_id(); - if (!account_registry.has_account(account_id)) error::err_sub_account_not_found(); - - // Dispatch - if (action == ACTION_OPEN_POSITION) { - execute_open_position( - global_config, account_registry, market, pool, - price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, - collateral, is_long, size, acceptable_price, - clock, ctx, - ) - } else if (action == ACTION_INCREASE_POSITION) { - let position_id = *position_id_opt.borrow(); - execute_increase_position( - global_config, market, pool, - price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, position_id, - collateral, size, acceptable_price, - clock, ctx, - ) - } else if (action == ACTION_CLOSE_POSITION) { - let position_id = *position_id_opt.borrow(); - collateral.destroy_zero(); - execute_close_position( - global_config, account_registry, market, pool, - price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, position_id, - acceptable_price, clock, ctx, - ) - } else if (action == ACTION_PLACE_ORDER) { - let trigger_price = *trigger_price_opt.borrow(); - execute_place_order( - global_config, market, pool, price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, collateral, - is_long, is_stop_order, reduce_only, size, - trigger_price, linked_position_id, - clock, ctx, - ) - } else if (action == ACTION_CANCEL_ORDER) { - let order_id = *position_id_opt.borrow(); - let trigger_price_key = withdraw_amount; - let order_type_tag = if (is_long) { - if (is_stop_order) { 2u8 } else { 0u8 } - } else { - if (is_stop_order) { 3u8 } else { 1u8 } - }; - collateral.destroy_zero(); - execute_cancel_order( - global_config, market, market_id, account_id, account_object_address, sender, - order_id, trigger_price_key, order_type_tag, clock, ctx, - ) - } else if (action == ACTION_DEPOSIT_COLLATERAL) { - let position_id = *position_id_opt.borrow(); - execute_deposit_collateral( - global_config, market, pool, price_result, - market_id, account_id, account_object_address, sender, position_id, collateral, - clock, ctx, - ) - } else if (action == ACTION_WITHDRAW_COLLATERAL) { - let position_id = *position_id_opt.borrow(); - collateral.destroy_zero(); - execute_withdraw_collateral( - global_config, market, pool, price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, position_id, withdraw_amount, - clock, ctx, - ) - } else if (action == ACTION_DECREASE_POSITION) { - let position_id = *position_id_opt.borrow(); - collateral.destroy_zero(); - execute_decrease_position( - global_config, account_registry, market, pool, - price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, position_id, size, - acceptable_price, ACTION_DECREASE_POSITION, clock, ctx, - ) - } else if (action == ACTION_LIQUIDATE) { - let position_id = *position_id_opt.borrow(); - collateral.destroy_zero(); - execute_liquidate( - global_config, account_registry, market, pool, price_result, collateral_price_result, - market_id, account_id, account_object_address, sender, position_id, - clock, ctx, - ) + let market_id = object::id(market); + + let ( + req_market_id, + account_object_address, + action, + sender, + is_long, + size, + collateral, + position_id_opt, + trigger_price_opt, + reduce_only, + is_stop_order, + linked_position_id, + withdraw_amount, + acceptable_price, + witnesses, + ) = req.destroy(); + + if (req_market_id != market_id) error::err_invalid_config(); + + // Validate request_checklist + let checklist = market.config.request_checklist(); + if (!checklist.all!(|rule| witnesses.contains(rule))) error::err_missing_request_witness(); + + let account_id = account_object_address.to_id(); + if (!account_registry.has_account(account_id)) error::err_sub_account_not_found(); + + // Dispatch + if (action == ACTION_OPEN_POSITION) { + execute_open_position( + global_config, + account_registry, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + collateral, + is_long, + size, + acceptable_price, + clock, + ctx, + ) + } else if (action == ACTION_INCREASE_POSITION) { + let position_id = *position_id_opt.borrow(); + execute_increase_position( + global_config, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + collateral, + size, + acceptable_price, + clock, + ctx, + ) + } else if (action == ACTION_CLOSE_POSITION) { + let position_id = *position_id_opt.borrow(); + collateral.destroy_zero(); + execute_close_position( + global_config, + account_registry, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + acceptable_price, + clock, + ctx, + ) + } else if (action == ACTION_PLACE_ORDER) { + let trigger_price = *trigger_price_opt.borrow(); + execute_place_order( + global_config, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + collateral, + is_long, + is_stop_order, + reduce_only, + size, + trigger_price, + linked_position_id, + clock, + ctx, + ) + } else if (action == ACTION_CANCEL_ORDER) { + let order_id = *position_id_opt.borrow(); + let trigger_price_key = withdraw_amount; + let order_type_tag = if (is_long) { + if (is_stop_order) { 2u8 } else { 0u8 } } else { - collateral.destroy_zero(); - abort error::invalid_action_code() - } + if (is_stop_order) { 3u8 } else { 1u8 } + }; + collateral.destroy_zero(); + execute_cancel_order( + global_config, + market, + market_id, + account_id, + account_object_address, + sender, + order_id, + trigger_price_key, + order_type_tag, + clock, + ctx, + ) + } else if (action == ACTION_DEPOSIT_COLLATERAL) { + let position_id = *position_id_opt.borrow(); + execute_deposit_collateral( + global_config, + market, + pool, + price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + collateral, + clock, + ctx, + ) + } else if (action == ACTION_WITHDRAW_COLLATERAL) { + let position_id = *position_id_opt.borrow(); + collateral.destroy_zero(); + execute_withdraw_collateral( + global_config, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + withdraw_amount, + clock, + ctx, + ) + } else if (action == ACTION_DECREASE_POSITION) { + let position_id = *position_id_opt.borrow(); + collateral.destroy_zero(); + execute_decrease_position( + global_config, + account_registry, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + size, + acceptable_price, + ACTION_DECREASE_POSITION, + clock, + ctx, + ) + } else if (action == ACTION_LIQUIDATE) { + let position_id = *position_id_opt.borrow(); + collateral.destroy_zero(); + execute_liquidate( + global_config, + account_registry, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_object_address, + sender, + position_id, + clock, + ctx, + ) + } else { + collateral.destroy_zero(); + abort error::invalid_action_code() + } } /// Destroys a TradingResponse. public fun destroy_response( - _global_config: &GlobalConfig, - market: &mut Market, - response: TradingResponse, + _global_config: &GlobalConfig, + market: &mut Market, + response: TradingResponse, ) { - let (market_id, _account_id, _action, _sender, position_id, - _pnl_amount, _pnl_is_profit, _fee_amount, - _is_long, _size_amount, _collateral_amount, _execution_price, - witnesses) = response.destroy(); - - if (market_id != object::id(market)) error::err_invalid_config(); - - // Validate response_checklist - let checklist = market.config.response_checklist(); - if (!checklist.all!(|rule| witnesses.contains(rule))) error::err_missing_response_witness(); - - // Unlock position - position_id.do!(|pid| market.config.unlock_position(pid)); + let ( + market_id, + _account_id, + _action, + _sender, + position_id, + _pnl_amount, + _pnl_is_profit, + _fee_amount, + _is_long, + _size_amount, + _collateral_amount, + _execution_price, + witnesses, + ) = response.destroy(); + + if (market_id != object::id(market)) error::err_invalid_config(); + + // Validate response_checklist + let checklist = market.config.response_checklist(); + if (!checklist.all!(|rule| witnesses.contains(rule))) error::err_missing_response_witness(); + + // Unlock position + position_id.do!(|pid| market.config.unlock_position(pid)); } // ================================================================ @@ -529,817 +734,1138 @@ public fun destroy_response( // ================================================================ fun execute_open_position( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_object_address: address, - sender: address, - collateral: Balance, - is_long: bool, - size: u64, - acceptable_price: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_object_address: address, + sender: address, + collateral: Balance, + is_long: bool, + size: u64, + acceptable_price: u64, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let m = &mut market.config; - if (!m.is_active()) error::err_market_not_active(); + let m = &mut market.config; + if (!m.is_active()) error::err_market_not_active(); - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - - if (acceptable_price > 0) { - let price_u64 = price.floor(); - if (is_long) { - if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); - } else { - if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); - }; - }; - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let collateral_amount = collateral.value(); - if (collateral_amount == 0) error::err_insufficient_collateral(); - let collateral_token = with_defining_ids(); - let collateral_decimal = pool.token_decimal(collateral_token); - let now = clock.timestamp_ms(); - - if (size < m.min_size()) error::err_invalid_size(); - if (size % m.lot_size() != 0) error::err_invalid_size(); + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + if (acceptable_price > 0) { + let price_u64 = price.floor(); if (is_long) { - if (m.long_oi() + size > m.max_long_oi()) error::err_exceed_max_open_interest(); + if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); } else { - if (m.short_oi() + size > m.max_short_oi()) error::err_exceed_max_open_interest(); + if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); }; - - let notional = math::amount_to_usd(size, m.size_decimal(), price); - let collateral_usd = math::amount_to_usd(collateral_amount, collateral_decimal, collateral_price); - let leverage_bps = if (collateral_usd.gt(float::zero())) { - notional.div(collateral_usd).mul_u64(math::bp_scale()).floor() - } else { 0 }; - if (leverage_bps > m.max_leverage_bps()) error::err_exceed_max_leverage(); - - let trading_fee_usd = math::fee_from_bps(notional, m.trading_fee_bps()); - let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); - - pool.update_borrow_rates(clock); - m.update_funding_rate(market_id, now); - - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let funding_sign = m.cumulative_funding_sign(); - let funding_index = m.cumulative_funding_index(); - - let reserve_amount = math::usd_to_amount(notional, collateral_decimal, collateral_price); - let total_oi_after = math::amount_to_usd( - m.long_oi() + m.short_oi() + size, - m.size_decimal(), price, - ); - pool.check_oi_cap(global_config, total_oi_after); - if (!pool.check_reserve_valid(collateral_token, reserve_amount)) error::err_exceed_reserve_ratio(); - pool.increase_reserve(collateral_token, reserve_amount); - - let position_id = m.increment_next_position_id(); - let pos = position::create_position( - position_id, account_object_address, market_id, - is_long, size, m.size_decimal(), - collateral, global_config.balance_mut(), collateral_decimal, - price, cumulative_borrow, funding_sign, funding_index, - trading_fee, clock, ctx, - ); - - m.adjust_oi(is_long, true, size); - - market.positions.push_back(position_id, pos); - user_account::add_position(account_registry, account_id, market_id, position_id); - - events::emit_position_opened( - account_object_address, market_id, position_id, - is_long, size, collateral_amount, leverage_bps, - price, trading_fee, now, sender - ); - - let resp = response::new( - object::id(market), account_id, ACTION_OPEN_POSITION, sender, - option::some(position_id), 0, false, trading_fee, - is_long, size, collateral_amount, price, - ); - (coin::zero(ctx), resp) + }; + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let collateral_amount = collateral.value(); + if (collateral_amount == 0) error::err_insufficient_collateral(); + let collateral_token = with_defining_ids(); + let collateral_decimal = pool.token_decimal(collateral_token); + let now = clock.timestamp_ms(); + + if (size < m.min_size()) error::err_invalid_size(); + if (size % m.lot_size() != 0) error::err_invalid_size(); + + if (is_long) { + if (m.long_oi() + size > m.max_long_oi()) error::err_exceed_max_open_interest(); + } else { + if (m.short_oi() + size > m.max_short_oi()) error::err_exceed_max_open_interest(); + }; + + let notional = math::amount_to_usd(size, m.size_decimal(), price); + let collateral_usd = math::amount_to_usd(collateral_amount, collateral_decimal, collateral_price); + let leverage_bps = if (collateral_usd.gt(float::zero())) { + notional.div(collateral_usd).mul_u64(math::bp_scale()).floor() + } else { 0 }; + if (leverage_bps > m.max_leverage_bps()) error::err_exceed_max_leverage(); + + let trading_fee_bps = calculate_total_trading_fee_bps(m, pool, price, is_long, size); + let trading_fee_usd = math::fee_from_bps(notional, trading_fee_bps); + let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); + + pool.update_borrow_rates(clock); + m.update_funding_rate(market_id, now); + + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let funding_sign = m.cumulative_funding_sign(); + let funding_index = m.cumulative_funding_index(); + + let reserve_amount = math::usd_to_amount(notional, collateral_decimal, collateral_price); + let total_oi_after = math::amount_to_usd( + m.long_oi() + m.short_oi() + size, + m.size_decimal(), + price, + ); + pool.check_oi_cap(global_config, total_oi_after); + if (!pool.check_reserve_valid(collateral_token, reserve_amount)) + error::err_exceed_reserve_ratio(); + pool.increase_reserve(collateral_token, reserve_amount); + + let position_id = m.increment_next_position_id(); + let pos = position::create_position( + position_id, + account_object_address, + market_id, + is_long, + size, + m.size_decimal(), + collateral, + global_config.balance_mut(), + collateral_decimal, + price, + cumulative_borrow, + funding_sign, + funding_index, + trading_fee, + clock, + ctx, + ); + + m.adjust_oi(is_long, true, size); + + market.positions.push_back(position_id, pos); + user_account::add_position(account_registry, account_id, market_id, position_id); + + events::emit_position_opened( + account_object_address, + market_id, + position_id, + is_long, + size, + collateral_amount, + leverage_bps, + price, + trading_fee, + now, + sender, + ); + + let resp = response::new( + object::id(market), + account_id, + ACTION_OPEN_POSITION, + sender, + option::some(position_id), + 0, + false, + trading_fee, + is_long, + size, + collateral_amount, + price, + ); + (coin::zero(ctx), resp) } fun execute_increase_position( - global_config: &mut GlobalConfig, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_object_address: address, - sender: address, - position_id: u64, - collateral: Balance, - size: u64, - acceptable_price: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_object_address: address, + sender: address, + position_id: u64, + collateral: Balance, + size: u64, + acceptable_price: u64, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - if (!market.config.is_active()) error::err_market_not_active(); - - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); - - if (size < market.config.min_size()) error::err_invalid_size(); - if (size % market.config.lot_size() != 0) error::err_invalid_size(); - - pool.update_borrow_rates(clock); - market.config.update_funding_rate(market_id, now); - - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let funding_sign = market.config.cumulative_funding_sign(); - let funding_idx = market.config.cumulative_funding_index(); - - let mut pos = take_owned_position( - market, position_id, account_object_address, now, false, - ); - let pos_is_long = pos.is_long(); - assert_open_slippage(pos_is_long, price, acceptable_price); - - pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); - - let collateral_decimal = pos.collateral_decimal(); - let added_collateral_amount = collateral.value(); - let reserve_amount = validate_position_growth( - global_config, - &market.config, - pool, - pos_is_long, - pos.size_amount(), - size, - pos.collateral_amount() + added_collateral_amount, - collateral_decimal, - collateral_token, - price, - collateral_price, - ); - - let trading_fee_usd = math::fee_from_bps( - math::amount_to_usd(size, market.config.size_decimal(), price), - market.config.trading_fee_bps(), - ); - let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); - let new_size = pos.size_amount() + size; - let new_avg_price = weighted_average_price( - market.config.size_decimal(), - pos.size_amount(), - pos.average_price(), - size, - price, - ); - - if (added_collateral_amount > 0) { - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, collateral); }; - } else { - collateral.destroy_zero(); + if (!market.config.is_active()) error::err_market_not_active(); + + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + + if (size < market.config.min_size()) error::err_invalid_size(); + if (size % market.config.lot_size() != 0) error::err_invalid_size(); + + pool.update_borrow_rates(clock); + market.config.update_funding_rate(market_id, now); + + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let funding_sign = market.config.cumulative_funding_sign(); + let funding_idx = market.config.cumulative_funding_index(); + + let mut pos = take_owned_position( + market, + position_id, + account_object_address, + now, + false, + ); + let pos_is_long = pos.is_long(); + assert_open_slippage(pos_is_long, price, acceptable_price); + + pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); + + let collateral_decimal = pos.collateral_decimal(); + let added_collateral_amount = collateral.value(); + let reserve_amount = validate_position_growth( + global_config, + &market.config, + pool, + pos_is_long, + pos.size_amount(), + size, + pos.collateral_amount() + added_collateral_amount, + collateral_decimal, + collateral_token, + price, + collateral_price, + ); + + let trading_fee_bps = calculate_total_trading_fee_bps( + &market.config, + pool, + price, + pos_is_long, + size, + ); + let trading_fee_usd = math::fee_from_bps( + math::amount_to_usd(size, market.config.size_decimal(), price), + trading_fee_bps, + ); + let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); + let new_size = pos.size_amount() + size; + let new_avg_price = weighted_average_price( + market.config.size_decimal(), + pos.size_amount(), + pos.average_price(), + size, + price, + ); + + if (added_collateral_amount > 0) { + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, collateral); }; - pos.add_trading_fee(trading_fee); - pos.update_position_size(new_size, new_avg_price, pos_is_long, clock); - - pool.increase_reserve(collateral_token, reserve_amount); - market.config.adjust_oi(pos_is_long, true, size); - - let new_collateral_amount = pos.collateral_amount(); - let account_object_address = pos.account_object_address(); - market.positions.push_back(position_id, pos); - - events::emit_position_modified( - account_object_address, market_id, position_id, pos_is_long, - true, size, new_size, new_collateral_amount, - price, 0, false, trading_fee, now, sender - ); - - let resp = response::new( - object::id(market), account_id, ACTION_INCREASE_POSITION, sender, - option::some(position_id), 0, false, trading_fee, - pos_is_long, new_size, new_collateral_amount, price, - ); - (coin::zero(ctx), resp) + } else { + collateral.destroy_zero(); + }; + pos.add_trading_fee(trading_fee); + pos.update_position_size(new_size, new_avg_price, pos_is_long, clock); + + pool.increase_reserve(collateral_token, reserve_amount); + market.config.adjust_oi(pos_is_long, true, size); + + let new_collateral_amount = pos.collateral_amount(); + let account_object_address = pos.account_object_address(); + market.positions.push_back(position_id, pos); + + events::emit_position_modified( + account_object_address, + market_id, + position_id, + pos_is_long, + true, + size, + new_size, + new_collateral_amount, + price, + 0, + false, + trading_fee, + now, + sender, + ); + + let resp = response::new( + object::id(market), + account_id, + ACTION_INCREASE_POSITION, + sender, + option::some(position_id), + 0, + false, + trading_fee, + pos_is_long, + new_size, + new_collateral_amount, + price, + ); + (coin::zero(ctx), resp) } fun execute_close_position( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_obj_id: address, - sender: address, - position_id: u64, - acceptable_price: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_obj_id: address, + sender: address, + position_id: u64, + acceptable_price: u64, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - execute_decrease_position( - global_config, account_registry, market, pool, - price_result, collateral_price_result, - market_id, account_id, account_obj_id, sender, position_id, 0, - acceptable_price, ACTION_CLOSE_POSITION, clock, ctx, - ) + execute_decrease_position( + global_config, + account_registry, + market, + pool, + price_result, + collateral_price_result, + market_id, + account_id, + account_obj_id, + sender, + position_id, + 0, + acceptable_price, + ACTION_CLOSE_POSITION, + clock, + ctx, + ) } fun execute_decrease_position( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_obj_id: address, - sender: address, - position_id: u64, - requested_size: u64, - acceptable_price: u64, - response_action: u8, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_obj_id: address, + sender: address, + position_id: u64, + requested_size: u64, + acceptable_price: u64, + response_action: u8, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let protocol_share_bps = global_config.protocol_fee_share_bps(); - let protocol_fee_addr = global_config.fee_address(); + let protocol_share_bps = global_config.protocol_fee_share_bps(); + let protocol_fee_addr = global_config.fee_address(); + + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + + pool.update_borrow_rates(clock); + market.config.update_funding_rate(market_id, now); + + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let funding_sign = market.config.cumulative_funding_sign(); + let funding_index = market.config.cumulative_funding_index(); + + let mut pos = take_owned_position( + market, + position_id, + account_obj_id, + now, + true, + ); + let pos_is_long = pos.is_long(); + assert_close_slippage(pos_is_long, price, acceptable_price); + + if (requested_size > 0) { + if (requested_size < market.config.min_size()) error::err_invalid_size(); + if (requested_size % market.config.lot_size() != 0) error::err_invalid_size(); + }; + + pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_index); + + let size = pos.size_amount(); + let reduce_size = if (requested_size == 0) { size } else { requested_size }; + if (reduce_size > size) error::err_position_flip_not_supported(); + let new_size = size - reduce_size; + if (new_size > 0 && new_size < market.config.min_size()) error::err_invalid_size(); + + let collateral_decimal = pos.collateral_decimal(); + let (is_profit, pnl) = pos.unrealized_pnl(price); + let realized_pnl = if (reduce_size == size) { + pnl + } else { + pnl.mul_u64(reduce_size).div(float::from(size)) + }; + let realized_pnl_amount = math::usd_to_amount(realized_pnl, collateral_decimal, collateral_price); + let close_fee_bps = calculate_total_trading_fee_bps( + &market.config, + pool, + price, + !pos_is_long, + reduce_size, + ); + let close_fee_usd = math::fee_from_bps( + math::amount_to_usd(reduce_size, pos.size_decimal(), price), + close_fee_bps, + ); + let close_fee = math::usd_to_amount(close_fee_usd, collateral_decimal, collateral_price); + let reduce_notional = math::amount_to_usd(reduce_size, market.config.size_decimal(), price); + let reduce_reserve_amount = math::usd_to_amount( + reduce_notional, + collateral_decimal, + collateral_price, + ); + + if (reduce_size == size) { + let borrow_fee = pos.unrealized_borrow_fee(); + let funding_fee = pos.unrealized_funding_fee(); + let funding_sign_pos = pos.unrealized_funding_sign(); + let open_fee = pos.unrealized_trading_fee(); + let (mut collateral, linked_order_ids, linked_order_price_keys) = { + let vault = global_config.balance_mut(); + pos.remove_position(vault) + }; - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); + if (is_profit) { + let profit = pool.request_collateral( + realized_pnl_amount, + collateral_price, + ); + collateral.join(profit); + } else if (realized_pnl_amount > 0) { + let actual_loss = realized_pnl_amount.min(collateral.value()); + let loss = collateral.split(actual_loss); + pool.put_collateral(loss, collateral_price); + }; - pool.update_borrow_rates(clock); - market.config.update_funding_rate(market_id, now); + let total_fee = (close_fee + borrow_fee + open_fee).min(collateral.value()); + if (total_fee > 0) { + let mut fee_balance = collateral.split(total_fee); + let protocol_amount = ( + (fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64, + ); + if (protocol_amount > 0) { + let protocol_fee = fee_balance.split(protocol_amount); + transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); + events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); + }; + pool.put_collateral(fee_balance, collateral_price); + }; - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let funding_sign = market.config.cumulative_funding_sign(); - let funding_index = market.config.cumulative_funding_index(); + if (funding_fee > 0) { + if (funding_sign_pos) { + let actual_funding = funding_fee.min(collateral.value()); + let funding_bal = collateral.split(actual_funding); + pool.put_collateral(funding_bal, collateral_price); + } else { + let funding_income = pool.request_collateral( + funding_fee, + collateral_price, + ); + collateral.join(funding_income); + }; + }; - let mut pos = take_owned_position( - market, position_id, account_obj_id, now, true, + let withdrawal_coll = cancel_linked_orders_to_balance( + global_config, + market, + linked_order_ids, + linked_order_price_keys, ); - let pos_is_long = pos.is_long(); - assert_close_slippage(pos_is_long, price, acceptable_price); - - if (requested_size > 0) { - if (requested_size < market.config.min_size()) error::err_invalid_size(); - if (requested_size % market.config.lot_size() != 0) error::err_invalid_size(); - }; + collateral.join(withdrawal_coll); - pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_index); + pool.decrease_reserve(collateral_token, reduce_reserve_amount); + market.config.adjust_oi(pos_is_long, false, reduce_size); + user_account::remove_position(account_registry, account_id, market_id, position_id); - let size = pos.size_amount(); - let reduce_size = if (requested_size == 0) { size } else { requested_size }; - if (reduce_size > size) error::err_position_flip_not_supported(); - let new_size = size - reduce_size; - if (new_size > 0 && new_size < market.config.min_size()) error::err_invalid_size(); + events::emit_position_closed( + account_obj_id, + market_id, + position_id, + pos_is_long, + size, + price, + realized_pnl_amount, + is_profit, + close_fee, + funding_fee, + borrow_fee, + now, + sender, + ); - let collateral_decimal = pos.collateral_decimal(); - let (is_profit, pnl) = pos.unrealized_pnl(price); - let realized_pnl = if (reduce_size == size) { - pnl - } else { - pnl.mul_u64(reduce_size).div(float::from(size)) - }; - let realized_pnl_amount = math::usd_to_amount(realized_pnl, collateral_decimal, collateral_price); - let close_fee_usd = math::fee_from_bps( - math::amount_to_usd(reduce_size, pos.size_decimal(), price), - market.config.trading_fee_bps(), + let resp = response::new( + object::id(market), + account_id, + response_action, + sender, + option::some(position_id), + realized_pnl_amount, + is_profit, + total_fee, + pos_is_long, + 0, + 0, + price, + ); + transfer::public_transfer(collateral.into_coin(ctx), account_obj_id); + (coin::zero(ctx), resp) + } else { + let (borrow_fee, funding_sign_pos, funding_fee, open_fee) = pos.realize_partial_fees( + reduce_size, + size, ); - let close_fee = math::usd_to_amount(close_fee_usd, collateral_decimal, collateral_price); - let reduce_notional = math::amount_to_usd(reduce_size, market.config.size_decimal(), price); - let reduce_reserve_amount = math::usd_to_amount(reduce_notional, collateral_decimal, collateral_price); - - if (reduce_size == size) { - let borrow_fee = pos.unrealized_borrow_fee(); - let funding_fee = pos.unrealized_funding_fee(); - let funding_sign_pos = pos.unrealized_funding_sign(); - let open_fee = pos.unrealized_trading_fee(); - let (mut collateral, linked_order_ids, linked_order_price_keys) = { - let vault = global_config.balance_mut(); - pos.remove_position(vault) - }; - if (is_profit) { - let profit = pool.request_collateral(realized_pnl_amount, collateral_price); - collateral.join(profit); - } else if (realized_pnl_amount > 0) { - let actual_loss = realized_pnl_amount.min(collateral.value()); - let loss = collateral.split(actual_loss); - pool.put_collateral(loss, collateral_price); - }; + if (is_profit) { + let profit = pool.request_collateral( + realized_pnl_amount, + collateral_price, + ); + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, profit); + }; + } else if (realized_pnl_amount > 0) { + let loss = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, realized_pnl_amount) + }; + pool.put_collateral(loss, collateral_price); + }; - let total_fee = close_fee + borrow_fee + open_fee.min(collateral.value()); - if (total_fee > 0) { - let mut fee_balance = collateral.split(total_fee); - let protocol_amount = ((fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64); - if (protocol_amount > 0) { - let protocol_fee = fee_balance.split(protocol_amount); - transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); - events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); - }; - pool.put_collateral(fee_balance, collateral_price); - }; + let total_fee = close_fee + borrow_fee + open_fee; + if (total_fee > 0) { + let mut fee_balance = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, total_fee) + }; + let protocol_amount = ( + (fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64, + ); + if (protocol_amount > 0) { + let protocol_fee = fee_balance.split(protocol_amount); + transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); + events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); + }; + pool.put_collateral(fee_balance, collateral_price); + }; - if (funding_fee > 0) { - if (funding_sign_pos) { - let actual_funding = funding_fee.min(collateral.value()); - let funding_bal = collateral.split(actual_funding); - pool.put_collateral(funding_bal, collateral_price); - } else { - let funding_income = pool.request_collateral(funding_fee, collateral_price); - collateral.join(funding_income); - }; + if (funding_fee > 0) { + if (funding_sign_pos) { + let funding_bal = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, funding_fee) }; - - let withdrawal_coll = cancel_linked_orders_to_balance( - global_config, market, linked_order_ids, linked_order_price_keys, + pool.put_collateral(funding_bal, collateral_price); + } else { + let funding_income = pool.request_collateral( + funding_fee, + collateral_price, ); - collateral.join(withdrawal_coll); - - pool.decrease_reserve(collateral_token, reduce_reserve_amount); - market.config.adjust_oi(pos_is_long, false, reduce_size); - user_account::remove_position(account_registry, account_id, market_id, position_id); - - events::emit_position_closed( - account_obj_id, market_id, position_id, - pos_is_long, size, price, realized_pnl_amount, is_profit, - close_fee, funding_fee, borrow_fee, now, sender - ); - - let resp = response::new( - object::id(market), account_id, response_action, sender, - option::some(position_id), realized_pnl_amount, is_profit, total_fee, - pos_is_long, 0, 0, price, - ); - transfer::public_transfer(collateral.into_coin(ctx), account_obj_id); - (coin::zero(ctx), resp) - } else { - let (borrow_fee, funding_sign_pos, funding_fee, open_fee) = pos.realize_partial_fees(reduce_size, size); - - if (is_profit) { - let profit = pool.request_collateral(realized_pnl_amount, collateral_price); - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, profit); }; - } else if (realized_pnl_amount > 0) { - let loss = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, realized_pnl_amount) }; - pool.put_collateral(loss, collateral_price); - }; - - let total_fee = close_fee + borrow_fee + open_fee; - if (total_fee > 0) { - let mut fee_balance = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, total_fee) }; - let protocol_amount = ((fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64); - if (protocol_amount > 0) { - let protocol_fee = fee_balance.split(protocol_amount); - transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); - events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); - }; - pool.put_collateral(fee_balance, collateral_price); - }; - - if (funding_fee > 0) { - if (funding_sign_pos) { - let funding_bal = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, funding_fee) }; - pool.put_collateral(funding_bal, collateral_price); - } else { - let funding_income = pool.request_collateral(funding_fee, collateral_price); - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, funding_income); }; - }; + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, funding_income); }; + }; + }; - let avg_price = pos.average_price(); - pos.update_position_size(new_size, avg_price, pos_is_long, clock); + let avg_price = pos.average_price(); + pos.update_position_size(new_size, avg_price, pos_is_long, clock); - pool.decrease_reserve(collateral_token, reduce_reserve_amount); - market.config.adjust_oi(pos_is_long, false, reduce_size); + pool.decrease_reserve(collateral_token, reduce_reserve_amount); + market.config.adjust_oi(pos_is_long, false, reduce_size); - let new_collateral_amount = pos.collateral_amount(); - market.positions.push_back(position_id, pos); + let new_collateral_amount = pos.collateral_amount(); + market.positions.push_back(position_id, pos); - let event_fee_amount = if (funding_sign_pos) { total_fee + funding_fee } else { total_fee }; - events::emit_position_modified( - account_obj_id, market_id, position_id, pos_is_long, - false, reduce_size, new_size, new_collateral_amount, - price, realized_pnl_amount, is_profit, event_fee_amount, now, sender - ); + let event_fee_amount = if (funding_sign_pos) { total_fee + funding_fee } else { total_fee }; + events::emit_position_modified( + account_obj_id, + market_id, + position_id, + pos_is_long, + false, + reduce_size, + new_size, + new_collateral_amount, + price, + realized_pnl_amount, + is_profit, + event_fee_amount, + now, + sender, + ); - let resp = response::new( - object::id(market), account_id, response_action, sender, - option::some(position_id), realized_pnl_amount, is_profit, event_fee_amount, - pos_is_long, new_size, new_collateral_amount, price, - ); - (coin::zero(ctx), resp) - } + let resp = response::new( + object::id(market), + account_id, + response_action, + sender, + option::some(position_id), + realized_pnl_amount, + is_profit, + event_fee_amount, + pos_is_long, + new_size, + new_collateral_amount, + price, + ); + (coin::zero(ctx), resp) + } } fun execute_place_order( - global_config: &mut GlobalConfig, - market: &mut Market, - pool: &WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_object_address: address, - sender: address, - collateral: Balance, - is_long: bool, - is_stop_order: bool, - reduce_only: bool, - size: u64, - trigger_price: Float, - linked_position_id: Option, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + market: &mut Market, + pool: &WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_object_address: address, + sender: address, + collateral: Balance, + is_long: bool, + is_stop_order: bool, + reduce_only: bool, + size: u64, + trigger_price: Float, + linked_position_id: Option, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); - let collateral_decimal = pool.token_decimal(collateral_token); - - let collateral_amount = collateral.value(); - if (size < market.config.min_size()) error::err_invalid_size(); - if (size % market.config.lot_size() != 0) error::err_invalid_size(); - - let mut order_is_reduction = false; - let leverage_bps = if (linked_position_id.is_some()) { - let pos_id = *linked_position_id.borrow(); - if (!market.positions.contains(pos_id)) error::err_position_not_found(); - let pos: &Position = &market.positions[pos_id]; - assert_position_owner(pos, account_object_address); - if (pos.collateral_token() != with_defining_ids()) error::err_invalid_collateral_type(); - - let next_order_id = market.config.next_order_id(); - let price_key = trigger_price.floor(); - let leverage = { - let pos: &Position = &market.positions[pos_id]; - if (pos.is_long() == is_long) { - if (reduce_only) error::err_invalid_linked_order(); - if (!market.config.is_active()) error::err_market_not_active(); - let _ = validate_position_growth( - global_config, - &market.config, - pool, - is_long, - pos.size_amount(), - size, - pos.collateral_amount() + collateral_amount, - collateral_decimal, - collateral_token, - trigger_price, - collateral_price, - ); - calculate_position_leverage_bps( - market.config.size_decimal(), - pos.size_amount() + size, - pos.collateral_amount() + collateral_amount, - collateral_decimal, - trigger_price, - collateral_price, - ) - } else { - order_is_reduction = true; - if (size > pos.size_amount()) error::err_position_flip_not_supported(); - let remaining_size = pos.size_amount() - size; - if (remaining_size > 0 && remaining_size < market.config.min_size()) { - error::err_invalid_size(); - }; - 0 - } - }; - - let pos: &mut Position = &mut market.positions[pos_id]; - pos.add_linked_order(next_order_id, price_key); - leverage - } else { - if (reduce_only) error::err_reduce_only_requires_position(); + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + let collateral_decimal = pool.token_decimal(collateral_token); + + let collateral_amount = collateral.value(); + if (size < market.config.min_size()) error::err_invalid_size(); + if (size % market.config.lot_size() != 0) error::err_invalid_size(); + + let mut order_is_reduction = false; + let leverage_bps = if (linked_position_id.is_some()) { + let pos_id = *linked_position_id.borrow(); + if (!market.positions.contains(pos_id)) error::err_position_not_found(); + let pos: &Position = &market.positions[pos_id]; + assert_position_owner(pos, account_object_address); + if (pos.collateral_token() != with_defining_ids()) + error::err_invalid_collateral_type(); + + let next_order_id = market.config.next_order_id(); + let price_key = trigger_price.floor(); + let leverage = { + let pos: &Position = &market.positions[pos_id]; + if (pos.is_long() == is_long) { + if (reduce_only) error::err_invalid_linked_order(); if (!market.config.is_active()) error::err_market_not_active(); + let _ = validate_position_growth( + global_config, + &market.config, + pool, + is_long, + pos.size_amount(), + size, + pos.collateral_amount() + collateral_amount, + collateral_decimal, + collateral_token, + trigger_price, + collateral_price, + ); calculate_position_leverage_bps( - market.config.size_decimal(), - size, - collateral_amount, - collateral_decimal, - trigger_price, - collateral_price, + market.config.size_decimal(), + pos.size_amount() + size, + pos.collateral_amount() + collateral_amount, + collateral_decimal, + trigger_price, + collateral_price, ) + } else { + order_is_reduction = true; + if (size > pos.size_amount()) error::err_position_flip_not_supported(); + let remaining_size = pos.size_amount() - size; + if (remaining_size > 0 && remaining_size < market.config.min_size()) { + error::err_invalid_size(); + }; + 0 + } }; - if (!order_is_reduction && leverage_bps > market.config.max_leverage_bps()) { - error::err_exceed_max_leverage(); - }; - - let order_id = market.config.increment_next_order_id(); - let order = position::create_order( - order_id, account_object_address, market_id, - linked_position_id, collateral, global_config.balance_mut(), collateral_decimal, - is_long, reduce_only, is_stop_order, - size, market.config.size_decimal(), trigger_price, leverage_bps, - price, clock, ctx, - ); - - let trigger_key = trigger_price.floor(); - let order_type_tag = order.order_type_tag(); - let max_orders = global_config.max_orders_per_price(); - let orders_map = borrow_mut_order_map(market, order_type_tag); - if (!orders_map.contains(&trigger_key)) { - orders_map.insert(trigger_key, vector[order]); - } else { - let orders_at_price = orders_map.get_mut(&trigger_key); - if (orders_at_price.length() >= max_orders) error::err_exceed_capacity(); - orders_at_price.push_back(order); - }; - - events::emit_order_created( - account_object_address, market_id, order_id, - order_type_tag, is_long, size, trigger_price, - collateral_amount, now, sender - ); - - let resp = response::new( - object::id(market), account_id, ACTION_PLACE_ORDER, sender, - option::some(order_id), 0, false, 0, - is_long, size, collateral_amount, trigger_price, - ); - (coin::zero(ctx), resp) + let pos: &mut Position = &mut market.positions[pos_id]; + pos.add_linked_order(next_order_id, price_key); + leverage + } else { + if (reduce_only) error::err_reduce_only_requires_position(); + if (!market.config.is_active()) error::err_market_not_active(); + calculate_position_leverage_bps( + market.config.size_decimal(), + size, + collateral_amount, + collateral_decimal, + trigger_price, + collateral_price, + ) + }; + + if (!order_is_reduction && leverage_bps > market.config.max_leverage_bps()) { + error::err_exceed_max_leverage(); + }; + + let order_id = market.config.increment_next_order_id(); + let order = position::create_order( + order_id, + account_object_address, + market_id, + linked_position_id, + collateral, + global_config.balance_mut(), + collateral_decimal, + is_long, + reduce_only, + is_stop_order, + size, + market.config.size_decimal(), + trigger_price, + leverage_bps, + price, + clock, + ctx, + ); + + let trigger_key = trigger_price.floor(); + let order_type_tag = order.order_type_tag(); + let max_orders = global_config.max_orders_per_price(); + let orders_map = borrow_mut_order_map(market, order_type_tag); + if (!orders_map.contains(&trigger_key)) { + orders_map.insert(trigger_key, vector[order]); + } else { + let orders_at_price = orders_map.get_mut(&trigger_key); + if (orders_at_price.length() >= max_orders) error::err_exceed_capacity(); + orders_at_price.push_back(order); + }; + + events::emit_order_created( + account_object_address, + market_id, + order_id, + order_type_tag, + is_long, + size, + trigger_price, + collateral_amount, + now, + sender, + ); + + let resp = response::new( + object::id(market), + account_id, + ACTION_PLACE_ORDER, + sender, + option::some(order_id), + 0, + false, + 0, + is_long, + size, + collateral_amount, + trigger_price, + ); + (coin::zero(ctx), resp) } fun execute_cancel_order( - global_config: &mut GlobalConfig, - market: &mut Market, - market_id: ID, - account_id: ID, - account_obj_id: address, - sender: address, - order_id: u64, - trigger_price_key: u64, - order_type_tag: u8, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + market: &mut Market, + market_id: ID, + account_id: ID, + account_obj_id: address, + sender: address, + order_id: u64, + trigger_price_key: u64, + order_type_tag: u8, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let now = clock.timestamp_ms(); - - let order = take_order_direct(market, trigger_price_key, order_id, account_obj_id, order_type_tag); - if (order.is_none()) error::err_order_not_found(); - let order = order.destroy_some(); - - let mut linked_pos = order.linked_position_id(); - if (linked_pos.is_some()) { - let pos_id = linked_pos.extract(); - if (market.positions.contains(pos_id)) { - let pos: &mut Position = &mut market.positions[pos_id]; - pos.remove_linked_order(order.order_id()); - }; + let now = clock.timestamp_ms(); + + let order = take_order_direct( + market, + trigger_price_key, + order_id, + account_obj_id, + order_type_tag, + ); + if (order.is_none()) error::err_order_not_found(); + let order = order.destroy_some(); + + let mut linked_pos = order.linked_position_id(); + if (linked_pos.is_some()) { + let pos_id = linked_pos.extract(); + if (market.positions.contains(pos_id)) { + let pos: &mut Position = &mut market.positions[pos_id]; + pos.remove_linked_order(order.order_id()); }; + }; - let withdrawal_coll = { let vault = global_config.balance_mut(); order.remove_order(vault) }; - let withdrawal_coll_amount = withdrawal_coll.value(); - - events::emit_order_cancelled( - account_obj_id, market_id, order_id, withdrawal_coll_amount, now, sender - ); - - let resp = response::new( - object::id(market), account_id, ACTION_CANCEL_ORDER, sender, - option::some(order_id), 0, false, 0, - false, 0, withdrawal_coll_amount, float::zero(), - ); - let return_coin = withdrawal_coll.into_coin(ctx); - transfer::public_transfer(return_coin, account_obj_id); - (coin::zero(ctx), resp) + let withdrawal_coll = { + let vault = global_config.balance_mut(); + order.remove_order(vault) + }; + let withdrawal_coll_amount = withdrawal_coll.value(); + + events::emit_order_cancelled( + account_obj_id, + market_id, + order_id, + withdrawal_coll_amount, + now, + sender, + ); + + let resp = response::new( + object::id(market), + account_id, + ACTION_CANCEL_ORDER, + sender, + option::some(order_id), + 0, + false, + 0, + false, + 0, + withdrawal_coll_amount, + float::zero(), + ); + let return_coin = withdrawal_coll.into_coin(ctx); + transfer::public_transfer(return_coin, account_obj_id); + (coin::zero(ctx), resp) } fun execute_deposit_collateral( - global_config: &mut GlobalConfig, - market: &mut Market, - _pool: &WlpPool, - price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_object_address: address, - sender: address, - position_id: u64, - collateral: Balance, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + market: &mut Market, + _pool: &WlpPool, + price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_object_address: address, + sender: address, + position_id: u64, + collateral: Balance, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let _price = price_result.aggregated_price(); - let now = clock.timestamp_ms(); - let amount = collateral.value(); - - let pos = borrow_owned_position_mut(market, position_id, account_object_address); + let _price = price_result.aggregated_price(); + let now = clock.timestamp_ms(); + let amount = collateral.value(); - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, collateral); }; - pos.touch_update_timestamp(clock); + let pos = borrow_owned_position_mut(market, position_id, account_object_address); - events::emit_collateral_modified( - pos.account_object_address(), market_id, position_id, - true, amount, pos.collateral_amount(), now, sender - ); - - let resp = response::new( - market_id, account_id, ACTION_DEPOSIT_COLLATERAL, sender, - option::some(position_id), 0, false, 0, - pos.is_long(), pos.size_amount(), pos.collateral_amount(), float::zero(), - ); - (coin::zero(ctx), resp) + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, collateral); + }; + pos.touch_update_timestamp(clock); + + events::emit_collateral_modified( + pos.account_object_address(), + market_id, + position_id, + true, + amount, + pos.collateral_amount(), + now, + sender, + ); + + let resp = response::new( + market_id, + account_id, + ACTION_DEPOSIT_COLLATERAL, + sender, + option::some(position_id), + 0, + false, + 0, + pos.is_long(), + pos.size_amount(), + pos.collateral_amount(), + float::zero(), + ); + (coin::zero(ctx), resp) } fun execute_withdraw_collateral( - global_config: &mut GlobalConfig, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_obj_id: address, - sender: address, - position_id: u64, - amount: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_obj_id: address, + sender: address, + position_id: u64, + amount: u64, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); - - pool.update_borrow_rates(clock); + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + + pool.update_borrow_rates(clock); + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let (funding_sign, funding_idx, maintenance_bps, cooldown) = { let m = &mut market.config; m.update_funding_rate(market_id, now); + ( + m.cumulative_funding_sign(), + m.cumulative_funding_index(), + m.maintenance_margin_bps(), + m.cooldown_ms(), + ) + }; - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let trading_fee_bps = m.trading_fee_bps(); - let funding_sign = m.cumulative_funding_sign(); - let funding_idx = m.cumulative_funding_index(); - let maintenance_bps = m.maintenance_margin_bps(); - let cooldown = market.config.cooldown_ms(); - - let pos = borrow_owned_position_mut(market, position_id, account_obj_id); - - if (cooldown > 0) { - if (now < pos.update_timestamp() + cooldown) error::err_cooldown_not_elapsed(); - }; - - pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); - let colleteral = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, amount) }; - - if (pos.is_liquidatable( - price, collateral_price, trading_fee_bps, maintenance_bps, - cumulative_borrow, funding_sign, funding_idx, - )) error::err_insufficient_collateral(); - - events::emit_collateral_modified( - account_obj_id, market_id, position_id, - false, amount, pos.collateral_amount(), now, sender - ); - - let resp = response::new( - market_id, account_id, ACTION_WITHDRAW_COLLATERAL, sender, - option::some(position_id), 0, false, 0, - pos.is_long(), pos.size_amount(), pos.collateral_amount(), float::zero(), - ); - let return_coin = colleteral.into_coin(ctx); - transfer::public_transfer(return_coin, account_obj_id); - (coin::zero(ctx), resp) + let (pos_is_long, pos_size, pos_size_decimal) = { + let pos_ref: &Position = &market.positions[position_id]; + (pos_ref.is_long(), pos_ref.size_amount(), pos_ref.size_decimal()) + }; + let closing_notional = math::amount_to_usd(pos_size, pos_size_decimal, price); + let closing_fee_bps = calculate_total_trading_fee_bps( + &market.config, + pool, + price, + !pos_is_long, + pos_size, + ); + let closing_fee_usd = math::fee_from_bps(closing_notional, closing_fee_bps); + + let pos = borrow_owned_position_mut(market, position_id, account_obj_id); + + if (cooldown > 0) { + if (now < pos.update_timestamp() + cooldown) error::err_cooldown_not_elapsed(); + }; + + pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); + let collateral = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, amount) + }; + + if ( + position::is_liquidatable( + pos, + price, + collateral_price, + closing_fee_usd, + maintenance_bps, + cumulative_borrow, + funding_sign, + funding_idx, + ) + ) error::err_insufficient_collateral(); + + events::emit_collateral_modified( + account_obj_id, + market_id, + position_id, + false, + amount, + pos.collateral_amount(), + now, + sender, + ); + + let resp = response::new( + market_id, + account_id, + ACTION_WITHDRAW_COLLATERAL, + sender, + option::some(position_id), + 0, + false, + 0, + pos.is_long(), + pos.size_amount(), + pos.collateral_amount(), + float::zero(), + ); + let return_coin = collateral.into_coin(ctx); + transfer::public_transfer(return_coin, account_obj_id); + (coin::zero(ctx), resp) } fun execute_liquidate( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - market_id: ID, - account_id: ID, - account_object_address: address, - sender: address, - position_id: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + market_id: ID, + account_id: ID, + account_object_address: address, + sender: address, + position_id: u64, + clock: &Clock, + ctx: &mut TxContext, ): (Coin, TradingResponse) { - let protocol_share_bps = global_config.protocol_fee_share_bps(); - let protocol_fee_addr = global_config.fee_address(); - let insurance_addr = global_config.insurance_address(); - let liquidator_fee_bps = global_config.liquidator_fee_bps(); - let insurance_fee_bps = global_config.insurance_fee_bps(); - + let insurance_addr = global_config.insurance_address(); + let liquidator_fee_bps = global_config.liquidator_fee_bps(); + let insurance_fee_bps = global_config.insurance_fee_bps(); + + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + + pool.update_borrow_rates(clock); + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let (funding_sign, funding_idx, maintenance_bps) = { let m = &mut market.config; - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); - - pool.update_borrow_rates(clock); m.update_funding_rate(market_id, now); - - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let funding_sign = m.cumulative_funding_sign(); - let trading_fee_bps = m.trading_fee_bps(); - let funding_idx = m.cumulative_funding_index(); - let maintenance_bps = m.maintenance_margin_bps(); - - if (!market.positions.contains(position_id)) error::err_position_not_found(); - - let pos_ref: &Position = &market.positions[position_id]; - assert_position_owner(pos_ref, account_object_address); - if (!pos_ref.is_liquidatable( - price, collateral_price, trading_fee_bps, maintenance_bps, - cumulative_borrow, funding_sign, funding_idx, - )) error::err_position_not_liquidatable(); - - let mut pos = take_owned_position( - market, position_id, account_object_address, now, false, - ); - pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); - - let user = pos.account_object_address(); - let size = pos.size_amount(); - let is_long = pos.is_long(); - let collateral_decimal = pos.collateral_decimal(); - let collateral_amount = pos.collateral_amount(); - - let notional = math::amount_to_usd(size, pos.size_decimal(), price); - let liquidator_fee_usd = math::fee_from_bps(notional, liquidator_fee_bps); - let liquidator_fee = math::usd_to_amount(liquidator_fee_usd, collateral_decimal, collateral_price); - - let (mut collateral, linked_order_ids, linked_order_price_keys) = { - let vault = global_config.balance_mut(); - pos.remove_position(vault) - }; - let withdrawal_coll = cancel_linked_orders_to_balance( - global_config, market, linked_order_ids, linked_order_price_keys, - ); - return_to_user(withdrawal_coll, user, ctx); - - let actual_liq_fee = liquidator_fee.min(collateral.value()); + (m.cumulative_funding_sign(), m.cumulative_funding_index(), m.maintenance_margin_bps()) + }; + + if (!market.positions.contains(position_id)) error::err_position_not_found(); + + let pos_ref: &Position = &market.positions[position_id]; + assert_position_owner(pos_ref, account_object_address); + let liquidation_notional = math::amount_to_usd( + pos_ref.size_amount(), + pos_ref.size_decimal(), + price, + ); + let liquidation_fee_bps = calculate_total_trading_fee_bps( + &market.config, + pool, + price, + !pos_ref.is_long(), + pos_ref.size_amount(), + ); + let liquidation_fee_usd = math::fee_from_bps(liquidation_notional, liquidation_fee_bps); + if ( + !position::is_liquidatable( + pos_ref, + price, + collateral_price, + liquidation_fee_usd, + maintenance_bps, + cumulative_borrow, + funding_sign, + funding_idx, + ) + ) error::err_position_not_liquidatable(); + + let mut pos = take_owned_position( + market, + position_id, + account_object_address, + now, + false, + ); + pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); + + let user = pos.account_object_address(); + let size = pos.size_amount(); + let is_long = pos.is_long(); + let collateral_decimal = pos.collateral_decimal(); + let collateral_amount = pos.collateral_amount(); + + let notional = math::amount_to_usd(size, pos.size_decimal(), price); + let liquidator_fee_usd = math::fee_from_bps(notional, liquidator_fee_bps); + let liquidator_fee = math::usd_to_amount( + liquidator_fee_usd, + collateral_decimal, + collateral_price, + ); + let insurance_fee_usd = math::fee_from_bps(notional, insurance_fee_bps); + let insurance_fee = math::usd_to_amount(insurance_fee_usd, collateral_decimal, collateral_price); + + let (mut collateral, linked_order_ids, linked_order_price_keys) = { + let vault = global_config.balance_mut(); + pos.remove_position(vault) + }; + let withdrawal_coll = cancel_linked_orders_to_balance( + global_config, + market, + linked_order_ids, + linked_order_price_keys, + ); + return_to_user(withdrawal_coll, user, ctx); + + let actual_liq_fee = liquidator_fee.min(collateral.value()); + if (actual_liq_fee > 0) { let liq_fee_bal = collateral.split(actual_liq_fee); transfer::public_transfer(liq_fee_bal.into_coin(ctx), sender); - - let insurance_fee = ((collateral.value() as u128) * (insurance_fee_bps as u128) / 10000 as u64); - let actual_insurance_fee = insurance_fee.min(collateral.value()); - if (actual_insurance_fee > 0) { - let insurance_bal = collateral.split(actual_insurance_fee); - transfer::public_transfer(insurance_bal.into_coin(ctx), insurance_addr); - }; - - let protocol_amount = ((collateral.value() as u128) * (protocol_share_bps as u128) / 10000 as u64); - if (protocol_amount > 0) { - let protocol_fee = collateral.split(protocol_amount); - transfer::public_transfer( - protocol_fee.into_coin(ctx), - protocol_fee_addr, - ); - events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); - }; - let remaining = collateral.value(); - pool.put_collateral(collateral, collateral_price); - - let liq_notional = math::amount_to_usd(size, market.config.size_decimal(), price); - let liq_reserve_amount = math::usd_to_amount(liq_notional, collateral_decimal, collateral_price); - pool.decrease_reserve(collateral_token, liq_reserve_amount); - - market.config.adjust_oi(is_long, false, size); - user_account::remove_position(account_registry, account_id, market_id, position_id); - - events::emit_position_liquidated( - user, sender, market_id, position_id, - is_long, size, collateral_amount, - actual_liq_fee, actual_insurance_fee, remaining, price, now, sender - ); - - let resp = response::new( - object::id(market), account_id, ACTION_LIQUIDATE, sender, - option::some(position_id), 0, false, actual_liq_fee, - is_long, 0, 0, price, - ); - (coin::zero(ctx), resp) + }; + + let actual_insurance_fee = insurance_fee.min(collateral.value()); + if (actual_insurance_fee > 0) { + let insurance_bal = collateral.split(actual_insurance_fee); + transfer::public_transfer(insurance_bal.into_coin(ctx), insurance_addr); + }; + + let remaining = collateral.value(); + pool.put_collateral(collateral, collateral_price); + + let liq_notional = math::amount_to_usd(size, market.config.size_decimal(), price); + let liq_reserve_amount = math::usd_to_amount(liq_notional, collateral_decimal, collateral_price); + pool.decrease_reserve(collateral_token, liq_reserve_amount); + + market.config.adjust_oi(is_long, false, size); + user_account::remove_position(account_registry, account_id, market_id, position_id); + + events::emit_position_liquidated( + user, + sender, + market_id, + position_id, + is_long, + size, + collateral_amount, + actual_liq_fee, + actual_insurance_fee, + remaining, + price, + now, + sender, + ); + + let resp = response::new( + object::id(market), + account_id, + ACTION_LIQUIDATE, + sender, + option::some(position_id), + 0, + false, + actual_liq_fee, + is_long, + 0, + 0, + price, + ); + (coin::zero(ctx), resp) } // ================================================================ @@ -1348,653 +1874,827 @@ fun execute_liquidate( #[allow(unused_assignment)] public fun match_orders( - global_config: &mut GlobalConfig, - account_registry: &mut AccountRegistry, - market: &mut Market, - pool: &mut WlpPool, - sender_request: &AccountRequest, - price_result: &PriceResult, - collateral_price_result: &PriceResult, - order_type_tag: u8, - trigger_price_key: u64, - max_fills: u64, - clock: &Clock, - ctx: &mut TxContext, + global_config: &mut GlobalConfig, + account_registry: &mut AccountRegistry, + market: &mut Market, + pool: &mut WlpPool, + sender_request: &AccountRequest, + price_result: &PriceResult, + collateral_price_result: &PriceResult, + order_type_tag: u8, + trigger_price_key: u64, + max_fills: u64, + clock: &Clock, + ctx: &mut TxContext, ) { - global_config.verify_keeper(sender_request.address()); - let protocol_share_bps = global_config.protocol_fee_share_bps(); - let protocol_fee_addr = global_config.fee_address(); - - let market_id = object::id(market); - let price = price_result.aggregated_price(); - if (price.eq(float::zero())) error::err_zero_price(); - let collateral_price = collateral_price_result.aggregated_price(); - if (collateral_price.eq(float::zero())) error::err_zero_price(); - let now = clock.timestamp_ms(); - let collateral_token = with_defining_ids(); - - pool.update_borrow_rates(clock); - let m = &mut market.config; - m.update_funding_rate(market_id, now); + global_config.verify_keeper(sender_request.address()); + let protocol_share_bps = global_config.protocol_fee_share_bps(); + let protocol_fee_addr = global_config.fee_address(); + + let market_id = object::id(market); + let price = price_result.aggregated_price(); + if (price.eq(float::zero())) error::err_zero_price(); + let collateral_price = collateral_price_result.aggregated_price(); + if (collateral_price.eq(float::zero())) error::err_zero_price(); + let now = clock.timestamp_ms(); + let collateral_token = with_defining_ids(); + + pool.update_borrow_rates(clock); + let m = &mut market.config; + m.update_funding_rate(market_id, now); + + let orders_map = borrow_mut_order_map(market, order_type_tag); + if (!orders_map.contains(&trigger_price_key)) return; + + let (_, mut orders) = orders_map.remove(&trigger_price_key); + let mut remaining = vector[]; + let mut fills: u64 = 0; + + while (orders.length() > 0 && fills < max_fills) { + let order = orders.pop_back(); + + if (!order.check_order_fillable(price)) { + remaining.push_back(order); + continue + }; - let orders_map = borrow_mut_order_map(market, order_type_tag); - if (!orders_map.contains(&trigger_price_key)) return; + if (order.collateral_token() != collateral_token) { + remaining.push_back(order); + continue + }; + let user = order.account_object_address(); + let is_long = order.is_long(); + let size = order.size_amount(); + let oid = order.order_id(); + let collateral_decimal = order.collateral_decimal(); + let linked = order.linked_position_id(); + let reduce_only = order.reduce_only(); + let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); + let funding_sign = market.config.cumulative_funding_sign(); + let funding_idx = market.config.cumulative_funding_index(); - let (_, mut orders) = orders_map.remove(&trigger_price_key); - let mut remaining = vector[]; - let mut fills: u64 = 0; + if (linked.is_some()) { + let pos_id = *linked.borrow(); + if (!market.positions.contains(pos_id)) { + let collateral = { + let vault = global_config.balance_mut(); + order.remove_order(vault) + }; + return_to_user(collateral, user, ctx); + fills = fills + 1; + continue + }; + let linked_pos_ref: &Position = &market.positions[pos_id]; + let linked_pos_is_long = linked_pos_ref.is_long(); + let linked_is_reduction = linked_pos_is_long != is_long; + if (!market.config.is_active() && !linked_is_reduction) { + remaining.push_back(order); + continue + }; + } else if (!market.config.is_active()) { + remaining.push_back(order); + continue + }; - while (orders.length() > 0 && fills < max_fills) { - let order = orders.pop_back(); + let trading_fee_bps = calculate_total_trading_fee_bps( + &market.config, + pool, + price, + is_long, + size, + ); + let trading_fee_usd = math::fee_from_bps( + math::amount_to_usd(size, market.config.size_decimal(), price), + trading_fee_bps, + ); + let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); + let collateral = { + let vault = global_config.balance_mut(); + order.remove_order(vault) + }; - if (!order.check_order_fillable(price)) { - remaining.push_back(order); - continue + let mut filled_position_id = 0; + let filled_size_amount = size; + let mut filled_fee_amount = trading_fee; + + if (linked.is_some()) { + let pos_id = *linked.borrow(); + let mut pos: Position = market.positions.swap_remove_by_key(pos_id); + pos.remove_linked_order(oid); + pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); + + if (pos.is_long() == is_long) { + if (reduce_only) { + market.positions.push_back(pos_id, pos); + return_to_user(collateral, user, ctx); + fills = fills + 1; + continue }; - if (order.collateral_token() != collateral_token) { - remaining.push_back(order); - continue + let reserve_amount = validate_position_growth( + global_config, + &market.config, + pool, + is_long, + pos.size_amount(), + size, + pos.collateral_amount() + collateral.value(), + collateral_decimal, + collateral_token, + price, + collateral_price, + ); + let new_size = pos.size_amount() + size; + let new_avg_price = weighted_average_price( + market.config.size_decimal(), + pos.size_amount(), + pos.average_price(), + size, + price, + ); + + if (collateral.value() > 0) { + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, collateral); + }; + } else { + collateral.destroy_zero(); }; - let user = order.account_object_address(); - let is_long = order.is_long(); - let size = order.size_amount(); - let oid = order.order_id(); - let collateral_decimal = order.collateral_decimal(); - let linked = order.linked_position_id(); - let reduce_only = order.reduce_only(); - let cumulative_borrow = pool.cumulative_borrow_rate(collateral_token); - let funding_sign = market.config.cumulative_funding_sign(); - let funding_idx = market.config.cumulative_funding_index(); - - if (linked.is_some()) { - let pos_id = *linked.borrow(); - if (!market.positions.contains(pos_id)) { - let collateral = { let vault = global_config.balance_mut(); order.remove_order(vault) }; - return_to_user(collateral, user, ctx); - fills = fills + 1; - continue - }; - let linked_pos_ref: &Position = &market.positions[pos_id]; - let linked_pos_is_long = linked_pos_ref.is_long(); - let linked_is_reduction = linked_pos_is_long != is_long; - if (!market.config.is_active() && !linked_is_reduction) { - remaining.push_back(order); - continue - }; - } else if (!market.config.is_active()) { - remaining.push_back(order); - continue + pos.add_trading_fee(trading_fee); + pos.update_position_size(new_size, new_avg_price, is_long, clock); + + pool.increase_reserve(collateral_token, reserve_amount); + market.config.adjust_oi(is_long, true, size); + market.positions.push_back(pos_id, pos); + filled_position_id = pos_id; + } else { + let pos_size = pos.size_amount(); + if (size > pos_size) { + market.positions.push_back(pos_id, pos); + return_to_user(collateral, user, ctx); + fills = fills + 1; + continue + }; + let remaining_size = pos_size - size; + if (remaining_size > 0 && remaining_size < market.config.min_size()) { + market.positions.push_back(pos_id, pos); + return_to_user(collateral, user, ctx); + fills = fills + 1; + continue }; - let trading_fee_usd = math::fee_from_bps( - math::amount_to_usd(size, market.config.size_decimal(), price), - market.config.trading_fee_bps(), + let (is_profit, pnl) = pos.unrealized_pnl(price); + let realized_pnl = if (size == pos_size) { + pnl + } else { + pnl.mul_u64(size).div(float::from(pos_size)) + }; + let realized_pnl_amount = math::usd_to_amount( + realized_pnl, + collateral_decimal, + collateral_price, ); - let trading_fee = math::usd_to_amount(trading_fee_usd, collateral_decimal, collateral_price); - let collateral = { let vault = global_config.balance_mut(); order.remove_order(vault) }; - - let mut filled_position_id = 0; - let filled_size_amount = size; - let mut filled_fee_amount = trading_fee; - - if (linked.is_some()) { - let pos_id = *linked.borrow(); - let mut pos: Position = market.positions.swap_remove_by_key(pos_id); - pos.remove_linked_order(oid); - pos.update_fees(price, collateral_price, cumulative_borrow, funding_sign, funding_idx); - - if (pos.is_long() == is_long) { - if (reduce_only) { - market.positions.push_back(pos_id, pos); - return_to_user(collateral, user, ctx); - fills = fills + 1; - continue - }; - - let reserve_amount = validate_position_growth( - global_config, - &market.config, - pool, - is_long, - pos.size_amount(), - size, - pos.collateral_amount() + collateral.value(), - collateral_decimal, - collateral_token, - price, - collateral_price, - ); - let new_size = pos.size_amount() + size; - let new_avg_price = weighted_average_price( - market.config.size_decimal(), - pos.size_amount(), - pos.average_price(), - size, - price, - ); - - if (collateral.value() > 0) { - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, collateral); }; - } else { - collateral.destroy_zero(); - }; - pos.add_trading_fee(trading_fee); - pos.update_position_size(new_size, new_avg_price, is_long, clock); - - pool.increase_reserve(collateral_token, reserve_amount); - market.config.adjust_oi(is_long, true, size); - market.positions.push_back(pos_id, pos); - filled_position_id = pos_id; - } else { - let pos_size = pos.size_amount(); - if (size > pos_size) { - market.positions.push_back(pos_id, pos); - return_to_user(collateral, user, ctx); - fills = fills + 1; - continue - }; - let remaining_size = pos_size - size; - if (remaining_size > 0 && remaining_size < market.config.min_size()) { - market.positions.push_back(pos_id, pos); - return_to_user(collateral, user, ctx); - fills = fills + 1; - continue - }; - - let (is_profit, pnl) = pos.unrealized_pnl(price); - let realized_pnl = if (size == pos_size) { - pnl - } else { - pnl.mul_u64(size).div(float::from(pos_size)) - }; - let realized_pnl_amount = math::usd_to_amount(realized_pnl, collateral_decimal, collateral_price); - let reduce_notional = math::amount_to_usd(size, market.config.size_decimal(), price); - let reduce_reserve_amount = math::usd_to_amount(reduce_notional, collateral_decimal, collateral_price); - - let (borrow_fee, funding_sign_pos, funding_fee, open_fee) = if (size == pos_size) { - ( - pos.unrealized_borrow_fee(), - pos.unrealized_funding_sign(), - pos.unrealized_funding_fee(), - pos.unrealized_trading_fee(), - ) - } else { - pos.realize_partial_fees(size, pos_size) - }; - - if (collateral.value() > 0) { - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, collateral); }; - } else { - collateral.destroy_zero(); - }; - - if (is_profit) { - let profit = pool.request_collateral(realized_pnl_amount, collateral_price); - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, profit); }; - } else if (realized_pnl_amount > 0) { - let loss = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, realized_pnl_amount) }; - pool.put_collateral(loss, collateral_price); - }; - - let total_fee = trading_fee + borrow_fee + open_fee; - if (total_fee > 0) { - let mut fee_balance = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, total_fee) }; - let protocol_amount = ((fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64); - if (protocol_amount > 0) { - let protocol_fee = fee_balance.split(protocol_amount); - transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); - events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); - }; - pool.put_collateral(fee_balance, collateral_price); - }; - - if (funding_fee > 0) { - if (funding_sign_pos) { - let funding_bal = { let vault = global_config.balance_mut(); pos.withdraw_collateral(vault, funding_fee) }; - pool.put_collateral(funding_bal, collateral_price); - } else { - let funding_income = pool.request_collateral(funding_fee, collateral_price); - { let vault = global_config.balance_mut(); pos.deposit_collateral(vault, funding_income); }; - }; - }; - - pool.decrease_reserve(collateral_token, reduce_reserve_amount); - market.config.adjust_oi(pos.is_long(), false, size); - - filled_fee_amount = if (funding_sign_pos) { total_fee + funding_fee } else { total_fee }; - if (size == pos_size) { - let (mut remaining_collateral, linked_order_ids, linked_order_price_keys) = { - let vault = global_config.balance_mut(); - pos.remove_position(vault) - }; - let withdrawal_coll = cancel_linked_orders_to_balance( - global_config, market, linked_order_ids, linked_order_price_keys, - ); - remaining_collateral.join(withdrawal_coll); - user_account::remove_position(account_registry, user.to_id(), market_id, pos_id); - return_to_user(remaining_collateral, user, ctx); - } else { - let avg_price = pos.average_price(); - let pos_is_long = pos.is_long(); - pos.update_position_size(remaining_size, avg_price, pos_is_long, clock); - market.positions.push_back(pos_id, pos); - }; - - filled_position_id = pos_id; - }; + let reduce_notional = math::amount_to_usd(size, market.config.size_decimal(), price); + let reduce_reserve_amount = math::usd_to_amount( + reduce_notional, + collateral_decimal, + collateral_price, + ); + + let (borrow_fee, funding_sign_pos, funding_fee, open_fee) = if (size == pos_size) { + ( + pos.unrealized_borrow_fee(), + pos.unrealized_funding_sign(), + pos.unrealized_funding_fee(), + pos.unrealized_trading_fee(), + ) } else { - let fill_reserve_amount = validate_position_growth( - global_config, - &market.config, - pool, - is_long, - 0, - size, - collateral.value(), - collateral_decimal, - collateral_token, - price, - collateral_price, - ); + pos.realize_partial_fees(size, pos_size) + }; - let position_id = market.config.increment_next_position_id(); - let pos = position::create_position( - position_id, user, market_id, - is_long, size, market.config.size_decimal(), - collateral, global_config.balance_mut(), collateral_decimal, - price, cumulative_borrow, funding_sign, funding_idx, - trading_fee, clock, ctx, + if (collateral.value() > 0) { + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, collateral); + }; + } else { + collateral.destroy_zero(); + }; + + if (is_profit) { + let profit = pool.request_collateral( + realized_pnl_amount, + collateral_price, + ); + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, profit); + }; + } else if (realized_pnl_amount > 0) { + let loss = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, realized_pnl_amount) + }; + pool.put_collateral(loss, collateral_price); + }; + + let total_fee = trading_fee + borrow_fee + open_fee; + if (total_fee > 0) { + let mut fee_balance = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, total_fee) + }; + let protocol_amount = ( + (fee_balance.value() as u128) * (protocol_share_bps as u128) / 10000 as u64, + ); + if (protocol_amount > 0) { + let protocol_fee = fee_balance.split(protocol_amount); + transfer::public_transfer(protocol_fee.into_coin(ctx), protocol_fee_addr); + events::emit_protocol_fee_collected(protocol_fee_addr, protocol_amount, market_id, now); + }; + pool.put_collateral(fee_balance, collateral_price); + }; + + if (funding_fee > 0) { + if (funding_sign_pos) { + let funding_bal = { + let vault = global_config.balance_mut(); + pos.withdraw_collateral(vault, funding_fee) + }; + pool.put_collateral(funding_bal, collateral_price); + } else { + let funding_income = pool.request_collateral( + funding_fee, + collateral_price, ); - pool.increase_reserve(collateral_token, fill_reserve_amount); - market.config.adjust_oi(is_long, true, size); - market.positions.push_back(position_id, pos); - filled_position_id = position_id; + { + let vault = global_config.balance_mut(); + pos.deposit_collateral(vault, funding_income); + }; + }; }; - let volume = math::amount_to_usd(filled_size_amount, market.config.size_decimal(), price); - events::emit_order_filled( - user, market_id, oid, filled_position_id, - price, filled_size_amount, filled_fee_amount, volume, now, sender_request.address() - ); + pool.decrease_reserve(collateral_token, reduce_reserve_amount); + market.config.adjust_oi(pos.is_long(), false, size); - fills = fills + 1; - }; + filled_fee_amount = if (funding_sign_pos) { total_fee + funding_fee } else { total_fee }; + if (size == pos_size) { + let (mut remaining_collateral, linked_order_ids, linked_order_price_keys) = { + let vault = global_config.balance_mut(); + pos.remove_position(vault) + }; + let withdrawal_coll = cancel_linked_orders_to_balance( + global_config, + market, + linked_order_ids, + linked_order_price_keys, + ); + remaining_collateral.join(withdrawal_coll); + user_account::remove_position(account_registry, user.to_id(), market_id, pos_id); + return_to_user(remaining_collateral, user, ctx); + } else { + let avg_price = pos.average_price(); + let pos_is_long = pos.is_long(); + pos.update_position_size(remaining_size, avg_price, pos_is_long, clock); + market.positions.push_back(pos_id, pos); + }; - if (orders.length() > 0) { - remaining.append(orders); - } else { - orders.destroy_empty(); - }; - if (remaining.length() > 0) { - let orders_map2 = borrow_mut_order_map(market, order_type_tag); - orders_map2.insert(trigger_price_key, remaining); + filled_position_id = pos_id; + }; } else { - remaining.destroy_empty(); + let fill_reserve_amount = validate_position_growth( + global_config, + &market.config, + pool, + is_long, + 0, + size, + collateral.value(), + collateral_decimal, + collateral_token, + price, + collateral_price, + ); + + let position_id = market.config.increment_next_position_id(); + let pos = position::create_position( + position_id, + user, + market_id, + is_long, + size, + market.config.size_decimal(), + collateral, + global_config.balance_mut(), + collateral_decimal, + price, + cumulative_borrow, + funding_sign, + funding_idx, + trading_fee, + clock, + ctx, + ); + pool.increase_reserve(collateral_token, fill_reserve_amount); + market.config.adjust_oi(is_long, true, size); + market.positions.push_back(position_id, pos); + user_account::add_position(account_registry, user.to_id(), market_id, position_id); + filled_position_id = position_id; }; + + let volume = math::amount_to_usd(filled_size_amount, market.config.size_decimal(), price); + events::emit_order_filled( + user, + market_id, + oid, + filled_position_id, + price, + filled_size_amount, + filled_fee_amount, + volume, + now, + sender_request.address(), + ); + + fills = fills + 1; + }; + + if (orders.length() > 0) { + remaining.append(orders); + } else { + orders.destroy_empty(); + }; + if (remaining.length() > 0) { + let orders_map2 = borrow_mut_order_map(market, order_type_tag); + orders_map2.insert(trigger_price_key, remaining); + } else { + remaining.destroy_empty(); + }; } /// Keeper: updates funding rate for a market. public fun update_funding_rate( - global_config: &GlobalConfig, - market: &mut Market, - sender_request: &AccountRequest, - clock: &Clock, + global_config: &GlobalConfig, + market: &mut Market, + sender_request: &AccountRequest, + clock: &Clock, ) { - global_config.verify_keeper(sender_request.address()); - let market_id = object::id(market); - market.config.update_funding_rate(market_id, clock.timestamp_ms()); + global_config.verify_keeper(sender_request.address()); + let market_id = object::id(market); + market.config.update_funding_rate(market_id, clock.timestamp_ms()); } /// Keeper: Creates a request to open a new position for an account by keeper public fun open_position_request_by_keeper( - global_config: &GlobalConfig, - account_registry: &mut AccountRegistry, - market: &Market, - keeper_request: &AccountRequest, - account_object_address: address, - collateral_coin: Coin, - is_long: bool, - size: u64, - acceptable_price: u64, + global_config: &GlobalConfig, + account_registry: &mut AccountRegistry, + market: &Market, + keeper_request: &AccountRequest, + account_object_address: address, + collateral_coin: Coin, + is_long: bool, + size: u64, + acceptable_price: u64, ): TradingRequest { - global_config.assert_version(); - let sender = keeper_request.address(); - global_config.verify_keeper(sender); - if (!account_registry.has_account(account_object_address.to_id())) error::err_sub_account_not_found(); - let market_id = object::id(market); - request::new( - market_id, account_object_address, ACTION_OPEN_POSITION, sender, - is_long, size, collateral_coin.into_balance(), - option::none(), option::none(), false, false, option::none(), 0, - acceptable_price, - ) + global_config.assert_version(); + let sender = keeper_request.address(); + global_config.verify_keeper(sender); + if (!account_registry.has_account(account_object_address.to_id())) + error::err_sub_account_not_found(); + let market_id = object::id(market); + request::new( + market_id, + account_object_address, + ACTION_OPEN_POSITION, + sender, + is_long, + size, + collateral_coin.into_balance(), + option::none(), + option::none(), + false, + false, + option::none(), + 0, + acceptable_price, + ) } /// Creates a request to force-close a position (keeper only, emergency use). /// Reads account_object_address from the position directly, like liquidate_request. public fun close_position_request_by_keeper( - global_config: &GlobalConfig, - market: &mut Market, - keeper_request: &AccountRequest, - position_id: u64, - acceptable_price: u64, + global_config: &GlobalConfig, + market: &mut Market, + keeper_request: &AccountRequest, + position_id: u64, + acceptable_price: u64, ): TradingRequest { - let sender = keeper_request.address(); - global_config.verify_keeper(sender); - market.config.lock_position(position_id); - let market_id = object::id(market); - if (!market.positions.contains(position_id)) error::err_position_not_found(); - let pos_ref: &Position = &market.positions[position_id]; - let account_object_address = pos_ref.account_object_address(); - request::new_no_collateral( - market_id, account_object_address, ACTION_CLOSE_POSITION, sender, - option::some(position_id), 0, acceptable_price, - ) + let sender = keeper_request.address(); + global_config.verify_keeper(sender); + market.config.lock_position(position_id); + let market_id = object::id(market); + if (!market.positions.contains(position_id)) error::err_position_not_found(); + let pos_ref: &Position = &market.positions[position_id]; + let account_object_address = pos_ref.account_object_address(); + request::new_no_collateral( + market_id, + account_object_address, + ACTION_CLOSE_POSITION, + sender, + option::some(position_id), + 0, + acceptable_price, + ) } // === Internal Helpers === fun assert_open_slippage(is_long: bool, price: Float, acceptable_price: u64) { - if (acceptable_price == 0) return; - let price_u64 = price.floor(); - if (is_long) { - if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); - } else { - if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); - }; + if (acceptable_price == 0) return; + let price_u64 = price.floor(); + if (is_long) { + if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); + } else { + if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); + }; } fun assert_close_slippage(is_long: bool, price: Float, acceptable_price: u64) { - if (acceptable_price == 0) return; - let price_u64 = price.floor(); - if (is_long) { - if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); + if (acceptable_price == 0) return; + let price_u64 = price.floor(); + if (is_long) { + if (price_u64 < acceptable_price) error::err_trading_slippage_exceeded(); + } else { + if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); + }; +} + +public(package) fun calculate_total_trading_fee_bps( + market_config: &MarketConfig, + pool: &WlpPool, + execution_price: Float, + order_is_long: bool, + order_size: u64, +): u64 { + market_config.trading_fee_bps() + + calculate_impact_fee_bps(market_config, pool, execution_price, order_is_long, order_size) +} + +fun calculate_impact_fee_bps( + market_config: &MarketConfig, + pool: &WlpPool, + execution_price: Float, + order_is_long: bool, + order_size: u64, +): u64 { + let max_impact_fee_bps = market_config.max_impact_fee_bps(); + if (max_impact_fee_bps == 0 || order_size == 0) return 0; + + let (lp_original_side, lp_original_size) = if ( + market_config.long_oi() > market_config.short_oi() + ) { + (false, market_config.long_oi() - market_config.short_oi()) + } else { + (true, market_config.short_oi() - market_config.long_oi()) + }; + let lp_new_size = if (lp_original_side == order_is_long) { + if (lp_original_size > order_size) { + lp_original_size - order_size } else { - if (price_u64 > acceptable_price) error::err_trading_slippage_exceeded(); - }; + order_size - lp_original_size + } + } else { + lp_original_size + order_size + }; + if (lp_new_size <= lp_original_size) return 0; + + let allocated_lp_exposure_bps = market_config.allocated_lp_exposure_bps(); + if (allocated_lp_exposure_bps == 0) return 0; + + let allocated_exposure_usd = pool + .pool_tvl_usd() + .mul_u64(allocated_lp_exposure_bps) + .div_u64(math::bp_scale()); + if (allocated_exposure_usd.eq(float::zero())) return 0; + + let exposure_change_usd = math::amount_to_usd( + lp_new_size - lp_original_size, + market_config.size_decimal(), + execution_price, + ); + let scaled_ratio = exposure_change_usd + .div(allocated_exposure_usd) + .div_u64(market_config.impact_fee_scale()); + let normalized_ratio = float::min(float::one(), scaled_ratio); + normalized_ratio.pow(market_config.impact_fee_curvature()).mul_u64(max_impact_fee_bps).floor() } fun calculate_position_leverage_bps( - size_decimal: u8, - total_size: u64, - total_collateral_amount: u64, - collateral_decimal: u8, - execution_price: Float, - collateral_price: Float, + size_decimal: u8, + total_size: u64, + total_collateral_amount: u64, + collateral_decimal: u8, + execution_price: Float, + collateral_price: Float, ): u64 { - let total_notional = math::amount_to_usd(total_size, size_decimal, execution_price); - let collateral_usd = math::amount_to_usd(total_collateral_amount, collateral_decimal, collateral_price); - if (collateral_usd.eq(float::zero())) error::err_exceed_max_leverage(); - total_notional.div(collateral_usd).mul_u64(math::bp_scale()).floor() + let total_notional = math::amount_to_usd(total_size, size_decimal, execution_price); + let collateral_usd = math::amount_to_usd( + total_collateral_amount, + collateral_decimal, + collateral_price, + ); + if (collateral_usd.eq(float::zero())) error::err_exceed_max_leverage(); + total_notional.div(collateral_usd).mul_u64(math::bp_scale()).floor() } fun validate_position_growth( - global_config: &GlobalConfig, - market_config: &MarketConfig, - pool: &WlpPool, - is_long: bool, - current_size: u64, - added_size: u64, - total_collateral_amount: u64, - collateral_decimal: u8, - collateral_token: std::type_name::TypeName, - execution_price: Float, - collateral_price: Float, + global_config: &GlobalConfig, + market_config: &MarketConfig, + pool: &WlpPool, + is_long: bool, + current_size: u64, + added_size: u64, + total_collateral_amount: u64, + collateral_decimal: u8, + collateral_token: std::type_name::TypeName, + execution_price: Float, + collateral_price: Float, ): u64 { - let _ = current_size; - let total_size = current_size + added_size; - let leverage_bps = calculate_position_leverage_bps( - market_config.size_decimal(), - total_size, - total_collateral_amount, - collateral_decimal, - execution_price, - collateral_price, - ); - if (leverage_bps > market_config.max_leverage_bps()) error::err_exceed_max_leverage(); - - if (is_long) { - if (market_config.long_oi() + added_size > market_config.max_long_oi()) error::err_exceed_max_open_interest(); - } else { - if (market_config.short_oi() + added_size > market_config.max_short_oi()) error::err_exceed_max_open_interest(); - }; - - let added_notional = math::amount_to_usd(added_size, market_config.size_decimal(), execution_price); - let reserve_amount = math::usd_to_amount(added_notional, collateral_decimal, collateral_price); - let total_oi_after = math::amount_to_usd( - market_config.long_oi() + market_config.short_oi() + added_size, - market_config.size_decimal(), - execution_price, - ); - pool.check_oi_cap(global_config, total_oi_after); - if (!pool.check_reserve_valid(collateral_token, reserve_amount)) error::err_exceed_reserve_ratio(); - reserve_amount + let _ = current_size; + let total_size = current_size + added_size; + let leverage_bps = calculate_position_leverage_bps( + market_config.size_decimal(), + total_size, + total_collateral_amount, + collateral_decimal, + execution_price, + collateral_price, + ); + if (leverage_bps > market_config.max_leverage_bps()) error::err_exceed_max_leverage(); + + if (is_long) { + if (market_config.long_oi() + added_size > market_config.max_long_oi()) + error::err_exceed_max_open_interest(); + } else { + if (market_config.short_oi() + added_size > market_config.max_short_oi()) + error::err_exceed_max_open_interest(); + }; + + let added_notional = math::amount_to_usd( + added_size, + market_config.size_decimal(), + execution_price, + ); + let reserve_amount = math::usd_to_amount(added_notional, collateral_decimal, collateral_price); + let total_oi_after = math::amount_to_usd( + market_config.long_oi() + market_config.short_oi() + added_size, + market_config.size_decimal(), + execution_price, + ); + pool.check_oi_cap(global_config, total_oi_after); + if (!pool.check_reserve_valid(collateral_token, reserve_amount)) + error::err_exceed_reserve_ratio(); + reserve_amount } fun weighted_average_price( - size_decimal: u8, - current_size: u64, - current_average_price: Float, - add_size: u64, - fill_price: Float, + size_decimal: u8, + current_size: u64, + current_average_price: Float, + add_size: u64, + fill_price: Float, ): Float { - let current_notional = math::amount_to_usd(current_size, size_decimal, current_average_price); - let added_notional = math::amount_to_usd(add_size, size_decimal, fill_price); - current_notional.add(added_notional) - .div(float::from(current_size + add_size)) - .mul_u64(10u64.pow(size_decimal)) + let current_notional = math::amount_to_usd(current_size, size_decimal, current_average_price); + let added_notional = math::amount_to_usd(add_size, size_decimal, fill_price); + current_notional + .add(added_notional) + .div(float::from(current_size + add_size)) + .mul_u64(10u64.pow(size_decimal)) } fun assert_position_owner(position: &Position, account_object_address: address) { - if (position.account_object_address() != account_object_address) { - error::err_user_mismatch(); - }; + if (position.account_object_address() != account_object_address) { + error::err_user_mismatch(); + }; } fun borrow_owned_position_mut( - market: &mut Market, - position_id: u64, - account_object_address: address, + market: &mut Market, + position_id: u64, + account_object_address: address, ): &mut Position { - if (!market.positions.contains(position_id)) error::err_position_not_found(); - let pos: &mut Position = &mut market.positions[position_id]; - assert_position_owner(pos, account_object_address); - pos + if (!market.positions.contains(position_id)) error::err_position_not_found(); + let pos: &mut Position = &mut market.positions[position_id]; + assert_position_owner(pos, account_object_address); + pos } fun take_owned_position( - market: &mut Market, - position_id: u64, - account_object_address: address, - now: u64, - require_cooldown: bool, + market: &mut Market, + position_id: u64, + account_object_address: address, + now: u64, + require_cooldown: bool, ): Position { - if (!market.positions.contains(position_id)) error::err_position_not_found(); - { - let pos_ref: &Position = &market.positions[position_id]; - assert_position_owner(pos_ref, account_object_address); - if (require_cooldown) { - let cooldown = market.config.cooldown_ms(); - if (cooldown > 0) { - if (now < pos_ref.update_timestamp() + cooldown) error::err_cooldown_not_elapsed(); - }; - }; + if (!market.positions.contains(position_id)) error::err_position_not_found(); + { + let pos_ref: &Position = &market.positions[position_id]; + assert_position_owner(pos_ref, account_object_address); + if (require_cooldown) { + let cooldown = market.config.cooldown_ms(); + if (cooldown > 0) { + if (now < pos_ref.update_timestamp() + cooldown) error::err_cooldown_not_elapsed(); + }; }; - market.positions.swap_remove_by_key(position_id) + }; + market.positions.swap_remove_by_key(position_id) } fun take_linked_order_at_key( - market: &mut Market, - trigger_price_key: u64, - order_id: u64, + market: &mut Market, + trigger_price_key: u64, + order_id: u64, ): Option { - let mut tag: u8 = 0; - while (tag <= 3) { - let orders_map = borrow_mut_order_map(market, tag); - if (orders_map.contains(&trigger_price_key)) { - let orders = orders_map.get_mut(&trigger_price_key); - let mut i = 0; - while (i < orders.length()) { - if (orders[i].order_id() == order_id) { - let order = orders.swap_remove(i); - if (orders.length() == 0) { - let (_, empty) = orders_map.remove(&trigger_price_key); - empty.destroy_empty(); - }; - return option::some(order) - }; - i = i + 1; - }; + let mut tag: u8 = 0; + while (tag <= 3) { + let orders_map = borrow_mut_order_map(market, tag); + if (orders_map.contains(&trigger_price_key)) { + let orders = orders_map.get_mut(&trigger_price_key); + let mut i = 0; + while (i < orders.length()) { + if (orders[i].order_id() == order_id) { + let order = orders.swap_remove(i); + if (orders.length() == 0) { + let (_, empty) = orders_map.remove(&trigger_price_key); + empty.destroy_empty(); + }; + return option::some(order) }; - tag = tag + 1; + i = i + 1; + }; }; - option::none() + tag = tag + 1; + }; + option::none() } fun cancel_linked_orders_to_balance( - global_config: &mut GlobalConfig, - market: &mut Market, - mut linked_order_ids: vector, - mut linked_order_price_keys: vector, + global_config: &mut GlobalConfig, + market: &mut Market, + mut linked_order_ids: vector, + mut linked_order_price_keys: vector, ): Balance { - let mut withdrawal_coll = balance::zero(); - while (linked_order_ids.length() > 0) { - let order_id = linked_order_ids.pop_back(); - let price_key = linked_order_price_keys.pop_back(); - let order_opt = take_linked_order_at_key(market, price_key, order_id); - if (order_opt.is_some()) { - let order = order_opt.destroy_some(); - let collateral = { let vault = global_config.balance_mut(); order.remove_order(vault) }; - withdrawal_coll.join(collateral); - } else { - order_opt.destroy_none(); - }; + let mut withdrawal_coll = balance::zero(); + while (linked_order_ids.length() > 0) { + let order_id = linked_order_ids.pop_back(); + let price_key = linked_order_price_keys.pop_back(); + let order_opt = take_linked_order_at_key(market, price_key, order_id); + if (order_opt.is_some()) { + let order = order_opt.destroy_some(); + let collateral = { + let vault = global_config.balance_mut(); + order.remove_order(vault) + }; + withdrawal_coll.join(collateral); + } else { + order_opt.destroy_none(); }; - linked_order_ids.destroy_empty(); - linked_order_price_keys.destroy_empty(); - withdrawal_coll + }; + linked_order_ids.destroy_empty(); + linked_order_price_keys.destroy_empty(); + withdrawal_coll } fun borrow_mut_order_map( - market: &mut Market, - tag: u8, + market: &mut Market, + tag: u8, ): &mut VecMap> { - if (tag == 0) &mut market.limit_buys - else if (tag == 1) &mut market.limit_sells - else if (tag == 2) &mut market.stop_buys - else &mut market.stop_sells + if (tag == 0) &mut market.limit_buys + else if (tag == 1) &mut market.limit_sells + else if (tag == 2) &mut market.stop_buys + else &mut market.stop_sells } fun take_order_direct( - market: &mut Market, - trigger_price_key: u64, - order_id: u64, - user: address, - order_type_tag: u8, + market: &mut Market, + trigger_price_key: u64, + order_id: u64, + user: address, + order_type_tag: u8, ): Option { - let orders_map = borrow_mut_order_map(market, order_type_tag); - if (orders_map.contains(&trigger_price_key)) { - let orders = orders_map.get_mut(&trigger_price_key); - let mut i = 0; - while (i < orders.length()) { - if (orders[i].order_id() == order_id && orders[i].account_object_address() == user) { - let order = orders.swap_remove(i); - if (orders.length() == 0) { - let (_, empty) = orders_map.remove(&trigger_price_key); - empty.destroy_empty(); - }; - return option::some(order) - }; - i = i + 1; + let orders_map = borrow_mut_order_map(market, order_type_tag); + if (orders_map.contains(&trigger_price_key)) { + let orders = orders_map.get_mut(&trigger_price_key); + let mut i = 0; + while (i < orders.length()) { + if (orders[i].order_id() == order_id && orders[i].account_object_address() == user) { + let order = orders.swap_remove(i); + if (orders.length() == 0) { + let (_, empty) = orders_map.remove(&trigger_price_key); + empty.destroy_empty(); }; + return option::some(order) + }; + i = i + 1; }; - option::none() + }; + option::none() } fun return_to_user(balance: Balance, user: address, ctx: &mut TxContext) { - if (balance.value() > 0) { - transfer::public_transfer(balance.into_coin(ctx), user); - } else { - balance.destroy_zero(); - }; + if (balance.value() > 0) { + transfer::public_transfer(balance.into_coin(ctx), user); + } else { + balance.destroy_zero(); + }; } // === Package-Level Accessors (for view.move) === /// Returns the object ID of this Market (trading vault). public(package) fun market_id(market: &Market): ID { - object::id(market) + object::id(market) } /// Borrows a position by ID. -public(package) fun borrow_position(market: &Market, position_id: u64): &Position { - let pos: &Position = &market.positions[position_id]; - pos +public(package) fun borrow_position( + market: &Market, + position_id: u64, +): &Position { + let pos: &Position = &market.positions[position_id]; + pos } /// Checks if a position exists. -public(package) fun has_position(market: &Market, position_id: u64): bool { - market.positions.contains(position_id) +public(package) fun has_position( + market: &Market, + position_id: u64, +): bool { + market.positions.contains(position_id) } /// Borrows the market config (for view.move). -public(package) fun borrow_config(market: &Market): &MarketConfig { - &market.config +public(package) fun borrow_config( + market: &Market, +): &MarketConfig { + &market.config } /// Borrows the market config mutably (for tests only). #[test_only] -public fun borrow_config_mut_for_testing(market: &mut Market): &mut MarketConfig { - &mut market.config +public fun borrow_config_mut_for_testing( + market: &mut Market, +): &mut MarketConfig { + &mut market.config } #[test_only] public fun remove_position_for_testing( - global_config: &mut GlobalConfig, - market: &mut Market, - position_id: u64, + global_config: &mut GlobalConfig, + market: &mut Market, + position_id: u64, ): (Balance, vector, vector) { - let pos: Position = market.positions.swap_remove_by_key(position_id); - let vault = global_config.balance_mut(); - pos.remove_position(vault) + let pos: Position = market.positions.swap_remove_by_key(position_id); + let vault = global_config.balance_mut(); + pos.remove_position(vault) } // === Test Helpers === #[test_only] public fun create_market_for_testing( - max_leverage_bps: u64, - min_size: u64, - lot_size: u64, - trading_fee_bps: u64, - size_decimal: u8, - maintenance_margin_bps: u64, - max_long_oi: u64, - max_short_oi: u64, - cooldown_ms: u64, - basic_funding_rate_bps: u64, - funding_interval_ms: u64, - clock: &Clock, - ctx: &mut TxContext, + max_leverage_bps: u64, + min_size: u64, + lot_size: u64, + trading_fee_bps: u64, + size_decimal: u8, + maintenance_margin_bps: u64, + max_long_oi: u64, + max_short_oi: u64, + cooldown_ms: u64, + basic_funding_rate_bps: u64, + funding_interval_ms: u64, + clock: &Clock, + ctx: &mut TxContext, ): Market { - let cap = waterx_perp::admin::create_admin_cap_for_testing(ctx); - let market = create_market( - &cap, - max_leverage_bps, - min_size, - lot_size, - trading_fee_bps, - size_decimal, - maintenance_margin_bps, - max_long_oi, - max_short_oi, - cooldown_ms, - basic_funding_rate_bps, - funding_interval_ms, - clock, - ctx, - ); - std::unit_test::destroy(cap); - market + let cap = waterx_perp::admin::create_admin_cap_for_testing(ctx); + let market = create_market( + &cap, + max_leverage_bps, + min_size, + lot_size, + trading_fee_bps, + size_decimal, + maintenance_margin_bps, + max_long_oi, + max_short_oi, + cooldown_ms, + basic_funding_rate_bps, + funding_interval_ms, + clock, + ctx, + ); + std::unit_test::destroy(cap); + market } diff --git a/contracts/waterx_perp/sources/view.move b/contracts/waterx_perp/sources/view.move index 613d6a7..a8a8246 100644 --- a/contracts/waterx_perp/sources/view.move +++ b/contracts/waterx_perp/sources/view.move @@ -55,6 +55,10 @@ public struct MarketSummary has copy, drop { max_short_oi: u64, max_leverage_bps: u64, trading_fee_bps: u64, + max_impact_fee_bps: u64, + allocated_lp_exposure_bps: u64, + impact_fee_curvature: u64, + impact_fee_scale: u64, maintenance_margin_bps: u64, size_decimal: u8, min_size: u64, @@ -153,6 +157,10 @@ public fun market_summary( max_short_oi: m.max_short_oi(), max_leverage_bps: m.max_leverage_bps(), trading_fee_bps: m.trading_fee_bps(), + max_impact_fee_bps: m.max_impact_fee_bps(), + allocated_lp_exposure_bps: m.allocated_lp_exposure_bps(), + impact_fee_curvature: m.impact_fee_curvature(), + impact_fee_scale: m.impact_fee_scale(), maintenance_margin_bps: m.maintenance_margin_bps(), size_decimal: m.size_decimal(), min_size: m.min_size(), diff --git a/contracts/waterx_perp/tests/market_tests.move b/contracts/waterx_perp/tests/market_tests.move index 7542c37..27c91ac 100644 --- a/contracts/waterx_perp/tests/market_tests.move +++ b/contracts/waterx_perp/tests/market_tests.move @@ -49,6 +49,10 @@ fun create_market_and_check_config() { assert!(config.is_active()); assert!(config.max_leverage_bps() == 1_000_000); assert!(config.trading_fee_bps() == 5); + assert!(config.max_impact_fee_bps() == 5); + assert!(config.allocated_lp_exposure_bps() == 2_000); + assert!(config.impact_fee_curvature() == 2); + assert!(config.impact_fee_scale() == 1); assert!(config.maintenance_margin_bps() == 150); assert!(config.long_oi() == 0); assert!(config.short_oi() == 0); @@ -86,6 +90,10 @@ fun update_market_config() { option::none(), option::none(), option::some(10), // 10 bps fee + option::some(20), // 20 bps max impact fee + option::some(3_000), // 30% allocated LP exposure + option::some(3), // cubic curve + option::some(2), // divide ratio by 2 before curvature option::none(), option::none(), option::none(), @@ -97,6 +105,10 @@ fun update_market_config() { let config = trading::borrow_config(&market); assert!(config.max_leverage_bps() == 2_000_000); assert!(config.trading_fee_bps() == 10); + assert!(config.max_impact_fee_bps() == 20); + assert!(config.allocated_lp_exposure_bps() == 3_000); + assert!(config.impact_fee_curvature() == 3); + assert!(config.impact_fee_scale() == 2); destroy(admin_cap); destroy(test_clock); diff --git a/contracts/waterx_perp/tests/position_tests.move b/contracts/waterx_perp/tests/position_tests.move index e659249..d2407f4 100644 --- a/contracts/waterx_perp/tests/position_tests.move +++ b/contracts/waterx_perp/tests/position_tests.move @@ -8,6 +8,7 @@ use std::unit_test::destroy; use bucket_v2_framework::float; use bucket_v2_framework::double; +use waterx_perp::math; use waterx_perp::position; // Test coin type @@ -354,7 +355,7 @@ fun liquidation_check_undercollateralized() { &pos, float::from_fraction(98, 100), // $0.98 base price float::from(1), // $1 collateral price (USDC) - 0, // closing fee bps + float::zero(), // closing fee 150, // 150 bps = 1.5% of notional = $1.47 margin 0, true, double::zero(), @@ -366,7 +367,7 @@ fun liquidation_check_undercollateralized() { &pos, float::from(1), // base price float::from(1), // collateral price - 0, // closing fee bps + float::zero(), // closing fee 150, 0, true, double::zero(), @@ -409,7 +410,7 @@ fun liquidation_check_respects_trading_fee_bps() { &pos, current_price, collateral_price, - 5, + math::fee_from_bps(float::from(50_000), 5), maintenance_bps, 0, true, @@ -421,7 +422,7 @@ fun liquidation_check_respects_trading_fee_bps() { &pos, current_price, collateral_price, - 10, + math::fee_from_bps(float::from(50_000), 10), maintenance_bps, 0, true, diff --git a/contracts/waterx_perp/tests/trading_tests.move b/contracts/waterx_perp/tests/trading_tests.move index fb6d47f..ddda70b 100644 --- a/contracts/waterx_perp/tests/trading_tests.move +++ b/contracts/waterx_perp/tests/trading_tests.move @@ -24,6 +24,7 @@ use waterx_perp::lp_pool::{Self, WlpPool}; use waterx_perp::trading::{Self, Market}; use waterx_perp::user_account::{Self, AccountRegistry}; use waterx_perp::position; +use waterx_perp::response; // =================================================================== // Test token types @@ -138,6 +139,49 @@ fun deposit_to_account( coin_id } +// =================================================================== +// Test 0: Impact fee reaches max when a trade materially increases LP exposure +// =================================================================== + +#[test] +fun test_impact_fee_applies_only_when_lp_exposure_worsens() { + let user = @0xA0; + let mut scenario = test_scenario::begin(user); + + global_config::init_for_testing(scenario.ctx()); + scenario.next_tx(user); + + let mut global_cfg = scenario.take_shared(); + let mut test_clock = clock::create_for_testing(scenario.ctx()); + clock::set_for_testing(&mut test_clock, 3_600_000); + + let pool = setup_pool_with_liquidity(&mut scenario, &global_cfg, &test_clock, 100_000_000_000); + let mut market = create_test_market(&mut global_cfg, &test_clock, scenario.ctx()); + + let opening_fee_bps = { + let config = trading::borrow_config(&market); + trading::calculate_total_trading_fee_bps(config, &pool, float::from(50_000), true, 1_000_000_000) + }; + assert!(opening_fee_bps == 10, 0); + + { + let config = trading::borrow_config_mut_for_testing(&mut market); + config.adjust_oi(true, true, 1_000_000_000); + }; + + let closing_fee_bps = { + let config = trading::borrow_config(&market); + trading::calculate_total_trading_fee_bps(config, &pool, float::from(50_000), false, 1_000_000_000) + }; + assert!(closing_fee_bps == 5, 1); + + destroy(pool); + destroy(market); + test_scenario::return_shared(global_cfg); + destroy(test_clock); + scenario.end(); +} + // =================================================================== // Test 1: Open position collateral deducted, position.collateral == collateral - open_fee // =================================================================== @@ -229,11 +273,12 @@ fun test_open_position_deducts_collateral() { assert!(config.short_oi() == 0, 3); // Verify position 0 exists and collateral_amount equals input minus open_fee - // open_fee = notional_usd / collateral_price * fee_bps / 10000 + // open_fee = notional_usd / collateral_price * total_fee_bps / 10000 // notional = 1 BTC * $50000 = $50000 - // open_fee = $50000 * 5 / 10000 = $25 = 25_000_000 raw USDC (6 dec) + // opening into an empty market hits max impact: total_fee_bps = 10 + // open_fee = $50000 * 10 / 10000 = $50 = 50_000_000 raw USDC (6 dec) let pos = trading::borrow_position(&market, 0); - let expected_open_fee: u64 = 25_000_000; // $25 in 6-dec USDC + let expected_open_fee: u64 = 50_000_000; // $50 in 6-dec USDC // collateral_amount stored in position = original - (nothing removed; fee tracked separately) // unrealized_trading_fee holds the open fee assert!(pos.unrealized_trading_fee() == expected_open_fee, 4); @@ -392,10 +437,10 @@ fun test_close_position_flat_pnl() { destroy(c2); // PnL = 0 ? user should receive collateral minus open_fee + close_fee - // open_fee = $25 = 25_000_000 raw USDC + // open_fee = $50 = 50_000_000 raw USDC (base + max impact on first trade) // close_fee = $25 = 25_000_000 raw USDC - // Expected return = 2_000_000_000 - 25_000_000 - 25_000_000 = 1_950_000_000 - let expected_return: u64 = 1_950_000_000; + // Expected return = 2_000_000_000 - 50_000_000 - 25_000_000 = 1_925_000_000 + let expected_return: u64 = 1_925_000_000; scenario.next_tx(user); let returned_coin = scenario.take_from_address>(account_obj_id); @@ -486,12 +531,12 @@ fun test_close_long_position_with_profit() { destroy(c2); // PnL = $5000 = 5_000_000_000 raw USDC - // open_fee (at $50k): notional=$50000 ? fee=$25 = 25_000_000 + // open_fee (at $50k): first trade hits max impact, fee=$50 = 50_000_000 // close_fee (at $55k): notional=$55000 ? fee=$27.50 = 27_500_000 // user receives = collateral + pnl - open_fee - close_fee - // = 2_000_000_000 + 5_000_000_000 - 25_000_000 - 27_500_000 - // = 6_947_500_000 - let expected_return: u64 = 6_947_500_000; + // = 2_000_000_000 + 5_000_000_000 - 50_000_000 - 27_500_000 + // = 6_922_500_000 + let expected_return: u64 = 6_922_500_000; scenario.next_tx(user); let returned_coin = scenario.take_from_address>(account_obj_id); @@ -578,11 +623,11 @@ fun test_close_long_position_with_loss() { destroy(c2); // Loss = $5000 = 5_000_000_000 raw USDC - // open_fee = $25 = 25_000_000 + // open_fee = $50 = 50_000_000 // close_fee = $22.50 = 22_500_000 - // user receives = 10_000_000_000 - 5_000_000_000 - 25_000_000 - 22_500_000 - // = 4_952_500_000 - let expected_return: u64 = 4_952_500_000; + // user receives = 10_000_000_000 - 5_000_000_000 - 50_000_000 - 22_500_000 + // = 4_927_500_000 + let expected_return: u64 = 4_927_500_000; scenario.next_tx(user); let returned_coin = scenario.take_from_address>(account_obj_id); @@ -674,11 +719,11 @@ fun test_close_short_position_with_profit() { destroy(c2); // PnL = $5000 profit - // open_fee = $50000 * 5/10000 = $25 = 25_000_000 + // open_fee = $50000 * 10/10000 = $50 = 50_000_000 // close_fee = $45000 * 5/10000 = $22.5 = 22_500_000 - // user receives = 2_000_000_000 + 5_000_000_000 - 25_000_000 - 22_500_000 - // = 6_952_500_000 - let expected_return: u64 = 6_952_500_000; + // user receives = 2_000_000_000 + 5_000_000_000 - 50_000_000 - 22_500_000 + // = 6_927_500_000 + let expected_return: u64 = 6_927_500_000; scenario.next_tx(user); let returned_coin = scenario.take_from_address>(account_obj_id); @@ -769,17 +814,17 @@ fun test_close_position_splits_protocol_fee() { destroy(c2); // Verify protocol fee address received 30% of (open_fee + close_fee) - // total_fees = open_fee + close_fee = 25_000_000 + 25_000_000 = 50_000_000 - // protocol_amount = 50_000_000 * 3000 / 10000 = 15_000_000 - let expected_protocol_fee: u64 = 15_000_000; + // total_fees = open_fee + close_fee = 50_000_000 + 25_000_000 = 75_000_000 + // protocol_amount = 75_000_000 * 3000 / 10000 = 22_500_000 + let expected_protocol_fee: u64 = 22_500_000; scenario.next_tx(user); let protocol_coin = scenario.take_from_address>(protocol_fee_addr); assert!(protocol_coin.value() == expected_protocol_fee, 1); - // Verify user received collateral - total_fees = 2_000_000_000 - 50_000_000 = 1_950_000_000 + // Verify user received collateral - total_fees = 2_000_000_000 - 75_000_000 = 1_925_000_000 let user_coin = scenario.take_from_address>(account_obj_id); - assert!(user_coin.value() == 1_950_000_000, 2); + assert!(user_coin.value() == 1_925_000_000, 2); destroy(protocol_coin); destroy(user_coin); @@ -791,6 +836,120 @@ fun test_close_position_splits_protocol_fee() { scenario.end(); } +// =================================================================== +// Test 8: Liquidation routes only insurance fee and pool collateral in phase 1 +// =================================================================== + +#[test] +fun test_liquidation_routes_collateral_to_insurance_and_wlp_only() { + let admin = @0xA8; + let victim = @0xA9; + let insurance_addr = @0xBEEF; + let protocol_fee_addr = @0xFEE; + let mut scenario = test_scenario::begin(admin); + + global_config::init_for_testing(scenario.ctx()); + user_account::init_for_testing(scenario.ctx()); + scenario.next_tx(admin); + + let mut global_cfg = scenario.take_shared(); + let registry = scenario.take_shared(); + let mut test_clock = clock::create_for_testing(scenario.ctx()); + clock::set_for_testing(&mut test_clock, 3_600_000); + + let admin_cap = admin::create_admin_cap_for_testing(scenario.ctx()); + global_config::set_fee_address(&admin_cap, &mut global_cfg, protocol_fee_addr); + global_config::set_protocol_fee_share(&admin_cap, &mut global_cfg, 3000); + global_config::set_insurance_address(&admin_cap, &mut global_cfg, insurance_addr); + global_config::set_liquidator_fee_bps(&admin_cap, &mut global_cfg, 0); + global_config::set_insurance_fee_bps(&admin_cap, &mut global_cfg, 100); + destroy(admin_cap); + + let mut pool = setup_pool_with_liquidity(&mut scenario, &global_cfg, &test_clock, 100_000_000_000); + let mut market = create_test_market(&mut global_cfg, &test_clock, scenario.ctx()); + + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + scenario.next_tx(victim); + + let global_cfg = scenario.take_shared(); + let mut registry = scenario.take_shared(); + let victim_account_id = user_account::create_account( + &mut registry, b"victim".to_string(), &test_clock, scenario.ctx(), + ); + let victim_account_object_address = user_account::account_object_id(®istry, victim_account_id); + let collateral_amount = 5_000_000_000u64; // 5,000 USDC + let open_coin_id = deposit_to_account(&mut scenario, ®istry, victim_account_id, collateral_amount); + + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + scenario.next_tx(victim); + + let mut global_cfg = scenario.take_shared(); + let mut registry = scenario.take_shared(); + let open_btc_price = oracle_result::new_for_testing(float::from(50_000)); + let usdc_price = oracle_result::new_for_testing(float::from(1)); + + let victim_req = account::request(scenario.ctx()); + let open_req = trading::open_position_request( + &global_cfg, + &mut registry, + &market, + &victim_req, + victim_account_object_address, + vector[test_scenario::receiving_ticket_by_id>(open_coin_id)], + collateral_amount, + true, + 1_000_000_000, + 0, + scenario.ctx(), + ); + let (open_change, open_resp) = trading::execute( + &mut global_cfg, &mut registry, &mut market, &mut pool, + open_req, &open_btc_price, &usdc_price, &test_clock, scenario.ctx(), + ); + trading::destroy_response(&global_cfg, &mut market, open_resp); + destroy(open_change); + + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + scenario.next_tx(admin); + + let mut global_cfg = scenario.take_shared(); + let mut registry = scenario.take_shared(); + let keeper_req = account::request(scenario.ctx()); + let pool_before = lp_pool::token_pool_info(&pool, with_defining_ids()).liquidity_amount(); + let liq_btc_price = oracle_result::new_for_testing(float::from(40_000)); + + let liq_req = trading::liquidate_request( + &global_cfg, &mut market, &keeper_req, 0, + ); + let (liq_change, liq_resp) = trading::execute( + &mut global_cfg, &mut registry, &mut market, &mut pool, + liq_req, &liq_btc_price, &usdc_price, &test_clock, scenario.ctx(), + ); + assert!(response::fee_amount(&liq_resp) == 0, 1); + trading::destroy_response(&global_cfg, &mut market, liq_resp); + destroy(liq_change); + + let pool_after = lp_pool::token_pool_info(&pool, with_defining_ids()).liquidity_amount(); + let expected_insurance_amount: u64 = 400_000_000; // 1% of $40,000 liquidation notional + let expected_pool_increase: u64 = collateral_amount - expected_insurance_amount; + assert!(pool_after - pool_before == expected_pool_increase, 2); + + scenario.next_tx(admin); + let insurance_coin = scenario.take_from_address>(insurance_addr); + assert!(insurance_coin.value() == expected_insurance_amount, 3); + + destroy(insurance_coin); + destroy(pool); + destroy(market); + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + destroy(test_clock); + scenario.end(); +} + // =================================================================== // Test 8: WLP TVL decreases proportionally on redeem (50%) // =================================================================== @@ -1041,12 +1200,12 @@ fun test_position_is_liquidatable_at_threshold() { ); let usdc_price = float::from(1); // $1 USDC - let closing_fee_bps = 0u64; + let closing_fee = float::zero(); // At entry price: not liquidatable let maintenance_bps = 150u64; let not_liq = position::is_liquidatable( - &pos, entry_price, usdc_price, closing_fee_bps, maintenance_bps, + &pos, entry_price, usdc_price, closing_fee, maintenance_bps, 0, true, double::zero(), ); assert!(not_liq == false, 1); @@ -1064,7 +1223,7 @@ fun test_position_is_liquidatable_at_threshold() { let liq_price = float::from(40_000); // -20% ? $10,000 loss >> $5,000 collateral let is_liq = position::is_liquidatable( - &pos, liq_price, usdc_price, closing_fee_bps, maintenance_bps, + &pos, liq_price, usdc_price, closing_fee, maintenance_bps, 0, true, double::zero(), ); assert!(is_liq == true, 2); @@ -1073,7 +1232,7 @@ fun test_position_is_liquidatable_at_threshold() { // remaining = $5000 - $4999 = $1 < maintenance margin $750 ? liquidatable let edge_price = float::from(45_001); let is_edge_liq = position::is_liquidatable( - &pos, edge_price, usdc_price, closing_fee_bps, maintenance_bps, + &pos, edge_price, usdc_price, closing_fee, maintenance_bps, 0, true, double::zero(), ); assert!(is_edge_liq == true, 3); @@ -1612,6 +1771,96 @@ fun test_linked_limit_add_merges_into_position() { scenario.end(); } +#[test] +fun test_standalone_limit_fill_tracks_position_in_account_registry() { + let user = @0xAF; + let mut scenario = test_scenario::begin(user); + + global_config::init_for_testing(scenario.ctx()); + user_account::init_for_testing(scenario.ctx()); + scenario.next_tx(user); + + let mut global_cfg = scenario.take_shared(); + let mut registry = scenario.take_shared(); + let mut test_clock = clock::create_for_testing(scenario.ctx()); + clock::set_for_testing(&mut test_clock, 3_600_000); + + let mut pool = setup_pool_with_liquidity(&mut scenario, &global_cfg, &test_clock, 200_000_000_000); + let mut market = create_test_market(&mut global_cfg, &test_clock, scenario.ctx()); + let account_id = user_account::create_account( + &mut registry, b"main".to_string(), &test_clock, scenario.ctx(), + ); + let account_object_address = user_account::account_object_id(®istry, account_id); + + let order_coin_id = deposit_to_account(&mut scenario, ®istry, account_id, 1_000_000_000); + + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + scenario.next_tx(user); + + let mut global_cfg = scenario.take_shared(); + let mut registry = scenario.take_shared(); + let btc_price = oracle_result::new_for_testing(float::from(50_000)); + let usdc_price = oracle_result::new_for_testing(float::from(1)); + + let sender_req = account::request(scenario.ctx()); + let order_req = trading::place_order_request( + &global_cfg, + &mut registry, + &market, + &sender_req, + account_object_address, + vector[test_scenario::receiving_ticket_by_id>(order_coin_id)], + 1_000_000_000, + true, + false, + false, + 1_000_000_000, + float::from(50_000), + option::none(), + scenario.ctx(), + ); + let (c1, order_resp) = trading::execute( + &mut global_cfg, &mut registry, &mut market, &mut pool, + order_req, &btc_price, &usdc_price, &test_clock, scenario.ctx(), + ); + trading::destroy_response(&global_cfg, &mut market, order_resp); + destroy(c1); + + trading::match_orders( + &mut global_cfg, + &mut registry, + &mut market, + &mut pool, + &sender_req, + &btc_price, + &usdc_price, + 0, + 50_000, + 10, + &test_clock, + scenario.ctx(), + ); + + let pos = trading::borrow_position(&market, 0); + assert!(pos.account_object_address() == account_object_address, 1); + assert!(pos.collateral_amount() == 1_000_000_000, 2); + assert!(pos.is_long(), 3); + + let account = user_account::borrow_account(®istry, account_id); + let market_id = object::id(&market); + assert!(user_account::account_positions(account).contains(&market_id), 4); + assert!(user_account::account_positions(account)[&market_id].length() == 1, 5); + assert!(user_account::account_positions(account)[&market_id][0] == 0, 6); + + destroy(pool); + destroy(market); + test_scenario::return_shared(global_cfg); + test_scenario::return_shared(registry); + destroy(test_clock); + scenario.end(); +} + // =================================================================== // Test 16: Linked reduce fully closes and cancels remaining linked orders // ===================================================================