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:
parent
efed7c66f7
commit
dd5598933c
|
@ -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"]
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
//! A lending program for the Solana blockchain.
|
||||
|
||||
pub mod dex_market;
|
||||
pub mod entrypoint;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
|
|
|
@ -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(
|
||||
trade_simulator
|
||||
.simulate_trade(
|
||||
TradeAction::Sell,
|
||||
obligation.borrowed_liquidity_wads,
|
||||
memory,
|
||||
dex_market_order_book_side_info,
|
||||
dex_market_info,
|
||||
&repay_reserve,
|
||||
&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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[
|
||||
&payer,
|
||||
&memory_keypair,
|
||||
let sol_obligation = add_obligation(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&user_transfer_authority,
|
||||
],
|
||||
recent_blockhash,
|
||||
&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),
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, _recent_blockhash) = test.start().await;
|
||||
|
||||
lending_market
|
||||
.liquidate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue