lending: Use fair obligation health factor calculation (#1119)

* lending: Use fair health factor calulation and handle dust

* ci: fix github action caching
This commit is contained in:
Justin Starry 2021-01-28 15:56:07 +08:00 committed by GitHub
parent 6b4e395959
commit f61d7a89a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 365 additions and 193 deletions

View File

@ -109,16 +109,12 @@ jobs:
path: |
~/.cargo/bin/rustfilt
key: cargo-bpf-bins-${{ runner.os }}
restore-keys: |
cargo-bpf-bins-${{ runner.os }}-
- uses: actions/cache@v2
with:
path: |
~/.cache
key: solana-${{ env.SOLANA_VERSION }}
restore-keys: |
solana-
- name: Install dependencies
run: |

View File

@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
source ./ci/rust-version.sh stable
source ./ci/solana-version.sh install
source ./ci/solana-version.sh
export RUSTFLAGS="-D warnings"
export RUSTBACKTRACE=1

View File

@ -14,17 +14,17 @@
if [[ -n $SOLANA_VERSION ]]; then
solana_version="$SOLANA_VERSION"
else
solana_version=v1.5.0
solana_version=v1.5.5
fi
export solana_version="$solana_version"
export solana_docker_image=solanalabs/solana:"$solana_version"
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
if [[ -n $1 ]]; then
case $1 in
install)
sh -c "$(curl -sSfL https://release.solana.com/$solana_version/install)"
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
solana --version
;;
*)

View File

