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"
|
uint = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
assert_matches = "1.4.0"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
assert_matches = "1.4.0"
|
|
||||||
proptest = "0.10"
|
proptest = "0.10"
|
||||||
solana-program-test = "1.5.0"
|
solana-program-test = "1.5.0"
|
||||||
solana-sdk = "1.5.0"
|
solana-sdk = "1.5.0"
|
||||||
tokio = { version = "0.3", features = ["macros"]}
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
|
tokio = { version = "0.3", features = ["macros"]}
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "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")]
|
#[error("Borrow amount too small")]
|
||||||
BorrowTooSmall,
|
BorrowTooSmall,
|
||||||
|
|
||||||
/// Dex order book error.
|
/// Trade simulation error
|
||||||
#[error("Dex order book error")]
|
#[error("Trade simulation error")]
|
||||||
DexOrderBookError,
|
TradeSimulationError,
|
||||||
/// Invalid dex order book side
|
/// Invalid dex order book side
|
||||||
#[error("Invalid dex order book side")]
|
#[error("Invalid dex order book side")]
|
||||||
DexInvalidOrderBookSide,
|
DexInvalidOrderBookSide,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
//! A lending program for the Solana blockchain.
|
//! A lending program for the Solana blockchain.
|
||||||
|
|
||||||
|
pub mod dex_market;
|
||||||
pub mod entrypoint;
|
pub mod entrypoint;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod instruction;
|
pub mod instruction;
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
//! Program state processor
|
//! Program state processor
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
dex_market::{DexMarket, TradeAction, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET},
|
||||||
error::LendingError,
|
error::LendingError,
|
||||||
instruction::{BorrowAmountType, LendingInstruction},
|
instruction::{BorrowAmountType, LendingInstruction},
|
||||||
math::{Decimal, Rate, WAD},
|
math::{Decimal, Rate, WAD},
|
||||||
state::{LendingMarket, Obligation, Reserve, ReserveConfig, ReserveState},
|
state::{LendingMarket, Obligation, Reserve, ReserveConfig, ReserveState},
|
||||||
};
|
};
|
||||||
use arrayref::{array_refs, mut_array_refs};
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use serum_dex::critbit::Slab;
|
|
||||||
use solana_program::{
|
use solana_program::{
|
||||||
account_info::{next_account_info, AccountInfo},
|
account_info::{next_account_info, AccountInfo},
|
||||||
decode_error::DecodeError,
|
decode_error::DecodeError,
|
||||||
|
@ -22,7 +21,6 @@ use solana_program::{
|
||||||
sysvar::{clock::Clock, rent::Rent, Sysvar},
|
sysvar::{clock::Clock, rent::Rent, Sysvar},
|
||||||
};
|
};
|
||||||
use spl_token::state::Account as Token;
|
use spl_token::state::Account as Token;
|
||||||
use std::cell::RefMut;
|
|
||||||
|
|
||||||
/// Processes an instruction
|
/// Processes an instruction
|
||||||
pub fn process_instruction(
|
pub fn process_instruction(
|
||||||
|
@ -173,26 +171,13 @@ fn process_init_reserve(
|
||||||
return Err(LendingError::NotRentExempt.into());
|
return Err(LendingError::NotRentExempt.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_mint_pubkey(data: &[u8]) -> Pubkey {
|
let dex_market_data = &dex_market_info.data.borrow();
|
||||||
let count_start = 5 + 6 * 8;
|
let market_quote_mint = DexMarket::pubkey_at_offset(&dex_market_data, QUOTE_MINT_OFFSET);
|
||||||
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());
|
|
||||||
if lending_market.quote_token_mint != market_quote_mint {
|
if lending_market.quote_token_mint != market_quote_mint {
|
||||||
msg!(&market_quote_mint.to_string().as_str());
|
|
||||||
return Err(LendingError::DexMarketMintMismatch.into());
|
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 {
|
if reserve_liquidity_mint_info.key != &market_base_mint {
|
||||||
msg!(&market_base_mint.to_string().as_str());
|
|
||||||
return Err(LendingError::DexMarketMintMismatch.into());
|
return Err(LendingError::DexMarketMintMismatch.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,7 +471,7 @@ fn process_borrow(
|
||||||
let lending_market_authority_info = next_account_info(account_info_iter)?;
|
let lending_market_authority_info = next_account_info(account_info_iter)?;
|
||||||
let user_transfer_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_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 memory = next_account_info(account_info_iter)?;
|
||||||
let clock = &Clock::from_account_info(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)?;
|
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 cumulative_borrow_rate = borrow_reserve.state.cumulative_borrow_rate_wads;
|
||||||
let deposit_reserve_collateral_exchange_rate = deposit_reserve.state.collateral_exchange_rate();
|
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 {
|
let (borrow_amount, mut collateral_deposit_amount) = match amount_type {
|
||||||
BorrowAmountType::LiquidityBorrowAmount => {
|
BorrowAmountType::LiquidityBorrowAmount => {
|
||||||
let borrow_amount = amount;
|
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),
|
Decimal::from(borrow_amount),
|
||||||
memory,
|
&borrow_reserve.liquidity_mint,
|
||||||
dex_market_order_book_side_info,
|
false,
|
||||||
dex_market_info,
|
|
||||||
&deposit_reserve,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let loan_in_deposit_collateral = deposit_reserve_collateral_exchange_rate
|
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
|
let loan_in_deposit_underlying = deposit_reserve_collateral_exchange_rate
|
||||||
.decimal_collateral_to_liquidity(loan_in_deposit_collateral);
|
.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,
|
loan_in_deposit_underlying,
|
||||||
memory,
|
&deposit_reserve.liquidity_mint,
|
||||||
dex_market_order_book_side_info,
|
false,
|
||||||
dex_market_info,
|
|
||||||
&deposit_reserve,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let borrow_amount = borrow_amount.round_u64();
|
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 lending_market_authority_info = next_account_info(account_info_iter)?;
|
||||||
let user_transfer_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_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 memory = next_account_info(account_info_iter)?;
|
||||||
let clock = &Clock::from_account_info(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)?;
|
let token_program_id = next_account_info(account_info_iter)?;
|
||||||
|
@ -1039,20 +1033,28 @@ fn process_liquidate(
|
||||||
withdraw_reserve.accrue_interest(clock.slot);
|
withdraw_reserve.accrue_interest(clock.slot);
|
||||||
obligation.accrue_interest(clock, repay_reserve.state.cumulative_borrow_rate_wads);
|
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
|
// calculate obligation health
|
||||||
let withdraw_reserve_collateral_exchange_rate =
|
let withdraw_reserve_collateral_exchange_rate =
|
||||||
withdraw_reserve.state.collateral_exchange_rate();
|
withdraw_reserve.state.collateral_exchange_rate();
|
||||||
let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate
|
let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate
|
||||||
.liquidity_to_collateral(
|
.liquidity_to_collateral(
|
||||||
simulate_market_order_fill(
|
trade_simulator
|
||||||
obligation.borrowed_liquidity_wads,
|
.simulate_trade(
|
||||||
memory,
|
TradeAction::Sell,
|
||||||
dex_market_order_book_side_info,
|
obligation.borrowed_liquidity_wads,
|
||||||
dex_market_info,
|
&repay_reserve.liquidity_mint,
|
||||||
&repay_reserve,
|
true,
|
||||||
)?
|
)?
|
||||||
.round_u64(),
|
.round_u64(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens
|
if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens
|
||||||
< withdraw_reserve.config.liquidation_threshold as u64
|
< withdraw_reserve.config.liquidation_threshold as u64
|
||||||
{
|
{
|
||||||
|
@ -1071,21 +1073,21 @@ fn process_liquidate(
|
||||||
|
|
||||||
// TODO: check math precision
|
// TODO: check math precision
|
||||||
// calculate the amount of collateral that will be withdrawn
|
// 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,
|
repay_amount,
|
||||||
memory,
|
&repay_reserve.liquidity_mint,
|
||||||
dex_market_order_book_side_info,
|
false,
|
||||||
dex_market_info,
|
|
||||||
&repay_reserve,
|
|
||||||
)?;
|
)?;
|
||||||
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)
|
.decimal_liquidity_to_collateral(withdraw_liquidity_amount)
|
||||||
.round_u64();
|
.round_u64();
|
||||||
let liquidation_bonus_amount =
|
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
|
let collateral_withdraw_amount = obligation
|
||||||
.deposited_collateral_tokens
|
.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(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
|
||||||
Reserve::pack(
|
Reserve::pack(
|
||||||
|
@ -1322,210 +1324,3 @@ impl PrintProgramError for LendingError {
|
||||||
msg!(&self.to_string());
|
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 LAMPORTS_TO_SOL: u64 = 1_000_000_000;
|
||||||
const FRACTIONAL_TO_USDC: u64 = 1_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]
|
#[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(
|
let mut test = ProgramTest::new(
|
||||||
"spl_token_lending",
|
"spl_token_lending",
|
||||||
spl_token_lending::id(),
|
spl_token_lending::id(),
|
||||||
|
@ -36,6 +37,9 @@ async fn test_success() {
|
||||||
let usdc_mint = add_usdc_mint(&mut test);
|
let usdc_mint = add_usdc_mint(&mut test);
|
||||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
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(
|
let usdc_reserve = add_reserve(
|
||||||
&mut test,
|
&mut test,
|
||||||
&user_accounts_owner,
|
&user_accounts_owner,
|
||||||
|
@ -44,7 +48,7 @@ async fn test_success() {
|
||||||
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
||||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||||
liquidity_mint_decimals: usdc_mint.decimals,
|
liquidity_mint_decimals: usdc_mint.decimals,
|
||||||
config: TEST_RESERVE_CONFIG,
|
config: reserve_config,
|
||||||
..AddReserveArgs::default()
|
..AddReserveArgs::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -58,14 +62,22 @@ async fn test_success() {
|
||||||
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
||||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||||
liquidity_mint_decimals: 9,
|
liquidity_mint_decimals: 9,
|
||||||
config: TEST_RESERVE_CONFIG,
|
config: reserve_config,
|
||||||
..AddReserveArgs::default()
|
..AddReserveArgs::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut banks_client, payer, _recent_blockhash) = test.start().await;
|
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
|
let obligation = lending_market
|
||||||
.borrow(
|
.borrow(
|
||||||
&mut banks_client,
|
&mut banks_client,
|
||||||
|
@ -75,13 +87,27 @@ async fn test_success() {
|
||||||
borrow_reserve: &usdc_reserve,
|
borrow_reserve: &usdc_reserve,
|
||||||
dex_market: &sol_usdc_dex_market,
|
dex_market: &sol_usdc_dex_market,
|
||||||
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
|
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
|
||||||
amount: borrow_amount / 2,
|
amount: collateral_deposit_amount,
|
||||||
user_accounts_owner: &user_accounts_owner,
|
user_accounts_owner: &user_accounts_owner,
|
||||||
obligation: None,
|
obligation: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.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
|
lending_market
|
||||||
.borrow(
|
.borrow(
|
||||||
&mut banks_client,
|
&mut banks_client,
|
||||||
|
@ -90,31 +116,174 @@ async fn test_success() {
|
||||||
deposit_reserve: &sol_reserve,
|
deposit_reserve: &sol_reserve,
|
||||||
borrow_reserve: &usdc_reserve,
|
borrow_reserve: &usdc_reserve,
|
||||||
dex_market: &sol_usdc_dex_market,
|
dex_market: &sol_usdc_dex_market,
|
||||||
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
|
borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount,
|
||||||
amount: borrow_amount / 2,
|
amount: USDC_BORROW_AMOUNT_FRACTIONAL,
|
||||||
user_accounts_owner: &user_accounts_owner,
|
user_accounts_owner: &user_accounts_owner,
|
||||||
obligation: Some(obligation),
|
obligation: Some(obligation),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.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
|
let (total_fee, host_fee) = TEST_RESERVE_CONFIG
|
||||||
.fees
|
.fees
|
||||||
.calculate_borrow_fees(borrow_amount)
|
.calculate_borrow_fees(2 * collateral_deposit_amount)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(total_fee > 0);
|
assert!(total_fee > 0);
|
||||||
assert!(host_fee > 0);
|
assert!(host_fee > 0);
|
||||||
|
|
||||||
let sol_collateral_supply =
|
let collateral_supply =
|
||||||
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
|
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;
|
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;
|
let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_host).await;
|
||||||
assert_eq!(sol_host_balance, host_fee);
|
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::{
|
use spl_token_lending::{
|
||||||
instruction::{
|
instruction::{
|
||||||
borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_reserve,
|
borrow_reserve_liquidity, deposit_reserve_liquidity, init_lending_market, init_reserve,
|
||||||
BorrowAmountType,
|
liquidate_obligation, BorrowAmountType,
|
||||||
},
|
},
|
||||||
math::Decimal,
|
math::Decimal,
|
||||||
processor::process_instruction,
|
processor::process_instruction,
|
||||||
|
@ -33,7 +33,7 @@ pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig {
|
||||||
optimal_utilization_rate: 80,
|
optimal_utilization_rate: 80,
|
||||||
loan_to_value_ratio: 50,
|
loan_to_value_ratio: 50,
|
||||||
liquidation_bonus: 5,
|
liquidation_bonus: 5,
|
||||||
liquidation_threshold: 0,
|
liquidation_threshold: 50,
|
||||||
min_borrow_rate: 0,
|
min_borrow_rate: 0,
|
||||||
optimal_borrow_rate: 4,
|
optimal_borrow_rate: 4,
|
||||||
max_borrow_rate: 30,
|
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(
|
pub fn add_obligation(
|
||||||
test: &mut ProgramTest,
|
test: &mut ProgramTest,
|
||||||
user_accounts_owner: &Keypair,
|
user_accounts_owner: &Keypair,
|
||||||
lending_market: &TestLendingMarket,
|
lending_market: &TestLendingMarket,
|
||||||
borrow_reserve: &TestReserve,
|
args: AddObligationArgs,
|
||||||
collateral_reserve: &TestReserve,
|
|
||||||
collateral_amount: u64,
|
|
||||||
borrowed_liquidity_wads: Decimal,
|
|
||||||
) -> TestObligation {
|
) -> TestObligation {
|
||||||
|
let AddObligationArgs {
|
||||||
|
slots_elapsed,
|
||||||
|
borrow_reserve,
|
||||||
|
collateral_reserve,
|
||||||
|
collateral_amount,
|
||||||
|
borrowed_liquidity_wads,
|
||||||
|
} = args;
|
||||||
|
|
||||||
let token_mint_pubkey = Pubkey::new_unique();
|
let token_mint_pubkey = Pubkey::new_unique();
|
||||||
test.add_packable_account(
|
test.add_packable_account(
|
||||||
token_mint_pubkey,
|
token_mint_pubkey,
|
||||||
|
@ -183,7 +196,7 @@ pub fn add_obligation(
|
||||||
obligation_pubkey,
|
obligation_pubkey,
|
||||||
u32::MAX as u64,
|
u32::MAX as u64,
|
||||||
&Obligation {
|
&Obligation {
|
||||||
last_update_slot: 1,
|
last_update_slot: 1u64.wrapping_sub(slots_elapsed),
|
||||||
deposited_collateral_tokens: collateral_amount,
|
deposited_collateral_tokens: collateral_amount,
|
||||||
collateral_reserve: collateral_reserve.pubkey,
|
collateral_reserve: collateral_reserve.pubkey,
|
||||||
cumulative_borrow_rate_wads: Decimal::one(),
|
cumulative_borrow_rate_wads: Decimal::one(),
|
||||||
|
@ -204,6 +217,7 @@ pub fn add_obligation(
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AddReserveArgs {
|
pub struct AddReserveArgs {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub slots_elapsed: u64,
|
||||||
pub config: ReserveConfig,
|
pub config: ReserveConfig,
|
||||||
pub liquidity_amount: u64,
|
pub liquidity_amount: u64,
|
||||||
pub liquidity_mint_pubkey: Pubkey,
|
pub liquidity_mint_pubkey: Pubkey,
|
||||||
|
@ -223,6 +237,7 @@ pub fn add_reserve(
|
||||||
) -> TestReserve {
|
) -> TestReserve {
|
||||||
let AddReserveArgs {
|
let AddReserveArgs {
|
||||||
name,
|
name,
|
||||||
|
slots_elapsed,
|
||||||
config,
|
config,
|
||||||
liquidity_amount,
|
liquidity_amount,
|
||||||
liquidity_mint_pubkey,
|
liquidity_mint_pubkey,
|
||||||
|
@ -319,7 +334,7 @@ pub fn add_reserve(
|
||||||
|
|
||||||
let reserve_keypair = Keypair::new();
|
let reserve_keypair = Keypair::new();
|
||||||
let reserve_pubkey = reserve_keypair.pubkey();
|
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();
|
reserve_state.add_borrow(borrow_amount).unwrap();
|
||||||
test.add_packable_account(
|
test.add_packable_account(
|
||||||
reserve_pubkey,
|
reserve_pubkey,
|
||||||
|
@ -407,6 +422,15 @@ pub struct BorrowArgs<'a> {
|
||||||
pub obligation: Option<TestObligation>,
|
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 {
|
impl TestLendingMarket {
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
banks_client: &mut BanksClient,
|
banks_client: &mut BanksClient,
|
||||||
|
@ -489,6 +513,81 @@ impl TestLendingMarket {
|
||||||
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
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(
|
pub async fn borrow(
|
||||||
&self,
|
&self,
|
||||||
banks_client: &mut BanksClient,
|
banks_client: &mut BanksClient,
|
||||||
|
@ -509,7 +608,7 @@ impl TestLendingMarket {
|
||||||
obligation,
|
obligation,
|
||||||
} = args;
|
} = 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
|
dex_market.asks_pubkey
|
||||||
} else {
|
} else {
|
||||||
dex_market.bids_pubkey
|
dex_market.bids_pubkey
|
||||||
|
@ -591,7 +690,7 @@ impl TestLendingMarket {
|
||||||
&memory_keypair.pubkey(),
|
&memory_keypair.pubkey(),
|
||||||
0,
|
0,
|
||||||
65548,
|
65548,
|
||||||
&solana_program::system_program::id(),
|
&spl_token_lending::id(),
|
||||||
),
|
),
|
||||||
borrow_reserve_liquidity(
|
borrow_reserve_liquidity(
|
||||||
spl_token_lending::id(),
|
spl_token_lending::id(),
|
||||||
|
|
|
@ -4,32 +4,18 @@ mod helpers;
|
||||||
|
|
||||||
use helpers::*;
|
use helpers::*;
|
||||||
use solana_program_test::*;
|
use solana_program_test::*;
|
||||||
use solana_sdk::{
|
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||||
pubkey::Pubkey,
|
|
||||||
signature::{Keypair, Signer},
|
|
||||||
system_instruction::create_account,
|
|
||||||
system_program,
|
|
||||||
transaction::Transaction,
|
|
||||||
};
|
|
||||||
use spl_token::instruction::approve;
|
|
||||||
use spl_token_lending::{
|
use spl_token_lending::{
|
||||||
instruction::liquidate_obligation, math::Decimal, processor::process_instruction,
|
math::Decimal,
|
||||||
state::INITIAL_COLLATERAL_RATE,
|
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;
|
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
|
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL;
|
||||||
// price of 2210.5
|
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC;
|
||||||
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);
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_success() {
|
async fn test_success() {
|
||||||
|
@ -39,26 +25,38 @@ async fn test_success() {
|
||||||
processor!(process_instruction),
|
processor!(process_instruction),
|
||||||
);
|
);
|
||||||
|
|
||||||
const OBLIGATION_USDC_LOAN: u64 = FRACTIONAL_TO_USDC;
|
// limit to track compute unit increase
|
||||||
const OBLIGATION_SOL_COLLATERAL: u64 = INITIAL_COLLATERAL_RATE * LAMPORTS_TO_SOL;
|
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_accounts_owner = Keypair::new();
|
||||||
let user_transfer_authority = Keypair::new();
|
|
||||||
let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC);
|
let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC);
|
||||||
let usdc_mint = add_usdc_mint(&mut test);
|
let usdc_mint = add_usdc_mint(&mut test);
|
||||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
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(
|
let usdc_reserve = add_reserve(
|
||||||
&mut test,
|
&mut test,
|
||||||
&user_accounts_owner,
|
&user_accounts_owner,
|
||||||
&lending_market,
|
&lending_market,
|
||||||
AddReserveArgs {
|
AddReserveArgs {
|
||||||
config: TEST_RESERVE_CONFIG,
|
config: reserve_config,
|
||||||
|
slots_elapsed: SLOTS_PER_YEAR,
|
||||||
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
||||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||||
liquidity_mint_decimals: usdc_mint.decimals,
|
liquidity_mint_decimals: usdc_mint.decimals,
|
||||||
borrow_amount: OBLIGATION_USDC_LOAN,
|
borrow_amount: USDC_LOAN,
|
||||||
user_liquidity_amount: OBLIGATION_USDC_LOAN,
|
user_liquidity_amount: USDC_LOAN,
|
||||||
|
collateral_amount: SOL_LOAN_USDC_COLLATERAL,
|
||||||
..AddReserveArgs::default()
|
..AddReserveArgs::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -68,76 +66,94 @@ async fn test_success() {
|
||||||
&user_accounts_owner,
|
&user_accounts_owner,
|
||||||
&lending_market,
|
&lending_market,
|
||||||
AddReserveArgs {
|
AddReserveArgs {
|
||||||
config: TEST_RESERVE_CONFIG,
|
config: reserve_config,
|
||||||
|
slots_elapsed: SLOTS_PER_YEAR,
|
||||||
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
||||||
liquidity_mint_decimals: 9,
|
liquidity_mint_decimals: 9,
|
||||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||||
dex_market_pubkey: Some(sol_usdc_dex_market.pubkey),
|
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()
|
..AddReserveArgs::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let obligation = add_obligation(
|
let usdc_obligation = add_obligation(
|
||||||
&mut test,
|
&mut test,
|
||||||
&user_accounts_owner,
|
&user_accounts_owner,
|
||||||
&lending_market,
|
&lending_market,
|
||||||
&usdc_reserve,
|
AddObligationArgs {
|
||||||
&sol_reserve,
|
slots_elapsed: SLOTS_PER_YEAR,
|
||||||
OBLIGATION_SOL_COLLATERAL,
|
borrow_reserve: &usdc_reserve,
|
||||||
Decimal::from(OBLIGATION_USDC_LOAN),
|
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 sol_obligation = add_obligation(
|
||||||
|
&mut test,
|
||||||
let memory_keypair = Keypair::new();
|
&user_accounts_owner,
|
||||||
let mut transaction = Transaction::new_with_payer(
|
&lending_market,
|
||||||
&[
|
AddObligationArgs {
|
||||||
create_account(
|
slots_elapsed: SLOTS_PER_YEAR,
|
||||||
&payer.pubkey(),
|
borrow_reserve: &sol_reserve,
|
||||||
&memory_keypair.pubkey(),
|
collateral_reserve: &usdc_reserve,
|
||||||
0,
|
collateral_amount: SOL_LOAN_USDC_COLLATERAL,
|
||||||
65548,
|
borrowed_liquidity_wads: Decimal::from(SOL_LOAN),
|
||||||
&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(
|
let (mut banks_client, payer, _recent_blockhash) = test.start().await;
|
||||||
&[
|
|
||||||
|
lending_market
|
||||||
|
.liquidate(
|
||||||
|
&mut banks_client,
|
||||||
&payer,
|
&payer,
|
||||||
&memory_keypair,
|
LiquidateArgs {
|
||||||
&user_accounts_owner,
|
repay_reserve: &usdc_reserve,
|
||||||
&user_transfer_authority,
|
withdraw_reserve: &sol_reserve,
|
||||||
],
|
dex_market: &sol_usdc_dex_market,
|
||||||
recent_blockhash,
|
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,
|
&mut test,
|
||||||
&user_accounts_owner,
|
&user_accounts_owner,
|
||||||
&lending_market,
|
&lending_market,
|
||||||
&usdc_reserve,
|
AddObligationArgs {
|
||||||
&sol_reserve,
|
slots_elapsed: 0,
|
||||||
OBLIGATION_COLLATERAL,
|
borrow_reserve: &usdc_reserve,
|
||||||
Decimal::from(OBLIGATION_LOAN),
|
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;
|
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||||
|
|
Loading…
Reference in New Issue