lending: Optimize liquidate and clean up dex market handling (#986)

* Optimize liquidate and clean up dex market handling

* fix clippy

* Feedback

* Rebase

* Reduce scope

* Test LTV ratio
This commit is contained in:
Justin Starry 2021-01-07 17:10:55 +08:00 committed by GitHub
parent efed7c66f7
commit dd5598933c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 778 additions and 381 deletions

View File

@ -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"]

View File

@ -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<Order>),
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<Order> {
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<Self, ProgramError> {
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<Decimal, ProgramError> {
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<Decimal, ProgramError> {
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<RefMut<'a, Slab>>,
side: Side,
}
impl<'a> DexMarketOrders<'a> {
/// Create a new DexMarketOrders
fn new(
dex_market: &DexMarket,
orders: &AccountInfo,
memory: &'a AccountInfo,
) -> Result<Self, ProgramError> {
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());
}
}

View File

@ -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,

View File

@ -2,6 +2,7 @@
//! A lending program for the Solana blockchain.
pub mod dex_market;
pub mod entrypoint;
pub mod error;
pub mod instruction;

View File

@ -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<Slab>,
side: Side,
fill: Fill,
mut input_quantity: Decimal,
) -> Result<Decimal, ProgramError> {
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<RefMut<'a, Slab>, 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<Decimal, ProgramError> {
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<Decimal, ProgramError> {
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)
}

View File

@ -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);
}

View File

@ -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<TestObligation>,
}
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(),

View File

@ -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());
}

View File

@ -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;