diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index a6fd80dc..bc2e0913 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -23,15 +23,15 @@ thiserror = "1.0" uint = "0.8" [dev-dependencies] +assert_matches = "1.4.0" base64 = "0.13" log = "0.4.8" -assert_matches = "1.4.0" proptest = "0.10" solana-program-test = "1.5.0" solana-sdk = "1.5.0" -tokio = { version = "0.3", features = ["macros"]} serde = "1.0" serde_yaml = "0.8" +tokio = { version = "0.3", features = ["macros"]} [lib] crate-type = ["cdylib", "lib"] diff --git a/token-lending/program/src/dex_market.rs b/token-lending/program/src/dex_market.rs new file mode 100644 index 00000000..ef7cbfcb --- /dev/null +++ b/token-lending/program/src/dex_market.rs @@ -0,0 +1,314 @@ +//! Dex market used for simulating trades + +use crate::{error::LendingError, math::Decimal}; +use arrayref::{array_refs, mut_array_refs}; +use serum_dex::critbit::Slab; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use std::{cell::RefMut, collections::VecDeque, convert::TryFrom}; + +/// Side of the dex market order book +#[derive(Clone, Copy, PartialEq)] +enum Side { + Bid, + Ask, +} + +/// Market currency +#[derive(Clone, Copy, PartialEq)] +enum Currency { + Base, + Quote, +} + +impl Currency { + fn opposite(&self) -> Self { + match self { + Currency::Base => Currency::Quote, + Currency::Quote => Currency::Base, + } + } +} + +/// Trade action for trade simulator +#[derive(PartialEq)] +pub enum TradeAction { + /// Sell tokens + Sell, + /// Buy tokens + Buy, +} + +/// Dex market order +struct Order { + price: u64, + quantity: u64, +} + +/// Dex market orders used for simulating trades +enum Orders<'a> { + DexMarket(DexMarketOrders<'a>), + Cached(VecDeque), + None, +} + +impl Orders<'_> { + // BPF rust version does not support matches! + #[allow(clippy::match_like_matches_macro)] + fn is_cacheable(&self) -> bool { + match &self { + Self::DexMarket(_) => true, + _ => false, + } + } +} + +impl Iterator for Orders<'_> { + type Item = Order; + + fn next(&mut self) -> Option { + match self { + Orders::DexMarket(dex_market_orders) => { + let leaf_node = match dex_market_orders.side { + Side::Bid => dex_market_orders + .heap + .as_mut() + .and_then(|heap| heap.remove_max()), + Side::Ask => dex_market_orders + .heap + .as_mut() + .and_then(|heap| heap.remove_min()), + }?; + + Some(Order { + price: leaf_node.price().get(), + quantity: leaf_node.quantity(), + }) + } + Orders::Cached(orders) => orders.pop_front(), + _ => None, + } + } +} + +/// Trade simulator +pub struct TradeSimulator<'a> { + dex_market: DexMarket, + orders: Orders<'a>, + orders_side: Side, + quote_token_mint: &'a Pubkey, +} + +impl<'a> TradeSimulator<'a> { + /// Create a new TradeSimulator + pub fn new( + dex_market_info: &AccountInfo, + dex_market_orders: &AccountInfo, + memory: &'a AccountInfo, + quote_token_mint: &'a Pubkey, + ) -> Result { + let dex_market = DexMarket::new(dex_market_info); + let dex_market_orders = DexMarketOrders::new(&dex_market, dex_market_orders, memory)?; + let orders_side = dex_market_orders.side; + + Ok(Self { + dex_market, + orders: Orders::DexMarket(dex_market_orders), + orders_side, + quote_token_mint, + }) + } + + /// Simulate a trade + pub fn simulate_trade( + &mut self, + action: TradeAction, + quantity: Decimal, + token_mint: &Pubkey, + cache_orders: bool, + ) -> Result { + let currency = if token_mint == self.quote_token_mint { + Currency::Quote + } else { + Currency::Base + }; + + let order_book_side = match (action, currency) { + (TradeAction::Buy, Currency::Base) => Side::Ask, + (TradeAction::Sell, Currency::Quote) => Side::Ask, + (TradeAction::Buy, Currency::Quote) => Side::Bid, + (TradeAction::Sell, Currency::Base) => Side::Bid, + }; + + if order_book_side != self.orders_side { + return Err(LendingError::DexInvalidOrderBookSide.into()); + } + + let input_quantity: Decimal = quantity / self.dex_market.get_lots(currency); + let output_quantity = + self.exchange_with_order_book(input_quantity, currency, cache_orders)?; + Ok(output_quantity * self.dex_market.get_lots(currency.opposite())) + } + + /// Exchange tokens by filling orders + fn exchange_with_order_book( + &mut self, + mut input_quantity: Decimal, + currency: Currency, + cache_orders: bool, + ) -> Result { + let mut output_quantity = Decimal::zero(); + let mut order_cache = VecDeque::new(); + + if cache_orders && !self.orders.is_cacheable() { + return Err(LendingError::TradeSimulationError.into()); + } + + let zero = Decimal::zero(); + while input_quantity > zero { + let next_order = self + .orders + .next() + .ok_or_else(|| ProgramError::from(LendingError::TradeSimulationError))?; + + let next_order_price = next_order.price; + let base_quantity = next_order.quantity; + + let (filled, output) = if currency == Currency::Base { + let filled = input_quantity.min(Decimal::from(base_quantity)); + (filled, filled * next_order_price) + } else { + let quote_quantity = base_quantity as u128 * next_order_price as u128; + let filled = input_quantity.min(Decimal::from(quote_quantity)); + (filled, filled / next_order_price) + }; + + input_quantity -= filled; + output_quantity += output; + + if cache_orders { + order_cache.push_back(next_order); + } + } + + if cache_orders { + self.orders = Orders::Cached(order_cache) + } else { + self.orders = Orders::None + } + + Ok(output_quantity) + } +} + +/// Dex market order account info +struct DexMarketOrders<'a> { + heap: Option>, + side: Side, +} + +impl<'a> DexMarketOrders<'a> { + /// Create a new DexMarketOrders + fn new( + dex_market: &DexMarket, + orders: &AccountInfo, + memory: &'a AccountInfo, + ) -> Result { + let side = match orders.key { + key if key == &dex_market.bids => Side::Bid, + key if key == &dex_market.asks => Side::Ask, + _ => return Err(LendingError::DexInvalidOrderBookSide.into()), + }; + + if memory.data_len() < orders.data_len() { + return Err(LendingError::MemoryTooSmall.into()); + } + + let mut memory_data = memory.data.borrow_mut(); + fast_copy(&orders.data.borrow(), &mut memory_data); + let heap = Some(RefMut::map(memory_data, |bytes| { + // strip padding and header + let start = 5 + 8; + let end = bytes.len() - 7; + Slab::new(&mut bytes[start..end]) + })); + + Ok(Self { heap, side }) + } +} + +/// Offset for dex market base mint +pub const BASE_MINT_OFFSET: usize = 6; +/// Offset for dex market quote mint +pub const QUOTE_MINT_OFFSET: usize = 10; + +const BIDS_OFFSET: usize = 35; +const ASKS_OFFSET: usize = 39; + +/// Dex market info +pub struct DexMarket { + bids: Pubkey, + asks: Pubkey, + base_lots: u64, + quote_lots: u64, +} + +impl DexMarket { + /// Create a new DexMarket + fn new(dex_market_info: &AccountInfo) -> Self { + let dex_market_data = dex_market_info.data.borrow(); + let bids = Self::pubkey_at_offset(&dex_market_data, BIDS_OFFSET); + let asks = Self::pubkey_at_offset(&dex_market_data, ASKS_OFFSET); + let base_lots = Self::base_lots(&dex_market_data); + let quote_lots = Self::quote_lots(&dex_market_data); + + Self { + bids, + asks, + base_lots, + quote_lots, + } + } + + fn get_lots(&self, currency: Currency) -> u64 { + match currency { + Currency::Base => self.base_lots, + Currency::Quote => self.quote_lots, + } + } + + fn base_lots(data: &[u8]) -> u64 { + let count_start = 5 + 43 * 8; + let count_end = count_start + 8; + u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) + } + + fn quote_lots(data: &[u8]) -> u64 { + let count_start = 5 + 44 * 8; + let count_end = count_start + 8; + u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) + } + + /// Get pubkey located at offset + pub fn pubkey_at_offset(data: &[u8], offset: usize) -> Pubkey { + let count_start = 5 + offset * 8; + let count_end = count_start + 32; + Pubkey::new(&data[count_start..count_end]) + } +} + +/// A more efficient `copy_from_slice` implementation. +fn fast_copy(mut src: &[u8], mut dst: &mut [u8]) { + const COPY_SIZE: usize = 512; + while src.len() >= COPY_SIZE { + #[allow(clippy::ptr_offset_with_cast)] + let (src_word, src_rem) = array_refs![src, COPY_SIZE; ..;]; + #[allow(clippy::ptr_offset_with_cast)] + let (dst_word, dst_rem) = mut_array_refs![dst, COPY_SIZE; ..;]; + *dst_word = *src_word; + src = src_rem; + dst = dst_rem; + } + unsafe { + std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len()); + } +} diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 20f216ae..e99780e7 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -78,9 +78,9 @@ pub enum LendingError { #[error("Borrow amount too small")] BorrowTooSmall, - /// Dex order book error. - #[error("Dex order book error")] - DexOrderBookError, + /// Trade simulation error + #[error("Trade simulation error")] + TradeSimulationError, /// Invalid dex order book side #[error("Invalid dex order book side")] DexInvalidOrderBookSide, diff --git a/token-lending/program/src/lib.rs b/token-lending/program/src/lib.rs index 9fee3f01..d8e2b2db 100644 --- a/token-lending/program/src/lib.rs +++ b/token-lending/program/src/lib.rs @@ -2,6 +2,7 @@ //! A lending program for the Solana blockchain. +pub mod dex_market; pub mod entrypoint; pub mod error; pub mod instruction; diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 31cac012..f89f67eb 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,14 +1,13 @@ //! Program state processor use crate::{ + dex_market::{DexMarket, TradeAction, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET}, error::LendingError, instruction::{BorrowAmountType, LendingInstruction}, math::{Decimal, Rate, WAD}, state::{LendingMarket, Obligation, Reserve, ReserveConfig, ReserveState}, }; -use arrayref::{array_refs, mut_array_refs}; use num_traits::FromPrimitive; -use serum_dex::critbit::Slab; use solana_program::{ account_info::{next_account_info, AccountInfo}, decode_error::DecodeError, @@ -22,7 +21,6 @@ use solana_program::{ sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::state::Account as Token; -use std::cell::RefMut; /// Processes an instruction pub fn process_instruction( @@ -173,26 +171,13 @@ fn process_init_reserve( return Err(LendingError::NotRentExempt.into()); } - fn base_mint_pubkey(data: &[u8]) -> Pubkey { - let count_start = 5 + 6 * 8; - let count_end = count_start + 32; - Pubkey::new(&data[count_start..count_end]) - } - - fn quote_mint_pubkey(data: &[u8]) -> Pubkey { - let count_start = 5 + 10 * 8; - let count_end = count_start + 32; - Pubkey::new(&data[count_start..count_end]) - } - - let market_base_mint = base_mint_pubkey(&dex_market_info.data.borrow()); - let market_quote_mint = quote_mint_pubkey(&dex_market_info.data.borrow()); + let dex_market_data = &dex_market_info.data.borrow(); + let market_quote_mint = DexMarket::pubkey_at_offset(&dex_market_data, QUOTE_MINT_OFFSET); if lending_market.quote_token_mint != market_quote_mint { - msg!(&market_quote_mint.to_string().as_str()); return Err(LendingError::DexMarketMintMismatch.into()); } + let market_base_mint = DexMarket::pubkey_at_offset(&dex_market_data, BASE_MINT_OFFSET); if reserve_liquidity_mint_info.key != &market_base_mint { - msg!(&market_base_mint.to_string().as_str()); return Err(LendingError::DexMarketMintMismatch.into()); } @@ -486,7 +471,7 @@ fn process_borrow( let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_order_book_side_info = next_account_info(account_info_iter)?; + let dex_market_orders_info = next_account_info(account_info_iter)?; let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; @@ -570,16 +555,24 @@ fn process_borrow( let cumulative_borrow_rate = borrow_reserve.state.cumulative_borrow_rate_wads; let deposit_reserve_collateral_exchange_rate = deposit_reserve.state.collateral_exchange_rate(); + let mut trade_simulator = TradeSimulator::new( + dex_market_info, + dex_market_orders_info, + memory, + &lending_market.quote_token_mint, + )?; + let (borrow_amount, mut collateral_deposit_amount) = match amount_type { BorrowAmountType::LiquidityBorrowAmount => { let borrow_amount = amount; - let loan_in_deposit_underlying = simulate_market_order_fill_maker( + // Simulate buying `borrow_amount` of borrow reserve underlying tokens + // to determine how much collateral is needed + let loan_in_deposit_underlying = trade_simulator.simulate_trade( + TradeAction::Buy, Decimal::from(borrow_amount), - memory, - dex_market_order_book_side_info, - dex_market_info, - &deposit_reserve, + &borrow_reserve.liquidity_mint, + false, )?; let loan_in_deposit_collateral = deposit_reserve_collateral_exchange_rate @@ -602,12 +595,13 @@ fn process_borrow( let loan_in_deposit_underlying = deposit_reserve_collateral_exchange_rate .decimal_collateral_to_liquidity(loan_in_deposit_collateral); - let borrow_amount = simulate_market_order_fill( + // Simulate selling `loan_in_deposit_underlying` amount of deposit reserve underlying + // tokens to determine how much to lend to the user + let borrow_amount = trade_simulator.simulate_trade( + TradeAction::Sell, loan_in_deposit_underlying, - memory, - dex_market_order_book_side_info, - dex_market_info, - &deposit_reserve, + &deposit_reserve.liquidity_mint, + false, )?; let borrow_amount = borrow_amount.round_u64(); @@ -954,7 +948,7 @@ fn process_liquidate( let lending_market_authority_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let dex_market_info = next_account_info(account_info_iter)?; - let dex_market_order_book_side_info = next_account_info(account_info_iter)?; + let dex_market_orders_info = next_account_info(account_info_iter)?; let memory = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; @@ -1039,20 +1033,28 @@ fn process_liquidate( withdraw_reserve.accrue_interest(clock.slot); obligation.accrue_interest(clock, repay_reserve.state.cumulative_borrow_rate_wads); + let mut trade_simulator = TradeSimulator::new( + dex_market_info, + dex_market_orders_info, + memory, + &lending_market.quote_token_mint, + )?; + // calculate obligation health let withdraw_reserve_collateral_exchange_rate = withdraw_reserve.state.collateral_exchange_rate(); let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate .liquidity_to_collateral( - simulate_market_order_fill( - obligation.borrowed_liquidity_wads, - memory, - dex_market_order_book_side_info, - dex_market_info, - &repay_reserve, - )? - .round_u64(), + trade_simulator + .simulate_trade( + TradeAction::Sell, + obligation.borrowed_liquidity_wads, + &repay_reserve.liquidity_mint, + true, + )? + .round_u64(), ); + if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens < withdraw_reserve.config.liquidation_threshold as u64 { @@ -1071,21 +1073,21 @@ fn process_liquidate( // TODO: check math precision // calculate the amount of collateral that will be withdrawn - let withdraw_liquidity_amount = simulate_market_order_fill( + + let withdraw_liquidity_amount = trade_simulator.simulate_trade( + TradeAction::Sell, repay_amount, - memory, - dex_market_order_book_side_info, - dex_market_info, - &repay_reserve, + &repay_reserve.liquidity_mint, + false, )?; - let repay_amount_as_collateral = withdraw_reserve_collateral_exchange_rate + let withdraw_amount_as_collateral = withdraw_reserve_collateral_exchange_rate .decimal_liquidity_to_collateral(withdraw_liquidity_amount) .round_u64(); let liquidation_bonus_amount = - repay_amount_as_collateral * (withdraw_reserve.config.liquidation_bonus as u64) / 100; + withdraw_amount_as_collateral * (withdraw_reserve.config.liquidation_bonus as u64) / 100; let collateral_withdraw_amount = obligation .deposited_collateral_tokens - .min(repay_amount_as_collateral + liquidation_bonus_amount); + .min(withdraw_amount_as_collateral + liquidation_bonus_amount); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; Reserve::pack( @@ -1322,210 +1324,3 @@ impl PrintProgramError for LendingError { msg!(&self.to_string()); } } - -/// A more efficient `copy_from_slice` implementation. -fn fast_copy(mut src: &[u8], mut dst: &mut [u8]) { - const COPY_SIZE: usize = 512; - while src.len() >= COPY_SIZE { - #[allow(clippy::ptr_offset_with_cast)] - let (src_word, src_rem) = array_refs![src, COPY_SIZE; ..;]; - #[allow(clippy::ptr_offset_with_cast)] - let (dst_word, dst_rem) = mut_array_refs![dst, COPY_SIZE; ..;]; - *dst_word = *src_word; - src = src_rem; - dst = dst_rem; - } - unsafe { - std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len()); - } -} - -/// A stack and instruction efficient memset -fn fast_set(mut dst: &mut [u8], val: u8) { - const SET_SIZE: usize = 1024; - while dst.len() >= SET_SIZE { - #[allow(clippy::ptr_offset_with_cast)] - let (dst_word, dst_rem) = mut_array_refs![dst, SET_SIZE; ..;]; - *dst_word = [val; SET_SIZE]; - dst = dst_rem; - } - unsafe { - std::ptr::write_bytes(dst.as_mut_ptr(), val, dst.len()); - } -} - -enum Side { - Bid, - Ask, -} - -#[derive(PartialEq)] -enum Fill { - Base, - Quote, -} - -/// Calculate output quantity from input using order book depth -fn exchange_with_order_book( - mut orders: RefMut, - side: Side, - fill: Fill, - mut input_quantity: Decimal, -) -> Result { - let mut output_quantity = Decimal::zero(); - - let zero = Decimal::zero(); - while input_quantity > zero { - let next_order = match side { - Side::Bid => orders.remove_max(), - Side::Ask => orders.remove_min(), - } - .ok_or_else(|| ProgramError::from(LendingError::DexOrderBookError))?; - - let next_order_price: u64 = next_order.price().get(); - let base_quantity = next_order.quantity(); - let quote_quantity = base_quantity as u128 * next_order_price as u128; - - let (filled, output) = if fill == Fill::Base { - let filled = input_quantity.min(Decimal::from(base_quantity)); - (filled, filled * next_order_price) - } else { - let filled = input_quantity.min(Decimal::from(quote_quantity)); - (filled, filled / next_order_price) - }; - - input_quantity -= filled; - output_quantity += output; - } - - Ok(output_quantity) -} - -fn mut_orders_copy<'a>( - orders: &AccountInfo, - memory: &'a AccountInfo, -) -> Result, ProgramError> { - if memory.data_len() < orders.data_len() { - return Err(LendingError::MemoryTooSmall.into()); - } - - let mut memory = memory.data.borrow_mut(); - fast_copy(&orders.data.borrow(), &mut memory); - Ok(RefMut::map(memory, |bytes| { - // strip padding and header - let start = 5 + 8; - let end = bytes.len() - 7; - Slab::new(&mut bytes[start..end]) - })) -} - -fn quote_mint_pubkey(data: &[u8]) -> Pubkey { - let count_start = 5 + 10 * 8; - let count_end = count_start + 32; - Pubkey::new(&data[count_start..count_end]) -} - -use std::convert::TryFrom; -fn base_lots(data: &[u8]) -> u64 { - let count_start = 5 + 43 * 8; - let count_end = count_start + 8; - u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) -} - -fn quote_lots(data: &[u8]) -> u64 { - let count_start = 5 + 44 * 8; - let count_end = count_start + 8; - u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap()) -} - -fn load_bids_pubkey(data: &[u8]) -> Pubkey { - let count_start = 5 + 35 * 8; - let count_end = count_start + 32; - Pubkey::new(&data[count_start..count_end]) -} - -fn load_asks_pubkey(data: &[u8]) -> Pubkey { - let count_start = 5 + 39 * 8; - let count_end = count_start + 32; - Pubkey::new(&data[count_start..count_end]) -} - -fn simulate_market_order_fill_maker( - amount: Decimal, - memory: &AccountInfo, - dex_market_order_book_side_info: &AccountInfo, - dex_market_info: &AccountInfo, - reserve: &Reserve, -) -> Result { - let market_quote_mint = quote_mint_pubkey(&dex_market_info.data.borrow()); - let market_bid_orders = load_bids_pubkey(&dex_market_info.data.borrow()); - let market_ask_orders = load_asks_pubkey(&dex_market_info.data.borrow()); - - let base_lots = base_lots(&dex_market_info.data.borrow()); - let quote_lots = quote_lots(&dex_market_info.data.borrow()); - - let (fill, side, source_lots, destination_lots) = if reserve.liquidity_mint != market_quote_mint - { - if &market_ask_orders != dex_market_order_book_side_info.key { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - (Fill::Quote, Side::Ask, base_lots, quote_lots) - } else { - if &market_bid_orders != dex_market_order_book_side_info.key { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - (Fill::Base, Side::Bid, quote_lots, base_lots) - }; - - let input_scale = - destination_lots * 10u64.pow(reserve.liquidity_mint_decimals as u32) / source_lots; - let input_quantity = amount / Decimal::from(input_scale); - - let orders = mut_orders_copy(dex_market_order_book_side_info, memory)?; - let output_quantity = exchange_with_order_book(orders, side, fill, input_quantity)?; - - let exchanged_amount = output_quantity * 10u64.pow(reserve.liquidity_mint_decimals as u32); - - fast_set(&mut memory.data.borrow_mut(), 0); - Ok(exchanged_amount) -} - -fn simulate_market_order_fill( - amount: Decimal, - memory: &AccountInfo, - dex_market_order_book_side_info: &AccountInfo, - dex_market_info: &AccountInfo, - reserve: &Reserve, -) -> Result { - let market_quote_mint = quote_mint_pubkey(&dex_market_info.data.borrow()); - let market_bid_orders = load_bids_pubkey(&dex_market_info.data.borrow()); - let market_ask_orders = load_asks_pubkey(&dex_market_info.data.borrow()); - - let base_lots = base_lots(&dex_market_info.data.borrow()); - let quote_lots = quote_lots(&dex_market_info.data.borrow()); - - let (fill, side, source_lots, destination_lots) = if reserve.liquidity_mint == market_quote_mint - { - if &market_bid_orders != dex_market_order_book_side_info.key { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - (Fill::Quote, Side::Bid, quote_lots, base_lots) - } else { - if &market_ask_orders != dex_market_order_book_side_info.key { - return Err(LendingError::DexInvalidOrderBookSide.into()); - } - (Fill::Base, Side::Ask, base_lots, quote_lots) - }; - - let input_quantity = amount / Decimal::from(10u64.pow(reserve.liquidity_mint_decimals as u32)); - - let orders = mut_orders_copy(dex_market_order_book_side_info, memory)?; - let output_quantity = exchange_with_order_book(orders, side, fill, input_quantity)?; - - let output_scale = - destination_lots * 10u64.pow(reserve.liquidity_mint_decimals as u32) / source_lots; - let exchanged_amount = output_quantity * output_scale; - - fast_set(&mut memory.data.borrow_mut(), 0); - Ok(exchanged_amount) -} diff --git a/token-lending/program/tests/borrow.rs b/token-lending/program/tests/borrow.rs index 7d205213..438e7f6d 100644 --- a/token-lending/program/tests/borrow.rs +++ b/token-lending/program/tests/borrow.rs @@ -12,19 +12,20 @@ use spl_token_lending::{ const LAMPORTS_TO_SOL: u64 = 1_000_000_000; const FRACTIONAL_TO_USDC: u64 = 1_000_000; -// Market and collateral are setup to fill two orders in the dex market at an average -// price of 2210.5 -const fn lamports_to_usdc_fractional(lamports: u64) -> u64 { - lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000 -} - -const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 42_500 * LAMPORTS_TO_SOL; -const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = - lamports_to_usdc_fractional(INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS); -const USER_SOL_COLLATERAL_LAMPORTS: u64 = 8_500 * LAMPORTS_TO_SOL; - #[tokio::test] -async fn test_success() { +async fn test_borrow_quote_currency() { + // Using SOL/USDC max 3 bids: + // $2.199, 300.0 SOL + // $2.192, 213.3 SOL + // $2.190, 1523.4 SOL + // + // Collateral amount = 750 * 0.8 (LTV) = 600 SOL + // Borrow amount = 2.199 * 300 + 2.192 * 213.3 + 2.19 * 86.7 = 1,317.1266 USDC + const SOL_COLLATERAL_AMOUNT_LAMPORTS: u64 = 750 * LAMPORTS_TO_SOL; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_317_126_600; + const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 10_000 * FRACTIONAL_TO_USDC; + const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 2 * SOL_COLLATERAL_AMOUNT_LAMPORTS; + let mut test = ProgramTest::new( "spl_token_lending", spl_token_lending::id(), @@ -36,6 +37,9 @@ async fn test_success() { let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 80; + let usdc_reserve = add_reserve( &mut test, &user_accounts_owner, @@ -44,7 +48,7 @@ async fn test_success() { liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, liquidity_mint_pubkey: usdc_mint.pubkey, liquidity_mint_decimals: usdc_mint.decimals, - config: TEST_RESERVE_CONFIG, + config: reserve_config, ..AddReserveArgs::default() }, ); @@ -58,14 +62,22 @@ async fn test_success() { liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, liquidity_mint_pubkey: spl_token::native_mint::id(), liquidity_mint_decimals: 9, - config: TEST_RESERVE_CONFIG, + config: reserve_config, ..AddReserveArgs::default() }, ); let (mut banks_client, payer, _recent_blockhash) = test.start().await; - let borrow_amount = INITIAL_COLLATERAL_RATE * USER_SOL_COLLATERAL_LAMPORTS; + let borrow_amount = + get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, 0); + + let collateral_supply = + get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + assert_eq!(collateral_supply, 0); + + let collateral_deposit_amount = INITIAL_COLLATERAL_RATE * SOL_COLLATERAL_AMOUNT_LAMPORTS; let obligation = lending_market .borrow( &mut banks_client, @@ -75,13 +87,27 @@ async fn test_success() { borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, borrow_amount_type: BorrowAmountType::CollateralDepositAmount, - amount: borrow_amount / 2, + amount: collateral_deposit_amount, user_accounts_owner: &user_accounts_owner, obligation: None, }, ) .await; + let borrow_amount = + get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL); + + let borrow_fees = TEST_RESERVE_CONFIG + .fees + .calculate_borrow_fees(collateral_deposit_amount) + .unwrap() + .0; + + let collateral_supply = + get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; + assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); + lending_market .borrow( &mut banks_client, @@ -90,31 +116,174 @@ async fn test_success() { deposit_reserve: &sol_reserve, borrow_reserve: &usdc_reserve, dex_market: &sol_usdc_dex_market, - borrow_amount_type: BorrowAmountType::CollateralDepositAmount, - amount: borrow_amount / 2, + borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount, + amount: USDC_BORROW_AMOUNT_FRACTIONAL, user_accounts_owner: &user_accounts_owner, obligation: Some(obligation), }, ) .await; - // check that fee accounts have been properly credited + let borrow_amount = + get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, 2 * USDC_BORROW_AMOUNT_FRACTIONAL); + let (total_fee, host_fee) = TEST_RESERVE_CONFIG .fees - .calculate_borrow_fees(borrow_amount) + .calculate_borrow_fees(2 * collateral_deposit_amount) .unwrap(); assert!(total_fee > 0); assert!(host_fee > 0); - let sol_collateral_supply = + let collateral_supply = get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await; - assert_eq!(sol_collateral_supply, borrow_amount - total_fee); + assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); - let sol_fee_balance = + let fee_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_fees_receiver).await; - assert_eq!(sol_fee_balance, total_fee - host_fee); + assert_eq!(fee_balance, total_fee - host_fee); - let sol_host_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_host).await; - assert_eq!(sol_host_balance, host_fee); + let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_host).await; + assert_eq!(host_fee_balance, host_fee); +} + +#[tokio::test] +async fn test_borrow_base_currency() { + // Using SOL/USDC min 3 asks: + // $2.212, 1825.6 SOL + // $2.211, 300.0 SOL + // $2.210, 212.5 SOL + // + // Borrow amount = 600 SOL + // Collateral amount = 2.21 * 212.5 + 2.211 * 300 + 2.212 * 87.5 = 1,329.475 USDC + const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 600 * LAMPORTS_TO_SOL; + const USDC_COLLATERAL_LAMPORTS: u64 = 1_326_475_000; + const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 5000 * LAMPORTS_TO_SOL; + const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 2 * USDC_COLLATERAL_LAMPORTS; + + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + let user_accounts_owner = Keypair::new(); + let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); + let usdc_mint = add_usdc_mint(&mut test); + let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 100; + + let usdc_reserve = add_reserve( + &mut test, + &user_accounts_owner, + &lending_market, + AddReserveArgs { + liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + ..AddReserveArgs::default() + }, + ); + + let sol_reserve = add_reserve( + &mut test, + &user_accounts_owner, + &lending_market, + AddReserveArgs { + dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), + liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + ..AddReserveArgs::default() + }, + ); + + let (mut banks_client, payer, _recent_blockhash) = test.start().await; + + let borrow_amount = + get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, 0); + + let collateral_supply = + get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; + assert_eq!(collateral_supply, 0); + + let collateral_deposit_amount = INITIAL_COLLATERAL_RATE * USDC_COLLATERAL_LAMPORTS; + let obligation = lending_market + .borrow( + &mut banks_client, + &payer, + BorrowArgs { + deposit_reserve: &usdc_reserve, + borrow_reserve: &sol_reserve, + dex_market: &sol_usdc_dex_market, + borrow_amount_type: BorrowAmountType::CollateralDepositAmount, + amount: collateral_deposit_amount, + user_accounts_owner: &user_accounts_owner, + obligation: None, + }, + ) + .await; + + let borrow_amount = + get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS); + + let borrow_fees = TEST_RESERVE_CONFIG + .fees + .calculate_borrow_fees(collateral_deposit_amount) + .unwrap() + .0; + + let collateral_supply = + get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; + assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees); + + lending_market + .borrow( + &mut banks_client, + &payer, + BorrowArgs { + deposit_reserve: &usdc_reserve, + borrow_reserve: &sol_reserve, + dex_market: &sol_usdc_dex_market, + borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount, + amount: borrow_amount, + user_accounts_owner: &user_accounts_owner, + obligation: Some(obligation), + }, + ) + .await; + + let borrow_amount = + get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await; + assert_eq!(borrow_amount, 2 * SOL_BORROW_AMOUNT_LAMPORTS); + + let (mut total_fee, mut host_fee) = TEST_RESERVE_CONFIG + .fees + .calculate_borrow_fees(collateral_deposit_amount) + .unwrap(); + + // avoid rounding error by assessing fees individually + total_fee *= 2; + host_fee *= 2; + + assert!(total_fee > 0); + assert!(host_fee > 0); + + let collateral_supply = + get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await; + assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee); + + let fee_balance = + get_token_balance(&mut banks_client, usdc_reserve.collateral_fees_receiver).await; + assert_eq!(fee_balance, total_fee - host_fee); + + let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.collateral_host).await; + assert_eq!(host_fee_balance, host_fee); } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 9a1924df..f74b07cd 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -16,7 +16,7 @@ use spl_token::{ use spl_token_lending::{ instruction::{ borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_reserve, - BorrowAmountType, + liquidate_obligation, BorrowAmountType, }, math::Decimal, processor::process_instruction, @@ -33,7 +33,7 @@ pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig { optimal_utilization_rate: 80, loan_to_value_ratio: 50, liquidation_bonus: 5, - liquidation_threshold: 0, + liquidation_threshold: 50, min_borrow_rate: 0, optimal_borrow_rate: 4, max_borrow_rate: 30, @@ -140,15 +140,28 @@ pub fn add_lending_market(test: &mut ProgramTest, quote_token_mint: Pubkey) -> T } } +pub struct AddObligationArgs<'a> { + pub slots_elapsed: u64, + pub borrow_reserve: &'a TestReserve, + pub collateral_reserve: &'a TestReserve, + pub collateral_amount: u64, + pub borrowed_liquidity_wads: Decimal, +} + pub fn add_obligation( test: &mut ProgramTest, user_accounts_owner: &Keypair, lending_market: &TestLendingMarket, - borrow_reserve: &TestReserve, - collateral_reserve: &TestReserve, - collateral_amount: u64, - borrowed_liquidity_wads: Decimal, + args: AddObligationArgs, ) -> TestObligation { + let AddObligationArgs { + slots_elapsed, + borrow_reserve, + collateral_reserve, + collateral_amount, + borrowed_liquidity_wads, + } = args; + let token_mint_pubkey = Pubkey::new_unique(); test.add_packable_account( token_mint_pubkey, @@ -183,7 +196,7 @@ pub fn add_obligation( obligation_pubkey, u32::MAX as u64, &Obligation { - last_update_slot: 1, + last_update_slot: 1u64.wrapping_sub(slots_elapsed), deposited_collateral_tokens: collateral_amount, collateral_reserve: collateral_reserve.pubkey, cumulative_borrow_rate_wads: Decimal::one(), @@ -204,6 +217,7 @@ pub fn add_obligation( #[derive(Default)] pub struct AddReserveArgs { pub name: String, + pub slots_elapsed: u64, pub config: ReserveConfig, pub liquidity_amount: u64, pub liquidity_mint_pubkey: Pubkey, @@ -223,6 +237,7 @@ pub fn add_reserve( ) -> TestReserve { let AddReserveArgs { name, + slots_elapsed, config, liquidity_amount, liquidity_mint_pubkey, @@ -319,7 +334,7 @@ pub fn add_reserve( let reserve_keypair = Keypair::new(); let reserve_pubkey = reserve_keypair.pubkey(); - let mut reserve_state = ReserveState::new(1, liquidity_amount); + let mut reserve_state = ReserveState::new(1u64.wrapping_sub(slots_elapsed), liquidity_amount); reserve_state.add_borrow(borrow_amount).unwrap(); test.add_packable_account( reserve_pubkey, @@ -407,6 +422,15 @@ pub struct BorrowArgs<'a> { pub obligation: Option, } +pub struct LiquidateArgs<'a> { + pub repay_reserve: &'a TestReserve, + pub withdraw_reserve: &'a TestReserve, + pub obligation: &'a TestObligation, + pub amount: u64, + pub dex_market: &'a TestDexMarket, + pub user_accounts_owner: &'a Keypair, +} + impl TestLendingMarket { pub async fn init( banks_client: &mut BanksClient, @@ -489,6 +513,81 @@ impl TestLendingMarket { assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); } + pub async fn liquidate( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + args: LiquidateArgs<'_>, + ) { + let LiquidateArgs { + repay_reserve, + withdraw_reserve, + obligation, + amount, + dex_market, + user_accounts_owner, + } = args; + + let dex_market_orders_pubkey = if repay_reserve.dex_market.is_none() { + dex_market.asks_pubkey + } else { + dex_market.bids_pubkey + }; + + let memory_keypair = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let mut transaction = Transaction::new_with_payer( + &[ + create_account( + &payer.pubkey(), + &memory_keypair.pubkey(), + 0, + 65548, + &spl_token_lending::id(), + ), + approve( + &spl_token::id(), + &repay_reserve.user_liquidity_account, + &user_transfer_authority.pubkey(), + &user_accounts_owner.pubkey(), + &[], + amount, + ) + .unwrap(), + liquidate_obligation( + spl_token_lending::id(), + amount, + repay_reserve.user_liquidity_account, + withdraw_reserve.user_collateral_account, + repay_reserve.pubkey, + repay_reserve.liquidity_supply, + withdraw_reserve.pubkey, + withdraw_reserve.collateral_supply, + obligation.keypair.pubkey(), + self.keypair.pubkey(), + self.authority, + user_transfer_authority.pubkey(), + dex_market.pubkey, + dex_market_orders_pubkey, + memory_keypair.pubkey(), + ), + ], + Some(&payer.pubkey()), + ); + + let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); + transaction.sign( + &[ + &payer, + &memory_keypair, + &user_accounts_owner, + &user_transfer_authority, + ], + recent_blockhash, + ); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + } + pub async fn borrow( &self, banks_client: &mut BanksClient, @@ -509,7 +608,7 @@ impl TestLendingMarket { obligation, } = args; - let dex_market_orders_pubkey = if deposit_reserve.dex_market.is_some() { + let dex_market_orders_pubkey = if deposit_reserve.dex_market.is_none() { dex_market.asks_pubkey } else { dex_market.bids_pubkey @@ -591,7 +690,7 @@ impl TestLendingMarket { &memory_keypair.pubkey(), 0, 65548, - &solana_program::system_program::id(), + &spl_token_lending::id(), ), borrow_reserve_liquidity( spl_token_lending::id(), diff --git a/token-lending/program/tests/liquidate.rs b/token-lending/program/tests/liquidate.rs index ec99c9d5..156cfb24 100644 --- a/token-lending/program/tests/liquidate.rs +++ b/token-lending/program/tests/liquidate.rs @@ -4,32 +4,18 @@ mod helpers; use helpers::*; use solana_program_test::*; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction::create_account, - system_program, - transaction::Transaction, -}; -use spl_token::instruction::approve; +use solana_sdk::{pubkey::Pubkey, signature::Keypair}; use spl_token_lending::{ - instruction::liquidate_obligation, math::Decimal, processor::process_instruction, - state::INITIAL_COLLATERAL_RATE, + math::Decimal, + processor::process_instruction, + state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR}, }; -const LAMPORTS_TO_SOL: u64 = 1_000_000_000; // -> 2_210_000 +const LAMPORTS_TO_SOL: u64 = 1_000_000_000; const FRACTIONAL_TO_USDC: u64 = 1_000_000; -// 0.000001 USDC -// Market and collateral are setup to fill two orders in the dex market at an average -// price of 2210.5 -const fn lamports_to_usdc_fractional(lamports: u64) -> u64 { - lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000 -} - -const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 42_500 * LAMPORTS_TO_SOL; -const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = - lamports_to_usdc_fractional(INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS); +const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; +const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; #[tokio::test] async fn test_success() { @@ -39,26 +25,38 @@ async fn test_success() { processor!(process_instruction), ); - const OBLIGATION_USDC_LOAN: u64 = FRACTIONAL_TO_USDC; - const OBLIGATION_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATE * LAMPORTS_TO_SOL; + // limit to track compute unit increase + test.set_bpf_compute_max_units(160_000); + + // set loan values to about 90% of collateral value so that it gets liquidated + const USDC_LOAN: u64 = 2 * FRACTIONAL_TO_USDC; + const USDC_LOAN_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATE * LAMPORTS_TO_SOL; + + const SOL_LOAN: u64 = LAMPORTS_TO_SOL; + const SOL_LOAN_USDC_COLLATERAL: u64 = 2 * INITIAL_COLLATERAL_RATE * FRACTIONAL_TO_USDC; let user_accounts_owner = Keypair::new(); - let user_transfer_authority = Keypair::new(); let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC); let usdc_mint = add_usdc_mint(&mut test); let lending_market = add_lending_market(&mut test, usdc_mint.pubkey); + // Loans are unhealthy if borrow is more than 80% of collateral + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.liquidation_threshold = 80; + let usdc_reserve = add_reserve( &mut test, &user_accounts_owner, &lending_market, AddReserveArgs { - config: TEST_RESERVE_CONFIG, + config: reserve_config, + slots_elapsed: SLOTS_PER_YEAR, liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL, liquidity_mint_pubkey: usdc_mint.pubkey, liquidity_mint_decimals: usdc_mint.decimals, - borrow_amount: OBLIGATION_USDC_LOAN, - user_liquidity_amount: OBLIGATION_USDC_LOAN, + borrow_amount: USDC_LOAN, + user_liquidity_amount: USDC_LOAN, + collateral_amount: SOL_LOAN_USDC_COLLATERAL, ..AddReserveArgs::default() }, ); @@ -68,76 +66,94 @@ async fn test_success() { &user_accounts_owner, &lending_market, AddReserveArgs { - config: TEST_RESERVE_CONFIG, + config: reserve_config, + slots_elapsed: SLOTS_PER_YEAR, liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS, liquidity_mint_decimals: 9, liquidity_mint_pubkey: spl_token::native_mint::id(), dex_market_pubkey: Some(sol_usdc_dex_market.pubkey), - collateral_amount: OBLIGATION_SOL_COLLATERAL, + collateral_amount: USDC_LOAN_SOL_COLLATERAL, + borrow_amount: SOL_LOAN, + user_liquidity_amount: SOL_LOAN, ..AddReserveArgs::default() }, ); - let obligation = add_obligation( + let usdc_obligation = add_obligation( &mut test, &user_accounts_owner, &lending_market, - &usdc_reserve, - &sol_reserve, - OBLIGATION_SOL_COLLATERAL, - Decimal::from(OBLIGATION_USDC_LOAN), + AddObligationArgs { + slots_elapsed: SLOTS_PER_YEAR, + borrow_reserve: &usdc_reserve, + collateral_reserve: &sol_reserve, + collateral_amount: USDC_LOAN_SOL_COLLATERAL, + borrowed_liquidity_wads: Decimal::from(USDC_LOAN), + }, ); - let (mut banks_client, payer, recent_blockhash) = test.start().await; - - let memory_keypair = Keypair::new(); - let mut transaction = Transaction::new_with_payer( - &[ - create_account( - &payer.pubkey(), - &memory_keypair.pubkey(), - 0, - 65548, - &system_program::id(), - ), - approve( - &spl_token::id(), - &usdc_reserve.user_liquidity_account, - &user_transfer_authority.pubkey(), - &user_accounts_owner.pubkey(), - &[], - OBLIGATION_USDC_LOAN, - ) - .unwrap(), - liquidate_obligation( - spl_token_lending::id(), - OBLIGATION_USDC_LOAN, - usdc_reserve.user_liquidity_account, - sol_reserve.user_collateral_account, - usdc_reserve.pubkey, - usdc_reserve.liquidity_supply, - sol_reserve.pubkey, - sol_reserve.collateral_supply, - obligation.keypair.pubkey(), - lending_market.keypair.pubkey(), - lending_market.authority, - user_transfer_authority.pubkey(), - sol_usdc_dex_market.pubkey, - sol_usdc_dex_market.bids_pubkey, - memory_keypair.pubkey(), - ), - ], - Some(&payer.pubkey()), + let sol_obligation = add_obligation( + &mut test, + &user_accounts_owner, + &lending_market, + AddObligationArgs { + slots_elapsed: SLOTS_PER_YEAR, + borrow_reserve: &sol_reserve, + collateral_reserve: &usdc_reserve, + collateral_amount: SOL_LOAN_USDC_COLLATERAL, + borrowed_liquidity_wads: Decimal::from(SOL_LOAN), + }, ); - transaction.sign( - &[ + let (mut banks_client, payer, _recent_blockhash) = test.start().await; + + lending_market + .liquidate( + &mut banks_client, &payer, - &memory_keypair, - &user_accounts_owner, - &user_transfer_authority, - ], - recent_blockhash, + LiquidateArgs { + repay_reserve: &usdc_reserve, + withdraw_reserve: &sol_reserve, + dex_market: &sol_usdc_dex_market, + amount: USDC_LOAN, + user_accounts_owner: &user_accounts_owner, + obligation: &usdc_obligation, + }, + ) + .await; + + lending_market + .liquidate( + &mut banks_client, + &payer, + LiquidateArgs { + repay_reserve: &sol_reserve, + withdraw_reserve: &usdc_reserve, + dex_market: &sol_usdc_dex_market, + amount: SOL_LOAN, + user_accounts_owner: &user_accounts_owner, + obligation: &sol_obligation, + }, + ) + .await; + + let usdc_liquidity_supply = + get_token_balance(&mut banks_client, usdc_reserve.liquidity_supply).await; + let usdc_loan_state = usdc_obligation.get_state(&mut banks_client).await; + let usdc_liquidated = usdc_liquidity_supply - INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL; + assert!(usdc_liquidated > USDC_LOAN / 2); + assert_eq!( + usdc_liquidated, + usdc_loan_state.borrowed_liquidity_wads.round_u64() + ); + + let sol_liquidity_supply = + get_token_balance(&mut banks_client, sol_reserve.liquidity_supply).await; + let sol_loan_state = sol_obligation.get_state(&mut banks_client).await; + let sol_liquidated = sol_liquidity_supply - INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS; + assert!(sol_liquidated > SOL_LOAN / 2); + assert_eq!( + sol_liquidated, + sol_loan_state.borrowed_liquidity_wads.round_u64() ); - assert!(banks_client.process_transaction(transaction).await.is_ok()); } diff --git a/token-lending/program/tests/repay.rs b/token-lending/program/tests/repay.rs index 5738e311..10875656 100644 --- a/token-lending/program/tests/repay.rs +++ b/token-lending/program/tests/repay.rs @@ -78,10 +78,13 @@ async fn test_success() { &mut test, &user_accounts_owner, &lending_market, - &usdc_reserve, - &sol_reserve, - OBLIGATION_COLLATERAL, - Decimal::from(OBLIGATION_LOAN), + AddObligationArgs { + slots_elapsed: 0, + borrow_reserve: &usdc_reserve, + collateral_reserve: &sol_reserve, + collateral_amount: OBLIGATION_COLLATERAL, + borrowed_liquidity_wads: Decimal::from(OBLIGATION_LOAN), + }, ); let (mut banks_client, payer, recent_blockhash) = test.start().await;