Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit f61d7a8

Browse files
authored
lending: Use fair obligation health factor calculation (#1119)
* lending: Use fair health factor calulation and handle dust * ci: fix github action caching
1 parent 6b4e395 commit f61d7a8

File tree

10 files changed

+363
-191
lines changed

10 files changed

+363
-191
lines changed

.github/workflows/pull-request.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,12 @@ jobs:
109109
path: |
110110
~/.cargo/bin/rustfilt
111111
key: cargo-bpf-bins-${{ runner.os }}
112-
restore-keys: |
113-
cargo-bpf-bins-${{ runner.os }}-
114112

115113
- uses: actions/cache@v2
116114
with:
117115
path: |
118116
~/.cache
119117
key: solana-${{ env.SOLANA_VERSION }}
120-
restore-keys: |
121-
solana-
122118

123119
- name: Install dependencies
124120
run: |

ci/cargo-build-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -e
44
cd "$(dirname "$0")/.."
55

66
source ./ci/rust-version.sh stable
7-
source ./ci/solana-version.sh install
7+
source ./ci/solana-version.sh
88

99
export RUSTFLAGS="-D warnings"
1010
export RUSTBACKTRACE=1

ci/solana-version.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414
if [[ -n $SOLANA_VERSION ]]; then
1515
solana_version="$SOLANA_VERSION"
1616
else
17-
solana_version=v1.5.0
17+
solana_version=v1.5.5
1818
fi
1919

2020
export solana_version="$solana_version"
2121
export solana_docker_image=solanalabs/solana:"$solana_version"
22+
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
2223

2324
if [[ -n $1 ]]; then
2425
case $1 in
2526
install)
2627
sh -c "$(curl -sSfL https://release.solana.com/$solana_version/install)"
27-
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
2828
solana --version
2929
;;
3030
*)

token-lending/program/src/dex_market.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
state::TokenConverter,
77
};
88
use arrayref::{array_refs, mut_array_refs};
9-
use serum_dex::critbit::Slab;
9+
use serum_dex::critbit::{Slab, SlabView};
1010
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
1111
use std::{cell::RefMut, convert::TryFrom};
1212

@@ -59,6 +59,43 @@ pub struct TradeSimulator<'a> {
5959
}
6060

6161
impl<'a> TokenConverter for TradeSimulator<'a> {
62+
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError> {
63+
let action = if token_mint == self.buy_token_mint {
64+
TradeAction::Buy
65+
} else {
66+
TradeAction::Sell
67+
};
68+
69+
let currency = if token_mint == self.quote_token_mint {
70+
Currency::Quote
71+
} else {
72+
Currency::Base
73+
};
74+
75+
let order_book_side = match (action, currency) {
76+
(TradeAction::Buy, Currency::Base) => Side::Ask,
77+
(TradeAction::Sell, Currency::Quote) => Side::Ask,
78+
(TradeAction::Buy, Currency::Quote) => Side::Bid,
79+
(TradeAction::Sell, Currency::Base) => Side::Bid,
80+
};
81+
if order_book_side != self.orders_side {
82+
return Err(LendingError::DexInvalidOrderBookSide.into());
83+
}
84+
85+
let best_order_price = self
86+
.orders
87+
.best_order_price()
88+
.ok_or(LendingError::TradeSimulationError)?;
89+
90+
let input_token = Decimal::one().try_div(self.dex_market.get_lots(currency))?;
91+
let output_token_price = if currency == Currency::Base {
92+
input_token.try_mul(best_order_price)
93+
} else {
94+
input_token.try_div(best_order_price)
95+
}?;
96+
Ok(output_token_price.try_mul(self.dex_market.get_lots(currency.opposite()))?)
97+
}
98+
6299
fn convert(
63100
self,
64101
from_amount: Decimal,
@@ -200,6 +237,18 @@ impl<'a> DexMarketOrders<'a> {
200237

201238
Ok(Self { heap, side })
202239
}
240+
241+
fn best_order_price(&mut self) -> Option<u64> {
242+
let side = self.side;
243+
self.heap.as_mut().and_then(|heap| {
244+
let handle = match side {
245+
Side::Bid => heap.find_max(),
246+
Side::Ask => heap.find_min(),
247+
}?;
248+
249+
Some(heap.get_mut(handle)?.as_leaf_mut()?.price().get())
250+
})
251+
}
203252
}
204253

205254
impl Iterator for DexMarketOrders<'_> {

token-lending/program/src/error.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use thiserror::Error;
77
/// Errors that may be returned by the TokenLending program.
88
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
99
pub enum LendingError {
10+
// 0
1011
/// Invalid instruction data passed in.
1112
#[error("Failed to unpack instruction data")]
1213
InstructionUnpackError,
@@ -22,6 +23,8 @@ pub enum LendingError {
2223
/// Expected a different market owner
2324
#[error("Market owner is invalid")]
2425
InvalidMarketOwner,
26+
27+
// 5
2528
/// The owner of the input isn't set to the program address generated by the program.
2629
#[error("Input account owner is not the program address")]
2730
InvalidAccountOwner,
@@ -37,6 +40,8 @@ pub enum LendingError {
3740
/// Invalid amount, must be greater than zero
3841
#[error("Input amount is invalid")]
3942
InvalidAmount,
43+
44+
// 10
4045
/// Invalid config value
4146
#[error("Input config value is invalid")]
4247
InvalidConfig,
@@ -53,6 +58,7 @@ pub enum LendingError {
5358
#[error("Interest rate is negative")]
5459
NegativeInterestRate,
5560

61+
// 15
5662
/// Memory is too small
5763
#[error("Memory is too small")]
5864
MemoryTooSmall,
@@ -68,6 +74,8 @@ pub enum LendingError {
6874
/// Insufficient liquidity available
6975
#[error("Insufficient liquidity available")]
7076
InsufficientLiquidity,
77+
78+
// 20
7179
/// This reserve's collateral cannot be used for borrows
7280
#[error("Input reserve has collateral disabled")]
7381
ReserveCollateralDisabled,
@@ -77,12 +85,14 @@ pub enum LendingError {
7785
/// Input reserves cannot use the same liquidity mint
7886
#[error("Input reserves cannot use the same liquidity mint")]
7987
DuplicateReserveMint,
80-
/// Obligation amount is too small to pay off
81-
#[error("Obligation amount is too small to pay off")]
82-
ObligationTooSmall,
88+
/// Obligation amount is empty
89+
#[error("Obligation amount is empty")]
90+
ObligationEmpty,
8391
/// Cannot liquidate healthy obligations
8492
#[error("Cannot liquidate healthy obligations")]
8593
HealthyObligation,
94+
95+
// 25
8696
/// Borrow amount too small
8797
#[error("Borrow amount too small")]
8898
BorrowTooSmall,
@@ -92,14 +102,14 @@ pub enum LendingError {
92102
/// Reserve state stale
93103
#[error("Reserve state needs to be updated for the current slot")]
94104
ReserveStale,
95-
96105
/// Trade simulation error
97106
#[error("Trade simulation error")]
98107
TradeSimulationError,
99108
/// Invalid dex order book side
100109
#[error("Invalid dex order book side")]
101110
DexInvalidOrderBookSide,
102111

112+
// 30
103113
/// Token initialize mint failed
104114
#[error("Token initialize mint failed")]
105115
TokenInitializeMintFailed,

token-lending/program/src/processor.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,9 @@ fn process_repay(
871871
msg!("Invalid withdraw reserve account");
872872
return Err(LendingError::InvalidAccountInput.into());
873873
}
874+
if obligation.deposited_collateral_tokens == 0 {
875+
return Err(LendingError::ObligationEmpty.into());
876+
}
874877

875878
let obligation_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?;
876879
if &obligation.token_mint != obligation_token_mint_info.key {
@@ -1030,6 +1033,9 @@ fn process_liquidate(
10301033
msg!("Invalid withdraw reserve account");
10311034
return Err(LendingError::InvalidAccountInput.into());
10321035
}
1036+
if obligation.deposited_collateral_tokens == 0 {
1037+
return Err(LendingError::ObligationEmpty.into());
1038+
}
10331039

10341040
let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?;
10351041
if repay_reserve_info.owner != program_id {
@@ -1104,7 +1110,6 @@ fn process_liquidate(
11041110
)?;
11051111

11061112
let LiquidateResult {
1107-
bonus_amount,
11081113
withdraw_amount,
11091114
repay_amount,
11101115
settle_amount,
@@ -1118,7 +1123,7 @@ fn process_liquidate(
11181123
repay_reserve.liquidity.repay(repay_amount, settle_amount)?;
11191124
Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
11201125

1121-
obligation.liquidate(settle_amount, withdraw_amount, bonus_amount)?;
1126+
obligation.liquidate(settle_amount, withdraw_amount)?;
11221127
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
11231128

11241129
let authority_signer_seeds = &[
@@ -1151,18 +1156,6 @@ fn process_liquidate(
11511156
token_program: token_program_id.clone(),
11521157
})?;
11531158

1154-
// pay bonus collateral
1155-
if bonus_amount > 0 {
1156-
spl_token_transfer(TokenTransferParams {
1157-
source: withdraw_reserve_collateral_supply_info.clone(),
1158-
destination: destination_collateral_info.clone(),
1159-
amount: bonus_amount,
1160-
authority: lending_market_authority_info.clone(),
1161-
authority_signer_seeds,
1162-
token_program: token_program_id.clone(),
1163-
})?;
1164-
}
1165-
11661159
Ok(())
11671160
}
11681161

token-lending/program/src/state/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pub const SLOTS_PER_YEAR: u64 =
3434

3535
/// Token converter
3636
pub trait TokenConverter {
37+
/// Return best price for specified token
38+
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError>;
39+
3740
/// Convert between two different tokens
3841
fn convert(
3942
self,

token-lending/program/src/state/obligation.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,37 @@ impl Obligation {
5252
}
5353
}
5454

55+
/// Maximum amount of loan that can be closed out by a liquidator due
56+
/// to the remaining balance being too small to be liquidated normally.
57+
pub fn max_closeable_amount(&self) -> Result<u64, ProgramError> {
58+
if self.borrowed_liquidity_wads < Decimal::from(CLOSEABLE_AMOUNT) {
59+
self.borrowed_liquidity_wads.try_ceil_u64()
60+
} else {
61+
Ok(0)
62+
}
63+
}
64+
65+
/// Maximum amount of loan that can be repaid by liquidators
66+
pub fn max_liquidation_amount(&self) -> Result<u64, ProgramError> {
67+
Ok(self
68+
.borrowed_liquidity_wads
69+
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
70+
.try_floor_u64()?)
71+
}
72+
73+
/// Ratio of loan balance to collateral value
74+
pub fn loan_to_value(
75+
&self,
76+
collateral_exchange_rate: CollateralExchangeRate,
77+
borrow_token_price: Decimal,
78+
) -> Result<Decimal, ProgramError> {
79+
let loan = self.borrowed_liquidity_wads;
80+
let collateral_value = collateral_exchange_rate
81+
.decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())?
82+
.try_div(borrow_token_price)?;
83+
loan.try_div(collateral_value)
84+
}
85+
5586
/// Accrue interest
5687
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
5788
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
@@ -72,15 +103,10 @@ impl Obligation {
72103
}
73104

74105
/// Liquidate part of obligation
75-
pub fn liquidate(
76-
&mut self,
77-
settle_amount: Decimal,
78-
withdraw_amount: u64,
79-
bonus_amount: u64,
80-
) -> ProgramResult {
106+
pub fn liquidate(&mut self, settle_amount: Decimal, withdraw_amount: u64) -> ProgramResult {
81107
self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(settle_amount)?;
82108
self.deposited_collateral_tokens
83-
.checked_sub(withdraw_amount + bonus_amount)
109+
.checked_sub(withdraw_amount)
84110
.ok_or(LendingError::MathOverflow)?;
85111
Ok(())
86112
}
@@ -95,7 +121,7 @@ impl Obligation {
95121
Decimal::from(liquidity_amount).min(self.borrowed_liquidity_wads);
96122
let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?;
97123
if integer_repay_amount == 0 {
98-
return Err(LendingError::ObligationTooSmall.into());
124+
return Err(LendingError::ObligationEmpty.into());
99125
}
100126

101127
let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?;

0 commit comments

Comments
 (0)