lending: Fixes and tests for borrow and liquidate (#1104)
* lending: Move borrow calculation to reserve * lending: Test and fix borrow and liquidate * fix: allow obligations to be disabled * Test cleanup
This commit is contained in:
parent
62e6a67eb2
commit
d6571949a6
|
@ -2,14 +2,13 @@
|
|||
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
instruction::BorrowAmountType,
|
||||
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
|
||||
state::Reserve,
|
||||
math::{Decimal, TryAdd, TryDiv, TryMul, TrySub},
|
||||
state::TokenConverter,
|
||||
};
|
||||
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};
|
||||
use std::{cell::RefMut, convert::TryFrom};
|
||||
|
||||
/// Side of the dex market order book
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
@ -49,58 +48,30 @@ struct Order {
|
|||
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: DexMarketOrders<'a>,
|
||||
orders_side: Side,
|
||||
quote_token_mint: &'a Pubkey,
|
||||
buy_token_mint: &'a Pubkey,
|
||||
sell_token_mint: &'a Pubkey,
|
||||
}
|
||||
|
||||
impl<'a> TokenConverter for TradeSimulator<'a> {
|
||||
fn convert(
|
||||
self,
|
||||
from_amount: Decimal,
|
||||
from_token_mint: &Pubkey,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let action = if from_token_mint == self.buy_token_mint {
|
||||
TradeAction::Buy
|
||||
} else {
|
||||
TradeAction::Sell
|
||||
};
|
||||
|
||||
self.simulate_trade(action, from_amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TradeSimulator<'a> {
|
||||
|
@ -110,27 +81,34 @@ impl<'a> TradeSimulator<'a> {
|
|||
dex_market_orders: &AccountInfo,
|
||||
memory: &'a AccountInfo,
|
||||
quote_token_mint: &'a Pubkey,
|
||||
buy_token_mint: &'a Pubkey,
|
||||
sell_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;
|
||||
let orders = DexMarketOrders::new(&dex_market, dex_market_orders, memory)?;
|
||||
let orders_side = orders.side;
|
||||
|
||||
Ok(Self {
|
||||
dex_market,
|
||||
orders: Orders::DexMarket(dex_market_orders),
|
||||
orders,
|
||||
orders_side,
|
||||
quote_token_mint,
|
||||
buy_token_mint,
|
||||
sell_token_mint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate a trade
|
||||
pub fn simulate_trade(
|
||||
&mut self,
|
||||
mut self,
|
||||
action: TradeAction,
|
||||
quantity: Decimal,
|
||||
token_mint: &Pubkey,
|
||||
cache_orders: bool,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let token_mint = match action {
|
||||
TradeAction::Buy => self.buy_token_mint,
|
||||
TradeAction::Sell => self.sell_token_mint,
|
||||
};
|
||||
|
||||
let currency = if token_mint == self.quote_token_mint {
|
||||
Currency::Quote
|
||||
} else {
|
||||
|
@ -149,8 +127,7 @@ impl<'a> TradeSimulator<'a> {
|
|||
}
|
||||
|
||||
let input_quantity: Decimal = quantity.try_div(self.dex_market.get_lots(currency))?;
|
||||
let output_quantity =
|
||||
self.exchange_with_order_book(input_quantity, currency, cache_orders)?;
|
||||
let output_quantity = self.exchange_with_order_book(input_quantity, currency)?;
|
||||
Ok(output_quantity.try_mul(self.dex_market.get_lots(currency.opposite()))?)
|
||||
}
|
||||
|
||||
|
@ -159,14 +136,8 @@ impl<'a> TradeSimulator<'a> {
|
|||
&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 {
|
||||
|
@ -189,86 +160,10 @@ impl<'a> TradeSimulator<'a> {
|
|||
|
||||
input_quantity = input_quantity.try_sub(filled)?;
|
||||
output_quantity = output_quantity.try_add(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)
|
||||
}
|
||||
|
||||
/// Calculate amount borrowed and collateral deposited for the given reserves
|
||||
/// based on the market state and calculation type
|
||||
pub fn calculate_borrow_amounts(
|
||||
&mut self,
|
||||
deposit_reserve: &Reserve,
|
||||
borrow_reserve: &Reserve,
|
||||
amount_type: BorrowAmountType,
|
||||
amount: u64,
|
||||
) -> Result<(u64, u64), ProgramError> {
|
||||
let deposit_reserve_collateral_exchange_rate =
|
||||
deposit_reserve.collateral_exchange_rate()?;
|
||||
match amount_type {
|
||||
BorrowAmountType::LiquidityBorrowAmount => {
|
||||
let borrow_amount = amount;
|
||||
|
||||
// Simulate buying `borrow_amount` of borrow reserve underlying tokens
|
||||
// to determine how much collateral is needed
|
||||
let loan_in_deposit_underlying = self.simulate_trade(
|
||||
TradeAction::Buy,
|
||||
Decimal::from(borrow_amount),
|
||||
&borrow_reserve.liquidity.mint_pubkey,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let loan_in_deposit_collateral = deposit_reserve_collateral_exchange_rate
|
||||
.decimal_liquidity_to_collateral(loan_in_deposit_underlying)?;
|
||||
let required_deposit_collateral: Decimal = loan_in_deposit_collateral.try_div(
|
||||
Rate::from_percent(deposit_reserve.config.loan_to_value_ratio),
|
||||
)?;
|
||||
|
||||
let collateral_deposit_amount = required_deposit_collateral.try_round_u64()?;
|
||||
if collateral_deposit_amount == 0 {
|
||||
return Err(LendingError::InvalidAmount.into());
|
||||
}
|
||||
|
||||
Ok((borrow_amount, collateral_deposit_amount))
|
||||
}
|
||||
BorrowAmountType::CollateralDepositAmount => {
|
||||
let collateral_deposit_amount = amount;
|
||||
|
||||
let loan_in_deposit_collateral: Decimal = Decimal::from(collateral_deposit_amount)
|
||||
.try_mul(Rate::from_percent(
|
||||
deposit_reserve.config.loan_to_value_ratio,
|
||||
))?;
|
||||
let loan_in_deposit_underlying = deposit_reserve_collateral_exchange_rate
|
||||
.decimal_collateral_to_liquidity(loan_in_deposit_collateral)?;
|
||||
|
||||
// Simulate selling `loan_in_deposit_underlying` amount of deposit reserve underlying
|
||||
// tokens to determine how much to lend to the user
|
||||
let borrow_amount = self.simulate_trade(
|
||||
TradeAction::Sell,
|
||||
loan_in_deposit_underlying,
|
||||
&deposit_reserve.liquidity.mint_pubkey,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let borrow_amount = borrow_amount.try_round_u64()?;
|
||||
if borrow_amount == 0 {
|
||||
return Err(LendingError::InvalidAmount.into());
|
||||
}
|
||||
|
||||
Ok((borrow_amount, collateral_deposit_amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dex market order account info
|
||||
|
@ -307,6 +202,22 @@ impl<'a> DexMarketOrders<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Iterator for DexMarketOrders<'_> {
|
||||
type Item = Order;
|
||||
|
||||
fn next(&mut self) -> Option<Order> {
|
||||
let leaf_node = match self.side {
|
||||
Side::Bid => self.heap.as_mut().and_then(|heap| heap.remove_max()),
|
||||
Side::Ask => self.heap.as_mut().and_then(|heap| heap.remove_min()),
|
||||
}?;
|
||||
|
||||
Some(Order {
|
||||
price: leaf_node.price().get(),
|
||||
quantity: leaf_node.quantity(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset for dex market base mint
|
||||
pub const BASE_MINT_OFFSET: usize = 6;
|
||||
/// Offset for dex market quote mint
|
||||
|
|
|
@ -86,6 +86,9 @@ pub enum LendingError {
|
|||
/// Borrow amount too small
|
||||
#[error("Borrow amount too small")]
|
||||
BorrowTooSmall,
|
||||
/// Liquidation amount too small
|
||||
#[error("Liquidation amount too small to receive collateral")]
|
||||
LiquidationTooSmall,
|
||||
/// Reserve state stale
|
||||
#[error("Reserve state needs to be updated for the current slot")]
|
||||
ReserveStale,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
//! Program state processor
|
||||
|
||||
use crate::{
|
||||
dex_market::{DexMarket, TradeAction, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET},
|
||||
dex_market::{DexMarket, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET},
|
||||
error::LendingError,
|
||||
instruction::{BorrowAmountType, LendingInstruction},
|
||||
math::{Decimal, Rate, TryAdd, TryMul, TrySub, WAD},
|
||||
math::{Decimal, TryAdd, WAD},
|
||||
state::{
|
||||
LendingMarket, NewObligationParams, NewReserveParams, Obligation, RepayResult, Reserve,
|
||||
ReserveCollateral, ReserveConfig, ReserveLiquidity, PROGRAM_VERSION,
|
||||
LendingMarket, LiquidateResult, NewObligationParams, NewReserveParams, Obligation,
|
||||
RepayResult, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, PROGRAM_VERSION,
|
||||
},
|
||||
};
|
||||
use num_traits::FromPrimitive;
|
||||
|
@ -123,16 +123,18 @@ fn process_init_reserve(
|
|||
msg!("Optimal utilization rate must be in range [0, 100]");
|
||||
return Err(LendingError::InvalidConfig.into());
|
||||
}
|
||||
if config.loan_to_value_ratio > 90 {
|
||||
msg!("Loan to value ratio must be in range [0, 90]");
|
||||
if config.loan_to_value_ratio >= 100 {
|
||||
msg!("Loan to value ratio must be in range [0, 100)");
|
||||
return Err(LendingError::InvalidConfig.into());
|
||||
}
|
||||
if config.liquidation_bonus > 100 {
|
||||
msg!("Liquidation bonus must be in range [0, 100]");
|
||||
return Err(LendingError::InvalidConfig.into());
|
||||
}
|
||||
if config.liquidation_threshold > 100 {
|
||||
msg!("Liquidation threshold must be in range [0, 100]");
|
||||
if config.liquidation_threshold <= config.loan_to_value_ratio
|
||||
|| config.liquidation_threshold > 100
|
||||
{
|
||||
msg!("Liquidation threshold must be in range (LTV, 100]");
|
||||
return Err(LendingError::InvalidConfig.into());
|
||||
}
|
||||
if config.optimal_borrow_rate < config.min_borrow_rate {
|
||||
|
@ -588,11 +590,11 @@ fn process_withdraw(
|
|||
#[inline(never)] // avoid stack frame limit
|
||||
fn process_borrow(
|
||||
program_id: &Pubkey,
|
||||
amount: u64,
|
||||
amount_type: BorrowAmountType,
|
||||
token_amount: u64,
|
||||
token_amount_type: BorrowAmountType,
|
||||
accounts: &[AccountInfo],
|
||||
) -> ProgramResult {
|
||||
if amount == 0 {
|
||||
if token_amount == 0 {
|
||||
return Err(LendingError::InvalidAmount.into());
|
||||
}
|
||||
|
||||
|
@ -727,33 +729,27 @@ fn process_borrow(
|
|||
assert_last_update_slot(&deposit_reserve, clock.slot)?;
|
||||
obligation.accrue_interest(borrow_reserve.cumulative_borrow_rate_wads)?;
|
||||
|
||||
let mut trade_simulator = TradeSimulator::new(
|
||||
let trade_simulator = TradeSimulator::new(
|
||||
dex_market_info,
|
||||
dex_market_orders_info,
|
||||
memory,
|
||||
&lending_market.quote_token_mint,
|
||||
&borrow_reserve.liquidity.mint_pubkey,
|
||||
&deposit_reserve.liquidity.mint_pubkey,
|
||||
)?;
|
||||
|
||||
let (borrow_amount, mut collateral_deposit_amount) = trade_simulator.calculate_borrow_amounts(
|
||||
&deposit_reserve,
|
||||
&borrow_reserve,
|
||||
amount_type,
|
||||
amount,
|
||||
let loan = deposit_reserve.create_loan(
|
||||
token_amount,
|
||||
token_amount_type,
|
||||
trade_simulator,
|
||||
&borrow_reserve.liquidity.mint_pubkey,
|
||||
)?;
|
||||
|
||||
let (mut borrow_fee, host_fee) = deposit_reserve
|
||||
.config
|
||||
.fees
|
||||
.calculate_borrow_fees(collateral_deposit_amount)?;
|
||||
// update amount actually deposited
|
||||
collateral_deposit_amount -= borrow_fee;
|
||||
|
||||
borrow_reserve.liquidity.borrow(borrow_amount)?;
|
||||
|
||||
borrow_reserve.liquidity.borrow(loan.borrow_amount)?;
|
||||
obligation.borrowed_liquidity_wads = obligation
|
||||
.borrowed_liquidity_wads
|
||||
.try_add(Decimal::from(borrow_amount))?;
|
||||
obligation.deposited_collateral_tokens += collateral_deposit_amount;
|
||||
.try_add(Decimal::from(loan.borrow_amount))?;
|
||||
obligation.deposited_collateral_tokens += loan.collateral_amount;
|
||||
|
||||
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
|
||||
Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?;
|
||||
|
@ -772,20 +768,21 @@ fn process_borrow(
|
|||
spl_token_transfer(TokenTransferParams {
|
||||
source: source_collateral_info.clone(),
|
||||
destination: deposit_reserve_collateral_supply_info.clone(),
|
||||
amount: collateral_deposit_amount,
|
||||
amount: loan.collateral_amount,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
|
||||
// transfer host fees if host is specified
|
||||
let mut owner_fee = loan.origination_fee;
|
||||
if let Ok(host_fee_recipient) = next_account_info(account_info_iter) {
|
||||
if host_fee > 0 {
|
||||
borrow_fee -= host_fee;
|
||||
if loan.host_fee > 0 {
|
||||
owner_fee -= loan.host_fee;
|
||||
spl_token_transfer(TokenTransferParams {
|
||||
source: source_collateral_info.clone(),
|
||||
destination: host_fee_recipient.clone(),
|
||||
amount: host_fee,
|
||||
amount: loan.host_fee,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
|
@ -794,11 +791,11 @@ fn process_borrow(
|
|||
}
|
||||
|
||||
// transfer remaining fees to owner
|
||||
if borrow_fee > 0 {
|
||||
if owner_fee > 0 {
|
||||
spl_token_transfer(TokenTransferParams {
|
||||
source: source_collateral_info.clone(),
|
||||
destination: deposit_reserve_collateral_fees_receiver_info.clone(),
|
||||
amount: borrow_fee,
|
||||
amount: owner_fee,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
|
@ -809,7 +806,7 @@ fn process_borrow(
|
|||
spl_token_transfer(TokenTransferParams {
|
||||
source: borrow_reserve_liquidity_supply_info.clone(),
|
||||
destination: destination_liquidity_info.clone(),
|
||||
amount: borrow_amount,
|
||||
amount: loan.borrow_amount,
|
||||
authority: lending_market_authority_info.clone(),
|
||||
authority_signer_seeds,
|
||||
token_program: token_program_id.clone(),
|
||||
|
@ -819,7 +816,7 @@ fn process_borrow(
|
|||
spl_token_mint_to(TokenMintToParams {
|
||||
mint: obligation_token_mint_info.clone(),
|
||||
destination: obligation_token_output_info.clone(),
|
||||
amount: collateral_deposit_amount,
|
||||
amount: loan.collateral_amount,
|
||||
authority: lending_market_authority_info.clone(),
|
||||
authority_signer_seeds,
|
||||
token_program: token_program_id.clone(),
|
||||
|
@ -1097,66 +1094,33 @@ fn process_liquidate(
|
|||
assert_last_update_slot(&withdraw_reserve, clock.slot)?;
|
||||
obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?;
|
||||
|
||||
let mut trade_simulator = TradeSimulator::new(
|
||||
let trade_simulator = TradeSimulator::new(
|
||||
dex_market_info,
|
||||
dex_market_orders_info,
|
||||
memory,
|
||||
&lending_market.quote_token_mint,
|
||||
&withdraw_reserve.liquidity.mint_pubkey,
|
||||
&repay_reserve.liquidity.mint_pubkey,
|
||||
)?;
|
||||
|
||||
// calculate obligation health
|
||||
let withdraw_reserve_collateral_exchange_rate = withdraw_reserve.collateral_exchange_rate()?;
|
||||
let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate
|
||||
.decimal_liquidity_to_collateral(trade_simulator.simulate_trade(
|
||||
TradeAction::Sell,
|
||||
obligation.borrowed_liquidity_wads,
|
||||
&repay_reserve.liquidity.mint_pubkey,
|
||||
true,
|
||||
)?)?
|
||||
.try_round_u64()?;
|
||||
|
||||
if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens
|
||||
< withdraw_reserve.config.liquidation_threshold as u64
|
||||
{
|
||||
return Err(LendingError::HealthyObligation.into());
|
||||
}
|
||||
|
||||
// calculate the amount of liquidity that will be repaid
|
||||
let close_factor = Rate::from_percent(50);
|
||||
let decimal_repay_amount = Decimal::from(liquidity_amount)
|
||||
.min(obligation.borrowed_liquidity_wads.try_mul(close_factor)?);
|
||||
let integer_repay_amount = decimal_repay_amount.try_round_u64()?;
|
||||
if integer_repay_amount == 0 {
|
||||
return Err(LendingError::ObligationTooSmall.into());
|
||||
}
|
||||
let LiquidateResult {
|
||||
bonus_amount,
|
||||
withdraw_amount,
|
||||
integer_repay_amount,
|
||||
decimal_repay_amount,
|
||||
} = withdraw_reserve.liquidate_obligation(
|
||||
&obligation,
|
||||
liquidity_amount,
|
||||
&repay_reserve.liquidity.mint_pubkey,
|
||||
trade_simulator,
|
||||
)?;
|
||||
repay_reserve
|
||||
.liquidity
|
||||
.repay(integer_repay_amount, decimal_repay_amount)?;
|
||||
|
||||
// TODO: check math precision
|
||||
// calculate the amount of collateral that will be withdrawn
|
||||
|
||||
let withdraw_liquidity_amount = trade_simulator.simulate_trade(
|
||||
TradeAction::Sell,
|
||||
decimal_repay_amount,
|
||||
&repay_reserve.liquidity.mint_pubkey,
|
||||
false,
|
||||
)?;
|
||||
let withdraw_amount_as_collateral = withdraw_reserve_collateral_exchange_rate
|
||||
.decimal_liquidity_to_collateral(withdraw_liquidity_amount)?
|
||||
.try_round_u64()?;
|
||||
let liquidation_bonus_amount =
|
||||
withdraw_amount_as_collateral * (withdraw_reserve.config.liquidation_bonus as u64) / 100;
|
||||
let collateral_withdraw_amount = obligation
|
||||
.deposited_collateral_tokens
|
||||
.min(withdraw_amount_as_collateral + liquidation_bonus_amount);
|
||||
|
||||
Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
|
||||
|
||||
obligation.borrowed_liquidity_wads = obligation
|
||||
.borrowed_liquidity_wads
|
||||
.try_sub(decimal_repay_amount)?;
|
||||
obligation.deposited_collateral_tokens -= collateral_withdraw_amount;
|
||||
obligation.liquidate(decimal_repay_amount, withdraw_amount + bonus_amount)?;
|
||||
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
|
||||
|
||||
let authority_signer_seeds = &[
|
||||
|
@ -1183,12 +1147,24 @@ fn process_liquidate(
|
|||
spl_token_transfer(TokenTransferParams {
|
||||
source: withdraw_reserve_collateral_supply_info.clone(),
|
||||
destination: destination_collateral_info.clone(),
|
||||
amount: collateral_withdraw_amount,
|
||||
amount: withdraw_amount,
|
||||
authority: lending_market_authority_info.clone(),
|
||||
authority_signer_seeds,
|
||||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
|
||||
// pay bonus collateral
|
||||
if bonus_amount > 0 {
|
||||
spl_token_transfer(TokenTransferParams {
|
||||
source: withdraw_reserve_collateral_supply_info.clone(),
|
||||
destination: destination_collateral_info.clone(),
|
||||
amount: bonus_amount,
|
||||
authority: lending_market_authority_info.clone(),
|
||||
authority_signer_seeds,
|
||||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,16 @@ pub const UNINITIALIZED_VERSION: u8 = 0;
|
|||
pub const SLOTS_PER_YEAR: u64 =
|
||||
DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365;
|
||||
|
||||
/// Token converter
|
||||
pub trait TokenConverter {
|
||||
/// Convert between two different tokens
|
||||
fn convert(
|
||||
self,
|
||||
from_amount: Decimal,
|
||||
from_token_mint: &Pubkey,
|
||||
) -> Result<Decimal, ProgramError>;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
|
||||
let (tag, body) = mut_array_refs![dst, 4, 32];
|
||||
|
|
|
@ -70,6 +70,19 @@ impl Obligation {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Liquidate part of obligation
|
||||
pub fn liquidate(
|
||||
&mut self,
|
||||
repay_amount: Decimal,
|
||||
withdraw_amount: u64,
|
||||
) -> Result<(), ProgramError> {
|
||||
self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?;
|
||||
self.deposited_collateral_tokens
|
||||
.checked_sub(withdraw_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repay borrowed tokens
|
||||
pub fn repay(
|
||||
&mut self,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
instruction::BorrowAmountType,
|
||||
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
|
||||
};
|
||||
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
|
||||
|
@ -14,6 +15,9 @@ use solana_program::{
|
|||
};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
/// Percentage of an obligation that can be repaid during each liquidation call
|
||||
pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50;
|
||||
|
||||
/// Lending market reserve state
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Reserve {
|
||||
|
@ -85,6 +89,155 @@ impl Reserve {
|
|||
}
|
||||
}
|
||||
|
||||
/// Liquidate part of an unhealthy obligation
|
||||
pub fn liquidate_obligation(
|
||||
&self,
|
||||
obligation: &Obligation,
|
||||
liquidity_amount: u64,
|
||||
liquidity_token_mint: &Pubkey,
|
||||
token_converter: impl TokenConverter,
|
||||
) -> Result<LiquidateResult, ProgramError> {
|
||||
// calculate the amount of liquidity that will be repaid
|
||||
let max_repayable = obligation
|
||||
.borrowed_liquidity_wads
|
||||
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
|
||||
.try_floor_u64()?;
|
||||
let integer_repay_amount = liquidity_amount.min(max_repayable);
|
||||
if integer_repay_amount == 0 {
|
||||
return Err(LendingError::ObligationTooSmall.into());
|
||||
}
|
||||
|
||||
// calculate the amount of collateral that will be received
|
||||
let decimal_repay_amount = Decimal::from(integer_repay_amount);
|
||||
let withdraw_amount_in_liquidity =
|
||||
token_converter.convert(decimal_repay_amount, liquidity_token_mint)?;
|
||||
let withdraw_amount = self
|
||||
.collateral_exchange_rate()?
|
||||
.decimal_liquidity_to_collateral(withdraw_amount_in_liquidity)?;
|
||||
if withdraw_amount == Decimal::zero() {
|
||||
return Err(LendingError::LiquidationTooSmall.into());
|
||||
}
|
||||
|
||||
// When the value of the loan (obligation.borrowed_liquidity_wads) divided by the value of
|
||||
// the collateral (collateral_token_price * obligation.deposited_collateral_tokens) is less
|
||||
// than the liquidation threshold, the loan is healthy and cannot be liquidated.
|
||||
let collateral_token_price = decimal_repay_amount.try_div(withdraw_amount)?;
|
||||
let obligation_loan_to_value = obligation
|
||||
.borrowed_liquidity_wads
|
||||
.try_div(collateral_token_price)?
|
||||
.try_div(obligation.deposited_collateral_tokens)?;
|
||||
let liquidation_threshold = Rate::from_percent(self.config.liquidation_threshold);
|
||||
if obligation_loan_to_value <= liquidation_threshold.into() {
|
||||
return Err(LendingError::HealthyObligation.into());
|
||||
}
|
||||
|
||||
// Don't pay out bonus if withdraw amount covers full collateral balance
|
||||
let (withdraw_amount, bonus_amount) =
|
||||
if withdraw_amount >= obligation.deposited_collateral_tokens.into() {
|
||||
(obligation.deposited_collateral_tokens, 0)
|
||||
} else {
|
||||
let withdraw_amount = withdraw_amount.try_floor_u64()?;
|
||||
let liquidation_bonus_rate = Rate::from_percent(self.config.liquidation_bonus);
|
||||
let bonus_amount = Decimal::from(liquidation_bonus_rate)
|
||||
.try_mul(withdraw_amount)?
|
||||
.try_floor_u64()?;
|
||||
let remaining_collateral = obligation.deposited_collateral_tokens - withdraw_amount;
|
||||
let bonus_amount = bonus_amount.min(remaining_collateral);
|
||||
(withdraw_amount, bonus_amount)
|
||||
};
|
||||
|
||||
Ok(LiquidateResult {
|
||||
bonus_amount,
|
||||
withdraw_amount,
|
||||
integer_repay_amount,
|
||||
decimal_repay_amount,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create new loan
|
||||
pub fn create_loan(
|
||||
&self,
|
||||
token_amount: u64,
|
||||
token_amount_type: BorrowAmountType,
|
||||
token_converter: impl TokenConverter,
|
||||
borrow_amount_token_mint: &Pubkey,
|
||||
) -> Result<LoanResult, ProgramError> {
|
||||
let (borrow_amount, mut collateral_amount) = match token_amount_type {
|
||||
BorrowAmountType::CollateralDepositAmount => {
|
||||
let collateral_amount = token_amount;
|
||||
let borrow_amount =
|
||||
self.allowed_borrow_for_collateral(collateral_amount, token_converter)?;
|
||||
if borrow_amount == 0 {
|
||||
return Err(LendingError::InvalidAmount.into());
|
||||
}
|
||||
(borrow_amount, collateral_amount)
|
||||
}
|
||||
BorrowAmountType::LiquidityBorrowAmount => {
|
||||
let borrow_amount = token_amount;
|
||||
let collateral_amount = self.required_collateral_for_borrow(
|
||||
borrow_amount,
|
||||
borrow_amount_token_mint,
|
||||
token_converter,
|
||||
)?;
|
||||
if collateral_amount == 0 {
|
||||
return Err(LendingError::InvalidAmount.into());
|
||||
}
|
||||
(borrow_amount, collateral_amount)
|
||||
}
|
||||
};
|
||||
|
||||
let (origination_fee, host_fee) =
|
||||
self.config.fees.calculate_borrow_fees(collateral_amount)?;
|
||||
|
||||
collateral_amount = collateral_amount
|
||||
.checked_sub(origination_fee)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
|
||||
Ok(LoanResult {
|
||||
borrow_amount,
|
||||
collateral_amount,
|
||||
origination_fee,
|
||||
host_fee,
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate allowed borrow for collateral
|
||||
pub fn allowed_borrow_for_collateral(
|
||||
&self,
|
||||
collateral_amount: u64,
|
||||
converter: impl TokenConverter,
|
||||
) -> Result<u64, ProgramError> {
|
||||
let collateral_exchange_rate = self.collateral_exchange_rate()?;
|
||||
let collateral_amount = Decimal::from(collateral_amount)
|
||||
.try_mul(Rate::from_percent(self.config.loan_to_value_ratio))?;
|
||||
let liquidity_amount =
|
||||
collateral_exchange_rate.decimal_collateral_to_liquidity(collateral_amount)?;
|
||||
|
||||
let borrow_amount = converter
|
||||
.convert(liquidity_amount, &self.liquidity.mint_pubkey)?
|
||||
.try_floor_u64()?;
|
||||
|
||||
Ok(borrow_amount)
|
||||
}
|
||||
|
||||
/// Calculate required collateral for borrow
|
||||
pub fn required_collateral_for_borrow(
|
||||
&self,
|
||||
borrow_amount: u64,
|
||||
borrow_amount_token_mint: &Pubkey,
|
||||
converter: impl TokenConverter,
|
||||
) -> Result<u64, ProgramError> {
|
||||
let collateral_exchange_rate = self.collateral_exchange_rate()?;
|
||||
let liquidity_amount =
|
||||
converter.convert(Decimal::from(borrow_amount), borrow_amount_token_mint)?;
|
||||
let collateral_amount = collateral_exchange_rate
|
||||
.decimal_liquidity_to_collateral(liquidity_amount)?
|
||||
.try_div(Rate::from_percent(self.config.loan_to_value_ratio))?
|
||||
.try_ceil_u64()?;
|
||||
|
||||
Ok(collateral_amount)
|
||||
}
|
||||
|
||||
/// Record deposited liquidity and return amount of collateral tokens to mint
|
||||
pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result<u64, ProgramError> {
|
||||
let collateral_exchange_rate = self.collateral_exchange_rate()?;
|
||||
|
@ -173,6 +326,31 @@ pub struct NewReserveParams {
|
|||
pub config: ReserveConfig,
|
||||
}
|
||||
|
||||
/// Create loan result
|
||||
pub struct LoanResult {
|
||||
/// Approved borrow amount
|
||||
pub borrow_amount: u64,
|
||||
/// Required collateral amount
|
||||
pub collateral_amount: u64,
|
||||
/// Loan origination fee
|
||||
pub origination_fee: u64,
|
||||
/// Host fee portion of origination fee
|
||||
pub host_fee: u64,
|
||||
}
|
||||
|
||||
/// Liquidate obligation result
|
||||
#[derive(Debug)]
|
||||
pub struct LiquidateResult {
|
||||
/// Amount of collateral to receive as bonus
|
||||
pub bonus_amount: u64,
|
||||
/// Amount of collateral to withdraw in exchange for repay amount
|
||||
pub withdraw_amount: u64,
|
||||
/// Amount that will be repaid as precise decimal
|
||||
pub decimal_repay_amount: Decimal,
|
||||
/// Amount that will be repaid as u64
|
||||
pub integer_repay_amount: u64,
|
||||
}
|
||||
|
||||
/// Reserve liquidity
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ReserveLiquidity {
|
||||
|
@ -284,6 +462,7 @@ impl ReserveCollateral {
|
|||
}
|
||||
|
||||
/// Collateral exchange rate
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CollateralExchangeRate(Rate);
|
||||
|
||||
impl CollateralExchangeRate {
|
||||
|
@ -544,12 +723,47 @@ impl Pack for Reserve {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::math::WAD;
|
||||
use crate::math::{PERCENT_SCALER, WAD};
|
||||
use proptest::prelude::*;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const MAX_LIQUIDITY: u64 = u64::MAX / 5;
|
||||
|
||||
struct MockConverter(Decimal);
|
||||
impl TokenConverter for MockConverter {
|
||||
fn convert(
|
||||
self,
|
||||
from_amount: Decimal,
|
||||
_from_token_mint: &Pubkey,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
from_amount.try_mul(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loan to value ratio
|
||||
fn loan_to_value_ratio(
|
||||
obligation: &Obligation,
|
||||
borrow_token_price: Decimal,
|
||||
collateral_exchange_rate: CollateralExchangeRate,
|
||||
) -> Result<Rate, ProgramError> {
|
||||
let borrow_value = borrow_token_price.try_mul(obligation.borrowed_liquidity_wads)?;
|
||||
let collateral_value = collateral_exchange_rate.decimal_collateral_to_liquidity(
|
||||
Decimal::from(obligation.deposited_collateral_tokens),
|
||||
)?;
|
||||
borrow_value.try_div(collateral_value)?.try_into()
|
||||
}
|
||||
|
||||
/// Convert reserve liquidity tokens to the collateral tokens of another reserve
|
||||
fn liquidity_in_other_collateral(
|
||||
liquidity_amount: u64,
|
||||
collateral_exchange_rate: CollateralExchangeRate,
|
||||
conversion_rate: Decimal,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
collateral_exchange_rate.decimal_liquidity_to_collateral(
|
||||
Decimal::from(liquidity_amount).try_mul(conversion_rate)?,
|
||||
)
|
||||
}
|
||||
|
||||
// Creates rates (min, opt, max) where 0 <= min <= opt <= max <= MAX
|
||||
prop_compose! {
|
||||
fn borrow_rates()(optimal_rate in 0..=u8::MAX)(
|
||||
|
@ -562,6 +776,84 @@ mod test {
|
|||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn liquidate_obligation(
|
||||
obligation_loan in 0..=u64::MAX,
|
||||
obligation_collateral in 0..=u64::MAX,
|
||||
liquidate_amount in 0..=u64::MAX,
|
||||
collateral_exchange_rate in PERCENT_SCALER..=5 * WAD,
|
||||
token_conversion_rate in 0..=u64::MAX as u128,
|
||||
liquidation_bonus in 0..=100u8,
|
||||
liquidation_threshold in 2..=100u8,
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from(obligation_loan);
|
||||
let obligation = Obligation {
|
||||
deposited_collateral_tokens: obligation_collateral,
|
||||
borrowed_liquidity_wads,
|
||||
..Obligation::default()
|
||||
};
|
||||
let total_liquidity = 1_000_000;
|
||||
let collateral_token_supply = Decimal::from(total_liquidity)
|
||||
.try_mul(Rate::from_scaled_val(collateral_exchange_rate))?
|
||||
.try_floor_u64()?;
|
||||
|
||||
let reserve = Reserve {
|
||||
collateral: ReserveCollateral {
|
||||
mint_total_supply: collateral_token_supply,
|
||||
..ReserveCollateral::default()
|
||||
},
|
||||
liquidity: ReserveLiquidity {
|
||||
available_amount: total_liquidity,
|
||||
..ReserveLiquidity::default()
|
||||
},
|
||||
config: ReserveConfig {
|
||||
liquidation_threshold,
|
||||
liquidation_bonus,
|
||||
..ReserveConfig::default()
|
||||
},
|
||||
..Reserve::default()
|
||||
};
|
||||
|
||||
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate);
|
||||
let liquidate_result = reserve.liquidate_obligation(
|
||||
&obligation,
|
||||
liquidate_amount,
|
||||
&Pubkey::default(),
|
||||
MockConverter(conversion_rate)
|
||||
);
|
||||
|
||||
let collateral_exchange_rate = reserve.collateral_exchange_rate()?;
|
||||
let obligation_ltv = loan_to_value_ratio(&obligation, conversion_rate, collateral_exchange_rate)?;
|
||||
if obligation_ltv <= Rate::from_percent(liquidation_threshold) {
|
||||
assert_eq!(
|
||||
liquidate_result.unwrap_err(),
|
||||
LendingError::HealthyObligation.into()
|
||||
);
|
||||
} else {
|
||||
let liquidate_result = liquidate_result.unwrap();
|
||||
assert!(
|
||||
Decimal::from(liquidate_result.withdraw_amount) <=
|
||||
liquidity_in_other_collateral(
|
||||
liquidate_result.integer_repay_amount,
|
||||
collateral_exchange_rate,
|
||||
conversion_rate,
|
||||
)?
|
||||
);
|
||||
assert!(
|
||||
Decimal::from(liquidate_result.integer_repay_amount) <=
|
||||
obligation.borrowed_liquidity_wads.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
|
||||
);
|
||||
assert!(
|
||||
Decimal::from(liquidate_result.bonus_amount) <=
|
||||
Decimal::from(liquidate_result.withdraw_amount).try_mul(Rate::from_percent(liquidation_bonus))?
|
||||
);
|
||||
assert!(
|
||||
liquidate_result.withdraw_amount + liquidate_result.bonus_amount <=
|
||||
obligation.deposited_collateral_tokens
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_borrow_rate(
|
||||
total_liquidity in 0..=MAX_LIQUIDITY,
|
||||
|
@ -611,6 +903,121 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowed_borrow_for_collateral(
|
||||
collateral_amount in 0..=u32::MAX as u64,
|
||||
collateral_exchange_rate in PERCENT_SCALER..=5 * WAD,
|
||||
token_conversion_rate in 0..=u64::MAX as u128,
|
||||
loan_to_value_ratio in 1..100u8,
|
||||
) {
|
||||
let total_liquidity = 1_000_000;
|
||||
let collateral_token_supply = Decimal::from(total_liquidity)
|
||||
.try_mul(Rate::from_scaled_val(collateral_exchange_rate))?
|
||||
.try_round_u64()?;
|
||||
let reserve = Reserve {
|
||||
collateral: ReserveCollateral {
|
||||
mint_total_supply: collateral_token_supply,
|
||||
..ReserveCollateral::default()
|
||||
},
|
||||
liquidity: ReserveLiquidity {
|
||||
available_amount: total_liquidity,
|
||||
..ReserveLiquidity::default()
|
||||
},
|
||||
config: ReserveConfig {
|
||||
loan_to_value_ratio,
|
||||
..ReserveConfig::default()
|
||||
},
|
||||
..Reserve::default()
|
||||
};
|
||||
|
||||
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate);
|
||||
let borrow_amount = reserve.allowed_borrow_for_collateral(
|
||||
collateral_amount,
|
||||
MockConverter(conversion_rate)
|
||||
)?;
|
||||
|
||||
// Allowed borrow should be conservatively low and therefore slightly less or equal
|
||||
// to the precise equivalent for the given collateral. When it is converted back
|
||||
// to collateral, the returned value should be equal or lower than the original
|
||||
// collateral amount.
|
||||
let collateral_amount_lower = reserve.required_collateral_for_borrow(
|
||||
borrow_amount,
|
||||
&Pubkey::default(),
|
||||
MockConverter(Decimal::one().try_div(conversion_rate)?)
|
||||
)?;
|
||||
|
||||
// After incrementing the allowed borrow, its value should be slightly more or equal
|
||||
// to the precise equivalent of the original collateral. When it is converted back
|
||||
// to collateral, the returned value should be equal or higher than the original
|
||||
// collateral amount since required collateral should be conservatively high.
|
||||
let collateral_amount_upper = reserve.required_collateral_for_borrow(
|
||||
borrow_amount + 1,
|
||||
&Pubkey::default(),
|
||||
MockConverter(Decimal::one().try_div(conversion_rate)?)
|
||||
)?;
|
||||
|
||||
// Assert that reversing the calculation returns approx original amount
|
||||
assert!(collateral_amount >= collateral_amount_lower);
|
||||
assert!(collateral_amount <= collateral_amount_upper);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_collateral_for_borrow(
|
||||
borrow_amount in 0..=u32::MAX as u64,
|
||||
collateral_exchange_rate in PERCENT_SCALER..=5 * WAD,
|
||||
token_conversion_rate in 0..=u64::MAX as u128,
|
||||
loan_to_value_ratio in 1..=100u8,
|
||||
) {
|
||||
let total_liquidity = 1_000_000;
|
||||
let collateral_token_supply = Decimal::from(total_liquidity)
|
||||
.try_mul(Rate::from_scaled_val(collateral_exchange_rate))?
|
||||
.try_round_u64()?;
|
||||
let reserve = Reserve {
|
||||
collateral: ReserveCollateral {
|
||||
mint_total_supply: collateral_token_supply,
|
||||
..ReserveCollateral::default()
|
||||
},
|
||||
liquidity: ReserveLiquidity {
|
||||
available_amount: total_liquidity,
|
||||
..ReserveLiquidity::default()
|
||||
},
|
||||
config: ReserveConfig {
|
||||
loan_to_value_ratio,
|
||||
..ReserveConfig::default()
|
||||
},
|
||||
..Reserve::default()
|
||||
};
|
||||
|
||||
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate);
|
||||
let collateral_amount = reserve.required_collateral_for_borrow(
|
||||
borrow_amount,
|
||||
&Pubkey::default(),
|
||||
MockConverter(conversion_rate)
|
||||
)?;
|
||||
|
||||
// Required collateral should be conservatively high and therefore slightly more or equal
|
||||
// to the precise equivalent for the desired borrow amount. When it is converted back
|
||||
// to borrow, the returned value should be equal or higher than the original
|
||||
// borrow amount.
|
||||
let borrow_amount_upper = reserve.allowed_borrow_for_collateral(
|
||||
collateral_amount,
|
||||
MockConverter(Decimal::one().try_div(conversion_rate)?)
|
||||
)?;
|
||||
|
||||
// After decrementing the required collateral, its value should be slightly less or equal
|
||||
// to the precise equivalent of the original borrow. When it is converted back
|
||||
// to borrow, the returned value should be equal or lower than the original
|
||||
// borrow amount since allowed borrow should be conservatively low.
|
||||
let borrow_amount_lower = reserve.allowed_borrow_for_collateral(
|
||||
collateral_amount.saturating_sub(1),
|
||||
MockConverter(Decimal::one().try_div(conversion_rate)?)
|
||||
)?;
|
||||
|
||||
// Assert that reversing the calculation returns approx original amount
|
||||
assert!(borrow_amount >= borrow_amount_lower);
|
||||
assert!(borrow_amount <= borrow_amount_upper);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_utilization_rate(
|
||||
total_liquidity in 0..=MAX_LIQUIDITY,
|
||||
|
|
|
@ -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: 50,
|
||||
liquidation_threshold: 55,
|
||||
min_borrow_rate: 0,
|
||||
optimal_borrow_rate: 4,
|
||||
max_borrow_rate: 30,
|
||||
|
|
|
@ -24,7 +24,7 @@ async fn test_success() {
|
|||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(90_000);
|
||||
test.set_bpf_compute_max_units(101_000);
|
||||
|
||||
// set loan values to about 90% of collateral value so that it gets liquidated
|
||||
const USDC_LOAN: u64 = 2 * FRACTIONAL_TO_USDC;
|
||||
|
@ -142,7 +142,7 @@ async fn test_success() {
|
|||
usdc_liquidated,
|
||||
usdc_loan_state
|
||||
.borrowed_liquidity_wads
|
||||
.try_round_u64()
|
||||
.try_floor_u64()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
|
@ -155,7 +155,7 @@ async fn test_success() {
|
|||
sol_liquidated,
|
||||
sol_loan_state
|
||||
.borrowed_liquidity_wads
|
||||
.try_round_u64()
|
||||
.try_floor_u64()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue