Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
lending: Fix obligation interest accrual (#1068)
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry authored Jan 14, 2021
1 parent 977ac9c commit 2859fbe
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 71 deletions.
3 changes: 3 additions & 0 deletions token-lending/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ pub enum LendingError {
/// Borrow amount too small
#[error("Borrow amount too small")]
BorrowTooSmall,
/// Negative interest rate
#[error("Interest rate cannot be negative")]
NegativeInterestRate,

/// Trade simulation error
#[error("Trade simulation error")]
Expand Down
38 changes: 12 additions & 26 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,15 +608,14 @@ fn process_borrow(
return Err(LendingError::InvalidAccountInput.into());
}

obligation.accrue_interest(clock, cumulative_borrow_rate);
obligation.accrue_interest(cumulative_borrow_rate)?;
obligation.borrowed_liquidity_wads += Decimal::from(borrow_amount);
obligation.deposited_collateral_tokens += collateral_deposit_amount;
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
} else {
assert_rent_exempt(rent, obligation_info)?;
let mut new_obligation = obligation;
new_obligation.version = PROGRAM_VERSION;
new_obligation.last_update_slot = clock.slot;
new_obligation.deposited_collateral_tokens = collateral_deposit_amount;
new_obligation.collateral_reserve = *deposit_reserve_info.key;
new_obligation.cumulative_borrow_rate_wads = cumulative_borrow_rate;
Expand Down Expand Up @@ -827,15 +826,10 @@ fn process_repay(

// accrue interest and update rates
repay_reserve.accrue_interest(clock.slot);
obligation.accrue_interest(clock, repay_reserve.state.cumulative_borrow_rate_wads);
obligation.accrue_interest(repay_reserve.state.cumulative_borrow_rate_wads)?;

let repay_amount = Decimal::from(liquidity_amount).min(obligation.borrowed_liquidity_wads);
let rounded_repay_amount = repay_amount.round_u64();
if rounded_repay_amount == 0 {
return Err(LendingError::ObligationTooSmall.into());
}

repay_reserve.state.subtract_repay(repay_amount);
let rounded_repay_amount = repay_reserve.state.subtract_repay(repay_amount)?;
Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;

let repay_pct: Decimal = repay_amount / obligation.borrowed_liquidity_wads;
Expand Down Expand Up @@ -1002,7 +996,7 @@ fn process_liquidate(
// accrue interest and update rates
repay_reserve.accrue_interest(clock.slot);
withdraw_reserve.accrue_interest(clock.slot);
obligation.accrue_interest(clock, repay_reserve.state.cumulative_borrow_rate_wads);
obligation.accrue_interest(repay_reserve.state.cumulative_borrow_rate_wads)?;

let mut trade_simulator = TradeSimulator::new(
dex_market_info,
Expand All @@ -1015,16 +1009,13 @@ fn process_liquidate(
let withdraw_reserve_collateral_exchange_rate =
withdraw_reserve.state.collateral_exchange_rate();
let borrow_amount_as_collateral = withdraw_reserve_collateral_exchange_rate
.liquidity_to_collateral(
trade_simulator
.simulate_trade(
TradeAction::Sell,
obligation.borrowed_liquidity_wads,
&repay_reserve.liquidity_mint,
true,
)?
.round_u64(),
);
.decimal_liquidity_to_collateral(trade_simulator.simulate_trade(
TradeAction::Sell,
obligation.borrowed_liquidity_wads,
&repay_reserve.liquidity_mint,
true,
)?)
.round_u64();

if 100 * borrow_amount_as_collateral / obligation.deposited_collateral_tokens
< withdraw_reserve.config.liquidation_threshold as u64
Expand All @@ -1036,11 +1027,7 @@ fn process_liquidate(
let close_factor = Rate::from_percent(50);
let repay_amount =
Decimal::from(liquidity_amount).min(obligation.borrowed_liquidity_wads * close_factor);
let rounded_repay_amount = repay_amount.round_u64();
if rounded_repay_amount == 0 {
return Err(LendingError::ObligationTooSmall.into());
}
repay_reserve.state.subtract_repay(repay_amount);
let rounded_repay_amount = repay_reserve.state.subtract_repay(repay_amount)?;

// TODO: check math precision
// calculate the amount of collateral that will be withdrawn
Expand All @@ -1066,7 +1053,6 @@ fn process_liquidate(
&mut withdraw_reserve_info.data.borrow_mut(),
)?;

obligation.last_update_slot = clock.slot;
obligation.borrowed_liquidity_wads -= repay_amount;
obligation.deposited_collateral_tokens -= collateral_withdraw_amount;
Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
Expand Down
51 changes: 26 additions & 25 deletions token-lending/program/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use solana_program::{
program_option::COption,
program_pack::{IsInitialized, Pack, Sealed},
pubkey::Pubkey,
sysvar::clock::Clock,
};

/// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity)
Expand Down Expand Up @@ -218,10 +217,17 @@ impl ReserveState {
Ok(())
}

/// Subtract repay amount from total borrows
pub fn subtract_repay(&mut self, repay_amount: Decimal) {
self.available_liquidity += repay_amount.round_u64();
/// Subtract repay amount from total borrows and return rounded repay value
pub fn subtract_repay(&mut self, repay_amount: Decimal) -> Result<u64, ProgramError> {
let rounded_repay_amount = repay_amount.round_u64();
if rounded_repay_amount == 0 {
return Err(LendingError::ObligationTooSmall.into());
}

self.available_liquidity += rounded_repay_amount;
self.borrowed_liquidity_wads -= repay_amount;

Ok(rounded_repay_amount)
}

/// Calculate the current utilization rate of the reserve
Expand Down Expand Up @@ -326,8 +332,6 @@ impl Reserve {
pub struct Obligation {
/// Version of the obligation
pub version: u8,
/// Slot when obligation was updated. Used for calculating interest.
pub last_update_slot: u64,
/// Amount of collateral tokens deposited for this obligation
pub deposited_collateral_tokens: u64,
/// Reserve which collateral tokens were deposited into
Expand All @@ -344,23 +348,24 @@ pub struct Obligation {

impl Obligation {
/// Accrue interest
pub fn accrue_interest(&mut self, clock: &Clock, cumulative_borrow_rate: Decimal) {
let slots_elapsed = clock.slot - self.last_update_slot;
let borrow_rate =
(cumulative_borrow_rate / self.cumulative_borrow_rate_wads - Decimal::one()).as_rate();
let yearly_interest: Decimal = self.borrowed_liquidity_wads * borrow_rate;
let accrued_interest: Decimal = yearly_interest * slots_elapsed / SLOTS_PER_YEAR;

self.borrowed_liquidity_wads += accrued_interest;
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> Result<(), ProgramError> {
let compounded_interest_rate: Rate =
(cumulative_borrow_rate / self.cumulative_borrow_rate_wads).as_rate();

if compounded_interest_rate < Rate::one() {
return Err(LendingError::NegativeInterestRate.into());
}

self.borrowed_liquidity_wads *= compounded_interest_rate;
self.cumulative_borrow_rate_wads = cumulative_borrow_rate;
self.last_update_slot = clock.slot;
Ok(())
}
}

impl Sealed for Reserve {}
impl IsInitialized for Reserve {
fn is_initialized(&self) -> bool {
self.state.last_update_slot > 0
self.version != UNINITIALIZED_VERSION
}
}

Expand Down Expand Up @@ -539,32 +544,30 @@ impl Pack for LendingMarket {
impl Sealed for Obligation {}
impl IsInitialized for Obligation {
fn is_initialized(&self) -> bool {
self.last_update_slot > 0
self.version != UNINITIALIZED_VERSION
}
}

const OBLIGATION_LEN: usize = 273;
const OBLIGATION_LEN: usize = 265;
impl Pack for Obligation {
const LEN: usize = 273;
const LEN: usize = 265;

/// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html).
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
let input = array_ref![input, 0, OBLIGATION_LEN];
#[allow(clippy::ptr_offset_with_cast)]
let (
version,
last_update_slot,
deposited_collateral_tokens,
collateral_supply,
cumulative_borrow_rate,
borrowed_liquidity_wads,
borrow_reserve,
token_mint,
_padding,
) = array_refs![input, 1, 8, 8, 32, 16, 16, 32, 32, 128];
) = array_refs![input, 1, 8, 32, 16, 16, 32, 32, 128];
Ok(Self {
version: u8::from_le_bytes(*version),
last_update_slot: u64::from_le_bytes(*last_update_slot),
deposited_collateral_tokens: u64::from_le_bytes(*deposited_collateral_tokens),
collateral_reserve: Pubkey::new_from_array(*collateral_supply),
cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate),
Expand All @@ -578,18 +581,16 @@ impl Pack for Obligation {
let output = array_mut_ref![output, 0, OBLIGATION_LEN];
let (
version,
last_update_slot,
deposited_collateral_tokens,
collateral_supply,
cumulative_borrow_rate,
borrowed_liquidity_wads,
borrow_reserve,
token_mint,
_padding,
) = mut_array_refs![output, 1, 8, 8, 32, 16, 16, 32, 32, 128];
) = mut_array_refs![output, 1, 8, 32, 16, 16, 32, 32, 128];

*version = self.version.to_le_bytes();
*last_update_slot = self.last_update_slot.to_le_bytes();
*deposited_collateral_tokens = self.deposited_collateral_tokens.to_le_bytes();
collateral_supply.copy_from_slice(self.collateral_reserve.as_ref());
pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate);
Expand Down
3 changes: 0 additions & 3 deletions token-lending/program/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ pub fn add_lending_market(test: &mut ProgramTest, quote_token_mint: Pubkey) -> T
}

pub struct AddObligationArgs<'a> {
pub slots_elapsed: u64,
pub borrow_reserve: &'a TestReserve,
pub collateral_reserve: &'a TestReserve,
pub collateral_amount: u64,
Expand All @@ -155,7 +154,6 @@ pub fn add_obligation(
args: AddObligationArgs,
) -> TestObligation {
let AddObligationArgs {
slots_elapsed,
borrow_reserve,
collateral_reserve,
collateral_amount,
Expand Down Expand Up @@ -197,7 +195,6 @@ pub fn add_obligation(
u32::MAX as u64,
&Obligation {
version: PROGRAM_VERSION,
last_update_slot: 1u64.wrapping_sub(slots_elapsed),
deposited_collateral_tokens: collateral_amount,
collateral_reserve: collateral_reserve.pubkey,
cumulative_borrow_rate_wads: Decimal::one(),
Expand Down
2 changes: 0 additions & 2 deletions token-lending/program/tests/liquidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ async fn test_success() {
&user_accounts_owner,
&lending_market,
AddObligationArgs {
slots_elapsed: SLOTS_PER_YEAR,
borrow_reserve: &usdc_reserve,
collateral_reserve: &sol_reserve,
collateral_amount: USDC_LOAN_SOL_COLLATERAL,
Expand All @@ -97,7 +96,6 @@ async fn test_success() {
&user_accounts_owner,
&lending_market,
AddObligationArgs {
slots_elapsed: SLOTS_PER_YEAR,
borrow_reserve: &sol_reserve,
collateral_reserve: &usdc_reserve,
collateral_amount: SOL_LOAN_USDC_COLLATERAL,
Expand Down
69 changes: 54 additions & 15 deletions token-lending/program/tests/repay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,15 @@ use solana_sdk::{
};
use spl_token::instruction::approve;
use spl_token_lending::{
instruction::repay_reserve_liquidity, math::Decimal, processor::process_instruction,
state::SLOTS_PER_YEAR,
instruction::repay_reserve_liquidity,
math::Decimal,
processor::process_instruction,
state::{INITIAL_COLLATERAL_RATE, SLOTS_PER_YEAR},
};

const LAMPORTS_TO_SOL: u64 = 1_000_000_000;
const FRACTIONAL_TO_USDC: u64 = 1_000_000;

// Market and collateral are setup to fill two orders in the dex market at an average
// price of 2210.5
const fn lamports_to_usdc_fractional(lamports: u64) -> u64 {
lamports / LAMPORTS_TO_SOL * (2210 + 2211) / 2 * FRACTIONAL_TO_USDC / 1000
}

const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 42_500 * LAMPORTS_TO_SOL;
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 =
lamports_to_usdc_fractional(INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS);

#[tokio::test]
async fn test_success() {
let mut test = ProgramTest::new(
Expand All @@ -39,8 +31,11 @@ async fn test_success() {
// limit to track compute unit increase
test.set_bpf_compute_max_units(79_000);

const OBLIGATION_LOAN: u64 = 1;
const OBLIGATION_COLLATERAL: u64 = 500;
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL;
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC;

const OBLIGATION_LOAN: u64 = 10 * FRACTIONAL_TO_USDC;
const OBLIGATION_COLLATERAL: u64 = 10 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATE;

let user_accounts_owner = Keypair::new();
let user_transfer_authority = Keypair::new();
Expand Down Expand Up @@ -85,7 +80,6 @@ async fn test_success() {
&user_accounts_owner,
&lending_market,
AddObligationArgs {
slots_elapsed: SLOTS_PER_YEAR,
borrow_reserve: &usdc_reserve,
collateral_reserve: &sol_reserve,
collateral_amount: OBLIGATION_COLLATERAL,
Expand All @@ -95,6 +89,9 @@ async fn test_success() {

let (mut banks_client, payer, recent_blockhash) = test.start().await;

let initial_user_collateral_balance =
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await;

let mut transaction = Transaction::new_with_payer(
&[
approve(
Expand Down Expand Up @@ -140,4 +137,46 @@ async fn test_success() {
recent_blockhash,
);
assert!(banks_client.process_transaction(transaction).await.is_ok());

let collateral_received =
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await
- initial_user_collateral_balance;
assert!(collateral_received > 0);

let borrow_reserve_state = usdc_reserve.get_state(&mut banks_client).await;
assert!(borrow_reserve_state.state.cumulative_borrow_rate_wads > Decimal::one());

let obligation_state = obligation.get_state(&mut banks_client).await;
assert_eq!(
obligation_state.cumulative_borrow_rate_wads,
borrow_reserve_state.state.cumulative_borrow_rate_wads
);
assert_eq!(
obligation_state.borrowed_liquidity_wads,
borrow_reserve_state.state.borrowed_liquidity_wads
);

// use cumulative borrow rate directly since test rate starts at 1.0
let expected_obligation_interest = (obligation_state.cumulative_borrow_rate_wads
* OBLIGATION_LOAN)
- Decimal::from(OBLIGATION_LOAN);
assert_eq!(
obligation_state.borrowed_liquidity_wads,
expected_obligation_interest
);

let expected_obligation_total = Decimal::from(OBLIGATION_LOAN) + expected_obligation_interest;

let expected_obligation_repaid_percent =
Decimal::from(OBLIGATION_LOAN) / expected_obligation_total;

let expected_collateral_received =
(expected_obligation_repaid_percent * OBLIGATION_COLLATERAL).round_u64();
assert_eq!(collateral_received, expected_collateral_received);

let expected_collateral_remaining = OBLIGATION_COLLATERAL - expected_collateral_received;
assert_eq!(
obligation_state.deposited_collateral_tokens,
expected_collateral_remaining
);
}

0 comments on commit 2859fbe

Please sign in to comment.