From dbcb6b8508e2ce4e5cdc9885e00becb8a678e047 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Thu, 25 Jul 2024 16:40:30 -0700 Subject: [PATCH 01/17] sdk: change postSwitchboardOnDemandUpdate --- sdk/src/driftClient.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index b82333743..89eb7731a 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -148,7 +148,7 @@ import { PythSolanaReceiver } from '@pythnetwork/pyth-solana-receiver/lib/idl/py import { getFeedIdUint8Array, trimFeedId } from './util/pythPullOracleUtils'; import { isVersionedTransaction } from './tx/utils'; import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; -import { PullFeed } from '@switchboard-xyz/on-demand'; +import { asV0Tx, PullFeed } from '@switchboard-xyz/on-demand'; import switchboardOnDemandIdl from './idl/switchboard_on_demand_30.json'; type RemainingAccountParams = { @@ -7320,10 +7320,18 @@ export class DriftClient { if (!pullIx) { return undefined; } - const tx = await this.buildTransaction(pullIx, undefined, 0, [ - await this.fetchMarketLookupTableAccount(), - ]); - const { txSig } = await this.sendTransaction(tx, [], this.opts); + const tx = await asV0Tx({ + connection: this.connection, + ixs: [pullIx], + payer: this.wallet.publicKey, + computeUnitLimitMultiple: 1.3, + lookupTables: [await this.fetchMarketLookupTableAccount()], + }); + const { txSig } = await this.sendTransaction(tx, [], { + commitment: 'processed', + skipPreflight: true, + maxRetries: 0, + }); return txSig; } From d3687e14a98adf4d7c2c015df188730bfbd56d96 Mon Sep 17 00:00:00 2001 From: lil perp Date: Thu, 25 Jul 2024 19:41:08 -0400 Subject: [PATCH 02/17] program: add token 2022 support (#1125) * program: add token 2022 support * add test * add test for swap * try fix order race condition * prettify * do transfer_checked * add token mint logic to drift client * add mock pyusd for devent * CHANGELOG --- CHANGELOG.md | 1 + programs/drift/src/controller/insurance.rs | 10 +- programs/drift/src/controller/token.rs | 115 ++- programs/drift/src/error.rs | 2 + programs/drift/src/instructions/admin.rs | 52 +- programs/drift/src/instructions/if_staker.rs | 38 +- programs/drift/src/instructions/keeper.rs | 57 +- .../src/instructions/optional_accounts.rs | 41 +- programs/drift/src/instructions/user.rs | 100 +- programs/drift/src/lib.rs | 16 +- .../src/state/fulfillment_params/drift.rs | 16 +- programs/drift/src/state/spot_market.rs | 6 +- sdk/src/adminClient.ts | 4 +- sdk/src/bankrun/bankrunConnection.ts | 4 +- sdk/src/constants/spotMarkets.ts | 11 + sdk/src/driftClient.ts | 131 ++- sdk/src/idl/drift.json | 49 + sdk/src/types.ts | 2 + test-scripts/run-anchor-tests.sh | 2 + tests/order.ts | 13 + tests/spotDepositWithdraw22.ts | 936 ++++++++++++++++++ tests/spotSwap22.ts | 311 ++++++ tests/testHelpers.ts | 40 +- 23 files changed, 1761 insertions(+), 196 deletions(-) create mode 100644 tests/spotDepositWithdraw22.ts create mode 100644 tests/spotSwap22.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a38b43445..29df7f12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - program: add switchboard on demand integration ([#1154](https://github.com/drift-labs/protocol-v2/pull/1154)) +- program: add support for token 2022 ([#1125](https://github.com/drift-labs/protocol-v2/pull/1125)) ### Fixes diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index b8494628e..06b8839c6 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{Token, TokenAccount}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use solana_program::msg; use crate::controller::spot_balance::{ @@ -633,13 +633,14 @@ pub fn transfer_protocol_insurance_fund_stake( } pub fn attempt_settle_revenue_to_insurance_fund<'info>( - spot_market_vault: &Account<'info, TokenAccount>, - insurance_fund_vault: &Account<'info, TokenAccount>, + spot_market_vault: &InterfaceAccount<'info, TokenAccount>, + insurance_fund_vault: &InterfaceAccount<'info, TokenAccount>, spot_market: &mut SpotMarket, now: i64, - token_program: &Program<'info, Token>, + token_program: &Interface<'info, TokenInterface>, drift_signer: &AccountInfo<'info>, state: &State, + mint: &Option>, ) -> Result<()> { let valid_revenue_settle_time = if spot_market.insurance_fund.revenue_settle_period > 0 { let time_until_next_update = on_the_hour_update( @@ -680,6 +681,7 @@ pub fn attempt_settle_revenue_to_insurance_fund<'info>( drift_signer, state.signer_nonce, token_amount.cast()?, + mint, )?; } diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index 647c42c70..9edb87943 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -1,47 +1,95 @@ +use crate::error::ErrorCode; use crate::signer::get_signer_seeds; +use crate::validate; use anchor_lang::prelude::*; -use anchor_spl::token::{self, CloseAccount, Token, TokenAccount, Transfer}; +use anchor_spl::token_2022::spl_token_2022::extension::transfer_fee::TransferFeeConfig; +use anchor_spl::token_2022::spl_token_2022::extension::{ + BaseStateWithExtensions, StateWithExtensions, +}; +use anchor_spl::token_2022::spl_token_2022::state::Mint as MintInner; +use anchor_spl::token_interface::{ + self, CloseAccount, Mint, TokenAccount, TokenInterface, Transfer, TransferChecked, +}; pub fn send_from_program_vault<'info>( - token_program: &Program<'info, Token>, - from: &Account<'info, TokenAccount>, - to: &Account<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + from: &InterfaceAccount<'info, TokenAccount>, + to: &InterfaceAccount<'info, TokenAccount>, authority: &AccountInfo<'info>, nonce: u8, amount: u64, + mint: &Option>, ) -> Result<()> { let signature_seeds = get_signer_seeds(&nonce); let signers = &[&signature_seeds[..]]; - let cpi_accounts = Transfer { - from: from.to_account_info().clone(), - to: to.to_account_info().clone(), - authority: authority.to_account_info().clone(), - }; - let cpi_program = token_program.to_account_info(); - let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); - token::transfer(cpi_context, amount) + + if let Some(mint) = mint { + let mint_account_info = mint.to_account_info().clone(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = TransferChecked { + from: from.to_account_info().clone(), + mint: mint_account_info, + to: to.to_account_info().clone(), + authority: authority.to_account_info().clone(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::transfer_checked(cpi_context, amount, mint.decimals) + } else { + let cpi_accounts = Transfer { + from: from.to_account_info().clone(), + to: to.to_account_info().clone(), + authority: authority.to_account_info().clone(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + #[allow(deprecated)] + token_interface::transfer(cpi_context, amount) + } } pub fn receive<'info>( - token_program: &Program<'info, Token>, - from: &Account<'info, TokenAccount>, - to: &Account<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + from: &InterfaceAccount<'info, TokenAccount>, + to: &InterfaceAccount<'info, TokenAccount>, authority: &AccountInfo<'info>, amount: u64, + mint: &Option>, ) -> Result<()> { - let cpi_accounts = Transfer { - from: from.to_account_info().clone(), - to: to.to_account_info().clone(), - authority: authority.to_account_info().clone(), - }; - let cpi_program = token_program.to_account_info(); - let cpi_context = CpiContext::new(cpi_program, cpi_accounts); - token::transfer(cpi_context, amount) + if let Some(mint) = mint { + let mint_account_info = mint.to_account_info().clone(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = TransferChecked { + from: from.to_account_info().clone(), + to: to.to_account_info().clone(), + mint: mint_account_info, + authority: authority.to_account_info().clone(), + }; + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new(cpi_program, cpi_accounts); + token_interface::transfer_checked(cpi_context, amount, mint.decimals) + } else { + let cpi_accounts = Transfer { + from: from.to_account_info().clone(), + to: to.to_account_info().clone(), + authority: authority.to_account_info().clone(), + }; + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new(cpi_program, cpi_accounts); + #[allow(deprecated)] + token_interface::transfer(cpi_context, amount) + } } pub fn close_vault<'info>( - token_program: &Program<'info, Token>, - account: &Account<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + account: &InterfaceAccount<'info, TokenAccount>, destination: &AccountInfo<'info>, authority: &AccountInfo<'info>, nonce: u8, @@ -55,5 +103,20 @@ pub fn close_vault<'info>( }; let cpi_program = token_program.to_account_info(); let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); - token::close_account(cpi_context) + token_interface::close_account(cpi_context) +} + +pub fn validate_mint_fee(account_info: &AccountInfo) -> Result<()> { + let mint_data = account_info.try_borrow_data()?; + let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; + if let Ok(fee_config) = mint_with_extension.get_extension::() { + let fee = u16::from( + fee_config + .get_epoch_fee(Clock::get()?.epoch) + .transfer_fee_basis_points, + ); + validate!(fee == 0, ErrorCode::NonZeroTransferFee)? + } + + Ok(()) } diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index cb8d16229..4ad92b081 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -569,6 +569,8 @@ pub enum ErrorCode { InvalidOpenbookV2Program, #[msg("InvalidOpenbookV2Market")] InvalidOpenbookV2Market, + #[msg("Non zero transfer fee")] + NonZeroTransferFee, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 14f1b1cbd..f771e05d9 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -2,7 +2,9 @@ use std::convert::identity; use std::mem::size_of; use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; +use anchor_spl::token::Token; +use anchor_spl::token_2022::Token2022; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use phoenix::quantities::WrapperU64; use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; @@ -29,6 +31,7 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::math::{amm, bn}; use crate::math_error; +use crate::optional_accounts::get_token_mint; use crate::state::events::CurveRecord; use crate::state::fulfillment_params::openbook_v2::{ OpenbookV2Context, OpenbookV2FulfillmentConfig, @@ -227,6 +230,15 @@ pub fn handle_initialize_spot_market( let decimals = ctx.accounts.spot_market_mint.decimals.cast::()?; + let token_program = if ctx.accounts.token_program.key() == Token2022::id() { + 1_u8 + } else if ctx.accounts.token_program.key() == Token::id() { + 0_u8 + } else { + msg!("unexpected program {:?}", ctx.accounts.token_program.key()); + return Err(ErrorCode::DefaultError.into()); + }; + **spot_market = SpotMarket { market_index: spot_market_index, pubkey: spot_market_pubkey, @@ -296,7 +308,8 @@ pub fn handle_initialize_spot_market( fuel_boost_taker: 0, fuel_boost_maker: 0, fuel_boost_insurance: 0, - padding: [0; 42], + token_program, + padding: [0; 41], insurance_fund: InsuranceFund { vault: *ctx.accounts.insurance_fund_vault.to_account_info().key, unstaking_period: THIRTEEN_DAY, @@ -1608,12 +1621,16 @@ pub fn handle_settle_expired_market_pools_to_revenue_pool( #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] -pub fn handle_deposit_into_perp_market_fee_pool( - ctx: Context, +pub fn handle_deposit_into_perp_market_fee_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoMarketFeePool<'info>>, amount: u64, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + + let mint = get_token_mint(remaining_accounts_iter)?; + msg!( "depositing {} into perp market {} fee pool", amount, @@ -1650,6 +1667,7 @@ pub fn handle_deposit_into_perp_market_fee_pool( &ctx.accounts.spot_market_vault, &ctx.accounts.admin.to_account_info(), amount, + &mint, )?; Ok(()) @@ -3932,12 +3950,12 @@ pub struct Initialize<'info> { payer = admin )] pub state: Box>, - pub quote_asset_mint: Box>, + pub quote_asset_mint: Box>, /// CHECK: checked in `initialize` pub drift_signer: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -3950,7 +3968,7 @@ pub struct InitializeSpotMarket<'info> { payer = admin )] pub spot_market: AccountLoader<'info, SpotMarket>, - pub spot_market_mint: Box>, + pub spot_market_mint: Box>, #[account( init, seeds = [b"spot_market_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], @@ -3959,7 +3977,7 @@ pub struct InitializeSpotMarket<'info> { token::mint = spot_market_mint, token::authority = drift_signer )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( init, seeds = [b"insurance_fund_vault".as_ref(), state.number_of_spot_markets.to_le_bytes().as_ref()], @@ -3968,7 +3986,7 @@ pub struct InitializeSpotMarket<'info> { token::mint = spot_market_mint, token::authority = drift_signer )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -3985,7 +4003,7 @@ pub struct InitializeSpotMarket<'info> { pub admin: Signer<'info>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -4005,16 +4023,16 @@ pub struct DeleteInitializedSpotMarket<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, /// CHECK: program signer pub drift_signer: AccountInfo<'info>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -4139,7 +4157,7 @@ pub struct UpdateSerumVault<'info> { pub state: Box>, #[account(mut)] pub admin: Signer<'info>, - pub srm_vault: Box>, + pub srm_vault: Box>, } #[derive(Accounts)] @@ -4238,7 +4256,7 @@ pub struct DepositIntoMarketFeePool<'info> { mut, token::authority = admin )] - pub source_vault: Box>, + pub source_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -4255,8 +4273,8 @@ pub struct DepositIntoMarketFeePool<'info> { seeds = [b"spot_market_vault".as_ref(), 0_u16.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, - pub token_program: Program<'info, Token>, + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 845264507..3af621cf8 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -1,9 +1,10 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{Token, TokenAccount}; +use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::controller::insurance::transfer_protocol_insurance_fund_stake; use crate::error::ErrorCode; use crate::instructions::constraints::*; +use crate::optional_accounts::get_token_mint; use crate::state::insurance_fund_stake::{InsuranceFundStake, ProtocolIfSharesTransferConfig}; use crate::state::paused_operations::InsuranceFundOperation; use crate::state::perp_market::MarketStatus; @@ -41,8 +42,8 @@ pub fn handle_initialize_insurance_fund_stake( Ok(()) } -pub fn handle_add_insurance_fund_stake( - ctx: Context, +pub fn handle_add_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, AddInsuranceFundStake<'info>>, market_index: u16, amount: u64, ) -> Result<()> { @@ -57,6 +58,9 @@ pub fn handle_add_insurance_fund_stake( let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; let state = &ctx.accounts.state; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + validate!( !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Add), ErrorCode::InsuranceFundOperationPaused, @@ -92,6 +96,7 @@ pub fn handle_add_insurance_fund_stake( &ctx.accounts.token_program, &ctx.accounts.drift_signer, state, + &mint, )?; // reload the vault balances so they're up-to-date @@ -118,6 +123,7 @@ pub fn handle_add_insurance_fund_stake( &ctx.accounts.insurance_fund_vault, &ctx.accounts.authority, amount, + &mint, )?; Ok(()) @@ -214,8 +220,8 @@ pub fn handle_cancel_request_remove_insurance_fund_stake( #[access_control( withdraw_not_paused(&ctx.accounts.state) )] -pub fn handle_remove_insurance_fund_stake( - ctx: Context, +pub fn handle_remove_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveInsuranceFundStake<'info>>, market_index: u16, ) -> Result<()> { let clock = Clock::get()?; @@ -225,6 +231,9 @@ pub fn handle_remove_insurance_fund_stake( let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; let state = &ctx.accounts.state; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + validate!( !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Remove), ErrorCode::InsuranceFundOperationPaused, @@ -259,6 +268,7 @@ pub fn handle_remove_insurance_fund_stake( &ctx.accounts.drift_signer, state.signer_nonce, amount, + &mint, )?; ctx.accounts.insurance_fund_vault.reload()?; @@ -368,13 +378,13 @@ pub struct AddInsuranceFundStake<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) @@ -386,8 +396,8 @@ pub struct AddInsuranceFundStake<'info> { token::mint = insurance_fund_vault.mint, token::authority = authority )] - pub user_token_account: Box>, - pub token_program: Program<'info, Token>, + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -414,7 +424,7 @@ pub struct RequestRemoveInsuranceFundStake<'info> { seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, } #[derive(Accounts)] @@ -442,7 +452,7 @@ pub struct RemoveInsuranceFundStake<'info> { seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -453,8 +463,8 @@ pub struct RemoveInsuranceFundStake<'info> { token::mint = insurance_fund_vault.mint, token::authority = authority )] - pub user_token_account: Box>, - pub token_program: Program<'info, Token>, + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -487,5 +497,5 @@ pub struct TransferProtocolIfShares<'info> { seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 82beefa1c..534c491ca 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{Token, TokenAccount}; +use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::controller::insurance::update_user_stats_if_stake_amount; use crate::error::ErrorCode; @@ -9,7 +9,7 @@ use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::spot_withdraw::validate_spot_market_vault_amount; -use crate::optional_accounts::update_prelaunch_oracle; +use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; use crate::state::fulfillment_params::openbook_v2::OpenbookV2FulfillmentParams; @@ -944,18 +944,21 @@ pub fn handle_resolve_perp_pnl_deficit<'c: 'info, 'info>( validate!(spot_market_index == 0, ErrorCode::InvalidSpotMarketAccount)?; let state = &ctx.accounts.state; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &get_writable_perp_market_set(perp_market_index), &get_writable_spot_market_set(spot_market_index), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + controller::repeg::update_amm( perp_market_index, &perp_market_map, @@ -974,6 +977,7 @@ pub fn handle_resolve_perp_pnl_deficit<'c: 'info, 'info>( &ctx.accounts.token_program, &ctx.accounts.drift_signer, state, + &mint, )?; // reload the spot market vault balance so it's up-to-date @@ -1040,6 +1044,7 @@ pub fn handle_resolve_perp_pnl_deficit<'c: 'info, 'info>( &ctx.accounts.drift_signer, state.signer_nonce, pay_from_insurance, + &mint, )?; validate!( @@ -1082,18 +1087,21 @@ pub fn handle_resolve_perp_bankruptcy<'c: 'info, 'info>( let liquidator = &mut load_mut!(ctx.accounts.liquidator)?; let state = &ctx.accounts.state; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(quote_spot_market_index), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + { let spot_market = &mut spot_market_map.get_ref_mut("e_spot_market_index)?; controller::insurance::attempt_settle_revenue_to_insurance_fund( @@ -1104,6 +1112,7 @@ pub fn handle_resolve_perp_bankruptcy<'c: 'info, 'info>( &ctx.accounts.token_program, &ctx.accounts.drift_signer, state, + &mint, )?; // reload the spot market vault balance so it's up-to-date @@ -1144,6 +1153,7 @@ pub fn handle_resolve_perp_bankruptcy<'c: 'info, 'info>( &ctx.accounts.drift_signer, state.signer_nonce, pay_from_insurance, + &mint, )?; validate!( @@ -1188,18 +1198,21 @@ pub fn handle_resolve_spot_bankruptcy<'c: 'info, 'info>( let user = &mut load_mut!(ctx.accounts.user)?; let liquidator = &mut load_mut!(ctx.accounts.liquidator)?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &MarketSet::new(), &get_writable_spot_market_set(market_index), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; controller::insurance::attempt_settle_revenue_to_insurance_fund( @@ -1210,6 +1223,7 @@ pub fn handle_resolve_spot_bankruptcy<'c: 'info, 'info>( &ctx.accounts.token_program, &ctx.accounts.drift_signer, state, + &mint, )?; // reload the spot market vault balance so it's up-to-date @@ -1242,6 +1256,7 @@ pub fn handle_resolve_spot_bankruptcy<'c: 'info, 'info>( &ctx.accounts.drift_signer, ctx.accounts.state.signer_nonce, pay_from_insurance, + &mint, )?; validate!( @@ -1441,13 +1456,16 @@ pub fn handle_update_perp_bid_ask_twap<'c: 'info, 'info>( #[access_control( withdraw_not_paused(&ctx.accounts.state) )] -pub fn handle_settle_revenue_to_insurance_fund( - ctx: Context, +pub fn handle_settle_revenue_to_insurance_fund<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleRevenueToInsuranceFund<'info>>, spot_market_index: u16, ) -> Result<()> { let state = &ctx.accounts.state; let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + validate!( spot_market_index == spot_market.market_index, ErrorCode::InvalidSpotMarketAccount, @@ -1497,6 +1515,7 @@ pub fn handle_settle_revenue_to_insurance_fund( &ctx.accounts.drift_signer, state.signer_nonce, token_amount, + &mint, )?; // reload the spot market vault balance so it's up-to-date @@ -1730,7 +1749,7 @@ pub struct SettlePNL<'info> { seeds = [b"spot_market_vault".as_ref(), 0_u16.to_le_bytes().as_ref()], bump )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, } #[derive(Accounts)] @@ -1866,19 +1885,19 @@ pub struct ResolveBankruptcy<'info> { seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, seeds = [b"insurance_fund_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], // todo: market_index=0 hardcode for perps? bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -1891,19 +1910,19 @@ pub struct ResolvePerpPnlDeficit<'info> { seeds = [b"spot_market_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, seeds = [b"insurance_fund_vault".as_ref(), spot_market_index.to_le_bytes().as_ref()], // todo: market_index=0 hardcode for perps? bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -1921,7 +1940,7 @@ pub struct SettleRevenueToInsuranceFund<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -1932,8 +1951,8 @@ pub struct SettleRevenueToInsuranceFund<'info> { seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, - pub token_program: Program<'info, Token>, + pub insurance_fund_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -1947,7 +1966,7 @@ pub struct UpdateSpotMarketCumulativeInterest<'info> { seeds = [b"spot_market_vault".as_ref(), spot_market.load()?.market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, } #[derive(Accounts)] @@ -2000,7 +2019,7 @@ pub struct UpdateUserQuoteAssetInsuranceStake<'info> { seeds = [b"insurance_fund_vault".as_ref(), 0_u16.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, } #[derive(Accounts)] diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index b6f7ad0b5..23a6e8ddc 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -1,5 +1,6 @@ use crate::error::{DriftResult, ErrorCode}; use std::cell::RefMut; +use std::convert::TryFrom; use crate::error::ErrorCode::UnableToLoadOracle; use crate::math::safe_unwrap::SafeUnwrap; @@ -14,10 +15,11 @@ use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::{validate, OracleSource}; use anchor_lang::accounts::account::Account; -use anchor_lang::prelude::AccountInfo; -use anchor_lang::prelude::AccountLoader; +use anchor_lang::prelude::{AccountInfo, Interface}; +use anchor_lang::prelude::{AccountLoader, InterfaceAccount}; use anchor_lang::Discriminator; use anchor_spl::token::TokenAccount; +use anchor_spl::token_interface::{Mint, TokenInterface}; use arrayref::array_ref; use solana_program::account_info::next_account_info; use solana_program::msg; @@ -198,3 +200,38 @@ pub fn get_whitelist_token<'a>( Ok(whitelist_token) } + +pub fn get_token_interface<'a>( + account_info_iter: &mut Peekable>>, +) -> DriftResult>> { + let token_interface_account_info = account_info_iter.peek(); + if token_interface_account_info.is_none() { + return Ok(None); + } + + let token_interface_account_info = token_interface_account_info.safe_unwrap()?; + let token_interface: Interface = + Interface::try_from(*token_interface_account_info).map_err(|e| { + msg!("Unable to deserialize token interface"); + msg!("{:?}", e); + ErrorCode::DefaultError + })?; + + Ok(Some(token_interface)) +} + +pub fn get_token_mint<'a>( + account_info_iter: &mut Peekable>>, +) -> DriftResult>> { + let mint_account_info = account_info_iter.peek(); + if mint_account_info.is_none() { + return Ok(None); + } + + let mint_account_info = mint_account_info.safe_unwrap()?; + + match InterfaceAccount::try_from(*mint_account_info) { + Ok(mint) => Ok(Some(mint)), + Err(_) => Ok(None), + } +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b8856ac54..8d09b55f9 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1,6 +1,10 @@ use anchor_lang::prelude::*; use anchor_lang::Discriminator; -use anchor_spl::token::{Token, TokenAccount}; +use anchor_spl::{ + token::Token, + token_2022::Token2022, + token_interface::{TokenAccount, TokenInterface}, +}; use solana_program::program::invoke; use solana_program::system_instruction::transfer; @@ -31,6 +35,7 @@ use crate::math::spot_balance::get_token_value; use crate::math::spot_swap; use crate::math::spot_swap::{calculate_swap_price, validate_price_bands_for_swap}; use crate::math_error; +use crate::optional_accounts::{get_token_interface, get_token_mint}; use crate::print_error; use crate::safe_decrement; use crate::safe_increment; @@ -269,18 +274,21 @@ pub fn handle_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; let slot = clock.slot; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &MarketSet::new(), &get_writable_spot_market_set(market_index), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + if amount == 0 { return Err(ErrorCode::InsufficientDeposit.into()); } @@ -384,6 +392,7 @@ pub fn handle_deposit<'c: 'info, 'info>( &ctx.accounts.spot_market_vault, &ctx.accounts.authority, amount, + &mint, )?; ctx.accounts.spot_market_vault.reload()?; @@ -436,18 +445,21 @@ pub fn handle_withdraw<'c: 'info, 'info>( let slot = clock.slot; let state = &ctx.accounts.state; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &MarketSet::new(), &get_writable_spot_market_set(market_index), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market_is_reduce_only = { @@ -579,6 +591,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( &ctx.accounts.drift_signer, state.signer_nonce, amount, + &mint, )?; // reload the spot market vault balance so it's up-to-date @@ -2010,8 +2023,8 @@ pub fn handle_reclaim_rent(ctx: Context) -> Result<()> { #[access_control( deposit_not_paused(&ctx.accounts.state) )] -pub fn handle_deposit_into_spot_market_revenue_pool( - ctx: Context, +pub fn handle_deposit_into_spot_market_revenue_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RevenuePoolDeposit<'info>>, amount: u64, ) -> Result<()> { if amount == 0 { @@ -2020,6 +2033,10 @@ pub fn handle_deposit_into_spot_market_revenue_pool( let mut spot_market = load_mut!(ctx.accounts.spot_market)?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + + let mint = get_token_mint(remaining_accounts_iter)?; + validate!( !spot_market.is_in_settlement(Clock::get()?.unix_timestamp), ErrorCode::DefaultError, @@ -2039,6 +2056,7 @@ pub fn handle_deposit_into_spot_market_revenue_pool( &ctx.accounts.spot_market_vault, &ctx.accounts.authority, amount, + &mint, )?; spot_market.validate_max_token_deposits_and_borrows()?; @@ -2147,14 +2165,14 @@ pub struct Deposit<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, constraint = &spot_market_vault.mint.eq(&user_token_account.mint), token::authority = authority )] - pub user_token_account: Box>, - pub token_program: Program<'info, Token>, + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -2169,14 +2187,14 @@ pub struct RevenuePoolDeposit<'info> { seeds = [b"spot_market_vault".as_ref(), spot_market.load()?.market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( mut, constraint = &spot_market_vault.mint.eq(&user_token_account.mint), token::authority = authority )] - pub user_token_account: Box>, - pub token_program: Program<'info, Token>, + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -2199,7 +2217,7 @@ pub struct Withdraw<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -2209,8 +2227,8 @@ pub struct Withdraw<'info> { mut, constraint = &spot_market_vault.mint.eq(&user_token_account.mint) )] - pub user_token_account: Box>, - pub token_program: Program<'info, Token>, + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] @@ -2237,7 +2255,7 @@ pub struct TransferDeposit<'info> { seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], bump, )] - pub spot_market_vault: Box>, + pub spot_market_vault: Box>, } #[derive(Accounts)] @@ -2388,26 +2406,26 @@ pub struct Swap<'info> { seeds = [b"spot_market_vault".as_ref(), out_market_index.to_le_bytes().as_ref()], bump, )] - pub out_spot_market_vault: Box>, + pub out_spot_market_vault: Box>, #[account( mut, seeds = [b"spot_market_vault".as_ref(), in_market_index.to_le_bytes().as_ref()], bump, )] - pub in_spot_market_vault: Box>, + pub in_spot_market_vault: Box>, #[account( mut, constraint = &out_spot_market_vault.mint.eq(&out_token_account.mint), token::authority = authority )] - pub out_token_account: Box>, + pub out_token_account: Box>, #[account( mut, constraint = &in_spot_market_vault.mint.eq(&in_token_account.mint), token::authority = authority )] - pub in_token_account: Box>, - pub token_program: Program<'info, Token>, + pub in_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, #[account( constraint = state.signer.eq(&drift_signer.key()) )] @@ -2432,18 +2450,21 @@ pub fn handle_begin_swap<'c: 'info, 'info>( let clock = Clock::get()?; let now = clock.unix_timestamp; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &MarketSet::new(), &get_writable_spot_market_set_from_many(vec![in_market_index, out_market_index]), clock.slot, Some(state.oracle_guard_rails), )?; + let mint = get_token_mint(remaining_accounts_iter)?; + let mut user = load_mut!(&ctx.accounts.user)?; let delegate_is_signer = user.delegate == ctx.accounts.authority.key(); @@ -2532,6 +2553,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( &ctx.accounts.drift_signer, state.signer_nonce, amount_in, + &mint, )?; let ixs = ctx.accounts.instructions.as_ref(); @@ -2634,6 +2656,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( ]; if !delegate_is_signer { whitelisted_programs.push(Token::id()); + whitelisted_programs.push(Token2022::id()); whitelisted_programs.push(marinade_mainnet::ID); } validate!( @@ -2684,17 +2707,22 @@ pub fn handle_end_swap<'c: 'info, 'info>( let slot = clock.slot; let now = clock.unix_timestamp; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts, &MarketSet::new(), &get_writable_spot_market_set_from_many(vec![in_market_index, out_market_index]), clock.slot, Some(state.oracle_guard_rails), )?; + let out_token_program = get_token_interface(remaining_accounts)?; + + let in_mint = get_token_mint(remaining_accounts)?; + let out_mint = get_token_mint(remaining_accounts)?; let user_key = ctx.accounts.user.key(); let mut user = load_mut!(&ctx.accounts.user)?; @@ -2753,6 +2781,7 @@ pub fn handle_end_swap<'c: 'info, 'info>( in_vault, &ctx.accounts.authority, residual, + &in_mint, )?; in_token_account.reload()?; in_vault.reload()?; @@ -2825,13 +2854,26 @@ pub fn handle_end_swap<'c: 'info, 'info>( .amount .safe_sub(out_spot_market.flash_loan_initial_token_amount)?; - controller::token::receive( - &ctx.accounts.token_program, - out_token_account, - out_vault, - &ctx.accounts.authority, - amount_out, - )?; + if let Some(token_interface) = out_token_program { + controller::token::receive( + &token_interface, + out_token_account, + out_vault, + &ctx.accounts.authority, + amount_out, + &out_mint, + )?; + } else { + controller::token::receive( + &ctx.accounts.token_program, + out_token_account, + out_vault, + &ctx.accounts.authority, + amount_out, + &out_mint, + )?; + } + out_vault.reload()?; } diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index e379ebe25..0320eb6ce 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -545,8 +545,8 @@ pub mod drift { handle_initialize_insurance_fund_stake(ctx, market_index) } - pub fn add_insurance_fund_stake( - ctx: Context, + pub fn add_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, AddInsuranceFundStake<'info>>, market_index: u16, amount: u64, ) -> Result<()> { @@ -568,8 +568,8 @@ pub mod drift { handle_cancel_request_remove_insurance_fund_stake(ctx, market_index) } - pub fn remove_insurance_fund_stake( - ctx: Context, + pub fn remove_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveInsuranceFundStake<'info>>, market_index: u16, ) -> Result<()> { handle_remove_insurance_fund_stake(ctx, market_index) @@ -812,15 +812,15 @@ pub mod drift { handle_settle_expired_market_pools_to_revenue_pool(ctx) } - pub fn deposit_into_perp_market_fee_pool( - ctx: Context, + pub fn deposit_into_perp_market_fee_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoMarketFeePool<'info>>, amount: u64, ) -> Result<()> { handle_deposit_into_perp_market_fee_pool(ctx, amount) } - pub fn deposit_into_spot_market_revenue_pool( - ctx: Context, + pub fn deposit_into_spot_market_revenue_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RevenuePoolDeposit<'info>>, amount: u64, ) -> Result<()> { handle_deposit_into_spot_market_revenue_pool(ctx, amount) diff --git a/programs/drift/src/state/fulfillment_params/drift.rs b/programs/drift/src/state/fulfillment_params/drift.rs index a529c3603..22dfd0f4c 100644 --- a/programs/drift/src/state/fulfillment_params/drift.rs +++ b/programs/drift/src/state/fulfillment_params/drift.rs @@ -8,9 +8,9 @@ use crate::state::spot_market::SpotMarket; use crate::{validate, PositionDirection}; -use anchor_lang::prelude::Account; +use anchor_lang::prelude::InterfaceAccount; -use anchor_spl::token::TokenAccount; +use anchor_spl::token_interface::TokenAccount; use arrayref::array_ref; use solana_program::account_info::AccountInfo; @@ -18,8 +18,8 @@ use solana_program::msg; use std::cell::Ref; pub struct MatchFulfillmentParams<'a> { - pub base_market_vault: Box>, - pub quote_market_vault: Box>, + pub base_market_vault: Box>, + pub quote_market_vault: Box>, } impl<'a> MatchFulfillmentParams<'a> { @@ -42,13 +42,13 @@ impl<'a> MatchFulfillmentParams<'a> { ErrorCode::InvalidFulfillmentConfig )?; - let base_market_vault: Box> = - Box::new(Account::try_from(base_market_vault).map_err(|e| { + let base_market_vault: Box> = + Box::new(InterfaceAccount::try_from(base_market_vault).map_err(|e| { msg!("{:?}", e); ErrorCode::InvalidFulfillmentConfig })?); - let quote_market_vault: Box> = - Box::new(Account::try_from(quote_market_vault).map_err(|e| { + let quote_market_vault: Box> = + Box::new(InterfaceAccount::try_from(quote_market_vault).map_err(|e| { msg!("{:?}", e); ErrorCode::InvalidFulfillmentConfig })?); diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 56a4c79ce..3e23a1fed 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -200,7 +200,8 @@ pub struct SpotMarket { /// fuel multiplier for spot insurance stake /// precision: 10 pub fuel_boost_insurance: u8, - pub padding: [u8; 42], + pub token_program: u8, + pub padding: [u8; 41], } impl Default for SpotMarket { @@ -267,7 +268,8 @@ impl Default for SpotMarket { fuel_boost_taker: 0, fuel_boost_maker: 0, fuel_boost_insurance: 0, - padding: [0; 42], + token_program: 0, + padding: [0; 41], } } } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index d137ed954..f8c6801ae 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -198,6 +198,8 @@ export class AdminClient extends DriftClient { spotMarketIndex ); + const tokenProgram = (await this.connection.getAccountInfo(mint)).owner; + const nameBuffer = encodeName(name); const initializeIx = await this.program.instruction.initializeSpotMarket( optimalUtilization, @@ -233,7 +235,7 @@ export class AdminClient extends DriftClient { oracle, rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, } ); diff --git a/sdk/src/bankrun/bankrunConnection.ts b/sdk/src/bankrun/bankrunConnection.ts index 55cc53c4c..0c3f8efde 100644 --- a/sdk/src/bankrun/bankrunConnection.ts +++ b/sdk/src/bankrun/bankrunConnection.ts @@ -35,7 +35,7 @@ import { import { BankrunProvider } from 'anchor-bankrun'; import bs58 from 'bs58'; import { BN, Wallet } from '@coral-xyz/anchor'; -import { Account, TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token'; +import { Account, unpackAccount } from '@solana/spl-token'; export type Connection = SolanaConnection | BankrunConnection; @@ -164,7 +164,7 @@ export class BankrunConnection { async getTokenAccount(publicKey: PublicKey): Promise { const info = await this.getAccountInfo(publicKey); - return unpackAccount(publicKey, info, TOKEN_PROGRAM_ID); + return unpackAccount(publicKey, info, info.owner); } async getMultipleAccountsInfo( diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index a5690f1ed..e4a97f75c 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -67,6 +67,17 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ pythFeedId: '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43', }, + { + symbol: 'PYUSD', + marketIndex: 3, + oracle: new PublicKey('HpMoKp3TCd3QT4MWYUKk2zCBwmhr5Df45fB6wdxYqEeh'), + oracleSource: OracleSource.PYTH_PULL, + mint: new PublicKey('GLfF72ZCUnS6N9iDJw8kedHzd6WFVf3VbpwdKKy76FRk'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0xc1da1b73d7f01e7ddd54b3766cf7fcd644395ad14f70aa706ec5384c59e76692', + }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 89eb7731a..9affbd4e4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -14,6 +14,7 @@ import { createInitializeAccountInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { StateAccount, @@ -1829,7 +1830,8 @@ export class DriftClient { account: PublicKey, payer: PublicKey, owner: PublicKey, - mint: PublicKey + mint: PublicKey, + tokenProgram = TOKEN_PROGRAM_ID ): TransactionInstruction { return new TransactionInstruction({ keys: [ @@ -1842,7 +1844,7 @@ export class DriftClient { isSigner: false, isWritable: false, }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: tokenProgram, isSigner: false, isWritable: false }, ], programId: ASSOCIATED_TOKEN_PROGRAM_ID, data: Buffer.from([0x1]), @@ -1856,7 +1858,7 @@ export class DriftClient { subAccountId?: number, reduceOnly = false, txParams?: TxParams - ): Promise> { + ): Promise { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); const isSolMarket = spotMarketAccount.mint.equals(WRAPPED_SOL_MINT); @@ -1970,6 +1972,8 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); + this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); return await this.program.instruction.deposit( marketIndex, amount, @@ -1983,7 +1987,7 @@ export class DriftClient { userStats: this.getUserStatsAccountPublicKey(), userTokenAccount: userTokenAccount, authority: this.wallet.publicKey, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, remainingAccounts, } @@ -2056,6 +2060,28 @@ export class DriftClient { return result; } + public getTokenProgramForSpotMarket( + spotMarketAccount: SpotMarketAccount + ): PublicKey { + if (spotMarketAccount.tokenProgram === 1) { + return TOKEN_2022_PROGRAM_ID; + } + return TOKEN_PROGRAM_ID; + } + + public addTokenMintToRemainingAccounts( + spotMarketAccount: SpotMarketAccount, + remainingAccounts: AccountMeta[] + ) { + if (spotMarketAccount.tokenProgram === 1) { + remainingAccounts.push({ + pubkey: spotMarketAccount.mint, + isSigner: false, + isWritable: false + }); + } + } + public getAssociatedTokenAccountCreationIx( tokenMintAddress: PublicKey, associatedTokenAddress: PublicKey @@ -2468,6 +2494,9 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); + this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + return await this.program.instruction.withdraw( marketIndex, amount, @@ -2482,7 +2511,7 @@ export class DriftClient { userStats: this.getUserStatsAccountPublicKey(), userTokenAccount: userTokenAccount, authority: this.wallet.publicKey, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, remainingAccounts, } @@ -4230,12 +4259,15 @@ export class DriftClient { outAssociatedTokenAccount ); if (!accountInfo) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( outAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, - outMarket.mint + outMarket.mint, + tokenProgram ) ); } @@ -4251,12 +4283,15 @@ export class DriftClient { inAssociatedTokenAccount ); if (!accountInfo) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( inAssociatedTokenAccount, this.provider.wallet.publicKey, this.provider.wallet.publicKey, - inMarket.mint + inMarket.mint, + tokenProgram ) ); } @@ -4471,6 +4506,30 @@ export class DriftClient { const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + + if (!outTokenProgram.equals(inTokenProgram)) { + remainingAccounts.push({ + pubkey: outTokenProgram, + isWritable: false, + isSigner: false, + }); + } + + if (outSpotMarket.tokenProgram === 1 || inSpotMarket.tokenProgram === 1) { + remainingAccounts.push({ + pubkey: inSpotMarket.mint, + isWritable: false, + isSigner: false, + }); + remainingAccounts.push({ + pubkey: outSpotMarket.mint, + isWritable: false, + isSigner: false, + }); + } + const beginSwapIx = await this.program.instruction.beginSwap( inMarketIndex, outMarketIndex, @@ -4485,7 +4544,7 @@ export class DriftClient { inSpotMarketVault: inSpotMarket.vault, inTokenAccount, outTokenAccount, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram: inTokenProgram, driftSigner: this.getStateAccount().signer, instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, }, @@ -4508,7 +4567,7 @@ export class DriftClient { inSpotMarketVault: inSpotMarket.vault, inTokenAccount, outTokenAccount, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram: inTokenProgram, driftSigner: this.getStateAccount().signer, instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, }, @@ -6279,6 +6338,8 @@ export class DriftClient { const spotMarket = this.getSpotMarketAccount(marketIndex); + this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); + return await this.program.instruction.resolveSpotBankruptcy(marketIndex, { accounts: { state: await this.getStatePublicKey(), @@ -6514,7 +6575,6 @@ export class DriftClient { marketIndex: number, amount: BN, collateralAccountPublicKey: PublicKey, - fromSubAccount?: boolean ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( @@ -6523,12 +6583,9 @@ export class DriftClient { marketIndex ); - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: fromSubAccount ? [this.getUserAccount()] : [], - useMarketLastSlotCache: fromSubAccount ? true : false, - writableSpotMarketIndexes: [marketIndex], - }); - + const remainingAccounts = []; + this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); const ix = this.program.instruction.addInsuranceFundStake( marketIndex, amount, @@ -6543,7 +6600,7 @@ export class DriftClient { insuranceFundVault: spotMarket.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), userTokenAccount: collateralAccountPublicKey, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, remainingAccounts, } @@ -6631,7 +6688,6 @@ export class DriftClient { marketIndex, amount, tokenAccount, - fromSubaccount ); addIfStakeIxs.push(addFundsIx); @@ -6670,11 +6726,6 @@ export class DriftClient { marketIndex ); - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [], - writableSpotMarketIndexes: [marketIndex], - }); - const ix = await this.program.instruction.requestRemoveInsuranceFundStake( marketIndex, amount, @@ -6687,7 +6738,6 @@ export class DriftClient { authority: this.wallet.publicKey, insuranceFundVault: spotMarketAccount.insuranceFund.vault, }, - remainingAccounts, } ); @@ -6708,12 +6758,6 @@ export class DriftClient { marketIndex ); - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount()], - useMarketLastSlotCache: true, - writableSpotMarketIndexes: [marketIndex], - }); - const ix = await this.program.instruction.cancelRequestRemoveInsuranceFundStake( marketIndex, @@ -6726,7 +6770,6 @@ export class DriftClient { authority: this.wallet.publicKey, insuranceFundVault: spotMarketAccount.insuranceFund.vault, }, - remainingAccounts, } ); @@ -6780,11 +6823,9 @@ export class DriftClient { } } - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [], - writableSpotMarketIndexes: [marketIndex], - }); - + const remainingAccounts = []; + this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); const removeStakeIx = await this.program.instruction.removeInsuranceFundStake(marketIndex, { accounts: { @@ -6796,7 +6837,7 @@ export class DriftClient { insuranceFundVault: spotMarketAccount.insuranceFund.vault, driftSigner: this.getSignerPublicKey(), userTokenAccount: tokenAccount, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, remainingAccounts, }); @@ -6827,13 +6868,11 @@ export class DriftClient { public async settleRevenueToInsuranceFund( spotMarketIndex: number, - subAccountId?: number, txParams?: TxParams ): Promise { const tx = await this.buildTransaction( await this.getSettleRevenueToInsuranceFundIx( spotMarketIndex, - subAccountId ), txParams ); @@ -6843,14 +6882,10 @@ export class DriftClient { public async getSettleRevenueToInsuranceFundIx( spotMarketIndex: number, - subAccountId?: number ): Promise { const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], - useMarketLastSlotCache: true, - writableSpotMarketIndexes: [spotMarketIndex], - }); + const remainingAccounts = []; + this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); const ix = await this.program.instruction.settleRevenueToInsuranceFund( spotMarketIndex, { @@ -6920,6 +6955,10 @@ export class DriftClient { userTokenAccountPublicKey: PublicKey ): Promise { const spotMarket = await this.getSpotMarketAccount(marketIndex); + + const remainingAccounts = []; + this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); const ix = await this.program.instruction.depositIntoSpotMarketRevenuePool( amount, { @@ -6929,7 +6968,7 @@ export class DriftClient { authority: this.wallet.publicKey, spotMarketVault: spotMarket.vault, userTokenAccount: userTokenAccountPublicKey, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram, }, } ); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 8e120c7f9..fbfeb02c5 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1675,6 +1675,47 @@ } ] }, + { + "name": "liquidatePerpWithFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, { "name": "liquidateSpot", "accounts": [ @@ -9833,6 +9874,9 @@ }, { "name": "PlaceAndTake" + }, + { + "name": "Liquidation" } ] } @@ -12764,6 +12808,11 @@ "code": 6281, "name": "InvalidOpenbookV2Market", "msg": "InvalidOpenbookV2Market" + }, + { + "code": 6282, + "name": "LiquidationOrderFailedToFill", + "msg": "Liquidation order failed to fill" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 05df531db..5b4b17659 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -746,6 +746,8 @@ export type SpotMarketAccount = { fuelBoostTaker: number; fuelBoostMaker: number; fuelBoostInsurance: number; + + tokenProgram: number; }; export type PoolBalance = { diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index e6ce20c8e..eb65c99dc 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -60,7 +60,9 @@ test_files=( roundInFavorBaseAsset.ts serumTest.ts spotDepositWithdraw.ts + spotDepositWithdraw22.ts spotSwap.ts + spotSwap22.ts stopLimits.ts subaccounts.ts surgePricing.ts diff --git a/tests/order.ts b/tests/order.ts index 2ca57a7f2..cf888870e 100644 --- a/tests/order.ts +++ b/tests/order.ts @@ -33,6 +33,7 @@ import { mockUSDCMint, setFeedPriceNoProgram, initializeQuoteSpotMarket, + sleep, } from './testHelpers'; import { AMM_RESERVE_PRECISION, @@ -164,6 +165,18 @@ describe('orders', () => { await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); await driftClient.updatePerpAuctionDuration(new BN(0)); + let oraclesLoaded = false; + while (!oraclesLoaded) { + const found = + !!driftClient.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + 0 + ); + if (found) { + oraclesLoaded = found; + } + await sleep(1000); + } + console.log(bulkAccountLoader.mostRecentSlot); const periodicity = new BN(60 * 60); // 1 HOUR diff --git a/tests/spotDepositWithdraw22.ts b/tests/spotDepositWithdraw22.ts new file mode 100644 index 000000000..5bcd5cf5f --- /dev/null +++ b/tests/spotDepositWithdraw22.ts @@ -0,0 +1,936 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; + +import { + TestClient, + BN, + EventSubscriber, + SPOT_MARKET_RATE_PRECISION, + SpotBalanceType, + isVariant, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION, + OracleInfo, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + createUserWithUSDCAndWSOLAccount, + mintUSDCToUser, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + sleep, +} from './testHelpers'; +import { + getBalance, + calculateInterestAccumulated, + getTokenAmount, +} from '../sdk/src/math/spotBalance'; +import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; +import { + QUOTE_PRECISION, + ZERO, + ONE, + SPOT_MARKET_BALANCE_PRECISION, + PRICE_PRECISION, +} from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('spot deposit and withdraw 22', () => { + const chProgram = anchor.workspace.Drift as Program; + + let admin: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let solOracle: PublicKey; + + let usdcMint; + + let firstUserDriftClient: TestClient; + let firstUserDriftClientUSDCAccount: PublicKey; + + let secondUserDriftClient: TestClient; + let secondUserDriftClientWSOLAccount: PublicKey; + let secondUserDriftClientUSDCAccount: PublicKey; + + const usdcAmount = new BN(10 * 10 ** 6); + const largeUsdcAmount = new BN(10_000 * 10 ** 6); + + const solAmount = new BN(1 * 10 ** 9); + + let marketIndexes: number[]; + let spotMarketIndexes: number[]; + let oracleInfos: OracleInfo[]; + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper, TOKEN_2022_PROGRAM_ID); + await mockUserUSDCAccount(usdcMint, largeUsdcAmount, bankrunContextWrapper); + + solOracle = await mockOracleNoProgram(bankrunContextWrapper, 30); + + marketIndexes = []; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solOracle, source: OracleSource.PYTH }]; + + admin = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await admin.initialize(usdcMint.publicKey, true); + await admin.subscribe(); + }); + + after(async () => { + await admin.unsubscribe(); + await eventSubscriber.unsubscribe(); + await firstUserDriftClient.unsubscribe(); + await secondUserDriftClient.unsubscribe(); + }); + + it('Initialize USDC Market', async () => { + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(20)).toNumber(); // 2000% APR + const maxRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(50)).toNumber(); // 5000% APR + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + await admin.initializeSpotMarket( + usdcMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + PublicKey.default, + OracleSource.QUOTE_ASSET, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined + ); + const txSig = await admin.updateWithdrawGuardThreshold( + 0, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + bankrunContextWrapper.printTxLogs(txSig); + await admin.fetchAccounts(); + const spotMarket = await admin.getSpotMarketAccount(0); + assert(spotMarket.marketIndex === 0); + assert(spotMarket.optimalUtilization === optimalUtilization); + assert(spotMarket.optimalBorrowRate === optimalRate); + assert(spotMarket.maxBorrowRate === maxRate); + assert( + spotMarket.cumulativeBorrowInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert( + spotMarket.cumulativeDepositInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert(spotMarket.initialAssetWeight === initialAssetWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + assert(spotMarket.initialLiabilityWeight === initialLiabilityWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + + assert(admin.getStateAccount().numberOfSpotMarkets === 1); + }); + + it('Initialize SOL Market', async () => { + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(20)).toNumber(); // 2000% APR + const maxRate = SPOT_MARKET_RATE_PRECISION.mul(new BN(50)).toNumber(); // 5000% APR + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(8)) + .div(new BN(10)) + .toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(9)) + .div(new BN(10)) + .toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul(new BN(12)) + .div(new BN(10)) + .toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.mul( + new BN(11) + ) + .div(new BN(10)) + .toNumber(); + + await admin.initializeSpotMarket( + NATIVE_MINT, + optimalUtilization, + optimalRate, + maxRate, + solOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight + ); + + const txSig = await admin.updateWithdrawGuardThreshold( + 1, + new BN(10 ** 10).mul(QUOTE_PRECISION) + ); + bankrunContextWrapper.printTxLogs(txSig); + await admin.fetchAccounts(); + const spotMarket = await admin.getSpotMarketAccount(1); + assert(spotMarket.marketIndex === 1); + assert(spotMarket.optimalUtilization === optimalUtilization); + assert(spotMarket.optimalBorrowRate === optimalRate); + assert(spotMarket.maxBorrowRate === maxRate); + assert( + spotMarket.cumulativeBorrowInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert( + spotMarket.cumulativeDepositInterest.eq( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert(spotMarket.initialAssetWeight === initialAssetWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + assert(spotMarket.initialLiabilityWeight === initialLiabilityWeight); + assert(spotMarket.maintenanceAssetWeight === maintenanceAssetWeight); + + console.log(spotMarket.historicalOracleData); + assert(spotMarket.historicalOracleData.lastOraclePriceTwapTs.eq(ZERO)); + + assert( + spotMarket.historicalOracleData.lastOraclePrice.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap5Min.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + + assert(admin.getStateAccount().numberOfSpotMarkets === 2); + }); + + it('First User Deposit USDC', async () => { + [firstUserDriftClient, firstUserDriftClientUSDCAccount] = + await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 0; + await sleep(100); + await firstUserDriftClient.fetchAccounts(); + const txSig = await firstUserDriftClient.deposit( + usdcAmount, + marketIndex, + firstUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + assert( + spotMarket.depositBalance.eq( + new BN(10 * SPOT_MARKET_BALANCE_PRECISION.toNumber()) + ) + ); + + const vaultAmount = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount(spotMarket.vault) + ).amount.toString() + ); + assert(vaultAmount.eq(usdcAmount)); + + const expectedBalance = getBalance( + usdcAmount, + spotMarket, + SpotBalanceType.DEPOSIT + ); + const spotPosition = firstUserDriftClient.getUserAccount().spotPositions[0]; + assert(isVariant(spotPosition.balanceType, 'deposit')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + assert(firstUserDriftClient.getUserAccount().totalDeposits.eq(usdcAmount)); + }); + + it('Second User Deposit SOL', async () => { + [ + secondUserDriftClient, + secondUserDriftClientWSOLAccount, + secondUserDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + solAmount, + ZERO, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 1; + const txSig = await secondUserDriftClient.deposit( + solAmount, + marketIndex, + secondUserDriftClientWSOLAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + assert(spotMarket.depositBalance.eq(SPOT_MARKET_BALANCE_PRECISION)); + console.log(spotMarket.historicalOracleData); + assert(spotMarket.historicalOracleData.lastOraclePriceTwapTs.gt(ZERO)); + assert( + spotMarket.historicalOracleData.lastOraclePrice.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + assert( + spotMarket.historicalOracleData.lastOraclePriceTwap5Min.eq( + new BN(30 * PRICE_PRECISION.toNumber()) + ) + ); + + const vaultAmount = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount(spotMarket.vault) + ).amount.toString() + ); + assert(vaultAmount.eq(solAmount)); + + const expectedBalance = getBalance( + solAmount, + spotMarket, + SpotBalanceType.DEPOSIT + ); + const spotPosition = + secondUserDriftClient.getUserAccount().spotPositions[1]; + assert(isVariant(spotPosition.balanceType, 'deposit')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + assert( + secondUserDriftClient + .getUserAccount() + .totalDeposits.eq(new BN(30).mul(PRICE_PRECISION)) + ); + }); + + it('Second User Withdraw First half USDC', async () => { + const marketIndex = 0; + const withdrawAmount = usdcAmount.div(new BN(2)); + const txSig = await secondUserDriftClient.withdraw( + withdrawAmount, + marketIndex, + secondUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotMarket = await admin.getSpotMarketAccount(marketIndex); + const expectedBorrowBalance = new BN(5000000001); + assert(spotMarket.borrowBalance.eq(expectedBorrowBalance)); + + const vaultAmount = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount(spotMarket.vault) + ).amount.toString() + ); + const expectedVaultAmount = usdcAmount.sub(withdrawAmount); + assert(vaultAmount.eq(expectedVaultAmount)); + + const expectedBalance = getBalance( + withdrawAmount, + spotMarket, + SpotBalanceType.BORROW + ); + + const spotPosition = + secondUserDriftClient.getUserAccount().spotPositions[0]; + assert(isVariant(spotPosition.balanceType, 'borrow')); + assert(spotPosition.scaledBalance.eq(expectedBalance)); + + const actualAmountWithdrawn = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + assert(withdrawAmount.eq(actualAmountWithdrawn)); + + assert( + secondUserDriftClient.getUserAccount().totalWithdraws.eq(withdrawAmount) + ); + }); + + it('Update Cumulative Interest with 50% utilization', async () => { + const usdcmarketIndex = 0; + const oldSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + await sleep(5000); + + const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + usdcmarketIndex + ); + bankrunContextWrapper.printTxLogs(txSig); + + await firstUserDriftClient.fetchAccounts(); + const newSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + const expectedInterestAccumulated = calculateInterestAccumulated( + oldSpotMarketAccount, + newSpotMarketAccount.lastInterestTs + ); + const expectedCumulativeDepositInterest = + oldSpotMarketAccount.cumulativeDepositInterest.add( + expectedInterestAccumulated.depositInterest + ); + const expectedCumulativeBorrowInterest = + oldSpotMarketAccount.cumulativeBorrowInterest.add( + expectedInterestAccumulated.borrowInterest + ); + + assert( + newSpotMarketAccount.cumulativeDepositInterest.eq( + expectedCumulativeDepositInterest + ) + ); + assert( + newSpotMarketAccount.cumulativeBorrowInterest.eq( + expectedCumulativeBorrowInterest + ) + ); + }); + + it('Second User Withdraw second half USDC', async () => { + const marketIndex = 0; + let spotMarketAccount = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const spotMarketDepositTokenAmountBefore = getTokenAmount( + spotMarketAccount.depositBalance, + spotMarketAccount, + SpotBalanceType.DEPOSIT + ); + const spotMarketBorrowTokenAmountBefore = getTokenAmount( + spotMarketAccount.borrowBalance, + spotMarketAccount, + SpotBalanceType.BORROW + ); + const spotMarketBorrowBalanceBefore = spotMarketAccount.borrowBalance; + + const userUSDCAmountBefore = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + const spotPositionBefore = + secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + + const withdrawAmount = spotMarketDepositTokenAmountBefore + .sub(spotMarketBorrowTokenAmountBefore) + .sub(ONE); + + const txSig = await secondUserDriftClient.withdraw( + withdrawAmount, + marketIndex, + secondUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + spotMarketAccount = secondUserDriftClient.getSpotMarketAccount(marketIndex); + const increaseInspotPosition = getBalance( + withdrawAmount, + spotMarketAccount, + SpotBalanceType.BORROW + ); + const expectedspotPosition = spotPositionBefore.add(increaseInspotPosition); + console.log('withdrawAmount:', withdrawAmount.toString()); + + assert( + secondUserDriftClient + .getSpotPosition(marketIndex) + .scaledBalance.eq(expectedspotPosition) + ); + + const expectedUserUSDCAmount = userUSDCAmountBefore.add(withdrawAmount); + const userUSDCAmountAfter = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + assert(expectedUserUSDCAmount.eq(userUSDCAmountAfter)); + assert( + secondUserDriftClient + .getUserAccount() + .totalWithdraws.eq(userUSDCAmountAfter) + ); + + const expectedSpotMarketBorrowBalance = spotMarketBorrowBalanceBefore.add( + increaseInspotPosition + ); + console.assert( + spotMarketAccount.borrowBalance.eq(expectedSpotMarketBorrowBalance) + ); + + const expectedVaultBalance = usdcAmount.sub(expectedUserUSDCAmount); + const vaultUSDCAmountAfter = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + spotMarketAccount.vault + ) + ).amount.toString() + ); + + assert(expectedVaultBalance.eq(vaultUSDCAmountAfter)); + + const spotMarketDepositTokenAmountAfter = getTokenAmount( + spotMarketAccount.depositBalance, + spotMarketAccount, + SpotBalanceType.DEPOSIT + ); + const spotMarketBorrowTokenAmountAfter = getTokenAmount( + spotMarketAccount.borrowBalance, + spotMarketAccount, + SpotBalanceType.BORROW + ); + + // TODO + console.log( + spotMarketDepositTokenAmountAfter.toString(), + spotMarketBorrowTokenAmountAfter.toString() + ); + assert( + spotMarketDepositTokenAmountAfter + .sub(spotMarketBorrowTokenAmountAfter) + .lte(ONE) + ); + }); + + it('Update Cumulative Interest with 100% utilization', async () => { + const usdcmarketIndex = 0; + const oldSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + await sleep(5000); + + const txSig = await firstUserDriftClient.updateSpotMarketCumulativeInterest( + usdcmarketIndex + ); + bankrunContextWrapper.printTxLogs(txSig); + + await firstUserDriftClient.fetchAccounts(); + const newSpotMarketAccount = + firstUserDriftClient.getSpotMarketAccount(usdcmarketIndex); + + const expectedInterestAccumulated = calculateInterestAccumulated( + oldSpotMarketAccount, + newSpotMarketAccount.lastInterestTs + ); + const expectedCumulativeDepositInterest = + oldSpotMarketAccount.cumulativeDepositInterest.add( + expectedInterestAccumulated.depositInterest + ); + const expectedCumulativeBorrowInterest = + oldSpotMarketAccount.cumulativeBorrowInterest.add( + expectedInterestAccumulated.borrowInterest + ); + + assert( + newSpotMarketAccount.cumulativeDepositInterest.eq( + expectedCumulativeDepositInterest + ) + ); + console.log( + newSpotMarketAccount.cumulativeBorrowInterest.sub(ONE).toString(), + expectedCumulativeBorrowInterest.toString() + ); + + // inconcistent time leads to slight differences over runs? + assert( + newSpotMarketAccount.cumulativeBorrowInterest + .sub(ONE) + .eq(expectedCumulativeBorrowInterest) || + newSpotMarketAccount.cumulativeBorrowInterest.eq( + expectedCumulativeBorrowInterest + ) + ); + }); + + it('Flip second user borrow to deposit', async () => { + const marketIndex = 0; + const mintAmount = new BN(2 * 10 ** 6); // $2 + const userUSDCAmountBefore = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + await mintUSDCToUser( + usdcMint, + secondUserDriftClientUSDCAccount, + mintAmount, + bankrunContextWrapper + ); + + const userBorrowBalanceBefore = + secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + const spotMarketDepositBalanceBefore = + secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + + const depositAmount = userUSDCAmountBefore.add(mintAmount.div(new BN(2))); + const txSig = await secondUserDriftClient.deposit( + depositAmount, + marketIndex, + secondUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + await secondUserDriftClient.fetchAccounts(); + const spotMarketAccount = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const borrowToPayOff = getTokenAmount( + userBorrowBalanceBefore, + spotMarketAccount, + SpotBalanceType.BORROW + ); + const newDepositTokenAmount = depositAmount.sub(borrowToPayOff); + + const expectedUserBalance = getBalance( + newDepositTokenAmount, + spotMarketAccount, + SpotBalanceType.DEPOSIT + ); + const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + + console.log( + expectedUserBalance.toString(), + userBalanceAfter.scaledBalance.toString() + ); + + assert(expectedUserBalance.eq(userBalanceAfter.scaledBalance)); + assert(isVariant(userBalanceAfter.balanceType, 'deposit')); + + const expectedSpotMarketDepositBalance = + spotMarketDepositBalanceBefore.add(expectedUserBalance); + + console.log( + spotMarketAccount.depositBalance.toString(), + expectedSpotMarketDepositBalance.toString() + ); + + assert( + spotMarketAccount.depositBalance.eq(expectedSpotMarketDepositBalance) + ); + assert(spotMarketAccount.borrowBalance.eq(ZERO)); + }); + + it('Flip second user deposit to borrow', async () => { + const marketIndex = 0; + + const spotMarketAccountBefore = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const userDepositBalanceBefore = + secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + const spotMarketDepositBalanceBefore = + secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + const userDepositokenAmountBefore = getTokenAmount( + userDepositBalanceBefore, + spotMarketAccountBefore, + SpotBalanceType.DEPOSIT + ); + + const borrowAmount = userDepositokenAmountBefore.add(new BN(1 * 10 ** 6)); + const txSig = await secondUserDriftClient.withdraw( + borrowAmount, + marketIndex, + secondUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + await secondUserDriftClient.fetchAccounts(); + const spotMarketAccount = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const depositToWithdrawAgainst = getTokenAmount( + userDepositBalanceBefore, + spotMarketAccount, + SpotBalanceType.DEPOSIT + ); + const newBorrowTokenAmount = borrowAmount.sub(depositToWithdrawAgainst); + + const expectedUserBalance = getBalance( + newBorrowTokenAmount, + spotMarketAccount, + SpotBalanceType.BORROW + ); + const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + + assert(expectedUserBalance.eq(userBalanceAfter.scaledBalance)); + assert(isVariant(userBalanceAfter.balanceType, 'borrow')); + + const expectedSpotMarketDepositBalance = spotMarketDepositBalanceBefore.sub( + userDepositBalanceBefore + ); + assert( + spotMarketAccount.depositBalance.eq(expectedSpotMarketDepositBalance) + ); + assert(spotMarketAccount.borrowBalance.eq(expectedUserBalance)); + }); + + it('Second user reduce only pay down borrow', async () => { + const marketIndex = 0; + const userUSDCAmountBefore = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + const currentUserBorrowBalance = + secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + const spotMarketDepositBalanceBefore = + secondUserDriftClient.getSpotMarketAccount(marketIndex).depositBalance; + + const depositAmount = userUSDCAmountBefore.mul(new BN(100000)); // huge number + const txSig = await secondUserDriftClient.deposit( + depositAmount, + marketIndex, + secondUserDriftClientUSDCAccount, + undefined, + true + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotMarketAccountAfter = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const borrowToPayBack = getTokenAmount( + currentUserBorrowBalance, + spotMarketAccountAfter, + SpotBalanceType.BORROW + ); + + const userUSDCAmountAfter = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientUSDCAccount + ) + ).amount.toString() + ); + + const expectedUserUSDCAmount = userUSDCAmountBefore.sub(borrowToPayBack); + console.log( + expectedUserUSDCAmount.toString(), + userUSDCAmountAfter.toString() + ); + assert(expectedUserUSDCAmount.eq(userUSDCAmountAfter)); + + const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + assert(userBalanceAfter.scaledBalance.eq(ZERO)); + + assert(spotMarketAccountAfter.borrowBalance.eq(ZERO)); + assert( + spotMarketAccountAfter.depositBalance.eq(spotMarketDepositBalanceBefore) + ); + }); + + it('Second user reduce only withdraw deposit', async () => { + const marketIndex = 1; + const userWSOLAmountBefore = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientWSOLAccount + ) + ).amount.toString() + ); + + const currentUserDepositBalance = + secondUserDriftClient.getSpotPosition(marketIndex).scaledBalance; + + const withdrawAmount = new BN(LAMPORTS_PER_SOL * 100); + const txSig = await secondUserDriftClient.withdraw( + withdrawAmount, + marketIndex, + secondUserDriftClientWSOLAccount, + true + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotMarketAccountAfter = + secondUserDriftClient.getSpotMarketAccount(marketIndex); + const amountAbleToWithdraw = getTokenAmount( + currentUserDepositBalance, + spotMarketAccountAfter, + SpotBalanceType.DEPOSIT + ); + + const userWSOLAmountAfter = new BN( + ( + await bankrunContextWrapper.connection.getTokenAccount( + secondUserDriftClientWSOLAccount + ) + ).amount.toString() + ); + + const expectedUserWSOLAmount = + amountAbleToWithdraw.sub(userWSOLAmountBefore); + console.log(expectedUserWSOLAmount.toString()); + console.log(userWSOLAmountAfter.toString()); + assert(expectedUserWSOLAmount.eq(userWSOLAmountAfter)); + + const userBalanceAfter = secondUserDriftClient.getSpotPosition(marketIndex); + assert(userBalanceAfter.scaledBalance.eq(ZERO)); + }); + + it('Third user deposits when cumulative interest off init value', async () => { + // rounding on spot market balance <-> token conversions can lead to tiny epislon of loss on deposits + + const [ + thirdUserDriftClient, + _thirdUserDriftClientWSOLAccount, + thirdUserDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + solAmount, + largeUsdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + + const marketIndex = 0; + + await thirdUserDriftClient.fetchAccounts(); + const spotPosition = thirdUserDriftClient.getSpotPosition(marketIndex); + console.log(spotPosition); + assert(spotPosition.scaledBalance.eq(ZERO)); + + const spotMarket = thirdUserDriftClient.getSpotMarketAccount(marketIndex); + + console.log(spotMarket.cumulativeDepositInterest.toString()); + console.log(spotMarket.cumulativeBorrowInterest.toString()); + + assert( + spotMarket.cumulativeDepositInterest.gt( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + assert( + spotMarket.cumulativeBorrowInterest.gt( + SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION + ) + ); + + console.log('usdcAmount:', largeUsdcAmount.toString(), 'user deposits'); + const txSig = await thirdUserDriftClient.deposit( + largeUsdcAmount, + marketIndex, + thirdUserDriftClientUSDCAccount + ); + bankrunContextWrapper.printTxLogs(txSig); + + const spotPositionAfter = thirdUserDriftClient.getSpotPosition(marketIndex); + const tokenAmount = getTokenAmount( + spotPositionAfter.scaledBalance, + spotMarket, + spotPositionAfter.balanceType + ); + console.log('tokenAmount:', tokenAmount.toString()); + assert( + tokenAmount.gte(largeUsdcAmount.sub(QUOTE_PRECISION.div(new BN(100)))) + ); // didnt lose more than a penny + assert(tokenAmount.lt(largeUsdcAmount)); // lose a lil bit + + await thirdUserDriftClient.unsubscribe(); + }); +}); diff --git a/tests/spotSwap22.ts b/tests/spotSwap22.ts new file mode 100644 index 000000000..6a92251eb --- /dev/null +++ b/tests/spotSwap22.ts @@ -0,0 +1,311 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; + +import { + BN, + TestClient, + EventSubscriber, + OracleSource, + OracleInfo, + QUOTE_PRECISION, + UserStatsAccount, + getUserStatsAccountPublicKey, +} from '../sdk/src'; + +import { + createUserWithUSDCAndWSOLAccount, + createWSolTokenAccountForUser, + initializeQuoteSpotMarket, + initializeSolSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, +} from './testHelpers'; +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createTransferInstruction, +} from '@solana/spl-token'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import { DRIFT_PROGRAM_ID } from '../sdk/lib'; + +describe('spot swap 22', () => { + const chProgram = anchor.workspace.Drift as Program; + + let makerDriftClient: TestClient; + let makerWSOL: PublicKey; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let solOracle: PublicKey; + + let usdcMint; + let makerUSDC; + + let takerDriftClient: TestClient; + let takerWSOL: PublicKey; + let takerUSDC: PublicKey; + + const usdcAmount = new BN(200 * 10 ** 6); + const solAmount = new BN(2 * 10 ** 9); + + let marketIndexes: number[]; + let spotMarketIndexes: number[]; + let oracleInfos: OracleInfo[]; + + let takerKeypair: Keypair; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper, TOKEN_2022_PROGRAM_ID); + makerUSDC = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + makerWSOL = await createWSolTokenAccountForUser( + bankrunContextWrapper, + // @ts-ignore + bankrunContextWrapper.provider.wallet, + solAmount + ); + + solOracle = await mockOracleNoProgram(bankrunContextWrapper, 100); + + marketIndexes = []; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solOracle, source: OracleSource.PYTH }]; + + makerDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await makerDriftClient.initialize(usdcMint.publicKey, true); + await makerDriftClient.subscribe(); + await makerDriftClient.initializeUserAccount(); + + await initializeQuoteSpotMarket(makerDriftClient, usdcMint.publicKey); + await initializeSolSpotMarket(makerDriftClient, solOracle); + await makerDriftClient.updateSpotMarketStepSizeAndTickSize( + 1, + new BN(100000000), + new BN(100) + ); + await makerDriftClient.updateSpotAuctionDuration(0); + + [takerDriftClient, takerWSOL, takerUSDC, takerKeypair] = + await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + solAmount, + usdcAmount, + [], + [0, 1], + [ + { + publicKey: solOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await bankrunContextWrapper.fundKeypair( + takerKeypair, + 10 * LAMPORTS_PER_SOL + ); + await takerDriftClient.deposit(usdcAmount, 0, takerUSDC); + }); + + after(async () => { + await takerDriftClient.unsubscribe(); + await makerDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('swap usdc for sol', async () => { + const amountIn = new BN(200).mul(QUOTE_PRECISION); + const { beginSwapIx, endSwapIx } = await takerDriftClient.getSwapIx({ + amountIn: amountIn, + inMarketIndex: 0, + outMarketIndex: 1, + inTokenAccount: takerUSDC, + outTokenAccount: takerWSOL, + }); + + const transferIn = createTransferInstruction( + takerUSDC, + makerUSDC.publicKey, + takerDriftClient.wallet.publicKey, + new BN(100).mul(QUOTE_PRECISION).toNumber(), + undefined, + TOKEN_2022_PROGRAM_ID + ); + + const transferOut = createTransferInstruction( + makerWSOL, + takerWSOL, + makerDriftClient.wallet.publicKey, + LAMPORTS_PER_SOL, + undefined, + TOKEN_PROGRAM_ID + ); + + const tx = new Transaction() + .add(beginSwapIx) + .add(transferIn) + .add(transferOut) + .add(endSwapIx); + + // @ts-ignore + const { txSig } = await takerDriftClient.sendTransaction(tx, [ + makerDriftClient.wallet.payer, + ]); + + bankrunContextWrapper.printTxLogs(txSig); + + const takerSOLAmount = await takerDriftClient.getTokenAmount(1); + assert(takerSOLAmount.eq(new BN(1000000000))); + const takerUSDCAmount = await takerDriftClient.getTokenAmount(0); + assert(takerUSDCAmount.eq(new BN(99999999))); + + const userStatsPublicKey = getUserStatsAccountPublicKey( + new PublicKey(DRIFT_PROGRAM_ID), + takerDriftClient.wallet.publicKey + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + userStatsPublicKey + ); + + const userStatsAccount = accountInfo + ? (takerDriftClient.program.account.user.coder.accounts.decodeUnchecked( + 'UserStats', + accountInfo.data + ) as UserStatsAccount) + : undefined; + + assert(userStatsAccount.takerVolume30D.eq(new BN(0))); + + const swapRecord = eventSubscriber.getEventsArray('SwapRecord')[0]; + assert(swapRecord.amountOut.eq(new BN(1000000000))); + assert(swapRecord.outMarketIndex === 1); + assert(swapRecord.amountIn.eq(new BN(100000000))); + assert(swapRecord.inMarketIndex === 0); + assert(swapRecord.fee.eq(new BN(0))); + + const solSpotMarket = takerDriftClient.getSpotMarketAccount(1); + + assert(solSpotMarket.totalSwapFee.eq(new BN(0))); + }); + + it('swap usdc for sol', async () => { + const amountIn = new BN(1).mul(new BN(LAMPORTS_PER_SOL)); + const { beginSwapIx, endSwapIx } = await takerDriftClient.getSwapIx({ + amountIn: amountIn, + inMarketIndex: 1, + outMarketIndex: 0, + inTokenAccount: takerWSOL, + outTokenAccount: takerUSDC, + }); + + const transferIn = createTransferInstruction( + takerWSOL, + makerWSOL, + takerDriftClient.wallet.publicKey, + LAMPORTS_PER_SOL, + undefined, + TOKEN_PROGRAM_ID + ); + + const transferOut = createTransferInstruction( + makerUSDC.publicKey, + takerUSDC, + makerDriftClient.wallet.publicKey, + new BN(100).mul(QUOTE_PRECISION).toNumber(), + undefined, + TOKEN_2022_PROGRAM_ID + ); + + const tx = new Transaction() + .add(beginSwapIx) + .add(transferIn) + .add(transferOut) + .add(endSwapIx); + + // @ts-ignore + const { txSig } = await takerDriftClient.sendTransaction(tx, [ + makerDriftClient.wallet.payer, + ]); + + bankrunContextWrapper.printTxLogs(txSig); + + const takerSOLAmount = await takerDriftClient.getTokenAmount(1); + assert(takerSOLAmount.eq(new BN(0))); + const takerUSDCAmount = await takerDriftClient.getTokenAmount(0); + console.log(takerUSDCAmount.toString()); + assert(takerUSDCAmount.eq(new BN(199999999))); + + const swapRecord = eventSubscriber.getEventsArray('SwapRecord')[0]; + assert(swapRecord.amountOut.eq(new BN(100000000))); + assert(swapRecord.outMarketIndex === 0); + assert(swapRecord.amountIn.eq(new BN(1000000000))); + assert(swapRecord.inMarketIndex === 1); + }); +}); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index 5bb1f69c2..c76e95474 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -115,7 +115,8 @@ export async function mockOracleNoProgram( } export async function mockUSDCMint( - context: BankrunContextWrapper + context: BankrunContextWrapper, + tokenProgram = TOKEN_PROGRAM_ID ): Promise { const fakeUSDCMint = anchor.web3.Keypair.generate(); const createUSDCMintAccountIx = SystemProgram.createAccount({ @@ -123,7 +124,7 @@ export async function mockUSDCMint( newAccountPubkey: fakeUSDCMint.publicKey, lamports: 10_000_000_000, space: MintLayout.span, - programId: TOKEN_PROGRAM_ID, + programId: tokenProgram, }); const initCollateralMintIx = createInitializeMintInstruction( fakeUSDCMint.publicKey, @@ -131,24 +132,14 @@ export async function mockUSDCMint( // @ts-ignore context.provider.wallet.publicKey, // @ts-ignore - context.provider.wallet.publicKey + context.provider.wallet.publicKey, + tokenProgram ); const fakeUSDCTx = new Transaction(); fakeUSDCTx.add(createUSDCMintAccountIx); fakeUSDCTx.add(initCollateralMintIx); await context.sendTransaction(fakeUSDCTx, [fakeUSDCMint]); - // await sendAndConfirmTransaction( - // provider.connection, - // fakeUSDCTx, - // // @ts-ignore - // [provider.wallet.payer, fakeUSDCMint], - // { - // skipPreflight: false, - // commitment: 'recent', - // preflightCommitment: 'recent', - // } - // ); return fakeUSDCMint; } @@ -165,19 +156,24 @@ export async function mockUserUSDCAccount( owner = context.context.payer.publicKey; } + const tokenProgram = ( + await context.connection.getAccountInfo(fakeUSDCMint.publicKey) + ).owner; + const createUSDCTokenAccountIx = SystemProgram.createAccount({ fromPubkey: context.context.payer.publicKey, newAccountPubkey: userUSDCAccount.publicKey, lamports: 100_000_000, space: AccountLayout.span, - programId: TOKEN_PROGRAM_ID, + programId: tokenProgram, }); fakeUSDCTx.add(createUSDCTokenAccountIx); const initUSDCTokenAccountIx = createInitializeAccountInstruction( userUSDCAccount.publicKey, fakeUSDCMint.publicKey, - owner + owner, + tokenProgram ); fakeUSDCTx.add(initUSDCTokenAccountIx); @@ -186,7 +182,9 @@ export async function mockUserUSDCAccount( userUSDCAccount.publicKey, // @ts-ignore context.context.payer.publicKey, - usdcMintAmount.toNumber() + usdcMintAmount.toNumber(), + undefined, + tokenProgram ); fakeUSDCTx.add(mintToUserAccountTx); @@ -243,12 +241,18 @@ export async function mintUSDCToUser( context: BankrunContextWrapper ): Promise { const tx = new Transaction(); + const tokenProgram = ( + await context.connection.getAccountInfo(fakeUSDCMint.publicKey) + ).owner; + const mintToUserAccountTx = await createMintToInstruction( fakeUSDCMint.publicKey, userUSDCAccount, // @ts-ignore context.provider.wallet.publicKey, - usdcMintAmount.toNumber() + usdcMintAmount.toNumber(), + undefined, + tokenProgram ); tx.add(mintToUserAccountTx); From 01eb88a65d06829f942c01b4da3e992d6fa5c33d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 25 Jul 2024 19:44:49 -0400 Subject: [PATCH 03/17] program: fix build with InterfaceAccount --- programs/drift/src/instructions/keeper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 534c491ca..e5748cefa 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -2046,7 +2046,7 @@ pub struct UpdateUserGovTokenInsuranceStake<'info> { seeds = [b"insurance_fund_vault".as_ref(), 15_u16.to_le_bytes().as_ref()], bump, )] - pub insurance_fund_vault: Box>, + pub insurance_fund_vault: Box>, } #[derive(Accounts)] From 0a579ea6819cdc7e4919c3b48f30ab3eaacac1de Mon Sep 17 00:00:00 2001 From: frank <98238480+soundsonacid@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:47:21 -0500 Subject: [PATCH 04/17] sdk: add openbook subscriber (#1160) * add openbookv2 subscriber * remove weird artifacts * revert accidental prettiers * revert prettiers * pr comment * prettify fix --- sdk/package.json | 1 + sdk/src/dlob/orderBookLevels.ts | 2 +- sdk/src/index.ts | 2 + .../openbookV2FulfillmentConfigMap.ts | 29 +++ sdk/src/openbook/openbookV2Subscriber.ts | 167 ++++++++++++++++++ sdk/tests/subscriber/openbook.ts | 58 ++++++ sdk/yarn.lock | 107 ++++++++++- 7 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 sdk/src/openbook/openbookV2FulfillmentConfigMap.ts create mode 100644 sdk/src/openbook/openbookV2Subscriber.ts create mode 100644 sdk/tests/subscriber/openbook.ts diff --git a/sdk/package.json b/sdk/package.json index 2f447fe58..ccafb6d0b 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -37,6 +37,7 @@ "@coral-xyz/anchor": "0.28.0", "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "^1.4.2", + "@openbook-dex/openbook-v2": "^0.2.10", "@project-serum/serum": "^0.13.38", "@pythnetwork/client": "2.5.3", "@pythnetwork/price-service-sdk": "^1.7.1", diff --git a/sdk/src/dlob/orderBookLevels.ts b/sdk/src/dlob/orderBookLevels.ts index 15d73db16..78af4b46d 100644 --- a/sdk/src/dlob/orderBookLevels.ts +++ b/sdk/src/dlob/orderBookLevels.ts @@ -21,7 +21,7 @@ import { import { PublicKey } from '@solana/web3.js'; import { assert } from '../assert/assert'; -type liquiditySource = 'serum' | 'vamm' | 'dlob' | 'phoenix'; +type liquiditySource = 'serum' | 'vamm' | 'dlob' | 'phoenix' | 'openbook'; export type L2Level = { price: BN; diff --git a/sdk/src/index.ts b/sdk/src/index.ts index f9343b187..3ecb063b8 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -73,6 +73,8 @@ export * from './serum/serumFulfillmentConfigMap'; export * from './phoenix/phoenixSubscriber'; export * from './priorityFee'; export * from './phoenix/phoenixFulfillmentConfigMap'; +export * from './openbook/openbookV2Subscriber'; +export * from './openbook/openbookV2FulfillmentConfigMap'; export * from './tx/fastSingleTxSender'; export * from './tx/retryTxSender'; export * from './tx/whileValidTxSender'; diff --git a/sdk/src/openbook/openbookV2FulfillmentConfigMap.ts b/sdk/src/openbook/openbookV2FulfillmentConfigMap.ts new file mode 100644 index 000000000..9c1b75647 --- /dev/null +++ b/sdk/src/openbook/openbookV2FulfillmentConfigMap.ts @@ -0,0 +1,29 @@ +import { PublicKey } from '@solana/web3.js'; +import { OpenbookV2FulfillmentConfigAccount } from '../types'; +import { DriftClient } from '../driftClient'; + +export class OpenbookV2FulfillmentConfigMap { + driftClient: DriftClient; + map = new Map(); + + public constructor(driftClient: DriftClient) { + this.driftClient = driftClient; + } + + public async add( + marketIndex: number, + openbookV2MarketAddress: PublicKey + ): Promise { + const account = await this.driftClient.getOpenbookV2FulfillmentConfig( + openbookV2MarketAddress + ); + + this.map.set(marketIndex, account); + } + + public get( + marketIndex: number + ): OpenbookV2FulfillmentConfigAccount | undefined { + return this.map.get(marketIndex); + } +} diff --git a/sdk/src/openbook/openbookV2Subscriber.ts b/sdk/src/openbook/openbookV2Subscriber.ts new file mode 100644 index 000000000..3a439b417 --- /dev/null +++ b/sdk/src/openbook/openbookV2Subscriber.ts @@ -0,0 +1,167 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { BulkAccountLoader } from '../accounts/bulkAccountLoader'; +import { PRICE_PRECISION } from '../constants/numericConstants'; +import { AnchorProvider, BN, Idl, Program, Wallet } from '@coral-xyz/anchor'; +import { L2Level, L2OrderBookGenerator } from '../dlob/orderBookLevels'; +import { Market, OpenBookV2Client } from '@openbook-dex/openbook-v2'; +import openbookV2Idl from '../idl/openbook.json'; + +export type OpenbookV2SubscriberConfig = { + connection: Connection; + programId: PublicKey; + marketAddress: PublicKey; + accountSubscription: + | { + // enables use to add web sockets in the future + type: 'polling'; + accountLoader: BulkAccountLoader; + } + | { + type: 'websocket'; + }; +}; + +export class OpenbookV2Subscriber implements L2OrderBookGenerator { + connection: Connection; + programId: PublicKey; + marketAddress: PublicKey; + subscriptionType: 'polling' | 'websocket'; + accountLoader: BulkAccountLoader | undefined; + subscribed: boolean; + market: Market; + marketCallbackId: string | number; + client: OpenBookV2Client; + + public constructor(config: OpenbookV2SubscriberConfig) { + this.connection = config.connection; + this.programId = config.programId; + this.marketAddress = config.marketAddress; + this.subscribed = false; + if (config.accountSubscription.type === 'polling') { + this.subscriptionType = 'polling'; + this.accountLoader = config.accountSubscription.accountLoader; + } else { + this.subscriptionType = 'websocket'; + } + } + + public async subscribe(): Promise { + if (this.subscribed === true) { + return; + } + + const anchorProvider = new AnchorProvider( + this.connection, + new Wallet(Keypair.generate()), + {} + ); + const openbookV2Program = new Program( + openbookV2Idl as Idl, + this.programId, + anchorProvider + ); + this.client = new OpenBookV2Client(anchorProvider); + const market = await Market.load(this.client, this.marketAddress); + this.market = await market.loadOrderBook(); + + if (this.subscriptionType === 'websocket') { + this.marketCallbackId = this.connection.onAccountChange( + this.marketAddress, + async (accountInfo, _) => { + const marketRaw = openbookV2Program.coder.accounts.decode( + 'Market', + accountInfo.data + ); + this.market = new Market(this.client, this.marketAddress, marketRaw); + await this.market.loadOrderBook(); + } + ); + } else { + this.marketCallbackId = await this.accountLoader.addAccount( + this.marketAddress, + (buffer, _) => { + const marketRaw = openbookV2Program.coder.accounts.decode( + 'Market', + buffer + ); + this.market = new Market(this.client, this.marketAddress, marketRaw); + (async () => { + await this.market.loadOrderBook(); + })(); + } + ); + } + + this.subscribed = true; + } + + public async getBestBid(): Promise { + const bids = await this.market.loadBids(); + const bestBid = bids.best(); + + if (bestBid === undefined) { + return undefined; + } + + return new BN(Math.floor(bestBid.price * PRICE_PRECISION.toNumber())); + } + + public async getBestAsk(): Promise { + const asks = await this.market.loadAsks(); + const bestAsk = asks.best(); + + if (bestAsk === undefined) { + return undefined; + } + + return new BN(Math.floor(bestAsk.price * PRICE_PRECISION.toNumber())); + } + + public getL2Bids(): Generator { + return this.getL2Levels('bids'); + } + + public getL2Asks(): Generator { + return this.getL2Levels('asks'); + } + + *getL2Levels(side: 'bids' | 'asks'): Generator { + const basePrecision = Math.ceil( + 1 / this.market.baseNativeFactor.toNumber() + ); + const pricePrecision = PRICE_PRECISION.toNumber(); + + const levels = side === 'bids' ? this.market.bids : this.market.asks; + + for (const order of levels.items()) { + const size = new BN(order.size * basePrecision); + const price = new BN(order.price * pricePrecision); + yield { + price, + size, + sources: { + openbook: size, + }, + }; + } + } + + public async unsubscribe(): Promise { + if (!this.subscribed) { + return; + } + + if (this.subscriptionType === 'websocket') { + await this.connection.removeAccountChangeListener( + this.marketCallbackId as number + ); + } else { + this.accountLoader.removeAccount( + this.marketAddress, + this.marketCallbackId as string + ); + } + + this.subscribed = false; + } +} diff --git a/sdk/tests/subscriber/openbook.ts b/sdk/tests/subscriber/openbook.ts new file mode 100644 index 000000000..8a7add8c0 --- /dev/null +++ b/sdk/tests/subscriber/openbook.ts @@ -0,0 +1,58 @@ +import { OpenbookV2Subscriber, PRICE_PRECISION } from '../../lib'; +import { Connection, PublicKey } from '@solana/web3.js'; + +describe('openbook v2 subscriber', function () { + this.timeout(100_000); + + it('works', async function () { + const connection = new Connection( + process.env.MAINNET_RPC_ENDPOINT as string + ); + const solUsdc = new PublicKey( + 'AFgkED1FUVfBe2trPUDqSqK9QKd4stJrfzq5q1RwAFTa' + ); + const openbook = new PublicKey( + 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' + ); + + const openbookV2Subscriber = new OpenbookV2Subscriber({ + connection, + programId: openbook, + marketAddress: solUsdc, + accountSubscription: { + type: 'websocket', + }, + }); + + await openbookV2Subscriber.subscribe(); + + // wait for updates + await new Promise((resolve) => setTimeout(resolve, 5_000)); + + const basePrecision = Math.ceil( + 1 / openbookV2Subscriber.market.baseNativeFactor.toNumber() + ); + + console.log('Bids'); + for (const bid of openbookV2Subscriber.getL2Bids()) { + console.log('Price: ', bid.price.toNumber() / PRICE_PRECISION.toNumber()); + console.log('Size: ', bid.size.toNumber() / basePrecision); + console.log('Source: ', bid.sources); + } + + console.log('Asks'); + for (const ask of openbookV2Subscriber.getL2Asks()) { + console.log('Price: ', ask.price.toNumber() / PRICE_PRECISION.toNumber()); + console.log('Size: ', ask.size.toNumber() / basePrecision); + console.log('Source: ', ask.sources); + } + + const bestBid = await openbookV2Subscriber.getBestBid(); + console.log('Best bid:', bestBid.toNumber()); + + const bestAsk = await openbookV2Subscriber.getBestAsk(); + console.log('Best ask:', bestAsk.toNumber()); + + await openbookV2Subscriber.unsubscribe(); + }); +}); diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 9023cb8a4..775191949 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -340,6 +340,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openbook-dex/openbook-v2@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@openbook-dex/openbook-v2/-/openbook-v2-0.2.10.tgz#a5cfcd30ce827ecd446b76429a5e41baa23a318c" + integrity sha512-JOroVQHeia+RbghpluDJB5psUIhdhYRPLu0zWoG0h5vgDU4SjXwlcC+LJiIa2HVPasvZjWuCtlKWFyrOS75lOA== + dependencies: + "@coral-xyz/anchor" "^0.29.0" + "@solana/spl-token" "^0.4.0" + "@solana/web3.js" "^1.77.3" + big.js "^6.2.1" + "@project-serum/anchor@^0.11.1": version "0.11.1" resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.11.1.tgz#155bff2c70652eafdcfd5559c81a83bb19cec9ff" @@ -533,6 +543,13 @@ dependencies: "@solana/errors" "2.0.0-preview.2" +"@solana/codecs-core@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-preview.4.tgz#770826105f2f884110a21662573e7a2014654324" + integrity sha512-A0VVuDDA5kNKZUinOqHxJQK32aKTucaVbvn31YenGzHX1gPqq+SOnFwgaEY6pq4XEopSmaK16w938ZQS8IvCnw== + dependencies: + "@solana/errors" "2.0.0-preview.4" + "@solana/codecs-data-structures@2.0.0-preview.2": version "2.0.0-preview.2" resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz#e82cb1b6d154fa636cd5c8953ff3f32959cc0370" @@ -542,6 +559,15 @@ "@solana/codecs-numbers" "2.0.0-preview.2" "@solana/errors" "2.0.0-preview.2" +"@solana/codecs-data-structures@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.4.tgz#f8a2470982a9792334737ea64000ccbdff287247" + integrity sha512-nt2k2eTeyzlI/ccutPcG36M/J8NAYfxBPI9h/nQjgJ+M+IgOKi31JV8StDDlG/1XvY0zyqugV3I0r3KAbZRJpA== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + "@solana/codecs-numbers@2.0.0-preview.2": version "2.0.0-preview.2" resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.2.tgz#56995c27396cd8ee3bae8bd055363891b630bbd0" @@ -550,6 +576,14 @@ "@solana/codecs-core" "2.0.0-preview.2" "@solana/errors" "2.0.0-preview.2" +"@solana/codecs-numbers@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.4.tgz#6a53b456bb7866f252d8c032c81a92651e150f66" + integrity sha512-Q061rLtMadsO7uxpguT+Z7G4UHnjQ6moVIxAQxR58nLxDPCC7MB1Pk106/Z7NDhDLHTcd18uO6DZ7ajHZEn2XQ== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + "@solana/codecs-strings@2.0.0-preview.2": version "2.0.0-preview.2" resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.2.tgz#8bd01a4e48614d5289d72d743c3e81305d445c46" @@ -559,6 +593,15 @@ "@solana/codecs-numbers" "2.0.0-preview.2" "@solana/errors" "2.0.0-preview.2" +"@solana/codecs-strings@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.4.tgz#4d06bb722a55a5d04598d362021bfab4bd446760" + integrity sha512-YDbsQePRWm+xnrfS64losSGRg8Wb76cjK1K6qfR8LPmdwIC3787x9uW5/E4icl/k+9nwgbIRXZ65lpF+ucZUnw== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + "@solana/codecs@2.0.0-preview.2": version "2.0.0-preview.2" resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.2.tgz#d6615fec98f423166fb89409f9a4ad5b74c10935" @@ -570,6 +613,17 @@ "@solana/codecs-strings" "2.0.0-preview.2" "@solana/options" "2.0.0-preview.2" +"@solana/codecs@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.4.tgz#a1923cc78a6f64ebe656c7ec6335eb6b70405b22" + integrity sha512-gLMupqI4i+G4uPi2SGF/Tc1aXcviZF2ybC81x7Q/fARamNSgNOCUUoSCg9nWu1Gid6+UhA7LH80sWI8XjKaRog== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-data-structures" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/codecs-strings" "2.0.0-preview.4" + "@solana/options" "2.0.0-preview.4" + "@solana/errors@2.0.0-preview.2", "@solana/errors@2.0.0-preview.4": version "2.0.0-preview.4" resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-preview.4.tgz#056ba76b6dd900dafa70117311bec3aef0f5250b" @@ -586,7 +640,26 @@ "@solana/codecs-core" "2.0.0-preview.2" "@solana/codecs-numbers" "2.0.0-preview.2" -"@solana/spl-token-metadata@^0.1.2": +"@solana/options@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-preview.4.tgz#212d35d1da87c7efb13de4d3569ad9eb070f013d" + integrity sha512-tv2O/Frxql/wSe3jbzi5nVicIWIus/BftH+5ZR+r9r3FO0/htEllZS5Q9XdbmSboHu+St87584JXeDx3xm4jaA== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-data-structures" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/codecs-strings" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + +"@solana/spl-token-group@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.5.tgz#f955dcca782031c85e862b2b46878d1bb02db6c2" + integrity sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ== + dependencies: + "@solana/codecs" "2.0.0-preview.4" + "@solana/spl-type-length-value" "0.1.0" + +"@solana/spl-token-metadata@^0.1.2", "@solana/spl-token-metadata@^0.1.3": version "0.1.4" resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.4.tgz#5cdc3b857a8c4a6877df24e24a8648c4132d22ba" integrity sha512-N3gZ8DlW6NWDV28+vCCDJoTqaCZiF/jDUnk3o8GRkAFzHObiR60Bs1gXHBa8zCPdvOwiG6Z3dg5pg7+RW6XNsQ== @@ -625,6 +698,17 @@ "@solana/spl-token-metadata" "^0.1.2" buffer "^6.0.3" +"@solana/spl-token@^0.4.0": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.8.tgz#a84e4131af957fa9fbd2727e5fc45dfbf9083586" + integrity sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.5" + "@solana/spl-token-metadata" "^0.1.3" + buffer "^6.0.3" + "@solana/spl-type-length-value@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz#b5930cf6c6d8f50c7ff2a70463728a4637a2f26b" @@ -695,6 +779,27 @@ rpc-websockets "^9.0.2" superstruct "^2.0.2" +"@solana/web3.js@^1.77.3": + version "1.95.2" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6" + integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w== + dependencies: + "@babel/runtime" "^7.24.8" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solana/web3.js@~1.77.3": version "1.77.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.4.tgz#aad8c44a02ced319493308ef765a2b36a9e9fa8c" From f87fdc448189eafa11f84ff4164439201835ad75 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 28 Jul 2024 09:18:06 -0400 Subject: [PATCH 05/17] sdk: udpate idl --- sdk/src/idl/drift.json | 54 ++++++------------------------------------ 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index fbfeb02c5..13c732e8e 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1675,47 +1675,6 @@ } ] }, - { - "name": "liquidatePerpWithFill", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "liquidator", - "isMut": true, - "isSigner": false - }, - { - "name": "liquidatorStats", - "isMut": true, - "isSigner": false - }, - { - "name": "user", - "isMut": true, - "isSigner": false - }, - { - "name": "userStats", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "marketIndex", - "type": "u16" - } - ] - }, { "name": "liquidateSpot", "accounts": [ @@ -6876,12 +6835,16 @@ ], "type": "u8" }, + { + "name": "tokenProgram", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 42 + 41 ] } } @@ -9874,9 +9837,6 @@ }, { "name": "PlaceAndTake" - }, - { - "name": "Liquidation" } ] } @@ -12811,8 +12771,8 @@ }, { "code": 6282, - "name": "LiquidationOrderFailedToFill", - "msg": "Liquidation order failed to fill" + "name": "NonZeroTransferFee", + "msg": "Non zero transfer fee" } ], "metadata": { From 5cd1a60ca18c48264ca0c1ad85d14216e4444fd1 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 28 Jul 2024 21:46:55 +0800 Subject: [PATCH 06/17] Luke/dpe 2149 integrate token22 on UI (#1161) * Refactoring for token22 support * Bump * Bump --- sdk/src/addresses/pda.ts | 10 ++++++++++ sdk/src/driftClient.ts | 29 ++++++++++------------------- sdk/src/math/spotMarket.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index a7af6c484..91e4a9121 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -1,6 +1,8 @@ import { PublicKey } from '@solana/web3.js'; import * as anchor from '@coral-xyz/anchor'; import { BN } from '@coral-xyz/anchor'; +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { SpotMarketAccount } from '..'; export async function getDriftStateAccountPublicKeyAndNonce( programId: PublicKey @@ -264,3 +266,11 @@ export function getPythPullOraclePublicKey( progarmId )[0]; } +export function getTokenProgramForSpotMarket( + spotMarketAccount: SpotMarketAccount +): PublicKey { + if (spotMarketAccount.tokenProgram === 1) { + return TOKEN_2022_PROGRAM_ID; + } + return TOKEN_PROGRAM_ID; +} diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 9affbd4e4..f5c1dcdf6 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -14,7 +14,6 @@ import { createInitializeAccountInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, - TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { StateAccount, @@ -120,6 +119,7 @@ import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; +import { getTokenProgramForSpotMarket } from './addresses/pda'; import { JupiterClient, QuoteResponse, @@ -1973,7 +1973,7 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); return await this.program.instruction.deposit( marketIndex, amount, @@ -2060,15 +2060,6 @@ export class DriftClient { return result; } - public getTokenProgramForSpotMarket( - spotMarketAccount: SpotMarketAccount - ): PublicKey { - if (spotMarketAccount.tokenProgram === 1) { - return TOKEN_2022_PROGRAM_ID; - } - return TOKEN_PROGRAM_ID; - } - public addTokenMintToRemainingAccounts( spotMarketAccount: SpotMarketAccount, remainingAccounts: AccountMeta[] @@ -2495,7 +2486,7 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); return await this.program.instruction.withdraw( marketIndex, @@ -4259,7 +4250,7 @@ export class DriftClient { outAssociatedTokenAccount ); if (!accountInfo) { - const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const tokenProgram = getTokenProgramForSpotMarket(outMarket); preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( @@ -4283,7 +4274,7 @@ export class DriftClient { inAssociatedTokenAccount ); if (!accountInfo) { - const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const tokenProgram = getTokenProgramForSpotMarket(outMarket); preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( @@ -4506,8 +4497,8 @@ export class DriftClient { const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); - const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); - const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + const outTokenProgram = getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = getTokenProgramForSpotMarket(inSpotMarket); if (!outTokenProgram.equals(inTokenProgram)) { remainingAccounts.push({ @@ -6585,7 +6576,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); + const tokenProgram = getTokenProgramForSpotMarket(spotMarket); const ix = this.program.instruction.addInsuranceFundStake( marketIndex, amount, @@ -6825,7 +6816,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); const removeStakeIx = await this.program.instruction.removeInsuranceFundStake(marketIndex, { accounts: { @@ -6958,7 +6949,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); - const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); + const tokenProgram = getTokenProgramForSpotMarket(spotMarket); const ix = await this.program.instruction.depositIntoSpotMarketRevenuePool( amount, { diff --git a/sdk/src/math/spotMarket.ts b/sdk/src/math/spotMarket.ts index 829f715f0..ee98bc94f 100644 --- a/sdk/src/math/spotMarket.ts +++ b/sdk/src/math/spotMarket.ts @@ -5,9 +5,10 @@ import { SpotBalanceType, SpotMarketAccount, } from '../types'; -import { calculateAssetWeight, calculateLiabilityWeight } from './spotBalance'; +import { calculateAssetWeight, calculateLiabilityWeight, getTokenAmount } from './spotBalance'; import { MARGIN_PRECISION } from '../constants/numericConstants'; import { numberToSafeBN } from './utils'; +import { ZERO } from '@drift-labs/sdk'; export function castNumberToSpotPrecision( value: number | BN, @@ -54,3 +55,27 @@ export function calculateSpotMarketMarginRatio( return marginRatio; } + +/** + * Returns the maximum remaining deposit that can be made to the spot market. If the maxTokenDeposits on the market is zero then there is no limit and this function will also return zero. (so that needs to be checked) + * @param market + * @returns + */ +export function calculateMaxRemainingDeposit( + market: SpotMarketAccount +) { + const marketMaxTokenDeposits = market.maxTokenDeposits; + + if (marketMaxTokenDeposits.eq(ZERO)) { + // If the maxTokenDeposits is set to zero then that means there is no limit. Return the largest number we can to represent infinite available deposit. + return ZERO; + } + + const totalDepositsTokenAmount = getTokenAmount( + market.depositBalance, + market, + SpotBalanceType.DEPOSIT + ); + + return marketMaxTokenDeposits.sub(totalDepositsTokenAmount); +} \ No newline at end of file From fbae3de0aed4525df6ed8c8fa981d3e03e1f2693 Mon Sep 17 00:00:00 2001 From: Luke Steyn Date: Sun, 28 Jul 2024 23:24:45 +0800 Subject: [PATCH 07/17] Patch import error --- sdk/src/math/spotMarket.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/src/math/spotMarket.ts b/sdk/src/math/spotMarket.ts index ee98bc94f..378d42873 100644 --- a/sdk/src/math/spotMarket.ts +++ b/sdk/src/math/spotMarket.ts @@ -6,9 +6,8 @@ import { SpotMarketAccount, } from '../types'; import { calculateAssetWeight, calculateLiabilityWeight, getTokenAmount } from './spotBalance'; -import { MARGIN_PRECISION } from '../constants/numericConstants'; +import { MARGIN_PRECISION, ZERO } from '../constants/numericConstants'; import { numberToSafeBN } from './utils'; -import { ZERO } from '@drift-labs/sdk'; export function castNumberToSpotPrecision( value: number | BN, From e6fe51f8b6f7f8393ceec8ffaeeaea25a71f7d05 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 28 Jul 2024 18:47:23 -0400 Subject: [PATCH 08/17] program: specific spot market is mutable for if staker ix --- programs/drift/src/instructions/if_staker.rs | 3 +++ sdk/src/idl/drift.json | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 3af621cf8..ed4ee875a 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -358,6 +358,7 @@ pub struct InitializeInsuranceFundStake<'info> { pub struct AddInsuranceFundStake<'info> { pub state: Box>, #[account( + mut, seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], bump )] @@ -404,6 +405,7 @@ pub struct AddInsuranceFundStake<'info> { #[instruction(market_index: u16,)] pub struct RequestRemoveInsuranceFundStake<'info> { #[account( + mut, seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], bump )] @@ -432,6 +434,7 @@ pub struct RequestRemoveInsuranceFundStake<'info> { pub struct RemoveInsuranceFundStake<'info> { pub state: Box>, #[account( + mut, seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], bump )] diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 13c732e8e..1534d0727 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -2341,7 +2341,7 @@ }, { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2401,7 +2401,7 @@ "accounts": [ { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2441,7 +2441,7 @@ "accounts": [ { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -2482,7 +2482,7 @@ }, { "name": "spotMarket", - "isMut": false, + "isMut": true, "isSigner": false }, { From bdc66dccebe9cea1b2dc8003b2014c9cd3e4f76b Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Mon, 29 Jul 2024 23:34:37 +0800 Subject: [PATCH 09/17] refactor(sdk): add resolution --- sdk/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index ccafb6d0b..111c0c21a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -78,6 +78,7 @@ "node": ">=18" }, "resolutions": { - "@solana/errors": "2.0.0-preview.4" + "@solana/errors": "2.0.0-preview.4", + "@solana/codecs-data-structures": "2.0.0-preview.4" } -} +} \ No newline at end of file From 4baafeeec741f1edd8c00089510f53d9734a980f Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 29 Jul 2024 12:23:38 -0400 Subject: [PATCH 10/17] sdk: update idl for prediction markets --- sdk/src/idl/drift.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 1534d0727..5fd6b97cb 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -3362,6 +3362,27 @@ } ] }, + { + "name": "initializePredictionMarket", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "deleteInitializedPerpMarket", "accounts": [ @@ -10104,6 +10125,9 @@ }, { "name": "Future" + }, + { + "name": "Prediction" } ] } @@ -12778,4 +12802,4 @@ "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} \ No newline at end of file +} From 26f9492b214296f35aedb100ed313eb5d05f51ee Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Mon, 29 Jul 2024 09:28:09 -0700 Subject: [PATCH 11/17] prettify --- sdk/src/driftClient.ts | 12 +++++------- sdk/src/math/spotMarket.ts | 16 +++++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f5c1dcdf6..5849168b1 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -2068,7 +2068,7 @@ export class DriftClient { remainingAccounts.push({ pubkey: spotMarketAccount.mint, isSigner: false, - isWritable: false + isWritable: false, }); } } @@ -6565,7 +6565,7 @@ export class DriftClient { public async getAddInsuranceFundStakeIx( marketIndex: number, amount: BN, - collateralAccountPublicKey: PublicKey, + collateralAccountPublicKey: PublicKey ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( @@ -6678,7 +6678,7 @@ export class DriftClient { const addFundsIx = await this.getAddInsuranceFundStakeIx( marketIndex, amount, - tokenAccount, + tokenAccount ); addIfStakeIxs.push(addFundsIx); @@ -6862,9 +6862,7 @@ export class DriftClient { txParams?: TxParams ): Promise { const tx = await this.buildTransaction( - await this.getSettleRevenueToInsuranceFundIx( - spotMarketIndex, - ), + await this.getSettleRevenueToInsuranceFundIx(spotMarketIndex), txParams ); const { txSig } = await this.sendTransaction(tx, [], this.opts); @@ -6872,7 +6870,7 @@ export class DriftClient { } public async getSettleRevenueToInsuranceFundIx( - spotMarketIndex: number, + spotMarketIndex: number ): Promise { const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); const remainingAccounts = []; diff --git a/sdk/src/math/spotMarket.ts b/sdk/src/math/spotMarket.ts index 378d42873..2863faf7e 100644 --- a/sdk/src/math/spotMarket.ts +++ b/sdk/src/math/spotMarket.ts @@ -5,7 +5,11 @@ import { SpotBalanceType, SpotMarketAccount, } from '../types'; -import { calculateAssetWeight, calculateLiabilityWeight, getTokenAmount } from './spotBalance'; +import { + calculateAssetWeight, + calculateLiabilityWeight, + getTokenAmount, +} from './spotBalance'; import { MARGIN_PRECISION, ZERO } from '../constants/numericConstants'; import { numberToSafeBN } from './utils'; @@ -57,12 +61,10 @@ export function calculateSpotMarketMarginRatio( /** * Returns the maximum remaining deposit that can be made to the spot market. If the maxTokenDeposits on the market is zero then there is no limit and this function will also return zero. (so that needs to be checked) - * @param market - * @returns + * @param market + * @returns */ -export function calculateMaxRemainingDeposit( - market: SpotMarketAccount -) { +export function calculateMaxRemainingDeposit(market: SpotMarketAccount) { const marketMaxTokenDeposits = market.maxTokenDeposits; if (marketMaxTokenDeposits.eq(ZERO)) { @@ -77,4 +79,4 @@ export function calculateMaxRemainingDeposit( ); return marketMaxTokenDeposits.sub(totalDepositsTokenAmount); -} \ No newline at end of file +} From 1684b2effb8520f468a860ba826f1c43eedda2bf Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:30:40 +0000 Subject: [PATCH 12/17] sdk: release v2.87.0-beta.6 --- sdk/VERSION | 2 +- sdk/package.json | 4 ++-- sdk/yarn.lock | 11 +---------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 8d07cec9c..7d3de8f03 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.87.0-beta.5 \ No newline at end of file +2.87.0-beta.6 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 111c0c21a..7cafd5ec8 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.87.0-beta.5", + "version": "2.87.0-beta.6", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "crispheaney", @@ -81,4 +81,4 @@ "@solana/errors": "2.0.0-preview.4", "@solana/codecs-data-structures": "2.0.0-preview.4" } -} \ No newline at end of file +} diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 775191949..1c1360b03 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -550,16 +550,7 @@ dependencies: "@solana/errors" "2.0.0-preview.4" -"@solana/codecs-data-structures@2.0.0-preview.2": - version "2.0.0-preview.2" - resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz#e82cb1b6d154fa636cd5c8953ff3f32959cc0370" - integrity sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg== - dependencies: - "@solana/codecs-core" "2.0.0-preview.2" - "@solana/codecs-numbers" "2.0.0-preview.2" - "@solana/errors" "2.0.0-preview.2" - -"@solana/codecs-data-structures@2.0.0-preview.4": +"@solana/codecs-data-structures@2.0.0-preview.2", "@solana/codecs-data-structures@2.0.0-preview.4": version "2.0.0-preview.4" resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.4.tgz#f8a2470982a9792334737ea64000ccbdff287247" integrity sha512-nt2k2eTeyzlI/ccutPcG36M/J8NAYfxBPI9h/nQjgJ+M+IgOKi31JV8StDDlG/1XvY0zyqugV3I0r3KAbZRJpA== From ea33c9bd2a99c0ef8a6fb22572857e5baf87ee42 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 30 Jul 2024 12:59:01 -0400 Subject: [PATCH 13/17] sdk: fix devnet constant for W --- sdk/src/constants/perpMarkets.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 1b4b8a01c..a8f1669e2 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -297,9 +297,9 @@ export const DevnetPerpMarkets: PerpMarketConfig[] = [ symbol: 'W-PERP', baseAssetSymbol: 'W', marketIndex: 23, - oracle: new PublicKey('4HbitGsdcFbtFotmYscikQFAAKJ3nYx4t7sV7fTvsk8U'), + oracle: new PublicKey('4iCi4DvXrubHQne8jzbMaWL3pd7v1Fip8iTe4H9vHNXB'), launchTs: 1709852537000, - oracleSource: OracleSource.PYTH_PULL, + oracleSource: OracleSource.SWITCHBOARD_ON_DEMAND, pythFeedId: '0xeff7446475e218517566ea99e72a4abec2e1bd8498b43b7d8331e29dcb059389', }, From 7d3a9229eb5be101759d3087e48ee554b771357d Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:01:29 +0000 Subject: [PATCH 14/17] sdk: release v2.87.0-beta.7 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 7d3de8f03..84e84aca3 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.87.0-beta.6 \ No newline at end of file +2.87.0-beta.7 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 7cafd5ec8..02c2794e3 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.87.0-beta.6", + "version": "2.87.0-beta.7", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "crispheaney", From a259778564afcf563b21399359eb2781753db20a Mon Sep 17 00:00:00 2001 From: lil perp Date: Tue, 30 Jul 2024 13:21:13 -0400 Subject: [PATCH 15/17] program: add liquidation via fill (#1106) * add liquidation fill mode logic * programs: init liquidate_perp_with_fill * fix tests * update drift client * update idl * add init cargo tests * fill quote asset amount for if fee * add bankrun test * clean/add test placeOrder fails * liquidator doesnt need position slot * program: is_amm_available_liquidity_source for FillMode::Liquidation * add test * smol tweaks * CHANGELOG --------- Co-authored-by: 0xbigz <83473873+0xbigz@users.noreply.github.com> --- CHANGELOG.md | 1 + programs/drift/src/controller/liquidation.rs | 471 ++++++++++- .../drift/src/controller/liquidation/tests.rs | 796 ++++++++++++++++++ programs/drift/src/controller/orders.rs | 166 ++-- programs/drift/src/controller/orders/tests.rs | 27 +- programs/drift/src/error.rs | 2 + programs/drift/src/instructions/keeper.rs | 54 ++ programs/drift/src/lib.rs | 7 + programs/drift/src/math/auction.rs | 4 +- programs/drift/src/math/fulfillment.rs | 8 +- programs/drift/src/math/fulfillment/tests.rs | 14 + programs/drift/src/math/liquidation.rs | 45 +- programs/drift/src/math/orders.rs | 3 + programs/drift/src/state/fill_mode.rs | 7 +- programs/drift/src/state/order_params.rs | 4 + sdk/src/driftClient.ts | 75 ++ sdk/src/idl/drift.json | 75 +- test-scripts/run-anchor-tests.sh | 1 + test-scripts/single-anchor-test.sh | 2 +- tests/liquidatePerpWithFill.ts | 336 ++++++++ 20 files changed, 1986 insertions(+), 112 deletions(-) create mode 100644 tests/liquidatePerpWithFill.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 29df7f12a..3ccdf0369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add liquidation via fill ([#1106](https://github.com/drift-labs/protocol-v2/pull/1106)) - program: add switchboard on demand integration ([#1154](https://github.com/drift-labs/protocol-v2/pull/1154)) - program: add support for token 2022 ([#1125](https://github.com/drift-labs/protocol-v2/pull/1125)) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 579ae3cfb..6456ccbc4 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -7,6 +7,7 @@ use crate::controller::amm::get_fee_pool_tokens; use crate::controller::funding::settle_funding_payment; use crate::controller::lp::burn_lp_shares; use crate::controller::orders; +use crate::controller::orders::{cancel_order, fill_perp_order, place_perp_order}; use crate::controller::position::{ get_position_index, update_position_and_market, update_quote_asset_amount, update_quote_asset_and_break_even_amount, PositionDirection, @@ -18,7 +19,6 @@ use crate::controller::spot_balance::{ }; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; -use crate::get_then_update_id; use crate::math::bankruptcy::is_user_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ @@ -33,7 +33,8 @@ use crate::math::liquidation::{ calculate_liability_transfer_implied_by_asset_amount, calculate_liability_transfer_to_cover_margin_shortage, calculate_liquidation_multiplier, calculate_max_pct_to_liquidate, calculate_perp_if_fee, calculate_spot_if_fee, - validate_transfer_satisfies_limit_price, LiquidationMultiplierType, + get_liquidation_order_params, validate_transfer_satisfies_limit_price, + LiquidationMultiplierType, }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, @@ -46,6 +47,7 @@ use crate::math::orders::{ }; use crate::math::position::calculate_base_asset_value_with_oracle_price; use crate::math::safe_math::SafeMath; + use crate::math::spot_balance::get_token_value; use crate::state::events::{ emit_stack, LPAction, LPRecord, LiquidateBorrowForPerpPnlRecord, @@ -53,8 +55,10 @@ use crate::state::events::{ LiquidationType, OrderAction, OrderActionExplanation, OrderActionRecord, OrderRecord, PerpBankruptcyRecord, SpotBankruptcyRecord, }; +use crate::state::fill_mode::FillMode; use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}; use crate::state::oracle_map::OracleMap; +use crate::state::order_params::PlaceOrderOptions; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::PerpMarketMap; @@ -63,7 +67,9 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; use crate::state::traits::Size; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; +use crate::state::user_map::{UserMap, UserStatsMap}; use crate::validate; +use crate::{get_then_update_id, load_mut}; #[cfg(test)] mod tests; @@ -510,7 +516,7 @@ pub fn liquidate_perp( ) }; - let margin_freed_for_perp_position = calculate_margin_freed( + let (margin_freed_for_perp_position, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -656,6 +662,451 @@ pub fn liquidate_perp( Ok(()) } +pub fn liquidate_perp_with_fill( + market_index: u16, + user_loader: &AccountLoader, + user_key: &Pubkey, + user_stats_loader: &AccountLoader, + liquidator_loader: &AccountLoader, + liquidator_key: &Pubkey, + liquidator_stats_loader: &AccountLoader, + makers_and_referrer: &UserMap, + makers_and_referrer_stats: &UserStatsMap, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + clock: &Clock, + state: &State, +) -> DriftResult { + let now = clock.unix_timestamp; + let slot = clock.slot; + + let mut user = load_mut!(user_loader)?; + let mut liquidator = load_mut!(liquidator_loader)?; + + let liquidation_margin_buffer_ratio = state.liquidation_margin_buffer_ratio; + let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; + let liquidation_duration = state.liquidation_duration as u128; + + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt", + )?; + + validate!( + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", + )?; + + let market = perp_market_map.get_ref(&market_index)?; + + validate!( + !market.is_operation_paused(PerpOperation::Liquidation), + ErrorCode::InvalidLiquidation, + "Liquidation operation is paused for market {}", + market_index + )?; + + drop(market); + + // Settle user's funding payments so that collateral is up to date + settle_funding_payment( + &mut user, + user_key, + perp_market_map.get_ref_mut(&market_index)?.deref_mut(), + now, + )?; + + // Settle user's funding payments so that collateral is up to date + settle_funding_payment( + &mut liquidator, + liquidator_key, + perp_market_map.get_ref_mut(&market_index)?.deref_mut(), + now, + )?; + + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + )?; + + if !user.is_being_liquidated() && margin_calculation.meets_margin_requirement() { + msg!("margin calculation: {:?}", margin_calculation); + return Err(ErrorCode::SufficientCollateral); + } else if user.is_being_liquidated() && margin_calculation.can_exit_liquidation()? { + user.exit_liquidation(); + return Ok(()); + } + + user.get_perp_position(market_index).map_err(|e| { + msg!( + "User does not have a position for perp market {}", + market_index + ); + e + })?; + + let liquidation_id = user.enter_liquidation(slot)?; + let mut margin_freed = 0_u64; + + let position_index = get_position_index(&user.perp_positions, market_index)?; + validate!( + user.perp_positions[position_index].is_open_position() + || user.perp_positions[position_index].has_open_order() + || user.perp_positions[position_index].is_lp(), + ErrorCode::PositionDoesntHaveOpenPositionOrOrders + )?; + + let canceled_order_ids = orders::cancel_orders( + &mut user, + user_key, + Some(liquidator_key), + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + OrderActionExplanation::Liquidation, + None, + None, + None, + )?; + + let mut market = perp_market_map.get_ref_mut(&market_index)?; + let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; + + update_amm_and_check_validity( + &mut market, + oracle_price_data, + state, + now, + slot, + Some(DriftAction::Liquidate), + )?; + + let oracle_price = if market.status == MarketStatus::Settlement { + market.expiry_price + } else { + oracle_price_data.price + }; + + drop(market); + + // burning lp shares = removing open bids/asks + let lp_shares = user.perp_positions[position_index].lp_shares; + if lp_shares > 0 { + let (position_delta, pnl) = burn_lp_shares( + &mut user.perp_positions[position_index], + perp_market_map.get_ref_mut(&market_index)?.deref_mut(), + lp_shares, + oracle_price, + )?; + + // emit LP record for shares removed + emit_stack::<_, { LPRecord::SIZE }>(LPRecord { + ts: now, + action: LPAction::RemoveLiquidity, + user: *user_key, + n_shares: lp_shares, + market_index, + delta_base_asset_amount: position_delta.base_asset_amount, + delta_quote_asset_amount: position_delta.quote_asset_amount, + pnl, + })?; + } + + // check if user exited liquidation territory + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + let intermediate_margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio) + .track_market_margin_requirement(MarketIdentifier::perp(market_index))?, + )?; + + let initial_margin_shortage = margin_calculation.margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + + margin_freed = initial_margin_shortage + .saturating_sub(new_margin_shortage) + .cast::()?; + user.increment_margin_freed(margin_freed)?; + + if intermediate_margin_calculation.can_exit_liquidation()? { + emit!(LiquidationRecord { + ts: now, + liquidation_id, + liquidation_type: LiquidationType::LiquidatePerp, + user: *user_key, + liquidator: *liquidator_key, + margin_requirement: margin_calculation.margin_requirement, + total_collateral: margin_calculation.total_collateral, + bankrupt: user.is_bankrupt(), + canceled_order_ids, + margin_freed, + liquidate_perp: LiquidatePerpRecord { + market_index, + oracle_price, + lp_shares, + ..LiquidatePerpRecord::default() + }, + ..LiquidationRecord::default() + }); + + user.exit_liquidation(); + return Ok(()); + } + + intermediate_margin_calculation + } else { + margin_calculation + }; + + if user.perp_positions[position_index].base_asset_amount == 0 { + msg!("User has no base asset amount"); + return Ok(()); + } + + let oracle_price_too_divergent = is_oracle_too_divergent_with_twap_5min( + oracle_price, + perp_market_map + .get_ref(&market_index)? + .amm + .historical_oracle_data + .last_oracle_price_twap_5min, + state + .oracle_guard_rails + .max_oracle_twap_5min_percent_divergence() + .cast()?, + )?; + + validate!(!oracle_price_too_divergent, ErrorCode::PriceBandsBreached)?; + + let user_base_asset_amount = user.perp_positions[position_index] + .base_asset_amount + .unsigned_abs(); + + let margin_ratio = perp_market_map.get_ref(&market_index)?.get_margin_ratio( + user_base_asset_amount.cast()?, + MarginRequirementType::Maintenance, + )?; + + let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; + + let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + + let market = perp_market_map.get_ref(&market_index)?; + let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; + let quote_oracle_price = oracle_map.get_price_data("e_spot_market.oracle)?.price; + let liquidator_fee = market.liquidator_fee; + let if_liquidation_fee = calculate_perp_if_fee( + intermediate_margin_calculation.tracked_market_margin_shortage(margin_shortage)?, + user_base_asset_amount, + margin_ratio_with_buffer, + liquidator_fee, + oracle_price, + quote_oracle_price, + market.if_liquidation_fee, + )?; + let base_asset_amount_to_cover_margin_shortage = standardize_base_asset_amount_ceil( + calculate_base_asset_amount_to_cover_margin_shortage( + margin_shortage, + margin_ratio_with_buffer, + liquidator_fee, + if_liquidation_fee, + oracle_price, + quote_oracle_price, + )?, + market.amm.order_step_size, + )?; + drop(market); + drop(quote_spot_market); + + let max_pct_allowed = calculate_max_pct_to_liquidate( + &user, + margin_shortage, + slot, + initial_pct_to_liquidate, + liquidation_duration, + )?; + let max_base_asset_amount_allowed_to_be_transferred = + base_asset_amount_to_cover_margin_shortage + .cast::()? + .saturating_mul(max_pct_allowed) + .safe_div(LIQUIDATION_PCT_PRECISION)? + .cast::()?; + + if max_base_asset_amount_allowed_to_be_transferred == 0 { + msg!("max_base_asset_amount_allowed_to_be_transferred == 0"); + return Ok(()); + } + + let base_asset_value = + calculate_base_asset_value_with_oracle_price(user_base_asset_amount.cast()?, oracle_price)? + .cast::()?; + + // if position is less than $50, liquidator can liq all of it + let min_base_asset_amount = if base_asset_value > 50 * QUOTE_PRECISION_U64 { + 0_u64 + } else { + user_base_asset_amount + }; + + let base_asset_amount = user_base_asset_amount + .min(max_base_asset_amount_allowed_to_be_transferred.max(min_base_asset_amount)); + let base_asset_amount = standardize_base_asset_amount_ceil( + base_asset_amount, + perp_market_map.get_ref(&market_index)?.amm.order_step_size, + )?; + + let existing_direction = user.perp_positions[position_index].get_direction(); + + let order_params = get_liquidation_order_params( + market_index, + existing_direction, + base_asset_amount, + oracle_price, + liquidator_fee, + )?; + + let order_id = user.next_order_id; + let fill_record_id = perp_market_map.get_ref(&market_index)?.next_fill_record_id; + place_perp_order( + state, + &mut user, + *user_key, + perp_market_map, + spot_market_map, + oracle_map, + &clock, + order_params, + PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + )?; + + drop(user); + drop(liquidator); + + let (fill_base_asset_amount, fill_quote_asset_amount) = fill_perp_order( + order_id, + state, + user_loader, + user_stats_loader, + spot_market_map, + perp_market_map, + oracle_map, + liquidator_loader, + liquidator_stats_loader, + makers_and_referrer, + makers_and_referrer_stats, + None, + &clock, + FillMode::Liquidation, + )?; + + let mut user = load_mut!(user_loader)?; + + if let Ok(order_index) = user.get_order_index(order_id) { + cancel_order( + order_index, + &mut user, + user_key, + perp_market_map, + spot_market_map, + oracle_map, + clock.unix_timestamp, + clock.slot, + OrderActionExplanation::None, + Some(liquidator_key), + 0, + false, + )?; + } + + // no fill + if fill_base_asset_amount == 0 { + return Err(ErrorCode::LiquidationOrderFailedToFill); + } + + let if_fee = -fill_quote_asset_amount + .cast::()? + .safe_mul(if_liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)? + .cast::()?; + + { + let mut market = perp_market_map.get_ref_mut(&market_index)?; + + let user_position = user.get_perp_position_mut(market_index)?; + update_quote_asset_and_break_even_amount(user_position, &mut market, if_fee)?; + + market.amm.total_liquidation_fee = market + .amm + .total_liquidation_fee + .safe_add(if_fee.unsigned_abs().cast()?)?; + } + + let (margin_freed_for_perp_position, margin_calculation_after) = calculate_margin_freed( + &user, + perp_market_map, + spot_market_map, + oracle_map, + liquidation_margin_buffer_ratio, + margin_shortage, + )?; + + margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; + user.increment_margin_freed(margin_freed_for_perp_position)?; + + if margin_calculation_after.meets_margin_requirement() { + user.exit_liquidation(); + } else if is_user_bankrupt(&user) { + user.enter_bankruptcy(); + } + + let user_position_delta = get_position_delta_for_fill( + fill_base_asset_amount, + fill_quote_asset_amount, + existing_direction, + )?; + + emit!(LiquidationRecord { + ts: now, + liquidation_id, + liquidation_type: LiquidationType::LiquidatePerp, + user: *user_key, + liquidator: *liquidator_key, + margin_requirement: margin_calculation.margin_requirement, + total_collateral: margin_calculation.total_collateral, + bankrupt: user.is_bankrupt(), + canceled_order_ids, + margin_freed, + liquidate_perp: LiquidatePerpRecord { + market_index, + oracle_price, + base_asset_amount: user_position_delta.base_asset_amount, + quote_asset_amount: user_position_delta.quote_asset_amount, + lp_shares, + user_order_id: order_id, + liquidator_order_id: 0, + fill_record_id, + liquidator_fee: 0, + if_fee: if_fee.abs().cast()?, + }, + ..LiquidationRecord::default() + }); + + Ok(()) +} + pub fn liquidate_spot( asset_market_index: u16, liability_market_index: u16, @@ -1131,7 +1582,7 @@ pub fn liquidate_spot( )?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -1610,7 +2061,7 @@ pub fn liquidate_borrow_for_perp_pnl( update_quote_asset_amount(user_position, &mut market, -pnl_transfer.cast()?)?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -2105,7 +2556,7 @@ pub fn liquidate_perp_pnl_for_deposit( update_quote_asset_amount(user_position, &mut perp_market, pnl_transfer.cast()?)?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -2546,7 +2997,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, -) -> DriftResult { +) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -2558,7 +3009,9 @@ pub fn calculate_margin_freed( let new_margin_shortage = margin_calculation_after.margin_shortage()?; - initial_margin_shortage + let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) - .cast::() + .cast::()?; + + Ok((margin_freed, margin_calculation_after)) } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 39b4c2365..936b76ed3 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2199,6 +2199,802 @@ pub mod liquidate_perp { } } +pub mod liquidate_perp_with_fill { + + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::prelude::AccountLoader; + use anchor_lang::Owner; + use solana_program::clock::Clock; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::liquidate_perp_with_fill; + use crate::controller::position::PositionDirection; + use crate::create_anchor_account_info; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, BASE_PRECISION_U64, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_PCT_PRECISION, PEG_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{ + Order, OrderStatus, OrderType, PerpPosition, SpotPosition, User, UserStats, + }; + use crate::state::user_map::{UserMap, UserStatsMap}; + use crate::test_utils::*; + use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_liquidate_perp_with_fill_long() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let maker_key = Pubkey::new_unique(); + let maker_authority = Pubkey::new_unique(); + let mut maker = User { + authority: maker_authority, + orders: get_orders(Order { + status: OrderStatus::Open, + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64 / 2, + price: 100 * PRICE_PRECISION_U64, + slot: slot - 1, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64 / 2, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + create_anchor_account_info!(maker, &maker_key, User, maker_account_info); + let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + + let mut maker_stats = UserStats { + authority: maker_authority, + ..UserStats::default() + }; + create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); + let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &makers_and_referrers, + &maker_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, 640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -64396000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let maker = makers_and_referrers.get_ref(&maker_key).unwrap(); + assert_eq!(maker.perp_positions[0].base_asset_amount, 360000000); + assert_eq!(maker.perp_positions[0].quote_asset_amount, -35992800); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3600); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 360000); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_short() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 100 * QUOTE_PRECISION_I64, + quote_entry_amount: 100 * QUOTE_PRECISION_I64, + quote_break_even_amount: 100 * QUOTE_PRECISION_I64, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let maker_key = Pubkey::new_unique(); + let maker_authority = Pubkey::new_unique(); + let mut maker = User { + authority: maker_authority, + orders: get_orders(Order { + status: OrderStatus::Open, + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64 / 2, + price: 100 * PRICE_PRECISION_U64, + slot: slot - 1, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64 / 2, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + create_anchor_account_info!(maker, &maker_key, User, maker_account_info); + let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + + let mut maker_stats = UserStats { + authority: maker_authority, + ..UserStats::default() + }; + create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); + let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &makers_and_referrers, + &maker_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, -640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, 63604000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let maker = makers_and_referrers.get_ref(&maker_key).unwrap(); + assert_eq!(maker.perp_positions[0].base_asset_amount, -360000000); + assert_eq!(maker.perp_positions[0].quote_asset_amount, 36007200); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3600); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 360000); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_long_with_amm() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + oracle_price.curr_slot = slot; + oracle_price.valid_slot = slot; + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + market.amm.max_fill_reserve_fraction = 1; + market.amm.max_base_asset_reserve = u64::MAX as u128; + market.amm.min_base_asset_reserve = 0; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &UserMap::empty(), + &UserStatsMap::empty(), + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, 640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -64523715); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3587); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 358708); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_short_with_amm() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + oracle_price.curr_slot = slot; + oracle_price.valid_slot = slot; + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 10000000, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData::default_price(oracle_price.agg.price), + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + if_liquidation_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + market.amm.max_fill_reserve_fraction = 1; + market.amm.max_base_asset_reserve = u64::MAX as u128; + market.amm.min_base_asset_reserve = 0; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let perp_market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + let mut user = User { + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: 100 * QUOTE_PRECISION_I64, + quote_entry_amount: 100 * QUOTE_PRECISION_I64, + quote_break_even_amount: 100 * QUOTE_PRECISION_I64, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &UserMap::empty(), + &UserStatsMap::empty(), + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, -640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, 63472500); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3613); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 361300); + } +} + pub mod liquidate_spot { use crate::state::state::State; use std::ops::Deref; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index a58735d67..b97c9deaa 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -108,13 +108,15 @@ pub fn place_perp_order( let now = clock.unix_timestamp; let slot = clock.slot; - validate_user_not_being_liquidated( - user, - perp_market_map, - spot_market_map, - oracle_map, - state.liquidation_margin_buffer_ratio, - )?; + if !options.is_liquidation() { + validate_user_not_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + state.liquidation_margin_buffer_ratio, + )?; + } validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -211,7 +213,10 @@ pub fn place_perp_order( let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; // updates auction params for crossing limit orders w/out auction duration - params.update_perp_auction_params(market, oracle_price_data.price)?; + // dont modify if it's a liquidation + if !options.is_liquidation() { + params.update_perp_auction_params(market, oracle_price_data.price)?; + } let (auction_start_price, auction_end_price, auction_duration) = get_auction_params( ¶ms, @@ -314,7 +319,7 @@ pub fn place_perp_order( options.update_risk_increasing(risk_increasing); // when orders are placed in bulk, only need to check margin on last place - if options.enforce_margin_check { + if options.enforce_margin_check && !options.is_liquidation() { meets_place_order_margin_requirement( user, perp_market_map, @@ -884,7 +889,7 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, -) -> DriftResult { +) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -948,20 +953,22 @@ pub fn fill_perp_order( if user.is_bankrupt() { msg!("user is bankrupt"); - return Ok(0); + return Ok((0, 0)); } - match validate_user_not_being_liquidated( - user, - perp_market_map, - spot_market_map, - oracle_map, - state.liquidation_margin_buffer_ratio, - ) { - Ok(_) => {} - Err(_) => { - msg!("user is being liquidated"); - return Ok(0); + if !fill_mode.is_liquidation() { + match validate_user_not_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + state.liquidation_margin_buffer_ratio, + ) { + Ok(_) => {} + Err(_) => { + msg!("user is being liquidated"); + return Ok((0, 0)); + } } } @@ -1042,13 +1049,18 @@ pub fn fill_perp_order( slot, )?; - let referrer_info = get_referrer_info( - user_stats, - &user_key, - makers_and_referrer, - makers_and_referrer_stats, - slot, - )?; + // no referrer bonus for liquidations + let referrer_info = if !fill_mode.is_liquidation() { + get_referrer_info( + user_stats, + &user_key, + makers_and_referrer, + makers_and_referrer_stats, + slot, + )? + } else { + None + }; let oracle_too_divergent_with_twap_5min = is_oracle_too_divergent_with_twap_5min( oracle_price, @@ -1064,10 +1076,17 @@ pub fn fill_perp_order( if let Some(filler) = filler.as_deref_mut() { filler.update_last_active_slot(slot); } - return Ok(0); + return Ok((0, 0)); } - validate_perp_fill_possible(state, user, order_index, slot, makers_and_referrer.0.len())?; + validate_perp_fill_possible( + state, + user, + order_index, + slot, + makers_and_referrer.0.len(), + fill_mode, + )?; let should_expire_order = should_expire_order_before_fill(user, order_index, now)?; @@ -1116,7 +1135,7 @@ pub fn fill_perp_order( false, )?; - return Ok(0); + return Ok((0, 0)); } let (base_asset_amount, quote_asset_amount) = fulfill_perp_order( @@ -1201,7 +1220,7 @@ pub fn fill_perp_order( } if base_asset_amount == 0 { - return Ok(0); + return Ok((base_asset_amount, quote_asset_amount)); } { @@ -1239,7 +1258,7 @@ pub fn fill_perp_order( user.update_last_active_slot(slot); - Ok(base_asset_amount) + Ok((base_asset_amount, quote_asset_amount)) } pub fn validate_market_within_price_band( @@ -1555,6 +1574,7 @@ fn fulfill_perp_order( amm_is_available, slot, min_auction_duration, + fill_mode, )? }; @@ -1621,6 +1641,7 @@ fn fulfill_perp_order( None, *maker_price, AMMLiquiditySplit::Shared, + fill_mode.is_liquidation(), )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1663,6 +1684,7 @@ fn fulfill_perp_order( slot, fee_structure, oracle_map, + fill_mode.is_liquidation(), )?; if maker_fill_base_asset_amount != 0 { @@ -1703,43 +1725,45 @@ fn fulfill_perp_order( base_asset_amount )?; - // if the maker is long, the user sold so - let taker_base_asset_amount_delta = if maker_direction == PositionDirection::Long { - base_asset_amount as i64 - } else { - -(base_asset_amount as i64) - }; + if !fill_mode.is_liquidation() { + // if the maker is long, the user sold so + let taker_base_asset_amount_delta = if maker_direction == PositionDirection::Long { + base_asset_amount as i64 + } else { + -(base_asset_amount as i64) + }; - let taker_margin_calculation = - calculate_margin_requirement_and_total_collateral_and_liability_info( + let taker_margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::standard(if user_order_position_decreasing { + MarginRequirementType::Maintenance + } else { + MarginRequirementType::Fill + }) + .fuel_perp_delta(market_index, taker_base_asset_amount_delta) + .fuel_numerator(user, now), + )?; + + user_stats.update_fuel_bonus( user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::standard(if user_order_position_decreasing { - MarginRequirementType::Maintenance - } else { - MarginRequirementType::Fill - }) - .fuel_perp_delta(market_index, taker_base_asset_amount_delta) - .fuel_numerator(user, now), + taker_margin_calculation.fuel_deposits, + taker_margin_calculation.fuel_borrows, + taker_margin_calculation.fuel_positions, + now, )?; - user_stats.update_fuel_bonus( - user, - taker_margin_calculation.fuel_deposits, - taker_margin_calculation.fuel_borrows, - taker_margin_calculation.fuel_positions, - now, - )?; - - if !taker_margin_calculation.meets_margin_requirement() { - msg!( - "taker breached fill requirements (margin requirement {}) (total_collateral {})", - taker_margin_calculation.margin_requirement, - taker_margin_calculation.total_collateral - ); - return Err(ErrorCode::InsufficientCollateral); + if !taker_margin_calculation.meets_margin_requirement() { + msg!( + "taker breached fill requirements (margin requirement {}) (total_collateral {})", + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral + ); + return Err(ErrorCode::InsufficientCollateral); + } } for (maker_key, maker_base_asset_amount_filled) in maker_fills { @@ -1875,6 +1899,7 @@ pub fn fulfill_perp_order_with_amm( override_base_asset_amount: Option, override_fill_price: Option, liquidity_split: AMMLiquiditySplit, + is_liquidation: bool, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2111,6 +2136,7 @@ pub fn fulfill_perp_order_with_amm( let fill_record_id = get_then_update_id!(market, next_fill_record_id); let order_action_explanation = match (override_base_asset_amount, override_fill_price) { + _ if is_liquidation => OrderActionExplanation::Liquidation, (Some(_), Some(_)) => liquidity_split.get_order_action_explanation(), _ => OrderActionExplanation::OrderFilledWithAMM, }; @@ -2204,6 +2230,7 @@ pub fn fulfill_perp_order_with_match( slot: u64, fee_structure: &FeeStructure, oracle_map: &mut OracleMap, + is_liquidation: bool, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2317,6 +2344,7 @@ pub fn fulfill_perp_order_with_match( Some(jit_base_asset_amount), Some(maker_price), // match the makers price amm_liquidity_split, + is_liquidation, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2526,7 +2554,9 @@ pub fn fulfill_perp_order_with_match( )?; let fill_record_id = get_then_update_id!(market, next_fill_record_id); - let order_action_explanation = if maker.orders[maker_order_index].is_jit_maker() { + let order_action_explanation = if is_liquidation { + OrderActionExplanation::Liquidation + } else if maker.orders[maker_order_index].is_jit_maker() { OrderActionExplanation::OrderFilledWithMatchJit } else { OrderActionExplanation::OrderFilledWithMatch diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 1d7094b5f..7113f67f7 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -130,6 +130,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -249,6 +250,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -368,6 +370,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -487,6 +490,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -606,6 +610,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -691,6 +696,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -777,6 +783,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -863,6 +870,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -949,6 +957,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1055,6 +1064,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1164,6 +1174,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1280,6 +1291,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1397,6 +1409,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1538,6 +1551,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1654,6 +1668,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1772,6 +1787,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -1912,6 +1928,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2045,6 +2062,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2184,6 +2202,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2311,6 +2330,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -2437,6 +2457,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -5144,7 +5165,7 @@ pub mod fill_order { ..State::default() }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, @@ -5352,7 +5373,7 @@ pub mod fill_order { ..State::default() }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, @@ -5481,7 +5502,7 @@ pub mod fill_order { unix_timestamp: 11, }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 4ad92b081..62e8dbea1 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -571,6 +571,8 @@ pub enum ErrorCode { InvalidOpenbookV2Market, #[msg("Non zero transfer fee")] NonZeroTransferFee, + #[msg("Liquidation order failed to fill")] + LiquidationOrderFailedToFill, } #[macro_export] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index e5748cefa..abf5bc682 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -751,6 +751,60 @@ pub fn handle_liquidate_perp<'c: 'info, 'info>( Ok(()) } +#[access_control( +liq_not_paused(&ctx.accounts.state) +)] +pub fn handle_liquidate_perp_with_fill<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LiquidatePerp<'info>>, + market_index: u16, +) -> Result<()> { + let clock = Clock::get()?; + let state = &ctx.accounts.state; + + let user_key = ctx.accounts.user.key(); + let liquidator_key = ctx.accounts.liquidator.key(); + + validate!( + user_key != liquidator_key, + ErrorCode::UserCantLiquidateThemself + )?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &get_writable_perp_market_set(market_index), + &MarketSet::new(), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let (makers_and_referrer, makers_and_referrer_stats) = + load_user_maps(remaining_accounts_iter, true)?; + + controller::liquidation::liquidate_perp_with_fill( + market_index, + &ctx.accounts.user, + &user_key, + &ctx.accounts.user_stats, + &ctx.accounts.liquidator, + &liquidator_key, + &ctx.accounts.liquidator_stats, + &makers_and_referrer, + &makers_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + state, + )?; + + Ok(()) +} + #[access_control( liq_not_paused(&ctx.accounts.state) )] diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 0320eb6ce..10a3bac39 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -412,6 +412,13 @@ pub mod drift { ) } + pub fn liquidate_perp_with_fill<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LiquidatePerp<'info>>, + market_index: u16, + ) -> Result<()> { + handle_liquidate_perp_with_fill(ctx, market_index) + } + pub fn liquidate_spot<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LiquidateSpot<'info>>, asset_market_index: u16, diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index c24796d00..c9c5bb46a 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -8,6 +8,7 @@ use crate::state::oracle::OraclePriceData; use crate::state::user::{Order, OrderType}; use solana_program::msg; +use crate::state::fill_mode::FillMode; use crate::state::perp_market::PerpMarket; use crate::OrderParams; use std::cmp::min; @@ -207,8 +208,9 @@ pub fn is_amm_available_liquidity_source( order: &Order, min_auction_duration: u8, slot: u64, + fill_mode: FillMode, ) -> DriftResult { - is_auction_complete(order.slot, min_auction_duration, slot) + Ok(is_auction_complete(order.slot, min_auction_duration, slot)? || fill_mode.is_liquidation()) } pub fn calculate_auction_params_for_trigger_order( diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index 985b7c984..337aeedcd 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -4,6 +4,7 @@ use crate::math::auction::is_amm_available_liquidity_source; use crate::math::casting::Cast; use crate::math::matching::do_orders_cross; use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; use crate::state::perp_market::AMM; use crate::state::user::Order; @@ -22,6 +23,7 @@ pub fn determine_perp_fulfillment_methods( amm_is_available: bool, slot: u64, min_auction_duration: u8, + fill_mode: FillMode, ) -> DriftResult> { if order.post_only { return determine_perp_fulfillment_methods_for_maker( @@ -33,6 +35,7 @@ pub fn determine_perp_fulfillment_methods( amm_is_available, slot, min_auction_duration, + fill_mode, ); } @@ -40,7 +43,7 @@ pub fn determine_perp_fulfillment_methods( let can_fill_with_amm = amm_is_available && valid_oracle_price.is_some() - && is_amm_available_liquidity_source(order, min_auction_duration, slot)?; + && is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?; let maker_direction = order.direction.opposite(); @@ -104,12 +107,13 @@ fn determine_perp_fulfillment_methods_for_maker( amm_is_available: bool, slot: u64, min_auction_duration: u8, + fill_mode: FillMode, ) -> DriftResult> { let maker_direction = order.direction; let can_fill_with_amm = amm_is_available && valid_oracle_price.is_some() - && is_amm_available_liquidity_source(order, min_auction_duration, slot)?; + && is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?; if !can_fill_with_amm { return Ok(vec![]); diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index 436053da1..a70e0de3e 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -5,6 +5,7 @@ mod determine_perp_fulfillment_methods { PRICE_PRECISION_U64, }; use crate::math::fulfillment::determine_perp_fulfillment_methods; + use crate::state::fill_mode::FillMode; use crate::state::fulfillment::PerpFulfillmentMethod; use crate::state::oracle::HistoricalOracleData; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -65,6 +66,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -125,6 +127,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -197,6 +200,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -267,6 +271,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -342,6 +347,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -412,6 +418,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -484,6 +491,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -555,6 +563,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -625,6 +634,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -697,6 +707,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -760,6 +771,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -821,6 +833,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -882,6 +895,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 99ef8ad40..26a15572f 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -17,8 +17,8 @@ use crate::state::perp_market::PerpMarket; use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; -use crate::state::user::User; -use crate::{validate, BASE_PRECISION}; +use crate::state::user::{OrderType, User}; +use crate::{validate, MarketType, OrderParams, PositionDirection, BASE_PRECISION}; use solana_program::msg; #[cfg(test)] @@ -445,3 +445,44 @@ pub fn calculate_spot_if_fee( Ok(max_if_fee.min(implied_if_fee)) } + +pub fn get_liquidation_order_params( + market_index: u16, + existing_direction: PositionDirection, + base_asset_amount: u64, + oracle_price: i64, + liquidation_fee: u32, +) -> DriftResult { + let direction = existing_direction.opposite(); + + let oracle_price_u128 = oracle_price.abs().cast::()?; + let limit_price = match direction { + PositionDirection::Long => oracle_price_u128 + .safe_add( + oracle_price_u128 + .safe_mul(liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, + )? + .cast::()?, + PositionDirection::Short => oracle_price_u128 + .safe_sub( + oracle_price_u128 + .safe_mul(liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, + )? + .cast::()?, + }; + + let order_params = OrderParams { + market_index, + direction, + price: limit_price, + order_type: OrderType::Limit, + market_type: MarketType::Perp, + base_asset_amount, + reduce_only: true, + ..OrderParams::default() + }; + + Ok(order_params) +} diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 87fadc736..3966ac6d7 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -9,6 +9,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::auction::is_amm_available_liquidity_source; use crate::math::casting::Cast; +use crate::state::fill_mode::FillMode; use crate::{ load, math, FeeTier, State, BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, OPEN_ORDER_MARGIN_REQUIREMENT, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_U64, @@ -324,11 +325,13 @@ pub fn validate_perp_fill_possible( order_index: usize, slot: u64, num_makers: usize, + fill_mode: FillMode, ) -> DriftResult { let amm_available = is_amm_available_liquidity_source( &user.orders[order_index], state.min_perp_auction_duration, slot, + fill_mode, )?; if !amm_available && num_makers == 0 && user.orders[order_index].is_limit_order() { diff --git a/programs/drift/src/state/fill_mode.rs b/programs/drift/src/state/fill_mode.rs index 9bb7d09bb..94edc21b6 100644 --- a/programs/drift/src/state/fill_mode.rs +++ b/programs/drift/src/state/fill_mode.rs @@ -12,6 +12,7 @@ pub enum FillMode { Fill, PlaceAndMake, PlaceAndTake, + Liquidation, } impl FillMode { @@ -23,7 +24,7 @@ impl FillMode { tick_size: u64, ) -> DriftResult> { match self { - FillMode::Fill | FillMode::PlaceAndMake => { + FillMode::Fill | FillMode::PlaceAndMake | FillMode::Liquidation => { order.get_limit_price(valid_oracle_price, None, slot, tick_size) } FillMode::PlaceAndTake => { @@ -41,4 +42,8 @@ impl FillMode { } } } + + pub fn is_liquidation(&self) -> bool { + self == &FillMode::Liquidation + } } diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 82c858a3f..055eb4769 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -706,4 +706,8 @@ impl PlaceOrderOptions { self.explanation = explanation; self } + + pub fn is_liquidation(&self) -> bool { + self.explanation == OrderActionExplanation::Liquidation + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 5849168b1..c8db47a0f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -5990,6 +5990,81 @@ export class DriftClient { ); } + public async liquidatePerpWithFill( + userAccountPublicKey: PublicKey, + userAccount: UserAccount, + marketIndex: number, + makerInfos: MakerInfo[], + txParams?: TxParams, + liquidatorSubAccountId?: number + ): Promise { + const { txSig, slot } = await this.sendTransaction( + await this.buildTransaction( + await this.getLiquidatePerpWithFillIx( + userAccountPublicKey, + userAccount, + marketIndex, + makerInfos, + liquidatorSubAccountId + ), + txParams + ), + [], + this.opts + ); + this.perpMarketLastSlotCache.set(marketIndex, slot); + return txSig; + } + + public async getLiquidatePerpWithFillIx( + userAccountPublicKey: PublicKey, + userAccount: UserAccount, + marketIndex: number, + makerInfos: MakerInfo[], + liquidatorSubAccountId?: number + ): Promise { + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + userAccount.authority + ); + + const liquidator = await this.getUserAccountPublicKey( + liquidatorSubAccountId + ); + const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], + useMarketLastSlotCache: true, + writablePerpMarketIndexes: [marketIndex], + }); + + for (const makerInfo of makerInfos) { + remainingAccounts.push({ + pubkey: makerInfo.maker, + isSigner: false, + isWritable: true, + }); + remainingAccounts.push({ + pubkey: makerInfo.makerStats, + isSigner: false, + isWritable: true, + }); + } + + return await this.program.instruction.liquidatePerpWithFill(marketIndex, { + accounts: { + state: await this.getStatePublicKey(), + authority: this.wallet.publicKey, + user: userAccountPublicKey, + userStats: userStatsPublicKey, + liquidator, + liquidatorStats: liquidatorStatsPublicKey, + }, + remainingAccounts: remainingAccounts, + }); + } + public async liquidateSpot( userAccountPublicKey: PublicKey, userAccount: UserAccount, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 5fd6b97cb..3044050e8 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1675,6 +1675,47 @@ } ] }, + { + "name": "liquidatePerpWithFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, { "name": "liquidateSpot", "accounts": [ @@ -3362,27 +3403,6 @@ } ] }, - { - "name": "initializePredictionMarket", - "accounts": [ - { - "name": "admin", - "isMut": false, - "isSigner": true - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "perpMarket", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "deleteInitializedPerpMarket", "accounts": [ @@ -9858,6 +9878,9 @@ }, { "name": "PlaceAndTake" + }, + { + "name": "Liquidation" } ] } @@ -10125,9 +10148,6 @@ }, { "name": "Future" - }, - { - "name": "Prediction" } ] } @@ -12797,9 +12817,14 @@ "code": 6282, "name": "NonZeroTransferFee", "msg": "Non zero transfer fee" + }, + { + "code": 6283, + "name": "LiquidationOrderFailedToFill", + "msg": "Liquidation order failed to fill" } ], "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index eb65c99dc..d68ff8b0e 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -30,6 +30,7 @@ test_files=( liquidateMaxLps.ts liquidatePerp.ts liquidatePerpAndLp.ts + liquidatePerpWithFill.ts liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 16b84fcfb..ea34682ad 100644 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,7 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(openbookTest.ts) +test_files=(liquidatePerpWithFill.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/liquidatePerpWithFill.ts b/tests/liquidatePerpWithFill.ts new file mode 100644 index 000000000..c67b115b2 --- /dev/null +++ b/tests/liquidatePerpWithFill.ts @@ -0,0 +1,336 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { + BASE_PRECISION, + BN, + EventSubscriber, + isVariant, + LIQUIDATION_PCT_PRECISION, + OracleGuardRails, + OracleSource, + PositionDirection, + PRICE_PRECISION, + QUOTE_PRECISION, + TestClient, + Wallet, +} from '../sdk/src'; +import { assert } from 'chai'; + +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPriceNoProgram, +} from './testHelpers'; +import { OrderType, PERCENTAGE_PRECISION, PerpOperation } from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('liquidate perp (no open orders)', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let usdcMint; + let userUSDCAccount; + + const liquidatorKeyPair = new Keypair(); + let liquidatorUSDCAccount: Keypair; + let liquidatorDriftClient: TestClient; + + let makerDriftClient: TestClient; + let makerUSDCAccount: PublicKey; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + const makerUsdcAmount = new BN(1000 * 10 ** 6); + + let oracle: PublicKey; + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + //@ts-ignore + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + oracle = await mockOracleNoProgram(bankrunContextWrapper, 1); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOraclePercentDivergence: PERCENTAGE_PRECISION.muln(100), + oracleTwap5MinPercentDivergence: PERCENTAGE_PRECISION.muln(100), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(11), // allow 11x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + 0, + + oracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + await driftClient.openPosition( + PositionDirection.LONG, + new BN(175).mul(BASE_PRECISION).div(new BN(10)), // 17.5 SOL + 0, + new BN(0) + ); + + bankrunContextWrapper.fundKeypair(liquidatorKeyPair, LAMPORTS_PER_SOL); + liquidatorUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper, + liquidatorKeyPair.publicKey + ); + liquidatorDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new Wallet(liquidatorKeyPair), + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await liquidatorDriftClient.subscribe(); + + await liquidatorDriftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + liquidatorUSDCAccount.publicKey + ); + + [makerDriftClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + makerUsdcAmount, + [0], + [0], + [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await makerDriftClient.deposit(makerUsdcAmount, 0, makerUSDCAccount); + }); + + after(async () => { + await driftClient.unsubscribe(); + await liquidatorDriftClient.unsubscribe(); + await makerDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('liquidate', async () => { + await setFeedPriceNoProgram(bankrunContextWrapper, 0.1, oracle); + await driftClient.updatePerpMarketPausedOperations( + 0, + PerpOperation.AMM_FILL + ); + + try { + const failToPlaceTxSig = await driftClient.placePerpOrder({ + direction: PositionDirection.SHORT, + baseAssetAmount: BASE_PRECISION, + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + reduceOnly: true, + marketIndex: 0, + }); + bankrunContextWrapper.connection.printTxLogs(failToPlaceTxSig); + throw new Error('Expected placePerpOrder to throw an error'); + } catch (error) { + if ( + error.message !== + 'Error processing Instruction 1: custom program error: 0x1773' + ) { + throw new Error(`Unexpected error message: ${error.message}`); + } + } + + await makerDriftClient.placePerpOrder({ + direction: PositionDirection.LONG, + baseAssetAmount: new BN(175).mul(BASE_PRECISION), + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + marketIndex: 0, + }); + + const makerInfos = [ + { + maker: await makerDriftClient.getUserAccountPublicKey(), + makerStats: makerDriftClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerDriftClient.getUserAccount(), + }, + ]; + + const txSig = await liquidatorDriftClient.liquidatePerpWithFill( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + makerInfos + ); + + bankrunContextWrapper.connection.printTxLogs(txSig); + + for (let i = 0; i < 32; i++) { + assert(isVariant(driftClient.getUserAccount().orders[i].status, 'init')); + } + + assert( + liquidatorDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(175)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(0)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-15769403)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(17500000000)) + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-1749650)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + await makerDriftClient.liquidatePerpPnlForDeposit( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + 0, + QUOTE_PRECISION.muln(20) + ); + + await makerDriftClient.resolvePerpBankruptcy( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + }); +}); From 083b87da59002f94afa0309b8f8eec2f3b6e9ed6 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:23:34 +0000 Subject: [PATCH 16/17] sdk: release v2.87.0-beta.8 --- sdk/VERSION | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/VERSION b/sdk/VERSION index 84e84aca3..3f2144f8f 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.87.0-beta.7 \ No newline at end of file +2.87.0-beta.8 \ No newline at end of file diff --git a/sdk/package.json b/sdk/package.json index 02c2794e3..a338fabe8 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@drift-labs/sdk", - "version": "2.87.0-beta.7", + "version": "2.87.0-beta.8", "main": "lib/index.js", "types": "lib/index.d.ts", "author": "crispheaney", From 24239ffa6140e6853dc23e3b5bf2d458928f4b89 Mon Sep 17 00:00:00 2001 From: frank <98238480+soundsonacid@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:27:51 -0500 Subject: [PATCH 17/17] sdk: remaining openbook stuff (#1165) * add openbookv2 subscriber * remove weird artifacts * revert accidental prettiers * revert prettiers * pr comment * prettify fix * remaining openbook sdk stuff * revert prettier * fxi --- sdk/src/config.ts | 3 +++ sdk/src/constants/spotMarkets.ts | 1 + sdk/src/driftClient.ts | 29 ++++++++++++++++-------- sdk/src/openbook/openbookV2Subscriber.ts | 10 ++++---- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/sdk/src/config.ts b/sdk/src/config.ts index 1b539f7b5..137d0e1ff 100644 --- a/sdk/src/config.ts +++ b/sdk/src/config.ts @@ -23,6 +23,7 @@ type DriftConfig = { USDC_MINT_ADDRESS: string; SERUM_V3: string; PHOENIX: string; + OPENBOOK: string; V2_ALPHA_TICKET_MINT_ADDRESS: string; PERP_MARKETS: PerpMarketConfig[]; SPOT_MARKETS: SpotMarketConfig[]; @@ -46,6 +47,7 @@ export const configs: { [key in DriftEnv]: DriftConfig } = { USDC_MINT_ADDRESS: '8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2', SERUM_V3: 'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY', PHOENIX: 'PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY', + OPENBOOK: 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb', V2_ALPHA_TICKET_MINT_ADDRESS: 'DeEiGWfCMP9psnLGkxGrBBMEAW5Jv8bBGMN8DCtFRCyB', PERP_MARKETS: DevnetPerpMarkets, @@ -61,6 +63,7 @@ export const configs: { [key in DriftEnv]: DriftConfig } = { USDC_MINT_ADDRESS: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', SERUM_V3: 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX', PHOENIX: 'PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY', + OPENBOOK: 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb', V2_ALPHA_TICKET_MINT_ADDRESS: 'Cmvhycb6LQvvzaShGw4iDHRLzeSSryioAsU98DSSkMNa', PERP_MARKETS: MainnetPerpMarkets, diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index e4a97f75c..82f7567a5 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -20,6 +20,7 @@ export type SpotMarketConfig = { precisionExp: BN; serumMarket?: PublicKey; phoenixMarket?: PublicKey; + openbookMarket?: PublicKey; launchTs?: number; pythFeedId?: string; }; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c8db47a0f..45dd12a4d 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -14,6 +14,7 @@ import { createInitializeAccountInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { StateAccount, @@ -119,7 +120,6 @@ import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; -import { getTokenProgramForSpotMarket } from './addresses/pda'; import { JupiterClient, QuoteResponse, @@ -1973,7 +1973,7 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); return await this.program.instruction.deposit( marketIndex, amount, @@ -2060,6 +2060,15 @@ export class DriftClient { return result; } + public getTokenProgramForSpotMarket( + spotMarketAccount: SpotMarketAccount + ): PublicKey { + if (spotMarketAccount.tokenProgram === 1) { + return TOKEN_2022_PROGRAM_ID; + } + return TOKEN_PROGRAM_ID; + } + public addTokenMintToRemainingAccounts( spotMarketAccount: SpotMarketAccount, remainingAccounts: AccountMeta[] @@ -2486,7 +2495,7 @@ export class DriftClient { const spotMarketAccount = this.getSpotMarketAccount(marketIndex); this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); return await this.program.instruction.withdraw( marketIndex, @@ -4250,7 +4259,7 @@ export class DriftClient { outAssociatedTokenAccount ); if (!accountInfo) { - const tokenProgram = getTokenProgramForSpotMarket(outMarket); + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( @@ -4274,7 +4283,7 @@ export class DriftClient { inAssociatedTokenAccount ); if (!accountInfo) { - const tokenProgram = getTokenProgramForSpotMarket(outMarket); + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); preInstructions.push( this.createAssociatedTokenAccountIdempotentInstruction( @@ -4497,8 +4506,8 @@ export class DriftClient { const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); - const outTokenProgram = getTokenProgramForSpotMarket(outSpotMarket); - const inTokenProgram = getTokenProgramForSpotMarket(inSpotMarket); + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); if (!outTokenProgram.equals(inTokenProgram)) { remainingAccounts.push({ @@ -6651,7 +6660,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); - const tokenProgram = getTokenProgramForSpotMarket(spotMarket); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); const ix = this.program.instruction.addInsuranceFundStake( marketIndex, amount, @@ -6891,7 +6900,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); - const tokenProgram = getTokenProgramForSpotMarket(spotMarketAccount); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); const removeStakeIx = await this.program.instruction.removeInsuranceFundStake(marketIndex, { accounts: { @@ -7022,7 +7031,7 @@ export class DriftClient { const remainingAccounts = []; this.addTokenMintToRemainingAccounts(spotMarket, remainingAccounts); - const tokenProgram = getTokenProgramForSpotMarket(spotMarket); + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarket); const ix = await this.program.instruction.depositIntoSpotMarketRevenuePool( amount, { diff --git a/sdk/src/openbook/openbookV2Subscriber.ts b/sdk/src/openbook/openbookV2Subscriber.ts index 3a439b417..9f1bb185d 100644 --- a/sdk/src/openbook/openbookV2Subscriber.ts +++ b/sdk/src/openbook/openbookV2Subscriber.ts @@ -95,9 +95,8 @@ export class OpenbookV2Subscriber implements L2OrderBookGenerator { this.subscribed = true; } - public async getBestBid(): Promise { - const bids = await this.market.loadBids(); - const bestBid = bids.best(); + public getBestBid(): BN | undefined { + const bestBid = this.market.bids.best(); if (bestBid === undefined) { return undefined; @@ -106,9 +105,8 @@ export class OpenbookV2Subscriber implements L2OrderBookGenerator { return new BN(Math.floor(bestBid.price * PRICE_PRECISION.toNumber())); } - public async getBestAsk(): Promise { - const asks = await this.market.loadAsks(); - const bestAsk = asks.best(); + public getBestAsk(): BN | undefined { + const bestAsk = this.market.asks.best(); if (bestAsk === undefined) { return undefined;