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

lending: Fix obligation interest accrual #1068

Merged
merged 1 commit into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
}