lending: Refactor and test obligation repays (#1084)
This commit is contained in:
parent
749774ca2a
commit
fdf2f1f909
|
@ -73,6 +73,27 @@ impl Decimal {
|
|||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(u64::try_from(rounded_val).map_err(|_| LendingError::MathOverflow)?)
|
||||
}
|
||||
|
||||
/// Ceiling scaled decimal to u64
|
||||
pub fn try_ceil_u64(&self) -> Result<u64, ProgramError> {
|
||||
let ceil_val = Self::wad()
|
||||
.checked_sub(U192::from(1u64))
|
||||
.ok_or(LendingError::MathOverflow)?
|
||||
.checked_add(self.0)
|
||||
.ok_or(LendingError::MathOverflow)?
|
||||
.checked_div(Self::wad())
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?)
|
||||
}
|
||||
|
||||
/// Floor scaled decimal to u64
|
||||
pub fn try_floor_u64(&self) -> Result<u64, ProgramError> {
|
||||
let ceil_val = self
|
||||
.0
|
||||
.checked_div(Self::wad())
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Decimal {
|
||||
|
|
|
@ -4,10 +4,10 @@ use crate::{
|
|||
dex_market::{DexMarket, TradeAction, TradeSimulator, BASE_MINT_OFFSET, QUOTE_MINT_OFFSET},
|
||||
error::LendingError,
|
||||
instruction::{BorrowAmountType, LendingInstruction},
|
||||
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD},
|
||||
math::{Decimal, Rate, TryAdd, TryMul, TrySub, WAD},
|
||||
state::{
|
||||
LendingMarket, NewReserveParams, Obligation, Reserve, ReserveCollateral, ReserveConfig,
|
||||
ReserveLiquidity, PROGRAM_VERSION,
|
||||
LendingMarket, NewReserveParams, Obligation, RepayResult, Reserve, ReserveCollateral,
|
||||
ReserveConfig, ReserveLiquidity, PROGRAM_VERSION,
|
||||
},
|
||||
};
|
||||
use num_traits::FromPrimitive;
|
||||
|
@ -789,10 +789,6 @@ fn process_repay(
|
|||
if obligation_info.owner != program_id {
|
||||
return Err(LendingError::InvalidAccountOwner.into());
|
||||
}
|
||||
if &obligation.token_mint != obligation_token_mint_info.key {
|
||||
msg!("Invalid obligation token mint account");
|
||||
return Err(LendingError::InvalidAccountInput.into());
|
||||
}
|
||||
if &obligation.borrow_reserve != repay_reserve_info.key {
|
||||
msg!("Invalid repay reserve account");
|
||||
return Err(LendingError::InvalidAccountInput.into());
|
||||
|
@ -802,6 +798,12 @@ fn process_repay(
|
|||
return Err(LendingError::InvalidAccountInput.into());
|
||||
}
|
||||
|
||||
let obligation_mint = unpack_mint(&obligation_token_mint_info.data.borrow())?;
|
||||
if &obligation.token_mint != obligation_token_mint_info.key {
|
||||
msg!("Invalid obligation token mint account");
|
||||
return Err(LendingError::InvalidAccountInput.into());
|
||||
}
|
||||
|
||||
let mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?;
|
||||
if repay_reserve_info.owner != program_id {
|
||||
return Err(LendingError::InvalidAccountOwner.into());
|
||||
|
@ -846,25 +848,17 @@ fn process_repay(
|
|||
repay_reserve.accrue_interest(clock.slot)?;
|
||||
obligation.accrue_interest(repay_reserve.cumulative_borrow_rate_wads)?;
|
||||
|
||||
let repay_amount = Decimal::from(liquidity_amount).min(obligation.borrowed_liquidity_wads);
|
||||
let rounded_repay_amount = repay_reserve.liquidity.repay(repay_amount)?;
|
||||
let RepayResult {
|
||||
integer_repay_amount,
|
||||
decimal_repay_amount,
|
||||
collateral_withdraw_amount,
|
||||
obligation_token_amount,
|
||||
} = obligation.repay(liquidity_amount, obligation_mint.supply)?;
|
||||
repay_reserve
|
||||
.liquidity
|
||||
.repay(integer_repay_amount, decimal_repay_amount)?;
|
||||
|
||||
Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
|
||||
|
||||
let repay_pct: Decimal = repay_amount.try_div(obligation.borrowed_liquidity_wads)?;
|
||||
let collateral_withdraw_amount = {
|
||||
let withdraw_amount: Decimal = repay_pct.try_mul(obligation.deposited_collateral_tokens)?;
|
||||
withdraw_amount.try_round_u64()?
|
||||
};
|
||||
|
||||
let obligation_token_amount = {
|
||||
let obligation_mint = &unpack_mint(&obligation_token_mint_info.data.borrow())?;
|
||||
let token_amount: Decimal = repay_pct.try_mul(obligation_mint.supply)?;
|
||||
token_amount.try_round_u64()?
|
||||
};
|
||||
|
||||
obligation.borrowed_liquidity_wads =
|
||||
obligation.borrowed_liquidity_wads.try_sub(repay_amount)?;
|
||||
obligation.deposited_collateral_tokens -= collateral_withdraw_amount;
|
||||
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
|
||||
|
||||
let authority_signer_seeds = &[
|
||||
|
@ -877,11 +871,21 @@ fn process_repay(
|
|||
return Err(LendingError::InvalidMarketAuthority.into());
|
||||
}
|
||||
|
||||
// burn obligation tokens
|
||||
spl_token_burn(TokenBurnParams {
|
||||
mint: obligation_token_mint_info.clone(),
|
||||
source: obligation_token_input_info.clone(),
|
||||
amount: obligation_token_amount,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
|
||||
// deposit repaid liquidity
|
||||
spl_token_transfer(TokenTransferParams {
|
||||
source: source_liquidity_info.clone(),
|
||||
destination: repay_reserve_liquidity_supply_info.clone(),
|
||||
amount: rounded_repay_amount,
|
||||
amount: integer_repay_amount,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
|
@ -897,16 +901,6 @@ fn process_repay(
|
|||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
|
||||
// burn obligation tokens
|
||||
spl_token_burn(TokenBurnParams {
|
||||
mint: obligation_token_mint_info.clone(),
|
||||
source: obligation_token_input_info.clone(),
|
||||
amount: obligation_token_amount,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1043,16 +1037,22 @@ fn process_liquidate(
|
|||
|
||||
// calculate the amount of liquidity that will be repaid
|
||||
let close_factor = Rate::from_percent(50);
|
||||
let repay_amount = Decimal::from(liquidity_amount)
|
||||
let decimal_repay_amount = Decimal::from(liquidity_amount)
|
||||
.min(obligation.borrowed_liquidity_wads.try_mul(close_factor)?);
|
||||
let rounded_repay_amount = repay_reserve.liquidity.repay(repay_amount)?;
|
||||
let integer_repay_amount = decimal_repay_amount.try_round_u64()?;
|
||||
if integer_repay_amount == 0 {
|
||||
return Err(LendingError::ObligationTooSmall.into());
|
||||
}
|
||||
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,
|
||||
repay_amount,
|
||||
decimal_repay_amount,
|
||||
&repay_reserve.liquidity.mint_pubkey,
|
||||
false,
|
||||
)?;
|
||||
|
@ -1071,8 +1071,9 @@ fn process_liquidate(
|
|||
&mut withdraw_reserve_info.data.borrow_mut(),
|
||||
)?;
|
||||
|
||||
obligation.borrowed_liquidity_wads =
|
||||
obligation.borrowed_liquidity_wads.try_sub(repay_amount)?;
|
||||
obligation.borrowed_liquidity_wads = obligation
|
||||
.borrowed_liquidity_wads
|
||||
.try_sub(decimal_repay_amount)?;
|
||||
obligation.deposited_collateral_tokens -= collateral_withdraw_amount;
|
||||
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
|
||||
|
||||
|
@ -1090,7 +1091,7 @@ fn process_liquidate(
|
|||
spl_token_transfer(TokenTransferParams {
|
||||
source: source_liquidity_info.clone(),
|
||||
destination: repay_reserve_liquidity_supply_info.clone(),
|
||||
amount: rounded_repay_amount,
|
||||
amount: integer_repay_amount,
|
||||
authority: user_transfer_authority_info.clone(),
|
||||
authority_signer_seeds: &[],
|
||||
token_program: token_program_id.clone(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
math::{Decimal, Rate, TryDiv, TryMul},
|
||||
math::{Decimal, Rate, TryDiv, TryMul, TrySub},
|
||||
};
|
||||
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
|
||||
use solana_program::{
|
||||
|
@ -49,6 +49,59 @@ impl Obligation {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repay borrowed tokens
|
||||
pub fn repay(
|
||||
&mut self,
|
||||
liquidity_amount: u64,
|
||||
obligation_token_supply: u64,
|
||||
) -> Result<RepayResult, ProgramError> {
|
||||
let decimal_repay_amount =
|
||||
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());
|
||||
}
|
||||
|
||||
let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?;
|
||||
let collateral_withdraw_amount = {
|
||||
let withdraw_amount: Decimal = repay_pct.try_mul(self.deposited_collateral_tokens)?;
|
||||
withdraw_amount.try_floor_u64()?
|
||||
};
|
||||
|
||||
let obligation_token_amount = {
|
||||
let withdraw_pct = Decimal::from(collateral_withdraw_amount)
|
||||
.try_div(self.deposited_collateral_tokens)?;
|
||||
let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?;
|
||||
token_amount.try_floor_u64()?
|
||||
};
|
||||
|
||||
self.borrowed_liquidity_wads =
|
||||
self.borrowed_liquidity_wads.try_sub(decimal_repay_amount)?;
|
||||
self.deposited_collateral_tokens = self
|
||||
.deposited_collateral_tokens
|
||||
.checked_sub(collateral_withdraw_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
|
||||
Ok(RepayResult {
|
||||
decimal_repay_amount,
|
||||
integer_repay_amount,
|
||||
collateral_withdraw_amount,
|
||||
obligation_token_amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Obligation repay result
|
||||
pub struct RepayResult {
|
||||
/// Amount of collateral to withdraw
|
||||
pub collateral_withdraw_amount: u64,
|
||||
/// Amount of obligation tokens to burn
|
||||
pub obligation_token_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,
|
||||
}
|
||||
|
||||
impl Sealed for Obligation {}
|
||||
|
@ -150,17 +203,96 @@ mod test {
|
|||
}
|
||||
|
||||
// Creates rates (r1, r2) where 0 < r1 <= r2 <= 100*r1
|
||||
fn cumulative_rates() -> impl Strategy<Value = (u128, u128)> {
|
||||
prop::num::u128::ANY.prop_flat_map(|rate| {
|
||||
let current_rate = rate.max(1);
|
||||
let max_new_rate = current_rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128);
|
||||
(Just(current_rate), current_rate..=max_new_rate)
|
||||
})
|
||||
prop_compose! {
|
||||
fn cumulative_rates()(rate in 1..=u128::MAX)(
|
||||
current_rate in Just(rate),
|
||||
max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128)
|
||||
) -> (u128, u128) {
|
||||
(current_rate, max_new_rate)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128;
|
||||
|
||||
// Creates liquidity amounts (repay, borrow) where repay < borrow
|
||||
prop_compose! {
|
||||
fn repay_partial_amounts()(repay in 1..=u64::MAX)(
|
||||
liquidity_amount in Just(repay),
|
||||
borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED
|
||||
) -> (u64, u128) {
|
||||
(liquidity_amount, borrowed_liquidity)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates liquidity amounts (repay, borrow) where repay >= borrow
|
||||
prop_compose! {
|
||||
fn repay_full_amounts()(repay in 1..=u64::MAX)(
|
||||
liquidity_amount in Just(repay),
|
||||
borrowed_liquidity in 0..=(WAD as u128 * repay as u128)
|
||||
) -> (u64, u128) {
|
||||
(liquidity_amount, borrowed_liquidity)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates collateral amounts (collateral, obligation tokens) where c <= ot
|
||||
prop_compose! {
|
||||
fn collateral_amounts()(collateral in 1..=u64::MAX)(
|
||||
deposited_collateral_tokens in Just(collateral),
|
||||
obligation_tokens in collateral..=u64::MAX
|
||||
) -> (u64, u64) {
|
||||
(deposited_collateral_tokens, obligation_tokens)
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn obligation_accrue_interest(
|
||||
fn repay_partial(
|
||||
(liquidity_amount, borrowed_liquidity) in repay_partial_amounts(),
|
||||
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity);
|
||||
let mut state = Obligation {
|
||||
borrowed_liquidity_wads,
|
||||
deposited_collateral_tokens,
|
||||
..Obligation::default()
|
||||
};
|
||||
|
||||
let repay_result = state.repay(liquidity_amount, obligation_tokens)?;
|
||||
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount));
|
||||
assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens);
|
||||
assert!(repay_result.obligation_token_amount < obligation_tokens);
|
||||
assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads);
|
||||
assert!(state.borrowed_liquidity_wads > Decimal::zero());
|
||||
assert!(state.deposited_collateral_tokens > 0);
|
||||
|
||||
let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?;
|
||||
let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?;
|
||||
assert!(obligation_token_rate <= collateral_withdraw_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repay_full(
|
||||
(liquidity_amount, borrowed_liquidity) in repay_full_amounts(),
|
||||
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity);
|
||||
let mut state = Obligation {
|
||||
borrowed_liquidity_wads,
|
||||
deposited_collateral_tokens,
|
||||
..Obligation::default()
|
||||
};
|
||||
|
||||
let repay_result = state.repay(liquidity_amount, obligation_tokens)?;
|
||||
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount));
|
||||
assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens);
|
||||
assert_eq!(repay_result.obligation_token_amount, obligation_tokens);
|
||||
assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads);
|
||||
assert_eq!(state.borrowed_liquidity_wads, Decimal::zero());
|
||||
assert_eq!(state.deposited_collateral_tokens, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accrue_interest(
|
||||
borrowed_liquidity in 0..=u64::MAX,
|
||||
(current_borrow_rate, new_borrow_rate) in cumulative_rates(),
|
||||
) {
|
||||
|
|
|
@ -218,20 +218,19 @@ impl ReserveLiquidity {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Subtract repay amount from total borrows and return rounded repay value
|
||||
pub fn repay(&mut self, repay_amount: Decimal) -> Result<u64, ProgramError> {
|
||||
let rounded_repay_amount = repay_amount.try_round_u64()?;
|
||||
if rounded_repay_amount == 0 {
|
||||
return Err(LendingError::ObligationTooSmall.into());
|
||||
}
|
||||
|
||||
/// Subtract repay amount from total borrows and add to available liquidity
|
||||
pub fn repay(
|
||||
&mut self,
|
||||
integer_amount: u64,
|
||||
decimal_amount: Decimal,
|
||||
) -> Result<(), ProgramError> {
|
||||
self.available_amount = self
|
||||
.available_amount
|
||||
.checked_add(rounded_repay_amount)
|
||||
.checked_add(integer_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(repay_amount)?;
|
||||
self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(decimal_amount)?;
|
||||
|
||||
Ok(rounded_repay_amount)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate the liquidity utilization rate of the reserve
|
||||
|
|
|
@ -29,7 +29,7 @@ async fn test_success() {
|
|||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(85_000);
|
||||
test.set_bpf_compute_max_units(87_000);
|
||||
|
||||
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL;
|
||||
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC;
|
||||
|
@ -179,7 +179,7 @@ async fn test_success() {
|
|||
let expected_collateral_received = expected_obligation_repaid_percent
|
||||
.try_mul(OBLIGATION_COLLATERAL)
|
||||
.unwrap()
|
||||
.try_round_u64()
|
||||
.try_floor_u64()
|
||||
.unwrap();
|
||||
assert_eq!(collateral_received, expected_collateral_received);
|
||||
|
||||
|
|
Loading…
Reference in New Issue