@ -6,7 +6,7 @@ use crate::{
state::TokenConverter,
};
use arrayref::{array_refs, mut_array_refs};
use serum_dex::critbit::Slab;
use serum_dex::critbit::{Slab, SlabView};
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
use std::{cell::RefMut, convert::TryFrom};
@ -59,6 +59,43 @@ pub struct TradeSimulator<'a> {
}
impl<'a> TokenConverter for TradeSimulator<'a> {
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError> {
let action = if token_mint == self.buy_token_mint {
TradeAction::Buy
} else {
TradeAction::Sell
};
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 best_order_price = self
.orders
.best_order_price()
.ok_or(LendingError::TradeSimulationError)?;
let input_token = Decimal::one().try_div(self.dex_market.get_lots(currency))?;
let output_token_price = if currency == Currency::Base {
input_token.try_mul(best_order_price)
} else {
input_token.try_div(best_order_price)
}?;
Ok(output_token_price.try_mul(self.dex_market.get_lots(currency.opposite()))?)
}
fn convert(
self,
from_amount: Decimal,
@ -200,6 +237,18 @@ impl<'a> DexMarketOrders<'a> {
Ok(Self { heap, side })
}
fn best_order_price(&mut self) -> Option<u64> {
let side = self.side;
self.heap.as_mut().and_then(|heap| {
let handle = match side {
Side::Bid => heap.find_max(),
Side::Ask => heap.find_min(),
}?;
Some(heap.get_mut(handle)?.as_leaf_mut()?.price().get())
})
}
}
impl Iterator for DexMarketOrders<'_> {

View File

@ -7,6 +7,7 @@ use thiserror::Error;
/// Errors that may be returned by the TokenLending program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum LendingError {
// 0
/// Invalid instruction data passed in.
#[error("Failed to unpack instruction data")]
InstructionUnpackError,
@ -22,6 +23,8 @@ pub enum LendingError {
/// Expected a different market owner
#[error("Market owner is invalid")]
InvalidMarketOwner,
// 5
/// The owner of the input isn't set to the program address generated by the program.
#[error("Input account owner is not the program address")]
InvalidAccountOwner,
@ -37,6 +40,8 @@ pub enum LendingError {
/// Invalid amount, must be greater than zero
#[error("Input amount is invalid")]
InvalidAmount,
// 10
/// Invalid config value
#[error("Input config value is invalid")]
InvalidConfig,
@ -53,6 +58,7 @@ pub enum LendingError {
#[error("Interest rate is negative")]
NegativeInterestRate,
// 15
/// Memory is too small
#[error("Memory is too small")]
MemoryTooSmall,
@ -68,6 +74,8 @@ pub enum LendingError {
/// Insufficient liquidity available
#[error("Insufficient liquidity available")]
InsufficientLiquidity,
// 20
/// This reserve's collateral cannot be used for borrows
#[error("Input reserve has collateral disabled")]
ReserveCollateralDisabled,
@ -77,12 +85,14 @@ pub enum LendingError {
/// Input reserves cannot use the same liquidity mint
#[error("Input reserves cannot use the same liquidity mint")]
DuplicateReserveMint,
/// Obligation amount is too small to pay off
#[error("Obligation amount is too small to pay off")]
ObligationTooSmall,
/// Obligation amount is empty
#[error("Obligation amount is empty")]
ObligationEmpty,
/// Cannot liquidate healthy obligations
#[error("Cannot liquidate healthy obligations")]
HealthyObligation,
// 25
/// Borrow amount too small
#[error("Borrow amount too small")]
BorrowTooSmall,
@ -92,7 +102,6 @@ pub enum LendingError {
/// Reserve state stale
#[error("Reserve state needs to be updated for the current slot")]
ReserveStale,
/// Trade simulation error
#[error("Trade simulation error")]
TradeSimulationError,
@ -100,6 +109,7 @@ pub enum LendingError {
#[error("Invalid dex order book side")]
DexInvalidOrderBookSide,
// 30
/// Token initialize mint failed
#[error("Token initialize mint failed")]
TokenInitializeMintFailed,

View File

@ -871,6 +871,9 @@ fn process_repay(
msg!("Invalid withdraw reserve account");
return Err(LendingError::InvalidAccountInput.into());
}
if obligation.deposited_collateral_tokens == 0 {
return Err(LendingError::ObligationEmpty.into());
}
let obligation_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?;
if &obligation.token_mint != obligation_token_mint_info.key {
@ -1030,6 +1033,9 @@ fn process_liquidate(
msg!("Invalid withdraw reserve account");
return Err(LendingError::InvalidAccountInput.into());
}
if obligation.deposited_collateral_tokens == 0 {
return Err(LendingError::ObligationEmpty.into());
}
let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?;
if repay_reserve_info.owner != program_id {
@ -1104,7 +1110,6 @@ fn process_liquidate(
)?;
let LiquidateResult {
bonus_amount,
withdraw_amount,
repay_amount,
settle_amount,
@ -1118,7 +1123,7 @@ fn process_liquidate(
repay_reserve.liquidity.repay(repay_amount, settle_amount)?;
Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
obligation.liquidate(settle_amount, withdraw_amount, bonus_amount)?;
obligation.liquidate(settle_amount, withdraw_amount)?;
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
let authority_signer_seeds = &[
@ -1151,18 +1156,6 @@ fn process_liquidate(
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(())
}

View File

@ -34,6 +34,9 @@ pub const SLOTS_PER_YEAR: u64 =
/// Token converter
pub trait TokenConverter {
/// Return best price for specified token
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError>;
/// Convert between two different tokens
fn convert(
self,

View File

@ -52,6 +52,37 @@ impl Obligation {
}
}
/// Maximum amount of loan that can be closed out by a liquidator due
/// to the remaining balance being too small to be liquidated normally.
pub fn max_closeable_amount(&self) -> Result<u64, ProgramError> {
if self.borrowed_liquidity_wads < Decimal::from(CLOSEABLE_AMOUNT) {
self.borrowed_liquidity_wads.try_ceil_u64()
} else {
Ok(0)
}
}
/// Maximum amount of loan that can be repaid by liquidators
pub fn max_liquidation_amount(&self) -> Result<u64, ProgramError> {
Ok(self
.borrowed_liquidity_wads
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
.try_floor_u64()?)
}
/// Ratio of loan balance to collateral value
pub fn loan_to_value(
&self,
collateral_exchange_rate: CollateralExchangeRate,
borrow_token_price: Decimal,
) -> Result<Decimal, ProgramError> {
let loan = self.borrowed_liquidity_wads;
let collateral_value = collateral_exchange_rate
.decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())?
.try_div(borrow_token_price)?;
loan.try_div(collateral_value)
}
/// Accrue interest
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
@ -72,15 +103,10 @@ impl Obligation {
}
/// Liquidate part of obligation
pub fn liquidate(
&mut self,
settle_amount: Decimal,
withdraw_amount: u64,
bonus_amount: u64,
) -> ProgramResult {
pub fn liquidate(&mut self, settle_amount: Decimal, withdraw_amount: u64) -> ProgramResult {
self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(settle_amount)?;
self.deposited_collateral_tokens
.checked_sub(withdraw_amount + bonus_amount)
.checked_sub(withdraw_amount)
.ok_or(LendingError::MathOverflow)?;
Ok(())
}
@ -95,7 +121,7 @@ impl Obligation {
Decimal::from(liquidity_amount).min(self.borrowed_liquidity_wads);
let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?;
if integer_repay_amount == 0 {
return Err(LendingError::ObligationTooSmall.into());
return Err(LendingError::ObligationEmpty.into());
}
let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?;

View File

@ -18,6 +18,9 @@ use std::convert::{TryFrom, TryInto};
/// Percentage of an obligation that can be repaid during each liquidation call
pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50;
/// Loan amount that is small enough to close out
pub const CLOSEABLE_AMOUNT: u64 = 2;
/// Lending market reserve state
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Reserve {
@ -93,79 +96,88 @@ impl Reserve {
pub fn liquidate_obligation(
&self,
obligation: &Obligation,
liquidity_amount: u64,
liquidate_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());
}
Self::_liquidate_obligation(
obligation,
liquidate_amount,
liquidity_token_mint,
self.collateral_exchange_rate()?,
&self.config,
token_converter,
)
}
// 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() {
fn _liquidate_obligation(
obligation: &Obligation,
liquidity_amount: u64,
liquidity_token_mint: &Pubkey,
collateral_exchange_rate: CollateralExchangeRate,
collateral_reserve_config: &ReserveConfig,
mut token_converter: impl TokenConverter,
) -> Result<LiquidateResult, ProgramError> {
// Check obligation health
let borrow_token_price = token_converter.best_price(liquidity_token_mint)?;
let liquidation_threshold =
Rate::from_percent(collateral_reserve_config.liquidation_threshold);
let obligation_loan_to_value =
obligation.loan_to_value(collateral_exchange_rate, borrow_token_price)?;
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, remaining_amount) =
if withdraw_amount >= obligation.deposited_collateral_tokens.into() {
(obligation.deposited_collateral_tokens, 0, 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_amount = obligation.deposited_collateral_tokens - withdraw_amount;
if bonus_amount >= remaining_amount {
(withdraw_amount, remaining_amount, 0)
} else {
(
withdraw_amount,
bonus_amount,
remaining_amount - bonus_amount,
)
}
};
// Special handling for small, closeable obligations
let max_closeable_amount = obligation.max_closeable_amount()?;
let close_amount = liquidity_amount.min(max_closeable_amount);
if close_amount > 0 {
return Ok(LiquidateResult {
withdraw_amount: obligation.deposited_collateral_tokens,
repay_amount: close_amount,
settle_amount: obligation.borrowed_liquidity_wads,
});
}
// Determine if the loan has defaulted
let settle_amount = if remaining_amount == 0 {
obligation.borrowed_liquidity_wads
} else {
decimal_repay_amount
// Calculate the amount of liquidity that will be repaid
let max_liquidation_amount = obligation.max_liquidation_amount()?;
let repay_amount = liquidity_amount.min(max_liquidation_amount);
let decimal_repay_amount = Decimal::from(repay_amount);
// Calculate the amount of collateral that will be received
let withdraw_amount = {
let receive_liquidity_amount =
token_converter.convert(decimal_repay_amount, liquidity_token_mint)?;
let collateral_amount = collateral_exchange_rate
.decimal_liquidity_to_collateral(receive_liquidity_amount)?;
let bonus_rate = Rate::from_percent(collateral_reserve_config.liquidation_bonus);
let bonus_amount = collateral_amount.try_mul(bonus_rate)?;
let withdraw_amount = collateral_amount.try_add(bonus_amount)?;
let withdraw_amount =
withdraw_amount.min(obligation.deposited_collateral_tokens.into());
if repay_amount == max_liquidation_amount {
withdraw_amount.try_ceil_u64()?
} else {
withdraw_amount.try_floor_u64()?
}
};
Ok(LiquidateResult {
bonus_amount,
withdraw_amount,
repay_amount: integer_repay_amount,
settle_amount,
})
if withdraw_amount > 0 {
// TODO: charge less liquidity if withdraw value exceeds loan collateral
let settle_amount = if withdraw_amount == obligation.deposited_collateral_tokens {
obligation.borrowed_liquidity_wads
} else {
decimal_repay_amount
};
Ok(LiquidateResult {
withdraw_amount,
repay_amount,
settle_amount,
})
} else {
Err(LendingError::LiquidationTooSmall.into())
}
}
/// Create new loan
@ -355,8 +367,6 @@ pub struct LoanResult {
/// 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 of liquidity that is settled from the obligation. It includes
@ -742,6 +752,10 @@ mod test {
struct MockConverter(Decimal);
impl TokenConverter for MockConverter {
fn best_price(&mut self, _token_mint: &Pubkey) -> Result<Decimal, ProgramError> {
Ok(self.0)
}
fn convert(
self,
from_amount: Decimal,
@ -751,19 +765,6 @@ mod test {
}
}
/// 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,
@ -786,87 +787,110 @@ mod test {
}
}
// Creates rates (threshold, ltv) where 2 <= threshold <= 100 and threshold <= ltv <= 1,000%
prop_compose! {
fn unhealthy_rates()(threshold in 2..=100u8)(
ltv_rate in threshold as u64..=1000u64,
threshold in Just(threshold),
) -> (Decimal, u8) {
(Decimal::from_scaled_val(ltv_rate as u128 * PERCENT_SCALER as u128), threshold)
}
}
// Creates a range of reasonable token conversion rates
prop_compose! {
fn token_conversion_rate()(
conversion_rate in 1..=u16::MAX,
invert_conversion_rate: bool,
) -> Decimal {
let conversion_rate = Decimal::from(conversion_rate as u64);
if invert_conversion_rate {
Decimal::one().try_div(conversion_rate).unwrap()
} else {
conversion_rate
}
}
}
// Creates a range of reasonable collateral exchange rates
prop_compose! {
fn collateral_exchange_rate_range()(percent in 1..=500u64) -> CollateralExchangeRate {
CollateralExchangeRate(Rate::from_scaled_val(percent * PERCENT_SCALER))
}
}
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,
fn unhealthy_obligations_can_be_liquidated(
obligation_collateral in 1..=u64::MAX,
(obligation_ltv, liquidation_threshold) in unhealthy_rates(),
collateral_exchange_rate in collateral_exchange_rate_range(),
token_conversion_rate in token_conversion_rate(),
) {
let borrowed_liquidity_wads = Decimal::from(obligation_loan);
let collateral_reserve_config = &ReserveConfig {
liquidation_threshold,
..ReserveConfig::default()
};
// Create unhealthy obligation at target LTV
let collateral_value = collateral_exchange_rate
.decimal_collateral_to_liquidity(Decimal::from(obligation_collateral as u64))?
.try_div(token_conversion_rate)?;
// Ensure that collateral value fits in u64
prop_assume!(collateral_value.try_round_u64().is_ok());
let borrowed_liquidity_wads = collateral_value
.try_mul(obligation_ltv)?
.try_add(Decimal::from_scaled_val(1u128))? // ensure loan is unhealthy
.max(Decimal::from(2u64)); // avoid dust account closure
// Ensure that borrow value fits in u64
prop_assume!(borrowed_liquidity_wads.try_round_u64().is_ok());
let obligation = Obligation {
deposited_collateral_tokens: obligation_collateral,
deposited_collateral_tokens: obligation_collateral as u64,
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(
// Liquidate with max amount to ensure obligation can be liquidated
let liquidate_result = Reserve::_liquidate_obligation(
&obligation,
liquidate_amount,
u64::MAX,
&Pubkey::default(),
MockConverter(conversion_rate)
collateral_exchange_rate,
collateral_reserve_config,
MockConverter(token_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.repay_amount,
collateral_exchange_rate,
conversion_rate,
)?
);
assert!(
Decimal::from(liquidate_result.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))?
);
let liquidate_result = liquidate_result.unwrap();
let expected_withdraw_amount = liquidity_in_other_collateral(
liquidate_result.repay_amount,
collateral_exchange_rate,
token_conversion_rate,
)?.min(obligation.deposited_collateral_tokens.into());
let total_withdraw = liquidate_result.withdraw_amount + liquidate_result.bonus_amount;
let defaulted = total_withdraw == obligation.deposited_collateral_tokens;
if defaulted {
assert_eq!(liquidate_result.settle_amount, borrowed_liquidity_wads);
} else {
assert_eq!(liquidate_result.settle_amount.try_ceil_u64()?, liquidate_result.repay_amount);
assert!(total_withdraw < obligation.deposited_collateral_tokens);
}
assert!(liquidate_result.repay_amount > 0);
assert!(liquidate_result.withdraw_amount > 0);
let min_withdraw_amount = expected_withdraw_amount.try_floor_u64()?;
let max_withdraw_amount = expected_withdraw_amount.try_ceil_u64()?;
let max_repay_amount = obligation.borrowed_liquidity_wads
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
.try_ceil_u64()?;
assert!(liquidate_result.withdraw_amount >= min_withdraw_amount);
assert!(liquidate_result.withdraw_amount <= max_withdraw_amount);
assert!(liquidate_result.repay_amount <= max_repay_amount);
let defaulted = liquidate_result.withdraw_amount == obligation.deposited_collateral_tokens;
if defaulted {
assert_eq!(liquidate_result.settle_amount, borrowed_liquidity_wads);
assert!(liquidate_result.repay_amount < liquidate_result.settle_amount.try_floor_u64()?);
} else {
assert_eq!(liquidate_result.settle_amount.try_ceil_u64()?, liquidate_result.repay_amount);
assert!(liquidate_result.withdraw_amount < obligation.deposited_collateral_tokens);
}
}
@ -922,14 +946,13 @@ 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,
collateral_exchange_rate in collateral_exchange_rate_range(),
token_conversion_rate in 1..=u64::MAX,
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 collateral_token_supply = collateral_exchange_rate
.liquidity_to_collateral(total_liquidity)?;
let reserve = Reserve {
collateral: ReserveCollateral {
mint_total_supply: collateral_token_supply,
@ -946,7 +969,7 @@ mod test {
..Reserve::default()
};
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate);
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128);
let borrow_amount = reserve.allowed_borrow_for_collateral(
collateral_amount,
MockConverter(conversion_rate)
@ -980,14 +1003,13 @@ mod test {
#[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,
collateral_exchange_rate in collateral_exchange_rate_range(),
token_conversion_rate in 1..=u64::MAX,
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 collateral_token_supply = collateral_exchange_rate
.liquidity_to_collateral(total_liquidity)?;
let reserve = Reserve {
collateral: ReserveCollateral {
mint_total_supply: collateral_token_supply,
@ -1004,7 +1026,7 @@ mod test {
..Reserve::default()
};
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate);
let conversion_rate = Decimal::from_scaled_val(token_conversion_rate as u128);
let collateral_amount = reserve.required_collateral_for_borrow(
borrow_amount,
&Pubkey::default(),
@ -1179,6 +1201,79 @@ mod test {
}
}
#[test]
fn liquidate_amount_too_small() {
let conversion_rate = Decimal::from_scaled_val(PERCENT_SCALER as u128); // 1%
let collateral_exchange_rate = CollateralExchangeRate(Rate::one());
let collateral_reserve_config = &ReserveConfig {
liquidation_threshold: 80u8,
liquidation_bonus: 5u8,
..ReserveConfig::default()
};
let obligation = Obligation {
deposited_collateral_tokens: 1,
borrowed_liquidity_wads: Decimal::from(100u64),
..Obligation::default()
};
let liquidate_result = Reserve::_liquidate_obligation(
&obligation,
1u64, // converts to 0.01 collateral
&Pubkey::default(),
collateral_exchange_rate,
collateral_reserve_config,
MockConverter(conversion_rate),
);
assert_eq!(
liquidate_result.unwrap_err(),
LendingError::LiquidationTooSmall.into()
);
}
#[test]
fn liquidate_dust_obligation() {
let conversion_rate = Decimal::one();
let collateral_exchange_rate = CollateralExchangeRate(Rate::one());
let collateral_reserve_config = &ReserveConfig {
liquidation_threshold: 80u8,
liquidation_bonus: 5u8,
..ReserveConfig::default()
};
let obligation = Obligation {
deposited_collateral_tokens: 1,
borrowed_liquidity_wads: Decimal::one()
.try_add(Decimal::from_scaled_val(1u128))
.unwrap(),
..Obligation::default()
};
let liquidate_result = Reserve::_liquidate_obligation(
&obligation,
2,
&Pubkey::default(),
collateral_exchange_rate,
collateral_reserve_config,
MockConverter(conversion_rate),
)
.unwrap();
assert_eq!(
liquidate_result.repay_amount,
obligation.borrowed_liquidity_wads.try_ceil_u64().unwrap()
);
assert_eq!(
liquidate_result.withdraw_amount,
obligation.deposited_collateral_tokens
);
assert_eq!(
liquidate_result.settle_amount,
obligation.borrowed_liquidity_wads
);
}
#[test]
fn borrow_fee_calculation_min_host() {
let fees = ReserveFees {

View File

@ -24,7 +24,7 @@ async fn test_success() {
);
// limit to track compute unit increase
test.set_bpf_compute_max_units(101_000);
test.set_bpf_compute_max_units(97_000);
// set loan values to about 90% of collateral value so that it gets liquidated
const USDC_LOAN: u64 = 2 * FRACTIONAL_TO_USDC;