lending: Refactor and test obligation repays (#1084)

This commit is contained in:
Justin Starry 2021-01-19 08:55:37 +08:00 committed by GitHub
parent 749774ca2a
commit fdf2f1f909
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 215 additions and 62 deletions

View File

@ -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 {

View File

@ -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(),

View File

@ -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(),
) {

View File

@ -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

View File

@ -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);