diff --git a/README.md b/README.md index 239c218..225217e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also run `./fetch-release.sh` to download the latest Linux and macOS bin * Historical and spot price via CoinGecko for SOL and supported tokens. * Data is contained in a local `sell-your-sol/` subdirectory that can be easily backed up, and is editable by hand if necessary * Full Excel export, useful to hand off to a CPA or your entity's finance department. Sorry no TurboTax import! -* Companion `sys-lend` program for easy stablecoin and memecoin lending into MarginFi, Kamino and Solend +* Companion `sys-lend` program for easy stablecoin and memecoin lending into MarginFi, Kamino, Drift and Solend ## Examples Explore the help system instead: diff --git a/src/bin/sys-lend.rs b/src/bin/sys-lend.rs index 0e377ea..3ee2c81 100644 --- a/src/bin/sys-lend.rs +++ b/src/bin/sys-lend.rs @@ -32,7 +32,7 @@ use { send_transaction_until_expired, token::*, vendor::{ - kamino, marginfi_v2, + drift, kamino, marginfi_v2, solend::{self, math::TryMul}, }, *, @@ -45,30 +45,30 @@ lazy_static::lazy_static! { Token::USDC, Token::USDT, Token::wSOL, - ])) , + ])), ("solend-turbosol", HashSet::from([ Token::USDC, - ])) , + ])), ("solend-jlp", HashSet::from([ Token::USDC, Token::wSOL, - ])) , + ])), ("mfi", HashSet::from([ Token::USDC, Token::USDT, Token::UXD, Token::wSOL, - ])) , + ])), ("kamino-main", HashSet::from([ Token::USDC, Token::USDT, Token::JitoSOL, Token::wSOL, - ])) , + ])), ("kamino-jlp", HashSet::from([ Token::USDC, Token::JLP, - ])) , + ])), ("kamino-altcoins", HashSet::from([ Token::USDC, Token::JUP, @@ -77,7 +77,10 @@ lazy_static::lazy_static! { Token::WEN, Token::WIF, Token::BONK, - ])) + ])), + ("drift", HashSet::from([ + Token::USDC, + ])), ]); } @@ -322,6 +325,8 @@ fn pool_supply_apr( solend_apr(pool, token, account_data_cache)? } else if pool == "mfi" { mfi_apr(token, account_data_cache)? + } else if pool == "drift" { + drift_apr(token, account_data_cache)? } else { unreachable!() }) @@ -354,6 +359,8 @@ fn pool_supply_balance( solend_deposited_amount(pool, address, token, account_data_cache)? } else if pool == "mfi" { mfi_deposited_amount(address, token, account_data_cache)? + } else if pool == "drift" { + drift_deposited_amount(address, token, account_data_cache)? } else { unreachable!() }) @@ -432,6 +439,8 @@ async fn build_instructions_for_ops<'a>( solend_deposit_or_withdraw(*op, pool, address, token, amount, account_data_cache)? } else if *pool == "mfi" { mfi_deposit_or_withdraw(*op, address, token, amount, false, account_data_cache)? + } else if *pool == "drift" { + drift_deposit_or_withdraw(*op, address, token, amount, false, account_data_cache)? } else { unreachable!(); }; @@ -2699,7 +2708,7 @@ fn solend_deposit_or_withdraw( AccountMeta::new_readonly(spl_token::id(), false), ], )); - (amount, 100_000) + (amount, 200_000) } Operation::Withdraw => { // Instruction: Solend: Refresh Reserve @@ -2817,7 +2826,7 @@ fn solend_deposit_or_withdraw( ( amount - 1, // HACK!! Sometimes Solend loses a lamport? This breaks `rebalance`... - 150_000, + 200_000, ) } }; @@ -2829,3 +2838,253 @@ fn solend_deposit_or_withdraw( address_lookup_table: Some(pubkey!["89ig7Cu6Roi9mJMqpY8sBkPYL2cnqzpgP16sJxSUbvct"]), }) } + +////////////////////////////////////////////////////////////////////////////// +// [ Drift Stuff ] /////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// + +const DRIFT_PROGRAM: Pubkey = pubkey!["dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH"]; + +fn drift_load_user( + user_address: Pubkey, + account_data_cache: &mut AccountDataCache, +) -> Result, Box> { + let (account_data, _context_slot) = account_data_cache.get(user_address)?; + + if account_data.is_empty() { + Ok(None) + } else { + const LEN: usize = std::mem::size_of::(); + assert_eq!(LEN, drift::user::SIZE - 8); + let account_data: [u8; LEN] = account_data[8..LEN + 8].try_into().unwrap(); + let user = unsafe { std::mem::transmute::<[u8; LEN], drift::user::User>(account_data) }; + Ok(Some(user)) + } +} + +fn drift_find_and_load_user( + wallet_address: Pubkey, + account_data_cache: &mut AccountDataCache, +) -> Result<(Pubkey, Option), Box> { + let user_address = Pubkey::find_program_address( + &[ + b"user", + &wallet_address.to_bytes(), + &[0, 0], // subaccount 0 + ], + &DRIFT_PROGRAM, + ) + .0; + + let user = drift_load_user(user_address, account_data_cache)?; + if let Some(ref user) = user { + assert_eq!(user.authority, wallet_address); + } + + Ok((user_address, user)) +} + +fn drift_load_spot_market( + spot_market_address: Pubkey, + account_data_cache: &mut AccountDataCache, +) -> Result> { + let (account_data, _context_slot) = account_data_cache.get(spot_market_address)?; + + const LEN: usize = std::mem::size_of::(); + assert_eq!(LEN, drift::spot_market::SIZE - 8); + let account_data: [u8; LEN] = account_data[8..LEN + 8].try_into().unwrap(); + let spot_market = + unsafe { std::mem::transmute::<[u8; LEN], drift::spot_market::SpotMarket>(account_data) }; + Ok(spot_market) +} + +fn drift_find_and_load_spot_market( + token: Token, + account_data_cache: &mut AccountDataCache, +) -> Result> { + let market_index: u16 = match token { + Token::USDC => 0, + _ => return Err(format!("Drift support for {} not added yet", token.name()).into()), + }; + + let spot_market_address = Pubkey::find_program_address( + &[b"spot_market", &market_index.to_le_bytes()], + &DRIFT_PROGRAM, + ) + .0; + + let spot_market = drift_load_spot_market(spot_market_address, account_data_cache)?; + assert_eq!(spot_market.mint, token.mint()); + assert_eq!(spot_market.pubkey, spot_market_address); + Ok(spot_market) +} + +fn drift_apr( + token: Token, + account_data_cache: &mut AccountDataCache, +) -> Result> { + let spot_market = drift_find_and_load_spot_market(token, account_data_cache)?; + Ok(drift::calculate_accumulated_interest(&spot_market).deposit_rate) +} + +fn drift_deposited_amount( + wallet_address: Pubkey, + token: Token, + account_data_cache: &mut AccountDataCache, +) -> Result<(/*balance: */ u64, /* available_balance: */ u64), Box> { + let spot_market = drift_find_and_load_spot_market(token, account_data_cache)?; + let (_user_address, user) = drift_find_and_load_user(wallet_address, account_data_cache)?; + + let deposited_amount = match user { + None => 0, + Some(user) => { + let scaled_balance = user + .spot_positions + .iter() + .find_map(|spot_position| { + if spot_position.market_index == spot_market.market_index + && spot_position.scaled_balance > 0 + && spot_position.balance_type == drift::user::SpotBalanceType::Deposit + { + Some(spot_position.scaled_balance as u128) + } else { + None + } + }) + .unwrap_or_default(); + + drift::scaled_balance_to_token_amount( + scaled_balance, + &spot_market, + drift::user::SpotBalanceType::Deposit, + ) + } + }; + + let remaining_outflow = u64::MAX; + Ok((deposited_amount, deposited_amount.min(remaining_outflow))) +} + +fn drift_deposit_or_withdraw( + op: Operation, + wallet_address: Pubkey, + token: Token, + amount: u64, + _verbose: bool, + account_data_cache: &mut AccountDataCache, +) -> Result> { + let state_address = Pubkey::find_program_address(&[b"drift_state"], &DRIFT_PROGRAM).0; + + let (user_address, _user) = drift_find_and_load_user(wallet_address, account_data_cache) + .map_err(|err| format!("No Drift account found for {wallet_address}. Manually deposit once into Drift and retry ({err})"))?; + let user_stats_address = + Pubkey::find_program_address(&[b"user_stats", &wallet_address.to_bytes()], &DRIFT_PROGRAM) + .0; + + let spot_market = drift_find_and_load_spot_market(token, account_data_cache)?; + + let (instructions, required_compute_units, amount) = match op { + Operation::Deposit => { + // Drift: Deposit + let deposit_data = { + let mut v = vec![0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6]; + v.extend(spot_market.market_index.to_le_bytes()); + v.extend(amount.to_le_bytes()); + v.extend([/* Reduce Only = */ 0]); + v + }; + + let account_meta = vec![ + // State + AccountMeta::new_readonly(state_address, false), + // User + AccountMeta::new(user_address, false), + // User Stats + AccountMeta::new(user_stats_address, false), + // Authority + AccountMeta::new(wallet_address, false), + // Spot Market Vault + AccountMeta::new(spot_market.vault, false), + // User Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &token.mint(), + ), + false, + ), + // Token Program + AccountMeta::new_readonly(spl_token::id(), false), + // Spot Market Oracle + AccountMeta::new_readonly(spot_market.oracle, false), + // Spot Market + AccountMeta::new(spot_market.pubkey, false), + ]; + + let instructions = vec![Instruction::new_with_bytes( + DRIFT_PROGRAM, + &deposit_data, + account_meta, + )]; + + (instructions, 100_000, amount) + } + Operation::Withdraw => { + let drift_signer_address = + Pubkey::find_program_address(&[b"drift_signer"], &DRIFT_PROGRAM).0; + + // Drift: Withdraw + let withdraw_data = { + let mut v = vec![0xb7, 0x12, 0x46, 0x9c, 0x94, 0x6d, 0xa1, 0x22]; + v.extend(spot_market.market_index.to_le_bytes()); + v.extend(amount.to_le_bytes()); + v.extend([/* Reduce Only = */ 1]); + v + }; + + let account_meta = vec![ + // State + AccountMeta::new_readonly(state_address, false), + // User + AccountMeta::new(user_address, false), + // User Stats + AccountMeta::new(user_stats_address, false), + // Authority + AccountMeta::new(wallet_address, false), + // Spot Market Vault + AccountMeta::new(spot_market.vault, false), + // Drift Signer + AccountMeta::new(drift_signer_address, false), + // User Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &token.mint(), + ), + false, + ), + // Token Program + AccountMeta::new_readonly(spl_token::id(), false), + // Spot Market Oracle + AccountMeta::new_readonly(spot_market.oracle, false), + // Spot Market + AccountMeta::new(spot_market.pubkey, false), + ]; + + let instructions = vec![Instruction::new_with_bytes( + DRIFT_PROGRAM, + &withdraw_data, + account_meta, + )]; + + (instructions, 200_000, amount) + } + }; + + Ok(DepositOrWithdrawResult { + instructions, + required_compute_units, + amount, + address_lookup_table: Some(pubkey!["D9cnvzswDikQDf53k4HpQ3KJ9y1Fv3HGGDFYMXnK5T6c"]), + }) +} diff --git a/src/vendor/drift/mod.rs b/src/vendor/drift/mod.rs new file mode 100644 index 0000000..0cc1417 --- /dev/null +++ b/src/vendor/drift/mod.rs @@ -0,0 +1,118 @@ +pub mod spot_market; +pub mod user; + +use {spot_market::SpotMarket, user::SpotBalanceType}; + +pub const ONE_YEAR: u128 = 31536000; + +pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) +pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; +pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; +pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; + +pub const SPOT_BALANCE_PRECISION: u128 = 1_000_000_000; // expo = -9 + //pub const SPOT_BALANCE_PRECISION_U64: u64 = 1_000_000_000; // expo = -9 +pub const SPOT_CUMULATIVE_INTEREST_PRECISION: u128 = 10_000_000_000; // expo = -10 + +pub const SPOT_UTILIZATION_PRECISION: u128 = PERCENTAGE_PRECISION; // expo = -6 +pub const SPOT_UTILIZATION_PRECISION_U32: u32 = PERCENTAGE_PRECISION as u32; // expo = -6 +pub const SPOT_RATE_PRECISION: u128 = PERCENTAGE_PRECISION; // expo = -6 +pub const SPOT_RATE_PRECISION_U32: u32 = PERCENTAGE_PRECISION as u32; // expo = -6 + +pub fn token_amount_to_scaled_balance( + token_amount: u64, + spot_market: &SpotMarket, + balance_type: SpotBalanceType, +) -> u64 { + let precision_increase = 10_u128.pow(19_u32.saturating_sub(spot_market.decimals)); + + let cumulative_interest = match balance_type { + SpotBalanceType::Deposit => spot_market.cumulative_deposit_interest, + SpotBalanceType::Borrow => spot_market.cumulative_borrow_interest, + }; + + ((token_amount as u128) * precision_increase / cumulative_interest) as u64 +} + +pub fn scaled_balance_to_token_amount( + scaled_balance: u128, + spot_market: &SpotMarket, + balance_type: SpotBalanceType, +) -> u64 { + let precision_increase = 10_u128.pow(19_u32.saturating_sub(spot_market.decimals)); + + let cumulative_interest = match balance_type { + SpotBalanceType::Deposit => spot_market.cumulative_deposit_interest, + SpotBalanceType::Borrow => spot_market.cumulative_borrow_interest, + }; + + (scaled_balance * cumulative_interest / precision_increase) as u64 +} + +pub fn calculate_utilization(deposit_token_amount: u64, borrow_token_amount: u64) -> u128 { + (borrow_token_amount as u128 * SPOT_UTILIZATION_PRECISION) + .checked_div(deposit_token_amount as u128) + .unwrap_or({ + if deposit_token_amount == 0 && borrow_token_amount == 0 { + 0_u128 + } else { + // if there are borrows without deposits, default to maximum utilization rate + SPOT_UTILIZATION_PRECISION + } + }) +} + +pub fn calculate_spot_market_utilization(spot_market: &SpotMarket) -> u128 { + let deposit_token_amount = scaled_balance_to_token_amount( + spot_market.deposit_balance, + spot_market, + SpotBalanceType::Deposit, + ); + let borrow_token_amount = scaled_balance_to_token_amount( + spot_market.borrow_balance, + spot_market, + SpotBalanceType::Borrow, + ); + calculate_utilization(deposit_token_amount, borrow_token_amount) +} + +#[derive(Default, Debug)] +pub struct InterestRate { + pub borrow_rate: f64, + pub deposit_rate: f64, +} + +pub fn calculate_accumulated_interest(spot_market: &SpotMarket) -> InterestRate { + let utilization = calculate_spot_market_utilization(spot_market); + + if utilization == 0 { + InterestRate::default() + } else { + let borrow_rate = if utilization > spot_market.optimal_utilization as u128 { + let surplus_utilization = + utilization.saturating_sub(spot_market.optimal_utilization as u128); + + let borrow_rate_slope = (spot_market.max_borrow_rate as u128) + .saturating_sub(spot_market.optimal_borrow_rate as u128) + * SPOT_UTILIZATION_PRECISION + / SPOT_UTILIZATION_PRECISION + .saturating_sub(spot_market.optimal_utilization as u128); + + (spot_market.optimal_borrow_rate as u128) + + (surplus_utilization * borrow_rate_slope / SPOT_UTILIZATION_PRECISION) + } else { + let borrow_rate_slope = (spot_market.optimal_borrow_rate as u128) + * SPOT_UTILIZATION_PRECISION + / (spot_market.optimal_utilization as u128); + + utilization * borrow_rate_slope / SPOT_UTILIZATION_PRECISION + }; + + let deposit_rate = borrow_rate * utilization / SPOT_UTILIZATION_PRECISION; + + InterestRate { + borrow_rate: borrow_rate as f64 / PERCENTAGE_PRECISION as f64, + deposit_rate: deposit_rate as f64 / PERCENTAGE_PRECISION as f64, + } + } +} diff --git a/src/vendor/drift/spot_market.rs b/src/vendor/drift/spot_market.rs new file mode 100644 index 0000000..5736800 --- /dev/null +++ b/src/vendor/drift/spot_market.rs @@ -0,0 +1,267 @@ +use solana_sdk::pubkey::Pubkey; + +pub const SIZE: usize = 776; + +#[derive(Debug, Clone)] +#[repr(C, packed(1))] +pub struct SpotMarket { + /// The address of the spot market. It is a pda of the market index + pub pubkey: Pubkey, + /// The oracle used to price the markets deposits/borrows + pub oracle: Pubkey, + /// The token mint of the market + pub mint: Pubkey, + /// The vault used to store the market's deposits + /// The amount in the vault should be equal to or greater than deposits - borrows + pub vault: Pubkey, + /// The encoded display name for the market e.g. SOL + pub name: [u8; 32], + pub historical_oracle_data: HistoricalOracleData, + pub historical_index_data: HistoricalIndexData, + /// Revenue the protocol has collected in this markets token + /// e.g. for SOL-PERP, funds can be settled in usdc and will flow into the USDC revenue pool + pub revenue_pool: PoolBalance, // in base asset + /// The fees collected from swaps between this market and the quote market + /// Is settled to the quote markets revenue pool + pub spot_fee_pool: PoolBalance, + /// Details on the insurance fund covering bankruptcies in this markets token + /// Covers bankruptcies for borrows with this markets token and perps settling in this markets token + pub insurance_fund: InsuranceFund, + /// The total spot fees collected for this market + /// precision: QUOTE_PRECISION + pub total_spot_fee: u128, + /// The sum of the scaled balances for deposits across users and pool balances + /// To convert to the deposit token amount, multiply by the cumulative deposit interest + /// precision: SPOT_BALANCE_PRECISION + pub deposit_balance: u128, + /// The sum of the scaled balances for borrows across users and pool balances + /// To convert to the borrow token amount, multiply by the cumulative borrow interest + /// precision: SPOT_BALANCE_PRECISION + pub borrow_balance: u128, + /// The cumulative interest earned by depositors + /// Used to calculate the deposit token amount from the deposit balance + /// precision: SPOT_CUMULATIVE_INTEREST_PRECISION + pub cumulative_deposit_interest: u128, + /// The cumulative interest earned by borrowers + /// Used to calculate the borrow token amount from the borrow balance + /// precision: SPOT_CUMULATIVE_INTEREST_PRECISION + pub cumulative_borrow_interest: u128, + /// The total socialized loss from borrows, in the mint's token + /// precision: token mint precision + pub total_social_loss: u128, + /// The total socialized loss from borrows, in the quote market's token + /// preicision: QUOTE_PRECISION + pub total_quote_social_loss: u128, + /// no withdraw limits/guards when deposits below this threshold + /// precision: token mint precision + pub withdraw_guard_threshold: u64, + /// The max amount of token deposits in this market + /// 0 if there is no limit + /// precision: token mint precision + pub max_token_deposits: u64, + /// 24hr average of deposit token amount + /// precision: token mint precision + pub deposit_token_twap: u64, + /// 24hr average of borrow token amount + /// precision: token mint precision + pub borrow_token_twap: u64, + /// 24hr average of utilization + /// which is borrow amount over token amount + /// precision: SPOT_UTILIZATION_PRECISION + pub utilization_twap: u64, + /// Last time the cumulative deposit and borrow interest was updated + pub last_interest_ts: u64, + /// Last time the deposit/borrow/utilization averages were updated + pub last_twap_ts: u64, + /// The time the market is set to expire. Only set if market is in reduce only mode + pub expiry_ts: i64, + /// Spot orders must be a multiple of the step size + /// precision: token mint precision + pub order_step_size: u64, + /// Spot orders must be a multiple of the tick size + /// precision: PRICE_PRECISION + pub order_tick_size: u64, + /// The minimum order size + /// precision: token mint precision + pub min_order_size: u64, + /// The maximum spot position size + /// if the limit is 0, there is no limit + /// precision: token mint precision + pub max_position_size: u64, + /// Every spot trade has a fill record id. This is the next id to use + pub next_fill_record_id: u64, + /// Every deposit has a deposit record id. This is the next id to use + pub next_deposit_record_id: u64, + /// The initial asset weight used to calculate a deposits contribution to a users initial total collateral + /// e.g. if the asset weight is .8, $100 of deposits contributes $80 to the users initial total collateral + /// precision: SPOT_WEIGHT_PRECISION + pub initial_asset_weight: u32, + /// The maintenance asset weight used to calculate a deposits contribution to a users maintenance total collateral + /// e.g. if the asset weight is .9, $100 of deposits contributes $90 to the users maintenance total collateral + /// precision: SPOT_WEIGHT_PRECISION + pub maintenance_asset_weight: u32, + /// The initial liability weight used to calculate a borrows contribution to a users initial margin requirement + /// e.g. if the liability weight is .9, $100 of borrows contributes $90 to the users initial margin requirement + /// precision: SPOT_WEIGHT_PRECISION + pub initial_liability_weight: u32, + /// The maintenance liability weight used to calculate a borrows contribution to a users maintenance margin requirement + /// e.g. if the liability weight is .8, $100 of borrows contributes $80 to the users maintenance margin requirement + /// precision: SPOT_WEIGHT_PRECISION + pub maintenance_liability_weight: u32, + /// The initial margin fraction factor. Used to increase liability weight/decrease asset weight for large positions + /// precision: MARGIN_PRECISION + pub imf_factor: u32, + /// The fee the liquidator is paid for taking over borrow/deposit + /// precision: LIQUIDATOR_FEE_PRECISION + pub liquidator_fee: u32, + /// The fee the insurance fund receives from liquidation + /// precision: LIQUIDATOR_FEE_PRECISION + pub if_liquidation_fee: u32, + /// The optimal utilization rate for this market. + /// Used to determine the markets borrow rate + /// precision: SPOT_UTILIZATION_PRECISION + pub optimal_utilization: u32, + /// The borrow rate for this market when the market has optimal utilization + /// precision: SPOT_RATE_PRECISION + pub optimal_borrow_rate: u32, + /// The borrow rate for this market when the market has 1000 utilization + /// precision: SPOT_RATE_PRECISION + pub max_borrow_rate: u32, + /// The market's token mint's decimals. To from decimals to a precision, 10^decimals + pub decimals: u32, + pub market_index: u16, + /// Whether or not spot trading is enabled + pub orders_enabled: bool, + pub oracle_source: OracleSource, + pub status: MarketStatus, + /// The asset tier affects how a deposit can be used as collateral and the priority for a borrow being liquidated + pub asset_tier: AssetTier, + pub paused_operations: u8, + pub if_paused_operations: u8, + pub fee_adjustment: i16, + pub padding1: [u8; 2], + /// For swaps, the amount of token loaned out in the begin_swap ix + /// precision: token mint precision + pub flash_loan_amount: u64, + /// For swaps, the amount in the users token account in the begin_swap ix + /// Used to calculate how much of the token left the system in end_swap ix + /// precision: token mint precision + pub flash_loan_initial_token_amount: u64, + /// The total fees received from swaps + /// precision: token mint precision + pub total_swap_fee: u64, + /// When to begin scaling down the initial asset weight + /// disabled when 0 + /// precision: QUOTE_PRECISION + pub scale_initial_asset_weight_start: u64, + pub padding: [u8; 48], +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq, PartialOrd, Ord)] +pub enum AssetTier { + /// full priviledge + Collateral, + /// collateral, but no borrow + Protected, + /// not collateral, allow multi-borrow + Cross, + /// not collateral, only single borrow + Isolated, + /// no privilege + Unlisted, +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum MarketStatus { + /// warm up period for initialization, fills are paused + Initialized, + /// all operations allowed + Active, + /// Deprecated in favor of PausedOperations + FundingPaused, + /// Deprecated in favor of PausedOperations + AmmPaused, + /// Deprecated in favor of PausedOperations + FillPaused, + /// Deprecated in favor of PausedOperations + WithdrawPaused, + /// fills only able to reduce liability + ReduceOnly, + /// market has determined settlement price and positions are expired must be settled + Settlement, + /// market has no remaining participants + Delisted, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct PoolBalance { + /// To get the pool's token amount, you must multiply the scaled balance by the market's cumulative + /// deposit interest + /// precision: SPOT_BALANCE_PRECISION + pub scaled_balance: u128, + /// The spot market the pool is for + pub market_index: u16, + pub padding: [u8; 6], +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum OracleSource { + Pyth, + Switchboard, + QuoteAsset, + Pyth1K, + Pyth1M, + PythStableCoin, + Prelaunch, + PythPull, + Pyth1KPull, + Pyth1MPull, + PythStableCoinPull, +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +#[repr(C, packed(1))] +pub struct HistoricalOracleData { + /// precision: PRICE_PRECISION + pub last_oracle_price: i64, + /// precision: PRICE_PRECISION + pub last_oracle_conf: u64, + /// number of slots since last update + pub last_oracle_delay: i64, + /// precision: PRICE_PRECISION + pub last_oracle_price_twap: i64, + /// precision: PRICE_PRECISION + pub last_oracle_price_twap_5min: i64, + /// unix_timestamp of last snapshot + pub last_oracle_price_twap_ts: i64, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct HistoricalIndexData { + /// precision: PRICE_PRECISION + pub last_index_bid_price: u64, + /// precision: PRICE_PRECISION + pub last_index_ask_price: u64, + /// precision: PRICE_PRECISION + pub last_index_price_twap: u64, + /// precision: PRICE_PRECISION + pub last_index_price_twap_5min: u64, + /// unix_timestamp of last snapshot + pub last_index_price_twap_ts: i64, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct InsuranceFund { + pub vault: Pubkey, + pub total_shares: u128, + pub user_shares: u128, + pub shares_base: u128, // exponent for lp shares (for rebasing) + pub unstaking_period: i64, // if_unstaking_period + pub last_revenue_settle_ts: i64, + pub revenue_settle_period: i64, + pub total_factor: u32, // percentage of interest for total insurance + pub user_factor: u32, // percentage of interest for user staked insurance +} diff --git a/src/vendor/drift/user.rs b/src/vendor/drift/user.rs new file mode 100644 index 0000000..92a995c --- /dev/null +++ b/src/vendor/drift/user.rs @@ -0,0 +1,220 @@ +use solana_sdk::pubkey::Pubkey; + +pub const SIZE: usize = 4376; + +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct User { + /// The owner/authority of the account + pub authority: Pubkey, + /// An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw + pub delegate: Pubkey, + /// Encoded display name e.g. "toly" + pub name: [u8; 32], + /// The user's spot positions + pub spot_positions: [SpotPosition; 8], + /// The user's perp positions + pub perp_positions: [PerpPosition; 8], + /// The user's orders + pub orders: [Order; 32], + /// The last time the user added perp lp positions + pub last_add_perp_lp_shares_ts: i64, + /// The total values of deposits the user has made + /// precision: QUOTE_PRECISION + pub total_deposits: u64, + /// The total values of withdrawals the user has made + /// precision: QUOTE_PRECISION + pub total_withdraws: u64, + /// The total socialized loss the users has incurred upon the protocol + /// precision: QUOTE_PRECISION + pub total_social_loss: u64, + /// Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps + /// precision: QUOTE_PRECISION + pub settled_perp_pnl: i64, + /// Fees (taker fees, maker rebate, filler reward) for spot + /// precision: QUOTE_PRECISION + pub cumulative_spot_fees: i64, + /// Cumulative funding paid/received for perps + /// precision: QUOTE_PRECISION + pub cumulative_perp_funding: i64, + /// The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time + /// Defaults to zero when not being liquidated + /// precision: QUOTE_PRECISION + pub liquidation_margin_freed: u64, + /// The last slot a user was active. Used to determine if a user is idle + pub last_active_slot: u64, + /// Every user order has an order id. This is the next order id to be used + pub next_order_id: u32, + /// Custom max initial margin ratio for the user + pub max_margin_ratio: u32, + /// The next liquidation id to be used for user + pub next_liquidation_id: u16, + /// The sub account id for this user + pub sub_account_id: u16, + /// Whether the user is active, being liquidated or bankrupt + pub status: u8, + /// Whether the user has enabled margin trading + pub is_margin_trading_enabled: bool, + /// User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows + /// Off-chain keeper bots can ignore users that are idle + pub idle: bool, + /// number of open orders + pub open_orders: u8, + /// Whether or not user has open order + pub has_open_order: bool, + /// number of open orders with auction + pub open_auctions: u8, + /// Whether or not user has open order with auction + pub has_open_auction: bool, + pub padding: [u8; 21], +} + +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct SpotPosition { + /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow + /// interest of corresponding market. + /// precision: SPOT_BALANCE_PRECISION + pub scaled_balance: u64, + /// How many spot bids the user has open + /// precision: token mint precision + pub open_bids: i64, + /// How many spot asks the user has open + /// precision: token mint precision + pub open_asks: i64, + /// The cumulative deposits/borrows a user has made into a market + /// precision: token mint precision + pub cumulative_deposits: i64, + /// The market index of the corresponding spot market + pub market_index: u16, + /// Whether the position is deposit or borrow + pub balance_type: SpotBalanceType, + /// Number of open orders + pub open_orders: u8, + pub padding: [u8; 4], +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum SpotBalanceType { + Deposit, + Borrow, +} + +#[derive(Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PerpPosition { + /// The perp market's last cumulative funding rate. Used to calculate the funding payment owed to user + /// precision: FUNDING_RATE_PRECISION + pub last_cumulative_funding_rate: i64, + /// the size of the users perp position + /// precision: BASE_PRECISION + pub base_asset_amount: i64, + /// Used to calculate the users pnl. Upon entry, is equal to base_asset_amount * avg entry price - fees + /// Updated when the user open/closes position or settles pnl. Includes fees/funding + /// precision: QUOTE_PRECISION + pub quote_asset_amount: i64, + /// The amount of quote the user would need to exit their position at to break even + /// Updated when the user open/closes position or settles pnl. Includes fees/funding + /// precision: QUOTE_PRECISION + pub quote_break_even_amount: i64, + /// The amount quote the user entered the position with. Equal to base asset amount * avg entry price + /// Updated when the user open/closes position. Excludes fees/funding + /// precision: QUOTE_PRECISION + pub quote_entry_amount: i64, + /// The amount of open bids the user has in this perp market + /// precision: BASE_PRECISION + pub open_bids: i64, + /// The amount of open asks the user has in this perp market + /// precision: BASE_PRECISION + pub open_asks: i64, + /// The amount of pnl settled in this market since opening the position + /// precision: QUOTE_PRECISION + pub settled_pnl: i64, + /// The number of lp (liquidity provider) shares the user has in this perp market + /// LP shares allow users to provide liquidity via the AMM + /// precision: BASE_PRECISION + pub lp_shares: u64, + /// The last base asset amount per lp the amm had + /// Used to settle the users lp position + /// precision: BASE_PRECISION + pub last_base_asset_amount_per_lp: i64, + /// The last quote asset amount per lp the amm had + /// Used to settle the users lp position + /// precision: QUOTE_PRECISION + pub last_quote_asset_amount_per_lp: i64, + /// Settling LP position can lead to a small amount of base asset being left over smaller than step size + /// This records that remainder so it can be settled later on + /// precision: BASE_PRECISION + pub remainder_base_asset_amount: i32, + /// The market index for the perp market + pub market_index: u16, + /// The number of open orders + pub open_orders: u8, + pub per_lp_base: i8, +} + +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct Order { + pub slot: u64, + pub price: u64, + pub base_asset_amount: u64, + pub base_asset_amount_filled: u64, + pub quote_asset_amount_filled: u64, + pub trigger_price: u64, + pub auction_start_price: i64, + pub auction_end_price: i64, + pub max_ts: i64, + pub oracle_price_offset: i32, + pub order_id: u32, + pub market_index: u16, + pub status: OrderStatus, + pub order_type: OrderType, + pub market_type: MarketType, + pub user_order_id: u8, + pub existing_position_direction: PositionDirection, + pub direction: PositionDirection, + pub reduce_only: bool, + pub post_only: bool, + pub immediate_or_cancel: bool, + pub trigger_condition: OrderTriggerCondition, + pub auction_duration: u8, + pub padding: [u8; 3], +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum OrderStatus { + Init, + Open, + Filled, + Canceled, +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum OrderType { + Market, + Limit, + TriggerMarket, + TriggerLimit, + Oracle, +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum OrderTriggerCondition { + Above, + Below, + TriggeredAbove, // above condition has been triggered + TriggeredBelow, // below condition has been triggered +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum MarketType { + Spot, + Perp, +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PositionDirection { + Long, + Short, +} diff --git a/src/vendor/mod.rs b/src/vendor/mod.rs index db0e99b..2574bed 100644 --- a/src/vendor/mod.rs +++ b/src/vendor/mod.rs @@ -1,4 +1,5 @@ /// These projects don't provide a usable Rust SDK.. +pub mod drift; pub mod kamino; pub mod marginfi_v2; pub mod solend;