├── programs └── perpetual-dex │ ├── src │ ├── events │ │ ├── mod.rs │ │ └── events.rs │ ├── utils │ │ ├── mod.rs │ │ ├── liquidation.rs │ │ ├── margin.rs │ │ └── calculations.rs │ ├── states │ │ ├── mod.rs │ │ ├── global_config.rs │ │ ├── user.rs │ │ ├── order.rs │ │ ├── market.rs │ │ └── position.rs │ ├── instructions │ │ ├── mod.rs │ │ ├── update_price.rs │ │ ├── withdraw_collateral.rs │ │ ├── initialize.rs │ │ ├── deposit_collateral.rs │ │ ├── cancel_order.rs │ │ ├── add_margin.rs │ │ ├── create_market.rs │ │ ├── remove_margin.rs │ │ ├── close_position.rs │ │ ├── place_order.rs │ │ ├── open_position.rs │ │ └── liquidate_position.rs │ ├── errors.rs │ ├── lib.rs │ └── consts.rs │ └── Cargo.toml ├── .prettierignore ├── .gitignore ├── Cargo.toml ├── Anchor.toml ├── package.json ├── tsconfig.json ├── tests └── perpetual-dex.ts └── README.md /programs/perpetual-dex/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | 3 | pub use events::*; 4 | 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | node_modules 5 | dist 6 | build 7 | test-ledger 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | node_modules 5 | dist 6 | build 7 | test-ledger 8 | *.log 9 | .idea 10 | .vscode 11 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod calculations; 2 | pub mod margin; 3 | pub mod liquidation; 4 | 5 | pub use calculations::*; 6 | pub use margin::*; 7 | pub use liquidation::*; 8 | 9 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod global_config; 2 | pub mod market; 3 | pub mod user; 4 | pub mod position; 5 | pub mod order; 6 | 7 | pub use global_config::*; 8 | pub use market::*; 9 | pub use user::*; 10 | pub use position::*; 11 | pub use order::*; 12 | 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | resolver = "2" 6 | 7 | [profile.release] 8 | overflow-checks = true 9 | lto = "fat" 10 | codegen-units = 1 11 | [profile.release.build-override] 12 | opt-level = 3 13 | incremental = false 14 | codegen-units = 1 15 | 16 | -------------------------------------------------------------------------------- /programs/perpetual-dex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "perpetual-dex" 3 | version = "0.1.0" 4 | description = "Perpetual DEX with Orderbook, Margin Trading, and Leverage on Solana" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "perpetual_dex" 10 | 11 | [features] 12 | default = [] 13 | cpi = ["no-entrypoint"] 14 | no-entrypoint = [] 15 | no-idl = [] 16 | no-log-ix-name = [] 17 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 18 | 19 | [dependencies] 20 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"]} 21 | anchor-spl = { version = "0.30.1", features = ["metadata", "token_2022_extensions"]} 22 | solana-program = "1.18.18" 23 | spl-token = "=4.0.3" 24 | 25 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | anchor_version = "0.30.1" 3 | 4 | [features] 5 | resolution = true 6 | skip-lint = false 7 | 8 | [programs.localnet] 9 | perpetual_dex = "PERP1111111111111111111111111111111111111111" 10 | 11 | [programs.devnet] 12 | perpetual_dex = "PERP1111111111111111111111111111111111111111" 13 | 14 | [programs.mainnet] 15 | perpetual_dex = "PERP1111111111111111111111111111111111111111" 16 | 17 | [registry] 18 | url = "https://api.apr.dev" 19 | 20 | [provider] 21 | cluster = "localnet" 22 | wallet = "~/.config/solana/id.json" 23 | 24 | [scripts] 25 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/perpetual-dex.ts" 26 | 27 | [test] 28 | startup_wait = 3000 29 | shutdown_wait = 2000 30 | upgradeable = false 31 | 32 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod initialize; 2 | pub mod create_market; 3 | pub mod place_order; 4 | pub mod cancel_order; 5 | pub mod open_position; 6 | pub mod close_position; 7 | pub mod add_margin; 8 | pub mod remove_margin; 9 | pub mod liquidate_position; 10 | pub mod deposit_collateral; 11 | pub mod withdraw_collateral; 12 | pub mod update_price; 13 | 14 | pub use initialize::*; 15 | pub use create_market::*; 16 | pub use place_order::*; 17 | pub use cancel_order::*; 18 | pub use open_position::*; 19 | pub use close_position::*; 20 | pub use add_margin::*; 21 | pub use remove_margin::*; 22 | pub use liquidate_position::*; 23 | pub use deposit_collateral::*; 24 | pub use withdraw_collateral::*; 25 | pub use update_price::*; 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-perpetual-dex-smart-contract", 3 | "version": "0.1.0", 4 | "description": "Perpetual DEX with Orderbook, Margin Trading, and Leverage on Solana", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "anchor test", 8 | "build": "anchor build", 9 | "deploy": "anchor deploy" 10 | }, 11 | "keywords": [ 12 | "solana", 13 | "perpetual", 14 | "dex", 15 | "futures", 16 | "margin-trading", 17 | "leverage", 18 | "orderbook", 19 | "blockchain", 20 | "anchor" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@coral-xyz/anchor": "^0.30.1", 26 | "@solana/web3.js": "^1.87.6", 27 | "@types/node": "^20.10.0", 28 | "typescript": "^5.3.3" 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/utils/liquidation.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | /// Check if position should be liquidated 5 | pub fn should_liquidate( 6 | current_price: u64, 7 | liquidation_price: u64, 8 | side: crate::consts::PositionSide, 9 | ) -> bool { 10 | match side { 11 | crate::consts::PositionSide::Long => current_price <= liquidation_price, 12 | crate::consts::PositionSide::Short => current_price >= liquidation_price, 13 | } 14 | } 15 | 16 | /// Calculate liquidation bonus for liquidator 17 | pub fn calculate_liquidation_bonus( 18 | position_value: u64, 19 | liquidation_fee_bps: u16, 20 | ) -> u64 { 21 | position_value 22 | .checked_mul(liquidation_fee_bps as u128) 23 | .unwrap() 24 | .checked_div(10000) 25 | .unwrap() as u64 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./tests", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "baseUrl": ".", 19 | "paths": { 20 | "*": ["node_modules/*"] 21 | }, 22 | "esModuleInterop": true, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true 28 | }, 29 | "include": ["tests/**/*"], 30 | "exclude": ["node_modules", "dist", "target"] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/update_price.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::Market; 3 | use crate::events::PriceUpdated; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct UpdatePrice<'info> { 8 | #[account( 9 | mut, 10 | seeds = [Market::SEEDS, market.market_id.as_ref()], 11 | bump 12 | )] 13 | pub market: Box>, 14 | 15 | /// CHECK: Oracle or authorized price updater 16 | pub oracle: Signer<'info>, 17 | 18 | pub clock: Sysvar<'info, Clock>, 19 | } 20 | 21 | impl<'info> UpdatePrice<'info> { 22 | pub fn process(&mut self, price: u64) -> Result<()> { 23 | require!( 24 | price > 0, 25 | PerpetualDexError::InvalidPrice 26 | ); 27 | 28 | let old_price = self.market.current_price; 29 | 30 | // Update price 31 | self.market.update_price(price, &self.clock)?; 32 | 33 | emit!(PriceUpdated { 34 | market: self.market.key(), 35 | old_price, 36 | new_price: price, 37 | updated_at: self.clock.unix_timestamp, 38 | }); 39 | 40 | Ok(()) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/withdraw_collateral.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::User; 3 | use crate::events::CollateralWithdrawn; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct WithdrawCollateral<'info> { 8 | #[account( 9 | mut, 10 | seeds = [User::SEEDS, user.key().as_ref()], 11 | bump, 12 | constraint = user_account.user == user.key() @ PerpetualDexError::Unauthorized 13 | )] 14 | pub user_account: Box>, 15 | 16 | #[account(mut)] 17 | pub user: Signer<'info>, 18 | 19 | pub clock: Sysvar<'info, Clock>, 20 | } 21 | 22 | impl<'info> WithdrawCollateral<'info> { 23 | pub fn process(&mut self, amount: u64) -> Result<()> { 24 | require!( 25 | amount > 0, 26 | PerpetualDexError::InsufficientMargin 27 | ); 28 | 29 | // Withdraw collateral 30 | self.user_account.withdraw_collateral(amount, &self.clock)?; 31 | 32 | // In production, this would transfer tokens from program to user 33 | // The actual transfer would be handled via token program CPI 34 | 35 | emit!(CollateralWithdrawn { 36 | user: self.user.key(), 37 | amount, 38 | withdrawn_at: self.clock.unix_timestamp, 39 | }); 40 | 41 | Ok(()) 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/initialize.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::GlobalConfig; 3 | use crate::errors::PerpetualDexError; 4 | 5 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 6 | pub struct InitializeParams { 7 | pub fee_recipient: Pubkey, 8 | pub trading_fee_bps: u16, 9 | pub liquidation_fee_bps: u16, 10 | pub max_leverage_bps: u16, 11 | pub initial_margin_bps: u16, 12 | pub maintenance_margin_bps: u16, 13 | pub liquidation_threshold_bps: u16, 14 | } 15 | 16 | #[derive(Accounts)] 17 | pub struct Initialize<'info> { 18 | #[account( 19 | init, 20 | payer = authority, 21 | space = GlobalConfig::SIZE, 22 | seeds = [GlobalConfig::SEEDS], 23 | bump 24 | )] 25 | pub global_config: Box>, 26 | 27 | #[account(mut)] 28 | pub authority: Signer<'info>, 29 | 30 | pub system_program: Program<'info, System>, 31 | } 32 | 33 | impl<'info> Initialize<'info> { 34 | pub fn process(&self, params: InitializeParams) -> Result<()> { 35 | self.global_config.init( 36 | self.authority.key(), 37 | params.fee_recipient, 38 | params.trading_fee_bps, 39 | params.liquidation_fee_bps, 40 | params.max_leverage_bps, 41 | params.initial_margin_bps, 42 | params.maintenance_margin_bps, 43 | params.liquidation_threshold_bps, 44 | )?; 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/utils/margin.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | /// Check if user has sufficient margin 5 | pub fn has_sufficient_margin( 6 | collateral: u64, 7 | used_margin: u64, 8 | required_margin: u64, 9 | ) -> bool { 10 | let free_margin = collateral.checked_sub(used_margin).unwrap_or(0); 11 | free_margin >= required_margin 12 | } 13 | 14 | /// Calculate margin ratio 15 | pub fn calculate_margin_ratio( 16 | collateral: u64, 17 | used_margin: u64, 18 | pnl: i64, 19 | ) -> u64 { 20 | let total_equity = if pnl >= 0 { 21 | collateral.checked_add(pnl as u64).unwrap_or(collateral) 22 | } else { 23 | collateral.checked_sub((-pnl) as u64).unwrap_or(0) 24 | }; 25 | 26 | if used_margin == 0 { 27 | return PRICE_PRECISION; // 100% 28 | } 29 | 30 | total_equity 31 | .checked_mul(PRICE_PRECISION) 32 | .unwrap() 33 | .checked_div(used_margin) 34 | .unwrap() 35 | } 36 | 37 | /// Check if position meets maintenance margin requirement 38 | pub fn meets_maintenance_margin( 39 | margin: u64, 40 | pnl: i64, 41 | maintenance_margin_bps: u16, 42 | ) -> bool { 43 | let equity = if pnl >= 0 { 44 | margin.checked_add(pnl as u64).unwrap_or(margin) 45 | } else { 46 | margin.checked_sub((-pnl) as u64).unwrap_or(0) 47 | }; 48 | 49 | let required_maintenance = margin 50 | .checked_mul(maintenance_margin_bps as u64) 51 | .unwrap() 52 | .checked_div(10000) 53 | .unwrap(); 54 | 55 | equity >= required_maintenance 56 | } 57 | 58 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/deposit_collateral.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::User; 3 | use crate::events::CollateralDeposited; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct DepositCollateral<'info> { 8 | #[account( 9 | init_if_needed, 10 | payer = user, 11 | space = User::SIZE, 12 | seeds = [User::SEEDS, user.key().as_ref()], 13 | bump 14 | )] 15 | pub user_account: Box>, 16 | 17 | #[account(mut)] 18 | pub user: Signer<'info>, 19 | 20 | pub system_program: Program<'info, System>, 21 | pub clock: Sysvar<'info, Clock>, 22 | } 23 | 24 | impl<'info> DepositCollateral<'info> { 25 | pub fn process(&mut self, amount: u64) -> Result<()> { 26 | require!( 27 | amount > 0, 28 | PerpetualDexError::InsufficientMargin 29 | ); 30 | 31 | // Initialize user account if needed 32 | if self.user_account.user == Pubkey::default() { 33 | self.user_account.init(self.user.key(), &self.clock)?; 34 | } 35 | 36 | // In production, this would transfer tokens from user to the program 37 | // For now, we'll just update the account 38 | // The actual transfer would be handled via token program CPI 39 | 40 | self.user_account.deposit_collateral(amount, &self.clock)?; 41 | 42 | emit!(CollateralDeposited { 43 | user: self.user.key(), 44 | amount, 45 | deposited_at: self.clock.unix_timestamp, 46 | }); 47 | 48 | Ok(()) 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/cancel_order.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{User, Order}; 3 | use crate::events::OrderCancelled; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct CancelOrder<'info> { 8 | #[account( 9 | seeds = [User::SEEDS, user.user.as_ref()], 10 | bump, 11 | constraint = user.user == trader.key() @ PerpetualDexError::Unauthorized 12 | )] 13 | pub user: Box>, 14 | 15 | #[account( 16 | mut, 17 | seeds = [Order::SEEDS, order.order_id.as_ref()], 18 | bump, 19 | constraint = order.user == trader.key() @ PerpetualDexError::Unauthorized 20 | )] 21 | pub order: Box>, 22 | 23 | #[account(mut)] 24 | pub trader: Signer<'info>, 25 | 26 | pub clock: Sysvar<'info, Clock>, 27 | } 28 | 29 | impl<'info> CancelOrder<'info> { 30 | pub fn process(&mut self, _order_id: u64) -> Result<()> { 31 | // Cancel order 32 | self.order.cancel(&self.clock)?; 33 | 34 | // Free up margin 35 | let required_margin = crate::utils::calculate_required_margin( 36 | self.order.size, 37 | self.order.price, 38 | self.order.leverage, 39 | )?; 40 | 41 | self.user.free_margin(required_margin, &self.clock)?; 42 | self.user.decrement_orders(); 43 | 44 | emit!(OrderCancelled { 45 | order_id: self.order.key(), 46 | user: self.trader.key(), 47 | market: self.order.market, 48 | cancelled_at: self.clock.unix_timestamp, 49 | }); 50 | 51 | Ok(()) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum PerpetualDexError { 5 | #[msg("Market not found")] 6 | MarketNotFound, 7 | 8 | #[msg("Position not found")] 9 | PositionNotFound, 10 | 11 | #[msg("Order not found")] 12 | OrderNotFound, 13 | 14 | #[msg("Insufficient margin")] 15 | InsufficientMargin, 16 | 17 | #[msg("Position is not liquidatable")] 18 | PositionNotLiquidatable, 19 | 20 | #[msg("Unauthorized: Only position owner can perform this action")] 21 | Unauthorized, 22 | 23 | #[msg("Invalid leverage: Must be between 1x and maximum allowed")] 24 | InvalidLeverage, 25 | 26 | #[msg("Invalid order size")] 27 | InvalidOrderSize, 28 | 29 | #[msg("Invalid price")] 30 | InvalidPrice, 31 | 32 | #[msg("Order already filled or cancelled")] 33 | OrderNotActive, 34 | 35 | #[msg("Market is paused")] 36 | MarketPaused, 37 | 38 | #[msg("Position is already closed")] 39 | PositionClosed, 40 | 41 | #[msg("Slippage exceeded")] 42 | SlippageExceeded, 43 | 44 | #[msg("Insufficient liquidity")] 45 | InsufficientLiquidity, 46 | 47 | #[msg("Invalid position side")] 48 | InvalidPositionSide, 49 | 50 | #[msg("Maintenance margin requirement not met")] 51 | MaintenanceMarginNotMet, 52 | 53 | #[msg("Cannot remove margin: Would violate maintenance margin")] 54 | CannotRemoveMargin, 55 | 56 | #[msg("Configuration already initialized")] 57 | AlreadyInitialized, 58 | 59 | #[msg("Invalid configuration parameters")] 60 | InvalidConfiguration, 61 | 62 | #[msg("Price oracle not updated")] 63 | PriceOracleStale, 64 | 65 | #[msg("Maximum position size exceeded")] 66 | MaxPositionSizeExceeded, 67 | 68 | #[msg("Maximum open orders exceeded")] 69 | MaxOpenOrdersExceeded, 70 | } 71 | 72 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/add_margin.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{User, Position}; 3 | use crate::events::MarginAdded; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct AddMargin<'info> { 8 | #[account( 9 | mut, 10 | seeds = [User::SEEDS, user.user.as_ref()], 11 | bump, 12 | constraint = user.user == trader.key() @ PerpetualDexError::Unauthorized 13 | )] 14 | pub user: Box>, 15 | 16 | #[account( 17 | mut, 18 | seeds = [Position::SEEDS, position.position_id.as_ref()], 19 | bump, 20 | constraint = position.user == trader.key() @ PerpetualDexError::Unauthorized, 21 | constraint = !position.is_closed @ PerpetualDexError::PositionClosed 22 | )] 23 | pub position: Box>, 24 | 25 | #[account(mut)] 26 | pub trader: Signer<'info>, 27 | 28 | pub clock: Sysvar<'info, Clock>, 29 | } 30 | 31 | impl<'info> AddMargin<'info> { 32 | pub fn process(&mut self, _position_id: u64, amount: u64) -> Result<()> { 33 | require!( 34 | amount > 0, 35 | PerpetualDexError::InsufficientMargin 36 | ); 37 | 38 | require!( 39 | self.user.free_margin >= amount, 40 | PerpetualDexError::InsufficientMargin 41 | ); 42 | 43 | // Use margin 44 | self.user.use_margin(amount, &self.clock)?; 45 | 46 | // Add to position margin 47 | self.position.margin = self.position.margin.checked_add(amount).unwrap(); 48 | 49 | // Recalculate liquidation price with new margin 50 | self.position.liquidation_price = self.position.calculate_liquidation_price( 51 | self.position.entry_price, 52 | self.position.leverage, 53 | self.position.side, 54 | ); 55 | 56 | self.position.last_updated = self.clock.unix_timestamp; 57 | 58 | emit!(MarginAdded { 59 | position_id: self.position.key(), 60 | user: self.trader.key(), 61 | amount, 62 | updated_at: self.clock.unix_timestamp, 63 | }); 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/create_market.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{GlobalConfig, Market}; 3 | use crate::events::MarketCreated; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 7 | pub struct CreateMarketArgs { 8 | pub base_mint: Pubkey, 9 | pub quote_mint: Pubkey, 10 | pub name: String, 11 | pub symbol: String, 12 | pub initial_price: u64, 13 | } 14 | 15 | #[derive(Accounts)] 16 | pub struct CreateMarket<'info> { 17 | #[account( 18 | seeds = [GlobalConfig::SEEDS], 19 | bump, 20 | constraint = !global_config.paused @ PerpetualDexError::MarketPaused 21 | )] 22 | pub global_config: Box>, 23 | 24 | /// CHECK: Market ID provided by user 25 | pub market_id: AccountInfo<'info>, 26 | 27 | #[account( 28 | init, 29 | payer = authority, 30 | space = 8 + Market::BASE_SIZE + 31 | 4 + 50 + // name (max 50) 32 | 4 + 10, // symbol (max 10) 33 | seeds = [Market::SEEDS, market_id.key().as_ref()], 34 | bump 35 | )] 36 | pub market: Box>, 37 | 38 | #[account(mut)] 39 | pub authority: Signer<'info>, 40 | 41 | pub system_program: Program<'info, System>, 42 | pub clock: Sysvar<'info, Clock>, 43 | } 44 | 45 | impl<'info> CreateMarket<'info> { 46 | pub fn process(&mut self, args: CreateMarketArgs) -> Result<()> { 47 | require!( 48 | args.initial_price > 0, 49 | PerpetualDexError::InvalidPrice 50 | ); 51 | 52 | self.market.init( 53 | self.market_id.key(), 54 | args.base_mint, 55 | args.quote_mint, 56 | args.name.clone(), 57 | args.symbol.clone(), 58 | args.initial_price, 59 | &self.clock, 60 | )?; 61 | 62 | self.global_config.increment_total_markets(); 63 | 64 | emit!(MarketCreated { 65 | market: self.market.key(), 66 | base_mint: args.base_mint, 67 | quote_mint: args.quote_mint, 68 | name: args.name, 69 | created_at: self.clock.unix_timestamp, 70 | }); 71 | 72 | Ok(()) 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/events/events.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::{OrderSide, OrderType, PositionSide}; 3 | 4 | #[event] 5 | pub struct MarketCreated { 6 | pub market: Pubkey, 7 | pub base_mint: Pubkey, 8 | pub quote_mint: Pubkey, 9 | pub name: String, 10 | pub created_at: i64, 11 | } 12 | 13 | #[event] 14 | pub struct OrderPlaced { 15 | pub order_id: Pubkey, 16 | pub user: Pubkey, 17 | pub market: Pubkey, 18 | pub side: OrderSide, 19 | pub order_type: OrderType, 20 | pub size: u64, 21 | pub price: u64, 22 | pub leverage: u16, 23 | pub created_at: i64, 24 | } 25 | 26 | #[event] 27 | pub struct OrderCancelled { 28 | pub order_id: Pubkey, 29 | pub user: Pubkey, 30 | pub market: Pubkey, 31 | pub cancelled_at: i64, 32 | } 33 | 34 | #[event] 35 | pub struct OrderFilled { 36 | pub order_id: Pubkey, 37 | pub user: Pubkey, 38 | pub market: Pubkey, 39 | pub filled_size: u64, 40 | pub fill_price: u64, 41 | pub filled_at: i64, 42 | } 43 | 44 | #[event] 45 | pub struct PositionOpened { 46 | pub position_id: Pubkey, 47 | pub user: Pubkey, 48 | pub market: Pubkey, 49 | pub side: PositionSide, 50 | pub size: u64, 51 | pub entry_price: u64, 52 | pub leverage: u16, 53 | pub margin: u64, 54 | pub opened_at: i64, 55 | } 56 | 57 | #[event] 58 | pub struct PositionClosed { 59 | pub position_id: Pubkey, 60 | pub user: Pubkey, 61 | pub market: Pubkey, 62 | pub exit_price: u64, 63 | pub pnl: i64, 64 | pub closed_at: i64, 65 | } 66 | 67 | #[event] 68 | pub struct MarginAdded { 69 | pub position_id: Pubkey, 70 | pub user: Pubkey, 71 | pub amount: u64, 72 | pub updated_at: i64, 73 | } 74 | 75 | #[event] 76 | pub struct MarginRemoved { 77 | pub position_id: Pubkey, 78 | pub user: Pubkey, 79 | pub amount: u64, 80 | pub updated_at: i64, 81 | } 82 | 83 | #[event] 84 | pub struct PositionLiquidated { 85 | pub position_id: Pubkey, 86 | pub user: Pubkey, 87 | pub market: Pubkey, 88 | pub liquidation_price: u64, 89 | pub pnl: i64, 90 | pub liquidated_at: i64, 91 | } 92 | 93 | #[event] 94 | pub struct CollateralDeposited { 95 | pub user: Pubkey, 96 | pub amount: u64, 97 | pub deposited_at: i64, 98 | } 99 | 100 | #[event] 101 | pub struct CollateralWithdrawn { 102 | pub user: Pubkey, 103 | pub amount: u64, 104 | pub withdrawn_at: i64, 105 | } 106 | 107 | #[event] 108 | pub struct PriceUpdated { 109 | pub market: Pubkey, 110 | pub old_price: u64, 111 | pub new_price: u64, 112 | pub updated_at: i64, 113 | } 114 | 115 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod consts; 4 | pub mod errors; 5 | pub mod events; 6 | pub mod instructions; 7 | pub mod states; 8 | pub mod utils; 9 | 10 | use crate::instructions::*; 11 | 12 | declare_id!("PERP1111111111111111111111111111111111111111"); 13 | 14 | #[program] 15 | pub mod perpetual_dex { 16 | 17 | use super::*; 18 | 19 | /// Initialize the global configuration 20 | pub fn initialize(ctx: Context, params: InitializeParams) -> Result<()> { 21 | ctx.accounts.process(params) 22 | } 23 | 24 | /// Create a new perpetual market 25 | pub fn create_market(ctx: Context, args: CreateMarketArgs) -> Result<()> { 26 | ctx.accounts.process(args) 27 | } 28 | 29 | /// Place an order in the orderbook 30 | pub fn place_order(ctx: Context, args: PlaceOrderArgs) -> Result<()> { 31 | ctx.accounts.process(args) 32 | } 33 | 34 | /// Cancel an existing order 35 | pub fn cancel_order(ctx: Context, order_id: u64) -> Result<()> { 36 | ctx.accounts.process(order_id) 37 | } 38 | 39 | /// Open a perpetual position (long or short) 40 | pub fn open_position(ctx: Context, args: OpenPositionArgs) -> Result<()> { 41 | ctx.accounts.process(args) 42 | } 43 | 44 | /// Close a perpetual position 45 | pub fn close_position(ctx: Context, position_id: u64) -> Result<()> { 46 | ctx.accounts.process(position_id) 47 | } 48 | 49 | /// Add margin to an existing position 50 | pub fn add_margin(ctx: Context, position_id: u64, amount: u64) -> Result<()> { 51 | ctx.accounts.process(position_id, amount) 52 | } 53 | 54 | /// Remove margin from a position 55 | pub fn remove_margin(ctx: Context, position_id: u64, amount: u64) -> Result<()> { 56 | ctx.accounts.process(position_id, amount) 57 | } 58 | 59 | /// Liquidate an undercollateralized position 60 | pub fn liquidate_position(ctx: Context, position_id: u64) -> Result<()> { 61 | ctx.accounts.process(position_id) 62 | } 63 | 64 | /// Deposit collateral to user account 65 | pub fn deposit_collateral(ctx: Context, amount: u64) -> Result<()> { 66 | ctx.accounts.process(amount) 67 | } 68 | 69 | /// Withdraw collateral from user account 70 | pub fn withdraw_collateral(ctx: Context, amount: u64) -> Result<()> { 71 | ctx.accounts.process(amount) 72 | } 73 | 74 | /// Update market price (oracle) 75 | pub fn update_price(ctx: Context, price: u64) -> Result<()> { 76 | ctx.accounts.process(price) 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/global_config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | pub struct GlobalConfig { 7 | pub authority: Pubkey, 8 | pub fee_recipient: Pubkey, 9 | pub trading_fee_bps: u16, // Trading fee in basis points 10 | pub liquidation_fee_bps: u16, // Liquidation fee in basis points 11 | pub max_leverage_bps: u16, // Maximum leverage in basis points 12 | pub initial_margin_bps: u16, // Initial margin requirement 13 | pub maintenance_margin_bps: u16, // Maintenance margin requirement 14 | pub liquidation_threshold_bps: u16, // Liquidation threshold 15 | pub is_initialized: bool, 16 | pub total_markets: u64, 17 | pub paused: bool, 18 | } 19 | 20 | impl GlobalConfig { 21 | pub const SEEDS: &'static [u8] = GLOBAL_CONFIG_SEED; 22 | pub const SIZE: usize = 8 + // discriminator 23 | 32 + // authority 24 | 32 + // fee_recipient 25 | 2 + // trading_fee_bps 26 | 2 + // liquidation_fee_bps 27 | 2 + // max_leverage_bps 28 | 2 + // initial_margin_bps 29 | 2 + // maintenance_margin_bps 30 | 2 + // liquidation_threshold_bps 31 | 1 + // is_initialized 32 | 8 + // total_markets 33 | 1; // paused 34 | 35 | pub fn init( 36 | &mut self, 37 | authority: Pubkey, 38 | fee_recipient: Pubkey, 39 | trading_fee_bps: u16, 40 | liquidation_fee_bps: u16, 41 | max_leverage_bps: u16, 42 | initial_margin_bps: u16, 43 | maintenance_margin_bps: u16, 44 | liquidation_threshold_bps: u16, 45 | ) -> Result<()> { 46 | require!(!self.is_initialized, crate::errors::PerpetualDexError::AlreadyInitialized); 47 | require!(max_leverage_bps <= MAX_LEVERAGE_BPS, crate::errors::PerpetualDexError::InvalidConfiguration); 48 | require!(trading_fee_bps <= 10000, crate::errors::PerpetualDexError::InvalidConfiguration); 49 | require!(liquidation_fee_bps <= 10000, crate::errors::PerpetualDexError::InvalidConfiguration); 50 | 51 | self.authority = authority; 52 | self.fee_recipient = fee_recipient; 53 | self.trading_fee_bps = trading_fee_bps; 54 | self.liquidation_fee_bps = liquidation_fee_bps; 55 | self.max_leverage_bps = max_leverage_bps; 56 | self.initial_margin_bps = initial_margin_bps; 57 | self.maintenance_margin_bps = maintenance_margin_bps; 58 | self.liquidation_threshold_bps = liquidation_threshold_bps; 59 | self.is_initialized = true; 60 | self.total_markets = 0; 61 | self.paused = false; 62 | 63 | Ok(()) 64 | } 65 | 66 | pub fn increment_total_markets(&mut self) { 67 | self.total_markets = self.total_markets.checked_add(1).unwrap(); 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/remove_margin.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{User, Position}; 3 | use crate::events::MarginRemoved; 4 | use crate::errors::PerpetualDexError; 5 | use crate::utils::meets_maintenance_margin; 6 | 7 | #[derive(Accounts)] 8 | pub struct RemoveMargin<'info> { 9 | #[account( 10 | mut, 11 | seeds = [User::SEEDS, user.user.as_ref()], 12 | bump, 13 | constraint = user.user == trader.key() @ PerpetualDexError::Unauthorized 14 | )] 15 | pub user: Box>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [Position::SEEDS, position.position_id.as_ref()], 20 | bump, 21 | constraint = position.user == trader.key() @ PerpetualDexError::Unauthorized, 22 | constraint = !position.is_closed @ PerpetualDexError::PositionClosed 23 | )] 24 | pub position: Box>, 25 | 26 | #[account(mut)] 27 | pub trader: Signer<'info>, 28 | 29 | pub clock: Sysvar<'info, Clock>, 30 | } 31 | 32 | impl<'info> RemoveMargin<'info> { 33 | pub fn process(&mut self, _position_id: u64, amount: u64) -> Result<()> { 34 | require!( 35 | amount > 0, 36 | PerpetualDexError::InsufficientMargin 37 | ); 38 | 39 | require!( 40 | self.position.margin > amount, 41 | PerpetualDexError::InsufficientMargin 42 | ); 43 | 44 | // Update PnL first 45 | // Note: In production, you'd get current price from market 46 | // For now, we'll use position's current_price 47 | self.position.update_pnl(self.position.current_price)?; 48 | 49 | // Check if removing margin would violate maintenance margin 50 | let new_margin = self.position.margin.checked_sub(amount).unwrap(); 51 | let maintenance_margin_bps = 500; // 5% default 52 | if !meets_maintenance_margin(new_margin, self.position.pnl, maintenance_margin_bps) { 53 | return Err(PerpetualDexError::CannotRemoveMargin.into()); 54 | } 55 | 56 | // Remove margin from position 57 | self.position.margin = new_margin; 58 | 59 | // Recalculate liquidation price 60 | self.position.liquidation_price = self.position.calculate_liquidation_price( 61 | self.position.entry_price, 62 | self.position.leverage, 63 | self.position.side, 64 | ); 65 | 66 | // Free margin for user 67 | self.user.free_margin(amount, &self.clock)?; 68 | 69 | self.position.last_updated = self.clock.unix_timestamp; 70 | 71 | emit!(MarginRemoved { 72 | position_id: self.position.key(), 73 | user: self.trader.key(), 74 | amount, 75 | updated_at: self.clock.unix_timestamp, 76 | }); 77 | 78 | Ok(()) 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /tests/perpetual-dex.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { PerpetualDex } from "../target/types/perpetual_dex"; 4 | import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; 5 | import { expect } from "chai"; 6 | 7 | describe("perpetual-dex", () => { 8 | // Configure the client to use the local cluster. 9 | const provider = anchor.AnchorProvider.env(); 10 | anchor.setProvider(provider); 11 | 12 | const program = anchor.workspace.PerpetualDex as Program; 13 | 14 | const authority = Keypair.generate(); 15 | const trader = Keypair.generate(); 16 | const marketId = Keypair.generate(); 17 | const baseMint = Keypair.generate(); 18 | const quoteMint = Keypair.generate(); 19 | 20 | it("Initializes the global configuration", async () => { 21 | const [globalConfig] = PublicKey.findProgramAddressSync( 22 | [Buffer.from("perpetual_config")], 23 | program.programId 24 | ); 25 | 26 | const feeRecipient = Keypair.generate().publicKey; 27 | 28 | const tx = await program.methods 29 | .initialize({ 30 | feeRecipient, 31 | tradingFeeBps: 10, // 0.1% 32 | liquidationFeeBps: 200, // 2% 33 | maxLeverageBps: 10000, // 100x 34 | initialMarginBps: 1000, // 10% 35 | maintenanceMarginBps: 500, // 5% 36 | liquidationThresholdBps: 400, // 4% 37 | }) 38 | .accounts({ 39 | globalConfig, 40 | authority: authority.publicKey, 41 | systemProgram: SystemProgram.programId, 42 | }) 43 | .signers([authority]) 44 | .rpc(); 45 | 46 | console.log("Initialize transaction signature", tx); 47 | 48 | const config = await program.account.globalConfig.fetch(globalConfig); 49 | expect(config.isInitialized).to.be.true; 50 | expect(config.maxLeverageBps).to.equal(10000); 51 | }); 52 | 53 | it("Creates a new market", async () => { 54 | const [globalConfig] = PublicKey.findProgramAddressSync( 55 | [Buffer.from("perpetual_config")], 56 | program.programId 57 | ); 58 | 59 | const [market] = PublicKey.findProgramAddressSync( 60 | [Buffer.from("perpetual_market"), marketId.publicKey.toBuffer()], 61 | program.programId 62 | ); 63 | 64 | const tx = await program.methods 65 | .createMarket({ 66 | baseMint: baseMint.publicKey, 67 | quoteMint: quoteMint.publicKey, 68 | name: "SOL-PERP", 69 | symbol: "SOL/USDC", 70 | initialPrice: new anchor.BN(100_000_000_000), // 100 USDC with 8 decimals 71 | }) 72 | .accounts({ 73 | globalConfig, 74 | marketId: marketId.publicKey, 75 | market, 76 | authority: authority.publicKey, 77 | systemProgram: SystemProgram.programId, 78 | }) 79 | .signers([authority]) 80 | .rpc(); 81 | 82 | console.log("Create market transaction signature", tx); 83 | 84 | const marketAccount = await program.account.market.fetch(market); 85 | expect(marketAccount.name).to.equal("SOL-PERP"); 86 | expect(marketAccount.currentPrice.toNumber()).to.equal(100_000_000_000); 87 | }); 88 | 89 | // Add more tests for other instructions... 90 | }); 91 | 92 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/close_position.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{GlobalConfig, Market, User, Position}; 3 | use crate::events::PositionClosed; 4 | use crate::errors::PerpetualDexError; 5 | 6 | #[derive(Accounts)] 7 | pub struct ClosePosition<'info> { 8 | #[account( 9 | seeds = [GlobalConfig::SEEDS], 10 | bump 11 | )] 12 | pub global_config: Box>, 13 | 14 | #[account( 15 | mut, 16 | seeds = [Market::SEEDS, market.market_id.as_ref()], 17 | bump 18 | )] 19 | pub market: Box>, 20 | 21 | #[account( 22 | mut, 23 | seeds = [User::SEEDS, user.user.as_ref()], 24 | bump, 25 | constraint = user.user == trader.key() @ PerpetualDexError::Unauthorized 26 | )] 27 | pub user: Box>, 28 | 29 | #[account( 30 | mut, 31 | seeds = [Position::SEEDS, position.position_id.as_ref()], 32 | bump, 33 | constraint = position.user == trader.key() @ PerpetualDexError::Unauthorized, 34 | constraint = !position.is_closed @ PerpetualDexError::PositionClosed 35 | )] 36 | pub position: Box>, 37 | 38 | #[account(mut)] 39 | pub trader: Signer<'info>, 40 | 41 | pub clock: Sysvar<'info, Clock>, 42 | } 43 | 44 | impl<'info> ClosePosition<'info> { 45 | pub fn process(&mut self, _position_id: u64) -> Result<()> { 46 | // Update PnL with current price 47 | self.position.update_pnl(self.market.current_price)?; 48 | 49 | let pnl = self.position.pnl; 50 | let margin = self.position.margin; 51 | 52 | // Free margin 53 | self.user.free_margin(margin, &self.clock)?; 54 | 55 | // Update user PnL 56 | if pnl >= 0 { 57 | self.user.collateral = self.user.collateral.checked_add(pnl as u64).unwrap(); 58 | self.user.free_margin = self.user.free_margin.checked_add(pnl as u64).unwrap(); 59 | } else { 60 | let loss = (-pnl) as u64; 61 | self.user.collateral = self.user.collateral.checked_sub(loss).unwrap_or(0); 62 | self.user.free_margin = self.user.free_margin.checked_sub(loss).unwrap_or(0); 63 | } 64 | 65 | self.user.total_pnl = self.user.total_pnl.checked_add(pnl).unwrap(); 66 | 67 | // Update market open interest 68 | let oi_delta = -(self.position.size as i64); 69 | match self.position.side { 70 | crate::consts::PositionSide::Long => { 71 | self.market.update_open_interest(oi_delta, 0)?; 72 | } 73 | crate::consts::PositionSide::Short => { 74 | self.market.update_open_interest(0, oi_delta)?; 75 | } 76 | } 77 | 78 | // Close position 79 | self.position.close(&self.clock)?; 80 | self.user.decrement_positions(); 81 | 82 | emit!(PositionClosed { 83 | position_id: self.position.key(), 84 | user: self.trader.key(), 85 | market: self.market.key(), 86 | exit_price: self.market.current_price, 87 | pnl, 88 | closed_at: self.clock.unix_timestamp, 89 | }); 90 | 91 | Ok(()) 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/consts.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Seed for the global configuration PDA 4 | pub const GLOBAL_CONFIG_SEED: &[u8] = b"perpetual_config"; 5 | 6 | /// Seed for market PDA 7 | pub const MARKET_SEED: &[u8] = b"perpetual_market"; 8 | 9 | /// Seed for user account PDA 10 | pub const USER_SEED: &[u8] = b"perpetual_user"; 11 | 12 | /// Seed for position PDA 13 | pub const POSITION_SEED: &[u8] = b"perpetual_position"; 14 | 15 | /// Seed for order PDA 16 | pub const ORDER_SEED: &[u8] = b"perpetual_order"; 17 | 18 | /// Maximum leverage (e.g., 100x = 10000 basis points) 19 | pub const MAX_LEVERAGE_BPS: u16 = 10000; // 100x 20 | 21 | /// Minimum leverage (1x) 22 | pub const MIN_LEVERAGE_BPS: u16 = 100; // 1x 23 | 24 | /// Initial margin requirement in basis points (e.g., 1000 = 10%) 25 | pub const INITIAL_MARGIN_BPS: u16 = 1000; // 10% 26 | 27 | /// Maintenance margin requirement in basis points (e.g., 500 = 5%) 28 | pub const MAINTENANCE_MARGIN_BPS: u16 = 500; // 5% 29 | 30 | /// Liquidation threshold in basis points (e.g., 400 = 4%) 31 | pub const LIQUIDATION_THRESHOLD_BPS: u16 = 400; // 4% 32 | 33 | /// Maximum number of open orders per user 34 | pub const MAX_OPEN_ORDERS: u8 = 50; 35 | 36 | /// Maximum position size (in base units) 37 | pub const MAX_POSITION_SIZE: u64 = 1_000_000_000_000_000; // 1 billion 38 | 39 | /// Price precision (8 decimals) 40 | pub const PRICE_PRECISION: u64 = 100_000_000; // 10^8 41 | 42 | /// Order side enum 43 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq)] 44 | pub enum OrderSide { 45 | /// Buy order (long) 46 | Buy, 47 | /// Sell order (short) 48 | Sell, 49 | } 50 | 51 | /// Order type enum 52 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq)] 53 | pub enum OrderType { 54 | /// Market order (execute immediately at best price) 55 | Market, 56 | /// Limit order (execute at specified price or better) 57 | Limit, 58 | /// Stop loss order 59 | StopLoss, 60 | /// Take profit order 61 | TakeProfit, 62 | } 63 | 64 | /// Order status enum 65 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq)] 66 | pub enum OrderStatus { 67 | /// Order is active and waiting to be filled 68 | Active, 69 | /// Order is partially filled 70 | PartiallyFilled, 71 | /// Order is fully filled 72 | Filled, 73 | /// Order is cancelled 74 | Cancelled, 75 | /// Order is rejected 76 | Rejected, 77 | } 78 | 79 | /// Position side enum 80 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq)] 81 | pub enum PositionSide { 82 | /// Long position (buy) 83 | Long, 84 | /// Short position (sell) 85 | Short, 86 | } 87 | 88 | impl Default for OrderSide { 89 | fn default() -> Self { 90 | OrderSide::Buy 91 | } 92 | } 93 | 94 | impl Default for OrderType { 95 | fn default() -> Self { 96 | OrderType::Limit 97 | } 98 | } 99 | 100 | impl Default for OrderStatus { 101 | fn default() -> Self { 102 | OrderStatus::Active 103 | } 104 | } 105 | 106 | impl Default for PositionSide { 107 | fn default() -> Self { 108 | PositionSide::Long 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/utils/calculations.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | /// Calculate required margin for a position 5 | pub fn calculate_required_margin( 6 | size: u64, 7 | price: u64, 8 | leverage: u16, 9 | ) -> Result { 10 | let position_value = (size as u128) 11 | .checked_mul(price as u128) 12 | .unwrap() 13 | .checked_div(PRICE_PRECISION as u128) 14 | .unwrap(); 15 | 16 | let leverage_ratio = (leverage as u128) * PRICE_PRECISION as u128 / 10000; 17 | let required_margin = position_value 18 | .checked_mul(PRICE_PRECISION as u128) 19 | .unwrap() 20 | .checked_div(leverage_ratio) 21 | .unwrap(); 22 | 23 | Ok(required_margin as u64) 24 | } 25 | 26 | /// Calculate profit and loss for a position 27 | pub fn calculate_pnl( 28 | size: u64, 29 | entry_price: u64, 30 | current_price: u64, 31 | side: crate::consts::PositionSide, 32 | ) -> i64 { 33 | let price_diff = if current_price > entry_price { 34 | current_price - entry_price 35 | } else { 36 | entry_price - current_price 37 | }; 38 | 39 | let pnl_amount = (size as u128) 40 | .checked_mul(price_diff as u128) 41 | .unwrap() 42 | .checked_div(PRICE_PRECISION as u128) 43 | .unwrap() as u64; 44 | 45 | match side { 46 | crate::consts::PositionSide::Long => { 47 | if current_price >= entry_price { 48 | pnl_amount as i64 49 | } else { 50 | -(pnl_amount as i64) 51 | } 52 | } 53 | crate::consts::PositionSide::Short => { 54 | if current_price <= entry_price { 55 | pnl_amount as i64 56 | } else { 57 | -(pnl_amount as i64) 58 | } 59 | } 60 | } 61 | } 62 | 63 | /// Calculate liquidation price 64 | pub fn calculate_liquidation_price( 65 | entry_price: u64, 66 | leverage: u16, 67 | side: crate::consts::PositionSide, 68 | maintenance_margin_bps: u16, 69 | ) -> u64 { 70 | let leverage_ratio = (leverage as u64) * PRICE_PRECISION / 10000; 71 | let maintenance_margin_ratio = (maintenance_margin_bps as u64) * PRICE_PRECISION / 10000; 72 | 73 | match side { 74 | crate::consts::PositionSide::Long => { 75 | let ratio = PRICE_PRECISION 76 | .checked_sub(PRICE_PRECISION / leverage_ratio) 77 | .unwrap() 78 | .checked_sub(maintenance_margin_ratio) 79 | .unwrap(); 80 | entry_price 81 | .checked_mul(ratio) 82 | .unwrap() 83 | .checked_div(PRICE_PRECISION) 84 | .unwrap() 85 | } 86 | crate::consts::PositionSide::Short => { 87 | let ratio = PRICE_PRECISION 88 | .checked_add(PRICE_PRECISION / leverage_ratio) 89 | .unwrap() 90 | .checked_add(maintenance_margin_ratio) 91 | .unwrap(); 92 | entry_price 93 | .checked_mul(ratio) 94 | .unwrap() 95 | .checked_div(PRICE_PRECISION) 96 | .unwrap() 97 | } 98 | } 99 | } 100 | 101 | /// Calculate trading fee 102 | pub fn calculate_trading_fee(amount: u64, fee_bps: u16) -> u64 { 103 | (amount as u128) 104 | .checked_mul(fee_bps as u128) 105 | .unwrap() 106 | .checked_div(10000) 107 | .unwrap() as u64 108 | } 109 | 110 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/user.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | pub struct User { 7 | pub user: Pubkey, 8 | pub collateral: u64, // Total collateral deposited 9 | pub used_margin: u64, // Margin currently used in positions 10 | pub free_margin: u64, // Available margin for new positions 11 | pub total_pnl: i64, // Total profit and loss 12 | pub total_positions: u64, // Number of open positions 13 | pub total_orders: u64, // Number of active orders 14 | pub created_at: i64, 15 | pub last_updated: i64, 16 | } 17 | 18 | impl User { 19 | pub const SEEDS: &'static [u8] = USER_SEED; 20 | pub const SIZE: usize = 8 + // discriminator 21 | 32 + // user 22 | 8 + // collateral 23 | 8 + // used_margin 24 | 8 + // free_margin 25 | 8 + // total_pnl 26 | 8 + // total_positions 27 | 8 + // total_orders 28 | 8 + // created_at 29 | 8; // last_updated 30 | 31 | pub fn init(&mut self, user: Pubkey, clock: &Clock) -> Result<()> { 32 | self.user = user; 33 | self.collateral = 0; 34 | self.used_margin = 0; 35 | self.free_margin = 0; 36 | self.total_pnl = 0; 37 | self.total_positions = 0; 38 | self.total_orders = 0; 39 | self.created_at = clock.unix_timestamp; 40 | self.last_updated = clock.unix_timestamp; 41 | Ok(()) 42 | } 43 | 44 | pub fn deposit_collateral(&mut self, amount: u64, clock: &Clock) -> Result<()> { 45 | self.collateral = self.collateral.checked_add(amount).unwrap(); 46 | self.free_margin = self.free_margin.checked_add(amount).unwrap(); 47 | self.last_updated = clock.unix_timestamp; 48 | Ok(()) 49 | } 50 | 51 | pub fn withdraw_collateral(&mut self, amount: u64, clock: &Clock) -> Result<()> { 52 | require!( 53 | self.free_margin >= amount, 54 | crate::errors::PerpetualDexError::InsufficientMargin 55 | ); 56 | self.collateral = self.collateral.checked_sub(amount).unwrap(); 57 | self.free_margin = self.free_margin.checked_sub(amount).unwrap(); 58 | self.last_updated = clock.unix_timestamp; 59 | Ok(()) 60 | } 61 | 62 | pub fn use_margin(&mut self, amount: u64, clock: &Clock) -> Result<()> { 63 | require!( 64 | self.free_margin >= amount, 65 | crate::errors::PerpetualDexError::InsufficientMargin 66 | ); 67 | self.used_margin = self.used_margin.checked_add(amount).unwrap(); 68 | self.free_margin = self.free_margin.checked_sub(amount).unwrap(); 69 | self.last_updated = clock.unix_timestamp; 70 | Ok(()) 71 | } 72 | 73 | pub fn free_margin(&mut self, amount: u64, clock: &Clock) -> Result<()> { 74 | self.used_margin = self.used_margin.checked_sub(amount).unwrap(); 75 | self.free_margin = self.free_margin.checked_add(amount).unwrap(); 76 | self.last_updated = clock.unix_timestamp; 77 | Ok(()) 78 | } 79 | 80 | pub fn increment_positions(&mut self) { 81 | self.total_positions = self.total_positions.checked_add(1).unwrap(); 82 | } 83 | 84 | pub fn decrement_positions(&mut self) { 85 | self.total_positions = self.total_positions.checked_sub(1).unwrap(); 86 | } 87 | 88 | pub fn increment_orders(&mut self) { 89 | self.total_orders = self.total_orders.checked_add(1).unwrap(); 90 | } 91 | 92 | pub fn decrement_orders(&mut self) { 93 | self.total_orders = self.total_orders.checked_sub(1).unwrap(); 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/place_order.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{GlobalConfig, Market, User, Order}; 3 | use crate::consts::{OrderSide, OrderType}; 4 | use crate::events::OrderPlaced; 5 | use crate::errors::PerpetualDexError; 6 | 7 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 8 | pub struct PlaceOrderArgs { 9 | pub side: OrderSide, 10 | pub order_type: OrderType, 11 | pub size: u64, 12 | pub price: u64, 13 | pub stop_price: Option, 14 | pub leverage: u16, 15 | } 16 | 17 | #[derive(Accounts)] 18 | pub struct PlaceOrder<'info> { 19 | #[account( 20 | seeds = [GlobalConfig::SEEDS], 21 | bump, 22 | constraint = !global_config.paused @ PerpetualDexError::MarketPaused 23 | )] 24 | pub global_config: Box>, 25 | 26 | #[account( 27 | seeds = [Market::SEEDS, market.market_id.as_ref()], 28 | bump, 29 | constraint = !market.paused @ PerpetualDexError::MarketPaused 30 | )] 31 | pub market: Box>, 32 | 33 | #[account( 34 | seeds = [User::SEEDS, user.user.as_ref()], 35 | bump, 36 | constraint = user.total_orders < crate::consts::MAX_OPEN_ORDERS as u64 @ PerpetualDexError::MaxOpenOrdersExceeded 37 | )] 38 | pub user: Box>, 39 | 40 | /// CHECK: Order ID provided by user 41 | pub order_id: AccountInfo<'info>, 42 | 43 | #[account( 44 | init, 45 | payer = trader, 46 | space = Order::SIZE, 47 | seeds = [Order::SEEDS, order_id.key().as_ref()], 48 | bump 49 | )] 50 | pub order: Box>, 51 | 52 | #[account(mut)] 53 | pub trader: Signer<'info>, 54 | 55 | pub system_program: Program<'info, System>, 56 | pub clock: Sysvar<'info, Clock>, 57 | } 58 | 59 | impl<'info> PlaceOrder<'info> { 60 | pub fn process(&mut self, args: PlaceOrderArgs) -> Result<()> { 61 | require!( 62 | args.leverage >= crate::consts::MIN_LEVERAGE_BPS 63 | && args.leverage <= self.global_config.max_leverage_bps, 64 | PerpetualDexError::InvalidLeverage 65 | ); 66 | 67 | // Calculate required margin 68 | let required_margin = crate::utils::calculate_required_margin( 69 | args.size, 70 | args.price, 71 | args.leverage, 72 | )?; 73 | 74 | require!( 75 | self.user.free_margin >= required_margin, 76 | PerpetualDexError::InsufficientMargin 77 | ); 78 | 79 | // Reserve margin for order 80 | self.user.use_margin(required_margin, &self.clock)?; 81 | 82 | // Initialize order 83 | self.order.init( 84 | self.order_id.key(), 85 | self.trader.key(), 86 | self.market.key(), 87 | args.side, 88 | args.order_type, 89 | args.size, 90 | args.price, 91 | args.stop_price, 92 | args.leverage, 93 | &self.clock, 94 | )?; 95 | 96 | self.user.increment_orders(); 97 | 98 | emit!(OrderPlaced { 99 | order_id: self.order.key(), 100 | user: self.trader.key(), 101 | market: self.market.key(), 102 | side: args.side, 103 | order_type: args.order_type, 104 | size: args.size, 105 | price: args.price, 106 | leverage: args.leverage, 107 | created_at: self.clock.unix_timestamp, 108 | }); 109 | 110 | Ok(()) 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/order.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | pub struct Order { 7 | pub order_id: Pubkey, 8 | pub user: Pubkey, 9 | pub market: Pubkey, 10 | pub side: OrderSide, 11 | pub order_type: OrderType, 12 | pub status: OrderStatus, 13 | pub size: u64, // Order size in base units 14 | pub filled_size: u64, // Filled size 15 | pub price: u64, // Limit price (for limit orders) 16 | pub stop_price: Option, // Stop price (for stop orders) 17 | pub leverage: u16, // Leverage in basis points 18 | pub created_at: i64, 19 | pub updated_at: i64, 20 | } 21 | 22 | impl Order { 23 | pub const SEEDS: &'static [u8] = ORDER_SEED; 24 | pub const SIZE: usize = 8 + // discriminator 25 | 32 + // order_id 26 | 32 + // user 27 | 32 + // market 28 | 1 + // side 29 | 1 + // order_type 30 | 1 + // status 31 | 8 + // size 32 | 8 + // filled_size 33 | 8 + // price 34 | 9 + // stop_price (Option) 35 | 2 + // leverage 36 | 8 + // created_at 37 | 8; // updated_at 38 | 39 | pub fn init( 40 | &mut self, 41 | order_id: Pubkey, 42 | user: Pubkey, 43 | market: Pubkey, 44 | side: OrderSide, 45 | order_type: OrderType, 46 | size: u64, 47 | price: u64, 48 | stop_price: Option, 49 | leverage: u16, 50 | clock: &Clock, 51 | ) -> Result<()> { 52 | require!( 53 | size > 0, 54 | crate::errors::PerpetualDexError::InvalidOrderSize 55 | ); 56 | require!( 57 | price > 0 || order_type == OrderType::Market, 58 | crate::errors::PerpetualDexError::InvalidPrice 59 | ); 60 | require!( 61 | leverage >= MIN_LEVERAGE_BPS && leverage <= MAX_LEVERAGE_BPS, 62 | crate::errors::PerpetualDexError::InvalidLeverage 63 | ); 64 | 65 | self.order_id = order_id; 66 | self.user = user; 67 | self.market = market; 68 | self.side = side; 69 | self.order_type = order_type; 70 | self.status = OrderStatus::Active; 71 | self.size = size; 72 | self.filled_size = 0; 73 | self.price = price; 74 | self.stop_price = stop_price; 75 | self.leverage = leverage; 76 | self.created_at = clock.unix_timestamp; 77 | self.updated_at = clock.unix_timestamp; 78 | 79 | Ok(()) 80 | } 81 | 82 | pub fn fill(&mut self, fill_size: u64, clock: &Clock) -> Result<()> { 83 | require!( 84 | self.status == OrderStatus::Active || self.status == OrderStatus::PartiallyFilled, 85 | crate::errors::PerpetualDexError::OrderNotActive 86 | ); 87 | require!( 88 | self.filled_size + fill_size <= self.size, 89 | crate::errors::PerpetualDexError::InvalidOrderSize 90 | ); 91 | 92 | self.filled_size += fill_size; 93 | self.updated_at = clock.unix_timestamp; 94 | 95 | if self.filled_size >= self.size { 96 | self.status = OrderStatus::Filled; 97 | } else { 98 | self.status = OrderStatus::PartiallyFilled; 99 | } 100 | 101 | Ok(()) 102 | } 103 | 104 | pub fn cancel(&mut self, clock: &Clock) -> Result<()> { 105 | require!( 106 | self.status == OrderStatus::Active || self.status == OrderStatus::PartiallyFilled, 107 | crate::errors::PerpetualDexError::OrderNotActive 108 | ); 109 | self.status = OrderStatus::Cancelled; 110 | self.updated_at = clock.unix_timestamp; 111 | Ok(()) 112 | } 113 | 114 | pub fn get_remaining_size(&self) -> u64 { 115 | self.size - self.filled_size 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/open_position.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{GlobalConfig, Market, User, Position}; 3 | use crate::consts::PositionSide; 4 | use crate::events::PositionOpened; 5 | use crate::errors::PerpetualDexError; 6 | use crate::utils::calculate_required_margin; 7 | 8 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 9 | pub struct OpenPositionArgs { 10 | pub side: PositionSide, 11 | pub size: u64, 12 | pub leverage: u16, 13 | pub margin: u64, 14 | } 15 | 16 | #[derive(Accounts)] 17 | pub struct OpenPosition<'info> { 18 | #[account( 19 | seeds = [GlobalConfig::SEEDS], 20 | bump, 21 | constraint = !global_config.paused @ PerpetualDexError::MarketPaused 22 | )] 23 | pub global_config: Box>, 24 | 25 | #[account( 26 | seeds = [Market::SEEDS, market.market_id.as_ref()], 27 | bump, 28 | constraint = !market.paused @ PerpetualDexError::MarketPaused 29 | )] 30 | pub market: Box>, 31 | 32 | #[account( 33 | mut, 34 | seeds = [User::SEEDS, user.user.as_ref()], 35 | bump 36 | )] 37 | pub user: Box>, 38 | 39 | /// CHECK: Position ID provided by user 40 | pub position_id: AccountInfo<'info>, 41 | 42 | #[account( 43 | init, 44 | payer = trader, 45 | space = Position::SIZE, 46 | seeds = [Position::SEEDS, position_id.key().as_ref()], 47 | bump 48 | )] 49 | pub position: Box>, 50 | 51 | #[account(mut)] 52 | pub trader: Signer<'info>, 53 | 54 | pub system_program: Program<'info, System>, 55 | pub clock: Sysvar<'info, Clock>, 56 | } 57 | 58 | impl<'info> OpenPosition<'info> { 59 | pub fn process(&mut self, args: OpenPositionArgs) -> Result<()> { 60 | require!( 61 | args.leverage >= crate::consts::MIN_LEVERAGE_BPS 62 | && args.leverage <= self.global_config.max_leverage_bps, 63 | PerpetualDexError::InvalidLeverage 64 | ); 65 | 66 | require!( 67 | args.size <= crate::consts::MAX_POSITION_SIZE, 68 | PerpetualDexError::MaxPositionSizeExceeded 69 | ); 70 | 71 | // Calculate required margin 72 | let required_margin = calculate_required_margin( 73 | args.size, 74 | self.market.current_price, 75 | args.leverage, 76 | )?; 77 | 78 | require!( 79 | args.margin >= required_margin, 80 | PerpetualDexError::InsufficientMargin 81 | ); 82 | 83 | require!( 84 | self.user.free_margin >= args.margin, 85 | PerpetualDexError::InsufficientMargin 86 | ); 87 | 88 | // Use margin 89 | self.user.use_margin(args.margin, &self.clock)?; 90 | 91 | // Initialize position 92 | self.position.init( 93 | self.position_id.key(), 94 | self.trader.key(), 95 | self.market.key(), 96 | args.side, 97 | args.size, 98 | self.market.current_price, 99 | args.leverage, 100 | args.margin, 101 | &self.clock, 102 | )?; 103 | 104 | // Update market open interest 105 | let oi_delta = args.size as i64; 106 | match args.side { 107 | PositionSide::Long => { 108 | self.market.update_open_interest(oi_delta, 0)?; 109 | } 110 | PositionSide::Short => { 111 | self.market.update_open_interest(0, oi_delta)?; 112 | } 113 | } 114 | 115 | self.user.increment_positions(); 116 | 117 | emit!(PositionOpened { 118 | position_id: self.position.key(), 119 | user: self.trader.key(), 120 | market: self.market.key(), 121 | side: args.side, 122 | size: args.size, 123 | entry_price: self.market.current_price, 124 | leverage: args.leverage, 125 | margin: args.margin, 126 | opened_at: self.clock.unix_timestamp, 127 | }); 128 | 129 | Ok(()) 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/instructions/liquidate_position.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::states::{GlobalConfig, Market, User, Position}; 3 | use crate::events::PositionLiquidated; 4 | use crate::errors::PerpetualDexError; 5 | use crate::utils::{should_liquidate, calculate_liquidation_bonus}; 6 | 7 | #[derive(Accounts)] 8 | pub struct LiquidatePosition<'info> { 9 | #[account( 10 | seeds = [GlobalConfig::SEEDS], 11 | bump 12 | )] 13 | pub global_config: Box>, 14 | 15 | #[account( 16 | seeds = [Market::SEEDS, market.market_id.as_ref()], 17 | bump 18 | )] 19 | pub market: Box>, 20 | 21 | #[account( 22 | mut, 23 | seeds = [User::SEEDS, user.user.as_ref()], 24 | bump 25 | )] 26 | pub user: Box>, 27 | 28 | #[account( 29 | mut, 30 | seeds = [Position::SEEDS, position.position_id.as_ref()], 31 | bump, 32 | constraint = !position.is_closed @ PerpetualDexError::PositionClosed 33 | )] 34 | pub position: Box>, 35 | 36 | #[account(mut)] 37 | pub liquidator: Signer<'info>, 38 | 39 | pub clock: Sysvar<'info, Clock>, 40 | } 41 | 42 | impl<'info> LiquidatePosition<'info> { 43 | pub fn process(&mut self, _position_id: u64) -> Result<()> { 44 | // Update PnL 45 | self.position.update_pnl(self.market.current_price)?; 46 | 47 | // Check if position is liquidatable 48 | require!( 49 | should_liquidate( 50 | self.market.current_price, 51 | self.position.liquidation_price, 52 | self.position.side, 53 | ), 54 | PerpetualDexError::PositionNotLiquidatable 55 | ); 56 | 57 | // Calculate liquidation bonus 58 | let position_value = (self.position.size as u128) 59 | .checked_mul(self.market.current_price as u128) 60 | .unwrap() 61 | .checked_div(crate::consts::PRICE_PRECISION as u128) 62 | .unwrap() as u64; 63 | 64 | let liquidation_bonus = calculate_liquidation_bonus( 65 | position_value, 66 | self.global_config.liquidation_fee_bps, 67 | ); 68 | 69 | // Free margin for user (what's left after liquidation) 70 | let remaining_margin = if self.position.pnl >= 0 { 71 | self.position.margin.checked_add(self.position.pnl as u64).unwrap_or(0) 72 | } else { 73 | self.position.margin.checked_sub((-self.position.pnl) as u64).unwrap_or(0) 74 | }; 75 | 76 | if remaining_margin > liquidation_bonus { 77 | let user_remaining = remaining_margin.checked_sub(liquidation_bonus).unwrap(); 78 | self.user.free_margin(user_remaining, &self.clock)?; 79 | self.user.collateral = self.user.collateral.checked_sub(self.position.margin).unwrap_or(0); 80 | self.user.collateral = self.user.collateral.checked_add(user_remaining).unwrap(); 81 | } else { 82 | self.user.collateral = self.user.collateral.checked_sub(self.position.margin).unwrap_or(0); 83 | } 84 | 85 | // Update market open interest 86 | let oi_delta = -(self.position.size as i64); 87 | match self.position.side { 88 | crate::consts::PositionSide::Long => { 89 | self.market.update_open_interest(oi_delta, 0)?; 90 | } 91 | crate::consts::PositionSide::Short => { 92 | self.market.update_open_interest(0, oi_delta)?; 93 | } 94 | } 95 | 96 | // Close position 97 | self.position.close(&self.clock)?; 98 | self.user.decrement_positions(); 99 | 100 | // Note: In production, liquidation bonus would be transferred to liquidator 101 | // This would require additional accounts for token transfers 102 | 103 | emit!(PositionLiquidated { 104 | position_id: self.position.key(), 105 | user: self.user.user, 106 | market: self.market.key(), 107 | liquidation_price: self.market.current_price, 108 | pnl: self.position.pnl, 109 | liquidated_at: self.clock.unix_timestamp, 110 | }); 111 | 112 | Ok(()) 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/market.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | pub struct Market { 7 | pub market_id: Pubkey, 8 | pub base_mint: Pubkey, // Base asset (e.g., SOL) 9 | pub quote_mint: Pubkey, // Quote asset (e.g., USDC) 10 | pub name: String, 11 | pub symbol: String, 12 | pub current_price: u64, // Current price in quote units (scaled by PRICE_PRECISION) 13 | pub last_update_time: i64, 14 | pub total_long_size: u64, // Total long position size 15 | pub total_short_size: u64, // Total short position size 16 | pub open_interest_long: u64, // Open interest for long positions 17 | pub open_interest_short: u64, // Open interest for short positions 18 | pub funding_rate: i64, // Funding rate in basis points (can be negative) 19 | pub next_funding_time: i64, // Next funding payment time 20 | pub paused: bool, 21 | pub created_at: i64, 22 | } 23 | 24 | impl Market { 25 | pub const SEEDS: &'static [u8] = MARKET_SEED; 26 | pub const BASE_SIZE: usize = 8 + // discriminator 27 | 32 + // market_id 28 | 32 + // base_mint 29 | 32 + // quote_mint 30 | 4 + // name (String length prefix) 31 | 4 + // symbol (String length prefix) 32 | 8 + // current_price 33 | 8 + // last_update_time 34 | 8 + // total_long_size 35 | 8 + // total_short_size 36 | 8 + // open_interest_long 37 | 8 + // open_interest_short 38 | 8 + // funding_rate 39 | 8 + // next_funding_time 40 | 1 + // paused 41 | 8; // created_at 42 | 43 | pub fn init( 44 | &mut self, 45 | market_id: Pubkey, 46 | base_mint: Pubkey, 47 | quote_mint: Pubkey, 48 | name: String, 49 | symbol: String, 50 | initial_price: u64, 51 | clock: &Clock, 52 | ) -> Result<()> { 53 | require!( 54 | name.len() <= 50, 55 | crate::errors::PerpetualDexError::InvalidConfiguration 56 | ); 57 | require!( 58 | symbol.len() <= 10, 59 | crate::errors::PerpetualDexError::InvalidConfiguration 60 | ); 61 | 62 | self.market_id = market_id; 63 | self.base_mint = base_mint; 64 | self.quote_mint = quote_mint; 65 | self.name = name; 66 | self.symbol = symbol; 67 | self.current_price = initial_price; 68 | self.last_update_time = clock.unix_timestamp; 69 | self.total_long_size = 0; 70 | self.total_short_size = 0; 71 | self.open_interest_long = 0; 72 | self.open_interest_short = 0; 73 | self.funding_rate = 0; 74 | self.next_funding_time = clock.unix_timestamp + 3600; // 1 hour default 75 | self.paused = false; 76 | self.created_at = clock.unix_timestamp; 77 | 78 | Ok(()) 79 | } 80 | 81 | pub fn update_price(&mut self, new_price: u64, clock: &Clock) -> Result<()> { 82 | require!( 83 | new_price > 0, 84 | crate::errors::PerpetualDexError::InvalidPrice 85 | ); 86 | self.current_price = new_price; 87 | self.last_update_time = clock.unix_timestamp; 88 | Ok(()) 89 | } 90 | 91 | pub fn update_open_interest(&mut self, long_delta: i64, short_delta: i64) -> Result<()> { 92 | if long_delta > 0 { 93 | self.open_interest_long = self.open_interest_long 94 | .checked_add(long_delta as u64) 95 | .ok_or(crate::errors::PerpetualDexError::InvalidConfiguration)?; 96 | } else if long_delta < 0 { 97 | self.open_interest_long = self.open_interest_long 98 | .checked_sub((-long_delta) as u64) 99 | .ok_or(crate::errors::PerpetualDexError::InvalidConfiguration)?; 100 | } 101 | 102 | if short_delta > 0 { 103 | self.open_interest_short = self.open_interest_short 104 | .checked_add(short_delta as u64) 105 | .ok_or(crate::errors::PerpetualDexError::InvalidConfiguration)?; 106 | } else if short_delta < 0 { 107 | self.open_interest_short = self.open_interest_short 108 | .checked_sub((-short_delta) as u64) 109 | .ok_or(crate::errors::PerpetualDexError::InvalidConfiguration)?; 110 | } 111 | 112 | Ok(()) 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /programs/perpetual-dex/src/states/position.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::consts::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | pub struct Position { 7 | pub position_id: Pubkey, 8 | pub user: Pubkey, 9 | pub market: Pubkey, 10 | pub side: PositionSide, 11 | pub size: u64, // Position size in base units 12 | pub entry_price: u64, // Entry price 13 | pub current_price: u64, // Current market price 14 | pub leverage: u16, // Leverage in basis points (e.g., 1000 = 10x) 15 | pub margin: u64, // Collateral margin 16 | pub liquidation_price: u64, // Price at which position will be liquidated 17 | pub pnl: i64, // Current profit and loss 18 | pub funding_payments: i64, // Cumulative funding payments 19 | pub opened_at: i64, 20 | pub last_updated: i64, 21 | pub is_closed: bool, 22 | } 23 | 24 | impl Position { 25 | pub const SEEDS: &'static [u8] = POSITION_SEED; 26 | pub const SIZE: usize = 8 + // discriminator 27 | 32 + // position_id 28 | 32 + // user 29 | 32 + // market 30 | 1 + // side 31 | 8 + // size 32 | 8 + // entry_price 33 | 8 + // current_price 34 | 2 + // leverage 35 | 8 + // margin 36 | 8 + // liquidation_price 37 | 8 + // pnl 38 | 8 + // funding_payments 39 | 8 + // opened_at 40 | 8 + // last_updated 41 | 1; // is_closed 42 | 43 | pub fn init( 44 | &mut self, 45 | position_id: Pubkey, 46 | user: Pubkey, 47 | market: Pubkey, 48 | side: PositionSide, 49 | size: u64, 50 | entry_price: u64, 51 | leverage: u16, 52 | margin: u64, 53 | clock: &Clock, 54 | ) -> Result<()> { 55 | require!( 56 | size > 0, 57 | crate::errors::PerpetualDexError::InvalidOrderSize 58 | ); 59 | require!( 60 | entry_price > 0, 61 | crate::errors::PerpetualDexError::InvalidPrice 62 | ); 63 | require!( 64 | leverage >= MIN_LEVERAGE_BPS && leverage <= MAX_LEVERAGE_BPS, 65 | crate::errors::PerpetualDexError::InvalidLeverage 66 | ); 67 | require!( 68 | margin > 0, 69 | crate::errors::PerpetualDexError::InsufficientMargin 70 | ); 71 | 72 | self.position_id = position_id; 73 | self.user = user; 74 | self.market = market; 75 | self.side = side; 76 | self.size = size; 77 | self.entry_price = entry_price; 78 | self.current_price = entry_price; 79 | self.leverage = leverage; 80 | self.margin = margin; 81 | self.liquidation_price = self.calculate_liquidation_price(entry_price, leverage, side); 82 | self.pnl = 0; 83 | self.funding_payments = 0; 84 | self.opened_at = clock.unix_timestamp; 85 | self.last_updated = clock.unix_timestamp; 86 | self.is_closed = false; 87 | 88 | Ok(()) 89 | } 90 | 91 | pub fn calculate_liquidation_price(&self, entry_price: u64, leverage: u16, side: PositionSide) -> u64 { 92 | // Simplified liquidation price calculation 93 | // For long: liquidation_price = entry_price * (1 - 1/leverage - maintenance_margin) 94 | // For short: liquidation_price = entry_price * (1 + 1/leverage + maintenance_margin) 95 | let leverage_ratio = (leverage as u64) * PRICE_PRECISION / 10000; 96 | let maintenance_margin_ratio = (MAINTENANCE_MARGIN_BPS as u64) * PRICE_PRECISION / 10000; 97 | 98 | match side { 99 | PositionSide::Long => { 100 | let ratio = PRICE_PRECISION - (PRICE_PRECISION / leverage_ratio) - maintenance_margin_ratio; 101 | entry_price * ratio / PRICE_PRECISION 102 | } 103 | PositionSide::Short => { 104 | let ratio = PRICE_PRECISION + (PRICE_PRECISION / leverage_ratio) + maintenance_margin_ratio; 105 | entry_price * ratio / PRICE_PRECISION 106 | } 107 | } 108 | } 109 | 110 | pub fn update_pnl(&mut self, current_price: u64) -> Result<()> { 111 | self.current_price = current_price; 112 | 113 | let price_diff = if current_price > self.entry_price { 114 | current_price - self.entry_price 115 | } else { 116 | self.entry_price - current_price 117 | }; 118 | 119 | let pnl_amount = (self.size as u128) 120 | .checked_mul(price_diff as u128) 121 | .unwrap() 122 | .checked_div(PRICE_PRECISION as u128) 123 | .unwrap() as u64; 124 | 125 | self.pnl = match self.side { 126 | PositionSide::Long => { 127 | if current_price >= self.entry_price { 128 | pnl_amount as i64 129 | } else { 130 | -(pnl_amount as i64) 131 | } 132 | } 133 | PositionSide::Short => { 134 | if current_price <= self.entry_price { 135 | pnl_amount as i64 136 | } else { 137 | -(pnl_amount as i64) 138 | } 139 | } 140 | }; 141 | 142 | self.last_updated = Clock::get()?.unix_timestamp; 143 | Ok(()) 144 | } 145 | 146 | pub fn is_liquidatable(&self, current_price: u64) -> bool { 147 | match self.side { 148 | PositionSide::Long => current_price <= self.liquidation_price, 149 | PositionSide::Short => current_price >= self.liquidation_price, 150 | } 151 | } 152 | 153 | pub fn close(&mut self, clock: &Clock) -> Result<()> { 154 | require!( 155 | !self.is_closed, 156 | crate::errors::PerpetualDexError::PositionClosed 157 | ); 158 | self.is_closed = true; 159 | self.last_updated = clock.unix_timestamp; 160 | Ok(()) 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perpetual DEX Smart Contract 2 | 3 | A comprehensive Solana smart contract for perpetual futures trading with orderbook, margin trading, and leverage support. Similar to Axiom.trade functionality, this contract enables decentralized perpetual trading on Solana. Feel free to reach out of me when you have any question [Telegram: https://t.me/DevCutup, Whatsapp: https://wa.me/13137423660]. 4 | 5 | ## Overview 6 | 7 | This smart contract provides a complete perpetual DEX solution on Solana, featuring orderbook-based trading, margin trading with leverage, position management, and automated liquidation. The contract supports both long and short positions with configurable leverage up to 100x. 8 | 9 | ### Key Features 10 | 11 | - **Orderbook Trading**: Limit orders, market orders, stop-loss, and take-profit orders 12 | - **Perpetual Futures**: No expiration dates, trade indefinitely 13 | - **Margin Trading**: Deposit collateral and trade with leverage 14 | - **High Leverage**: Support for up to 100x leverage (configurable) 15 | - **Position Management**: Open, close, add/remove margin from positions 16 | - **Automated Liquidation**: Under-collateralized positions are automatically liquidated 17 | - **PnL Tracking**: Real-time profit and loss calculation 18 | - **Funding Rates**: Perpetual funding mechanism (structure in place) 19 | - **Open Interest Tracking**: Monitor total long and short positions 20 | 21 | ## Architecture 22 | 23 | The contract follows a modular structure similar to production Solana programs: 24 | 25 | ``` 26 | programs/perpetual-dex/src/ 27 | ├── lib.rs # Main program entry point 28 | ├── consts.rs # Constants and enums 29 | ├── errors.rs # Custom error definitions 30 | ├── events/ # Event definitions 31 | │ └── events.rs 32 | ├── instructions/ # Instruction handlers 33 | │ ├── initialize.rs 34 | │ ├── create_market.rs 35 | │ ├── place_order.rs 36 | │ ├── cancel_order.rs 37 | │ ├── open_position.rs 38 | │ ├── close_position.rs 39 | │ ├── add_margin.rs 40 | │ ├── remove_margin.rs 41 | │ ├── liquidate_position.rs 42 | │ ├── deposit_collateral.rs 43 | │ ├── withdraw_collateral.rs 44 | │ └── update_price.rs 45 | ├── states/ # Account state structures 46 | │ ├── global_config.rs 47 | │ ├── market.rs 48 | │ ├── user.rs 49 | │ ├── position.rs 50 | │ └── order.rs 51 | └── utils/ # Utility functions 52 | ├── calculations.rs 53 | ├── margin.rs 54 | └── liquidation.rs 55 | ``` 56 | 57 | ## Smart Contract Instructions 58 | 59 | ### 1. `initialize` 60 | 61 | Initializes the global configuration for the perpetual DEX platform. Must be called once before any markets can be created. 62 | 63 | **Parameters:** 64 | - `fee_recipient`: Address that receives trading and liquidation fees 65 | - `trading_fee_bps`: Trading fee in basis points (10000 = 100%) 66 | - `liquidation_fee_bps`: Liquidation fee in basis points 67 | - `max_leverage_bps`: Maximum leverage in basis points (10000 = 100x) 68 | - `initial_margin_bps`: Initial margin requirement in basis points 69 | - `maintenance_margin_bps`: Maintenance margin requirement in basis points 70 | - `liquidation_threshold_bps`: Liquidation threshold in basis points 71 | 72 | **Accounts:** 73 | - `global_config`: Global configuration PDA 74 | - `authority`: Authority that can update configuration 75 | - `system_program`: Solana system program 76 | 77 | ### 2. `create_market` 78 | 79 | Creates a new perpetual market for trading. 80 | 81 | **Parameters:** 82 | - `base_mint`: Base asset mint (e.g., SOL) 83 | - `quote_mint`: Quote asset mint (e.g., USDC) 84 | - `name`: Market name 85 | - `symbol`: Market symbol 86 | - `initial_price`: Initial price in quote units 87 | 88 | **Accounts:** 89 | - `global_config`: Global configuration PDA 90 | - `market_id`: Unique market identifier 91 | - `market`: Market account PDA 92 | - `authority`: Authority creating the market 93 | - `system_program`: Solana system program 94 | 95 | **Events:** 96 | - `MarketCreated`: Emitted when a market is created 97 | 98 | ### 3. `place_order` 99 | 100 | Places an order in the orderbook. 101 | 102 | **Parameters:** 103 | - `side`: Order side (Buy or Sell) 104 | - `order_type`: Order type (Market, Limit, StopLoss, TakeProfit) 105 | - `size`: Order size in base units 106 | - `price`: Limit price (for limit orders) 107 | - `stop_price`: Stop price (for stop orders, optional) 108 | - `leverage`: Leverage in basis points (e.g., 1000 = 10x) 109 | 110 | **Accounts:** 111 | - `global_config`: Global configuration PDA 112 | - `market`: Market account PDA 113 | - `user`: User account PDA 114 | - `order_id`: Unique order identifier 115 | - `order`: Order account PDA 116 | - `trader`: Trader (signer) 117 | - `system_program`: Solana system program 118 | 119 | **Events:** 120 | - `OrderPlaced`: Emitted when an order is placed 121 | 122 | ### 4. `cancel_order` 123 | 124 | Cancels an existing order. 125 | 126 | **Parameters:** 127 | - `order_id`: Order ID to cancel 128 | 129 | **Accounts:** 130 | - `user`: User account PDA 131 | - `order`: Order account PDA 132 | - `trader`: Trader (signer) 133 | 134 | **Events:** 135 | - `OrderCancelled`: Emitted when an order is cancelled 136 | 137 | ### 5. `open_position` 138 | 139 | Opens a new perpetual position (long or short). 140 | 141 | **Parameters:** 142 | - `side`: Position side (Long or Short) 143 | - `size`: Position size in base units 144 | - `leverage`: Leverage in basis points 145 | - `margin`: Collateral margin amount 146 | 147 | **Accounts:** 148 | - `global_config`: Global configuration PDA 149 | - `market`: Market account PDA 150 | - `user`: User account PDA 151 | - `position_id`: Unique position identifier 152 | - `position`: Position account PDA 153 | - `trader`: Trader (signer) 154 | - `system_program`: Solana system program 155 | 156 | **Events:** 157 | - `PositionOpened`: Emitted when a position is opened 158 | 159 | ### 6. `close_position` 160 | 161 | Closes an existing perpetual position. 162 | 163 | **Parameters:** 164 | - `position_id`: Position ID to close 165 | 166 | **Accounts:** 167 | - `global_config`: Global configuration PDA 168 | - `market`: Market account PDA 169 | - `user`: User account PDA 170 | - `position`: Position account PDA 171 | - `trader`: Trader (signer) 172 | 173 | **Events:** 174 | - `PositionClosed`: Emitted when a position is closed 175 | 176 | ### 7. `add_margin` 177 | 178 | Adds margin to an existing position to reduce liquidation risk. 179 | 180 | **Parameters:** 181 | - `position_id`: Position ID 182 | - `amount`: Amount of margin to add 183 | 184 | **Accounts:** 185 | - `user`: User account PDA 186 | - `position`: Position account PDA 187 | - `trader`: Trader (signer) 188 | 189 | **Events:** 190 | - `MarginAdded`: Emitted when margin is added 191 | 192 | ### 8. `remove_margin` 193 | 194 | Removes margin from a position (must maintain maintenance margin). 195 | 196 | **Parameters:** 197 | - `position_id`: Position ID 198 | - `amount`: Amount of margin to remove 199 | 200 | **Accounts:** 201 | - `user`: User account PDA 202 | - `position`: Position account PDA 203 | - `trader`: Trader (signer) 204 | 205 | **Events:** 206 | - `MarginRemoved`: Emitted when margin is removed 207 | 208 | ### 9. `liquidate_position` 209 | 210 | Liquidates an under-collateralized position. 211 | 212 | **Parameters:** 213 | - `position_id`: Position ID to liquidate 214 | 215 | **Accounts:** 216 | - `global_config`: Global configuration PDA 217 | - `market`: Market account PDA 218 | - `user`: User account PDA 219 | - `position`: Position account PDA 220 | - `liquidator`: Liquidator (signer) 221 | 222 | **Events:** 223 | - `PositionLiquidated`: Emitted when a position is liquidated 224 | 225 | ### 10. `deposit_collateral` 226 | 227 | Deposits collateral to user account for trading. 228 | 229 | **Parameters:** 230 | - `amount`: Amount of collateral to deposit 231 | 232 | **Accounts:** 233 | - `user_account`: User account PDA 234 | - `user`: User (signer) 235 | - `system_program`: Solana system program 236 | 237 | **Events:** 238 | - `CollateralDeposited`: Emitted when collateral is deposited 239 | 240 | ### 11. `withdraw_collateral` 241 | 242 | Withdraws collateral from user account. 243 | 244 | **Parameters:** 245 | - `amount`: Amount of collateral to withdraw 246 | 247 | **Accounts:** 248 | - `user_account`: User account PDA 249 | - `user`: User (signer) 250 | 251 | **Events:** 252 | - `CollateralWithdrawn`: Emitted when collateral is withdrawn 253 | 254 | ### 12. `update_price` 255 | 256 | Updates the market price (oracle). 257 | 258 | **Parameters:** 259 | - `price`: New market price 260 | 261 | **Accounts:** 262 | - `market`: Market account PDA 263 | - `oracle`: Oracle or authorized price updater (signer) 264 | 265 | **Events:** 266 | - `PriceUpdated`: Emitted when price is updated 267 | 268 | ## Margin and Leverage 269 | 270 | ### Margin Requirements 271 | 272 | - **Initial Margin**: Minimum margin required to open a position (default: 10%) 273 | - **Maintenance Margin**: Minimum margin to maintain a position (default: 5%) 274 | - **Liquidation Threshold**: Price level at which position is liquidated (default: 4%) 275 | 276 | ### Leverage 277 | 278 | - **Minimum Leverage**: 1x (100 basis points) 279 | - **Maximum Leverage**: 100x (10000 basis points, configurable) 280 | - **Leverage Calculation**: `position_value = margin * leverage` 281 | 282 | ## Position Types 283 | 284 | ### Long Position 285 | - Profit when price increases 286 | - Loss when price decreases 287 | - Liquidated when price falls below liquidation price 288 | 289 | ### Short Position 290 | - Profit when price decreases 291 | - Loss when price increases 292 | - Liquidated when price rises above liquidation price 293 | 294 | ## Order Types 295 | 296 | - **Market Order**: Executes immediately at best available price 297 | - **Limit Order**: Executes at specified price or better 298 | - **Stop Loss**: Triggers when price reaches stop price 299 | - **Take Profit**: Triggers when price reaches target price 300 | 301 | ## Requirements 302 | 303 | - **Anchor Framework**: Version 0.30.1 304 | - **Solana CLI**: Latest version 305 | - **Rust**: Latest stable version 306 | - **Node.js**: 18+ (for tests) 307 | 308 | ## Installation 309 | 310 | 1. Clone the repository: 311 | ```bash 312 | git clone 313 | cd Solana-Perpetual-Dex-Smart-Contract 314 | ``` 315 | 316 | 2. Install dependencies: 317 | ```bash 318 | anchor build 319 | ``` 320 | 321 | 3. Run tests: 322 | ```bash 323 | anchor test 324 | ``` 325 | 326 | ## Configuration 327 | 328 | The contract can be configured through the `initialize` instruction: 329 | 330 | - **Trading Fee**: Default configurable (e.g., 0.1% = 10 basis points) 331 | - **Liquidation Fee**: Default configurable (e.g., 2% = 200 basis points) 332 | - **Max Leverage**: Default 100x (10000 basis points) 333 | - **Initial Margin**: Default 10% (1000 basis points) 334 | - **Maintenance Margin**: Default 5% (500 basis points) 335 | 336 | ## Security Considerations 337 | 338 | 1. **Access Control**: Only authorized accounts can perform sensitive operations 339 | 2. **Margin Validation**: All margin requirements are validated before operations 340 | 3. **Liquidation Checks**: Positions are checked for liquidation eligibility 341 | 4. **Price Validation**: All prices are validated for correctness 342 | 5. **Slippage Protection**: Orders can specify maximum slippage 343 | 6. **Pause Mechanism**: Markets can be paused for emergency situations 344 | 345 | ## Contact Information 346 | 347 | - **X (Twitter)**: [@devcutup](https://twitter.com/devcutup) 348 | - **Telegram**: [@DevCutup](https://t.me/DevCutup) 349 | - **WhatsApp**: [Contact via WhatsApp](https://wa.me/13137423660) 350 | --------------------------------------------------------------------------------