From 46cdf9a9c5ddd64fcf67d39e52535f88672602c3 Mon Sep 17 00:00:00 2001 From: nope <83512286+nope-finance@users.noreply.github.com> Date: Mon, 13 Jun 2022 00:11:56 -0700 Subject: [PATCH] trying take rate other way (#89) * started take rate cleaning up all the boilerplate adding reserve config take rate claim fees config check + fixing/adding tests fix comment in instruction.rs fixed tests for native token and added pr feedback properly keeping track of availible amount when redeeming fees add in fees when compounding try other method * change to accumulating and redeem when can also added staleness checks * fixed tests * cleanup --- token-lending/cli/src/main.rs | 37 ++- token-lending/program/src/error.rs | 4 + token-lending/program/src/instruction.rs | 52 ++- token-lending/program/src/processor.rs | 89 ++++++ token-lending/program/src/state/reserve.rs | 78 ++++- token-lending/program/tests/helpers/mod.rs | 10 +- token-lending/program/tests/init_reserve.rs | 1 + ...uidate_obligation_and_redeem_collateral.rs | 10 +- .../program/tests/obligation_end_to_end.rs | 2 +- token-lending/program/tests/redeem_fees.rs | 295 ++++++++++++++++++ .../tests/redeem_reserve_collateral.rs | 2 +- .../program/tests/refresh_obligation.rs | 22 +- .../program/tests/refresh_reserve.rs | 17 +- 13 files changed, 592 insertions(+), 27 deletions(-) create mode 100644 token-lending/program/tests/redeem_fees.rs diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 4ff58f5ba9b..05b1ef42c80 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -68,6 +68,8 @@ struct PartialReserveConfig { pub fee_receiver: Option, /// Cut of the liquidation bonus that the protocol receives, as a percentage pub protocol_liquidation_fee: Option, + /// Protocol take rate is the amount borrowed interest protocol recieves, as a percentage + pub protocol_take_rate: Option, } /// Reserve Fees with optional fields @@ -376,7 +378,16 @@ fn main() { .takes_value(true) .required(false) .default_value("30") - .help("Amount of liquidation bonus going to fee reciever: [0, 100]"), + .help("Amount of liquidation bonus going to fee receiver: [0, 100]"), + ) + .arg( + Arg::with_name("protocol_take_rate") + .long("protocol-take-rate") + .validator(is_parsable::) + .value_name("INTEGER_PERCENT") + .takes_value(true) + .required(false) + .help("Amount of interest spread going to fee receiver: [0, 100]"), ) .arg( Arg::with_name("deposit_limit") @@ -527,7 +538,16 @@ fn main() { .value_name("INTEGER_PERCENT") .takes_value(true) .required(false) - .help("Amount of liquidation bonus going to fee reciever: [0, 100]"), + .help("Amount of liquidation bonus going to fee receiver: [0, 100]"), + ) + .arg( + Arg::with_name("protocol_take_rate") + .long("protocol-take-rate") + .validator(is_parsable::) + .value_name("INTEGER_PERCENT") + .takes_value(true) + .required(false) + .help("Amount of interest spread going to fee receiver: [0, 100]"), ) .arg( Arg::with_name("deposit_limit") @@ -669,6 +689,7 @@ fn main() { let liquidity_fee_receiver_keypair = Keypair::new(); let protocol_liquidation_fee = value_of(arg_matches, "protocol_liquidation_fee").unwrap(); + let protocol_take_rate = value_of(arg_matches, "protocol_take_rate").unwrap(); let source_liquidity_account = config .rpc_client @@ -707,6 +728,7 @@ fn main() { borrow_limit, fee_receiver: liquidity_fee_receiver_keypair.pubkey(), protocol_liquidation_fee, + protocol_take_rate, }, source_liquidity_pubkey, source_liquidity_owner_keypair, @@ -738,6 +760,7 @@ fn main() { let borrow_limit = value_of(arg_matches, "borrow_limit"); let fee_receiver = pubkey_of(arg_matches, "fee_receiver"); let protocol_liquidation_fee = value_of(arg_matches, "protocol_liquidation_fee"); + let protocol_take_rate = value_of(arg_matches, "protocol_take_rate"); let pyth_product_pubkey = pubkey_of(arg_matches, "pyth_product"); let pyth_price_pubkey = pubkey_of(arg_matches, "pyth_price"); let switchboard_feed_pubkey = pubkey_of(arg_matches, "switchboard_feed"); @@ -764,6 +787,7 @@ fn main() { borrow_limit, fee_receiver, protocol_liquidation_fee, + protocol_take_rate, }, pyth_product_pubkey, pyth_price_pubkey, @@ -1199,6 +1223,15 @@ fn command_update_reserve( reserve.config.protocol_liquidation_fee = reserve_config.protocol_liquidation_fee.unwrap(); } + if reserve_config.protocol_take_rate.is_some() { + println!( + "Updating protocol_take_rate from {} to {}", + reserve.config.protocol_take_rate, + reserve_config.protocol_take_rate.unwrap(), + ); + reserve.config.protocol_take_rate = reserve_config.protocol_take_rate.unwrap(); + } + let mut new_pyth_product_pubkey = solend_program::NULL_PUBKEY; if pyth_price_pubkey.is_some() { println!( diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 27706f65365..12447c8fc1d 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -159,10 +159,14 @@ pub enum LendingError { /// Not enough liquidity after flash loan #[error("Not enough liquidity after flash loan")] NotEnoughLiquidityAfterFlashLoan, + // 45 /// Null oracle config #[error("Null oracle config")] NullOracleConfig, + /// Insufficent protocol fees to redeem or no liquidity availible to process redeem + #[error("Insufficent protocol fees to claim or no liquidity availible")] + InsufficientProtocolFeesToRedeem, } impl From for ProgramError { diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 8b0f90fb59a..33edad27b13 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -408,6 +408,16 @@ pub enum LendingInstruction { /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount liquidity_amount: u64, }, + + // 18 + /// 0. `[writable]` Reserve account. + /// 1. `[writable]` Borrow reserve liquidity fee receiver account. + /// Must be the fee account specified at InitReserve. + /// 2. `[writable]` Reserve liquidity supply SPL Token account. + /// 3. `[]` Lending market account. + /// 4. `[]` Derived lending market authority. + /// 5. `[]` Token program id. + RedeemFees, } impl LendingInstruction { @@ -444,7 +454,8 @@ impl LendingInstruction { let (deposit_limit, rest) = Self::unpack_u64(rest)?; let (borrow_limit, rest) = Self::unpack_u64(rest)?; let (fee_receiver, rest) = Self::unpack_pubkey(rest)?; - let (protocol_liquidation_fee, _rest) = Self::unpack_u8(rest)?; + let (protocol_liquidation_fee, rest) = Self::unpack_u8(rest)?; + let (protocol_take_rate, _rest) = Self::unpack_u8(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { @@ -464,6 +475,7 @@ impl LendingInstruction { borrow_limit, fee_receiver, protocol_liquidation_fee, + protocol_take_rate, }, } } @@ -524,7 +536,8 @@ impl LendingInstruction { let (deposit_limit, rest) = Self::unpack_u64(rest)?; let (borrow_limit, rest) = Self::unpack_u64(rest)?; let (fee_receiver, rest) = Self::unpack_pubkey(rest)?; - let (protocol_liquidation_fee, _rest) = Self::unpack_u8(rest)?; + let (protocol_liquidation_fee, rest) = Self::unpack_u8(rest)?; + let (protocol_take_rate, _rest) = Self::unpack_u8(rest)?; Self::UpdateReserveConfig { config: ReserveConfig { optimal_utilization_rate, @@ -543,6 +556,7 @@ impl LendingInstruction { borrow_limit, fee_receiver, protocol_liquidation_fee, + protocol_take_rate, }, } } @@ -550,6 +564,7 @@ impl LendingInstruction { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } } + 18 => Self::RedeemFees, _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -646,6 +661,7 @@ impl LendingInstruction { borrow_limit, fee_receiver, protocol_liquidation_fee, + protocol_take_rate, }, } => { buf.push(2); @@ -664,6 +680,7 @@ impl LendingInstruction { buf.extend_from_slice(&borrow_limit.to_le_bytes()); buf.extend_from_slice(&fee_receiver.to_bytes()); buf.extend_from_slice(&protocol_liquidation_fee.to_le_bytes()); + buf.extend_from_slice(&protocol_take_rate.to_le_bytes()); } Self::RefreshReserve => { buf.push(3); @@ -730,11 +747,15 @@ impl LendingInstruction { buf.extend_from_slice(&config.borrow_limit.to_le_bytes()); buf.extend_from_slice(&config.fee_receiver.to_bytes()); buf.extend_from_slice(&config.protocol_liquidation_fee.to_le_bytes()); + buf.extend_from_slice(&config.protocol_take_rate.to_le_bytes()); } Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => { buf.push(17); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } + Self::RedeemFees {} => { + buf.push(18); + } } buf } @@ -1352,3 +1373,30 @@ pub fn liquidate_obligation_and_redeem_reserve_collateral( .pack(), } } + +/// Creates a `RedeemFees` instruction +pub fn redeem_fees( + program_id: Pubkey, + reserve_pubkey: Pubkey, + reserve_liquidity_fee_receiver_pubkey: Pubkey, + reserve_supply_liquidity_pubkey: Pubkey, + lending_market_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); + let accounts = vec![ + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new(reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(reserve_supply_liquidity_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + Instruction { + program_id, + accounts, + data: LendingInstruction::RedeemFees.pack(), + } +} diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c415f5e8245..f215385f86e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -144,6 +144,10 @@ pub fn process_instruction( accounts, ) } + LendingInstruction::RedeemFees => { + msg!("Instruction: RedeemFees"); + process_redeem_fees(program_id, accounts) + } } } @@ -2197,6 +2201,87 @@ fn process_update_reserve_config( Ok(()) } +#[inline(never)] // avoid stack frame limit +fn process_redeem_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_info = next_account_info(account_info_iter)?; + let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let reserve_supply_liquidity_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let lending_market_authority_info = next_account_info(account_info_iter)?; + let token_program_id = next_account_info(account_info_iter)?; + let clock = &Clock::get()?; + + let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!( + "Reserve provided is not owned by the lending program {} != {}", + &reserve_info.owner.to_string(), + &program_id.to_string(), + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + + if &reserve.config.fee_receiver != reserve_liquidity_fee_receiver_info.key { + msg!("Reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.liquidity.supply_pubkey != reserve_supply_liquidity_info.key { + msg!("Reserve liquidity supply must be used as the reserve supply liquidity provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if &reserve.lending_market != lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if reserve.last_update.is_stale(clock.slot)? { + msg!("reserve is stale and must be refreshed in the current slot"); + return Err(LendingError::ReserveStale.into()); + } + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.token_program_id != token_program_id.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } + let authority_signer_seeds = &[ + lending_market_info.key.as_ref(), + &[lending_market.bump_seed], + ]; + let lending_market_authority_pubkey = + Pubkey::create_program_address(authority_signer_seeds, program_id)?; + if &lending_market_authority_pubkey != lending_market_authority_info.key { + msg!( + "Derived lending market authority does not match the lending market authority provided" + ); + return Err(LendingError::InvalidMarketAuthority.into()); + } + + let withdraw_amount = reserve.calculate_redeem_fees()?; + if withdraw_amount == 0 { + return Err(LendingError::InsufficientProtocolFeesToRedeem.into()); + } + + reserve.liquidity.redeem_fees(withdraw_amount)?; + reserve.last_update.mark_stale(); + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + + spl_token_transfer(TokenTransferParams { + source: reserve_supply_liquidity_info.clone(), + destination: reserve_liquidity_fee_receiver_info.clone(), + amount: withdraw_amount, + authority: lending_market_authority_info.clone(), + authority_signer_seeds, + token_program: token_program_id.clone(), + })?; + + Ok(()) +} + fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult { if !rent.is_exempt(account_info.lamports(), account_info.data_len()) { msg!( @@ -2605,6 +2690,10 @@ fn validate_reserve_config(config: ReserveConfig) -> ProgramResult { msg!("Protocol liquidation fee must be in range [0, 100]"); return Err(LendingError::InvalidConfig.into()); } + if config.protocol_take_rate > 100 { + msg!("Protocol take rate must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } Ok(()) } diff --git a/token-lending/program/src/state/reserve.rs b/token-lending/program/src/state/reserve.rs index f5ba8410453..f2ea8647d8a 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -13,7 +13,7 @@ use solana_program::{ pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::{ - cmp::Ordering, + cmp::{min, Ordering}, convert::{TryFrom, TryInto}, }; @@ -144,8 +144,9 @@ impl Reserve { let slots_elapsed = self.last_update.slots_elapsed(current_slot)?; if slots_elapsed > 0 { let current_borrow_rate = self.current_borrow_rate()?; + let take_rate = Rate::from_percent(self.config.protocol_take_rate); self.liquidity - .compound_interest(current_borrow_rate, slots_elapsed)?; + .compound_interest(current_borrow_rate, slots_elapsed, take_rate)?; } Ok(()) } @@ -327,6 +328,16 @@ impl Reserve { let protocol_fee = std::cmp::max(bonus.try_mul(Rate::from_percent(0))?.try_ceil_u64()?, 1); Ok(protocol_fee) } + + /// Calculate protocol fee redemption accounting for availible liquidity and accumulated fees + pub fn calculate_redeem_fees(&self) -> Result { + Ok(min( + self.liquidity.available_amount, + self.liquidity + .accumulated_protocol_fees_wads + .try_floor_u64()?, + )) + } } /// Initialize a reserve @@ -396,6 +407,8 @@ pub struct ReserveLiquidity { pub borrowed_amount_wads: Decimal, /// Reserve liquidity cumulative borrow rate pub cumulative_borrow_rate_wads: Decimal, + /// Reserve cumulative protocol fees + pub accumulated_protocol_fees_wads: Decimal, /// Reserve liquidity market price in quote currency pub market_price: Decimal, } @@ -412,13 +425,16 @@ impl ReserveLiquidity { available_amount: 0, borrowed_amount_wads: Decimal::zero(), cumulative_borrow_rate_wads: Decimal::one(), + accumulated_protocol_fees_wads: Decimal::zero(), market_price: params.market_price, } } /// Calculate the total reserve supply including active loans pub fn total_supply(&self) -> Result { - Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads) + Decimal::from(self.available_amount) + .try_add(self.borrowed_amount_wads)? + .try_sub(self.accumulated_protocol_fees_wads) } /// Add liquidity to available amount @@ -472,6 +488,19 @@ impl ReserveLiquidity { Ok(()) } + /// Subtract settle amount from accumulated_protocol_fees_wads and withdraw_amount from available liquidity + pub fn redeem_fees(&mut self, withdraw_amount: u64) -> ProgramResult { + self.available_amount = self + .available_amount + .checked_sub(withdraw_amount) + .ok_or(LendingError::MathOverflow)?; + self.accumulated_protocol_fees_wads = self + .accumulated_protocol_fees_wads + .try_sub(Decimal::from(withdraw_amount))?; + + Ok(()) + } + /// Calculate the liquidity utilization rate of the reserve pub fn utilization_rate(&self) -> Result { let total_supply = self.total_supply()?; @@ -486,6 +515,7 @@ impl ReserveLiquidity { &mut self, current_borrow_rate: Rate, slots_elapsed: u64, + take_rate: Rate, ) -> ProgramResult { let slot_interest_rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?; let compounded_interest_rate = Rate::one() @@ -494,9 +524,17 @@ impl ReserveLiquidity { self.cumulative_borrow_rate_wads = self .cumulative_borrow_rate_wads .try_mul(compounded_interest_rate)?; - self.borrowed_amount_wads = self + + let net_new_debt = self .borrowed_amount_wads - .try_mul(compounded_interest_rate)?; + .try_mul(compounded_interest_rate)? + .try_sub(self.borrowed_amount_wads)?; + + self.accumulated_protocol_fees_wads = net_new_debt + .try_mul(take_rate)? + .try_add(self.accumulated_protocol_fees_wads)?; + + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(net_new_debt)?; Ok(()) } } @@ -648,6 +686,8 @@ pub struct ReserveConfig { pub fee_receiver: Pubkey, /// Cut of the liquidation bonus that the protocol receives, as a percentage pub protocol_liquidation_fee: u8, + /// Protocol take rate is the amount borrowed interest protocol recieves, as a percentage + pub protocol_take_rate: u8, } /// Additional fee information on a reserve @@ -758,7 +798,7 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 619; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 8 + 8 + 32 + 1 + 247 +const RESERVE_LEN: usize = 619; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 8 + 8 + 32 + 1 + 1 + 16 + 230 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -797,6 +837,8 @@ impl Pack for Reserve { config_borrow_limit, config_fee_receiver, config_protocol_liquidation_fee, + config_protocol_take_rate, + liquidity_accumulated_protocol_fees_wads, _padding, ) = mut_array_refs![ output, @@ -830,7 +872,9 @@ impl Pack for Reserve { 8, PUBKEY_BYTES, 1, - 247 + 1, + 16, + 230 ]; // reserve @@ -855,6 +899,10 @@ impl Pack for Reserve { self.liquidity.cumulative_borrow_rate_wads, liquidity_cumulative_borrow_rate_wads, ); + pack_decimal( + self.liquidity.accumulated_protocol_fees_wads, + liquidity_accumulated_protocol_fees_wads, + ); pack_decimal(self.liquidity.market_price, liquidity_market_price); // collateral @@ -877,6 +925,7 @@ impl Pack for Reserve { *config_borrow_limit = self.config.borrow_limit.to_le_bytes(); config_fee_receiver.copy_from_slice(self.config.fee_receiver.as_ref()); *config_protocol_liquidation_fee = self.config.protocol_liquidation_fee.to_le_bytes(); + *config_protocol_take_rate = self.config.protocol_take_rate.to_le_bytes(); } /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). @@ -914,6 +963,8 @@ impl Pack for Reserve { config_borrow_limit, config_fee_receiver, config_protocol_liquidation_fee, + config_protocol_take_rate, + liquidity_accumulated_protocol_fees_wads, _padding, ) = array_refs![ input, @@ -947,7 +998,9 @@ impl Pack for Reserve { 8, PUBKEY_BYTES, 1, - 247 + 1, + 16, + 230 ]; let version = u8::from_le_bytes(*version); @@ -974,6 +1027,9 @@ impl Pack for Reserve { available_amount: u64::from_le_bytes(*liquidity_available_amount), borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), + accumulated_protocol_fees_wads: unpack_decimal( + liquidity_accumulated_protocol_fees_wads, + ), market_price: unpack_decimal(liquidity_market_price), }, collateral: ReserveCollateral { @@ -998,6 +1054,7 @@ impl Pack for Reserve { borrow_limit: u64::from_le_bytes(*config_borrow_limit), fee_receiver: Pubkey::new_from_array(*config_fee_receiver), protocol_liquidation_fee: u8::from_le_bytes(*config_protocol_liquidation_fee), + protocol_take_rate: u8::from_le_bytes(*config_protocol_take_rate), }, }) } @@ -1165,15 +1222,18 @@ mod test { fn compound_interest( slots_elapsed in 0..=SLOTS_PER_YEAR, borrow_rate in 0..=u8::MAX, + take_rate in 0..=100u8, ) { let mut reserve = Reserve::default(); let borrow_rate = Rate::from_percent(borrow_rate); + let take_rate = Rate::from_percent(take_rate); // Simulate running for max 1000 years, assuming that interest is // compounded at least once a year for _ in 0..1000 { - reserve.liquidity.compound_interest(borrow_rate, slots_elapsed)?; + reserve.liquidity.compound_interest(borrow_rate, slots_elapsed, take_rate)?; reserve.liquidity.cumulative_borrow_rate_wads.to_scaled_val()?; + reserve.liquidity.accumulated_protocol_fees_wads.to_scaled_val()?; } } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 50805befdab..b95afe8e03d 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -60,6 +60,7 @@ pub fn test_reserve_config() -> ReserveConfig { borrow_limit: u64::MAX, fee_receiver: Keypair::new().pubkey(), protocol_liquidation_fee: 30, + protocol_take_rate: 10, } } @@ -265,7 +266,7 @@ pub fn add_reserve( } = args; let is_native = if liquidity_mint_pubkey == spl_token::native_mint::id() { - COption::Some(1) + COption::Some(2039280u64) } else { COption::None }; @@ -1217,6 +1218,7 @@ pub fn add_sol_oracle(test: &mut ProgramTest) -> TestOracle { Pubkey::from_str(SOL_SWITCHBOARD_FEED).unwrap(), // Set SOL price to $20 Decimal::from(20u64), + 0, ) } @@ -1228,6 +1230,7 @@ pub fn add_sol_oracle_switchboardv2(test: &mut ProgramTest) -> TestOracle { Pubkey::from_str(SOL_SWITCHBOARDV2_FEED).unwrap(), // Set SOL price to $20 Decimal::from(20u64), + 0, ) } @@ -1240,6 +1243,7 @@ pub fn add_usdc_oracle(test: &mut ProgramTest) -> TestOracle { Pubkey::from_str(SRM_SWITCHBOARD_FEED).unwrap(), // Set USDC price to $1 Decimal::from(1u64), + 0, ) } @@ -1252,6 +1256,7 @@ pub fn add_usdc_oracle_switchboardv2(test: &mut ProgramTest) -> TestOracle { Pubkey::from_str(SRM_SWITCHBOARDV2_FEED).unwrap(), // Set USDC price to $1 Decimal::from(1u64), + 0, ) } @@ -1261,6 +1266,7 @@ pub fn add_oracle( pyth_price_pubkey: Pubkey, switchboard_feed_pubkey: Pubkey, price: Decimal, + valid_slot: u64, ) -> TestOracle { let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json").unwrap(); @@ -1286,7 +1292,7 @@ pub fn add_oracle( .checked_pow(pyth_price.expo.checked_abs().unwrap().try_into().unwrap()) .unwrap(); - pyth_price.valid_slot = 0; + pyth_price.valid_slot = valid_slot; pyth_price.agg.price = price .try_round_u64() .unwrap() diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 66a8c75c32e..a3396758836 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -419,6 +419,7 @@ async fn test_update_reserve_config() { borrow_limit: 300_000, fee_receiver: Keypair::new().pubkey(), protocol_liquidation_fee: 30, + protocol_take_rate: 10, }; let (mut banks_client, payer, recent_blockhash) = test.start().await; diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index c97545979d4..a4e50db96dc 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -111,7 +111,7 @@ async fn test_success() { get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; let initial_user_withdraw_liquidity_balance = get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let initial_fee_reciever_withdraw_liquidity_balance = + let initial_fee_receiver_withdraw_liquidity_balance = get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; let mut transaction = Transaction::new_with_payer( @@ -165,12 +165,12 @@ async fn test_success() { let user_withdraw_liquidity_balance = get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; - let fee_reciever_withdraw_liquidity_balance = + let fee_receiver_withdraw_liquidity_balance = get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; assert_eq!( - user_withdraw_liquidity_balance + fee_reciever_withdraw_liquidity_balance, + user_withdraw_liquidity_balance + fee_receiver_withdraw_liquidity_balance, initial_user_withdraw_liquidity_balance - + initial_fee_reciever_withdraw_liquidity_balance + + initial_fee_receiver_withdraw_liquidity_balance + SOL_LIQUIDATION_AMOUNT_LAMPORTS ); @@ -179,7 +179,7 @@ async fn test_success() { // SOL_LIQUIDATION_AMOUNT_LAMPORTS * 3 / 10 / 11, // 0 % min 1 for now max(SOL_LIQUIDATION_AMOUNT_LAMPORTS * 0 / 10 / 11, 1), - (fee_reciever_withdraw_liquidity_balance - initial_fee_reciever_withdraw_liquidity_balance) + (fee_receiver_withdraw_liquidity_balance - initial_fee_receiver_withdraw_liquidity_balance) ); let collateral_supply_balance = diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index eba68c3d68a..83312bd3be8 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -291,7 +291,7 @@ async fn test_success2() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(156_000); + test.set_bpf_compute_max_units(158_000); const FEE_AMOUNT: u64 = 100; const HOST_FEE_AMOUNT: u64 = 20; diff --git a/token-lending/program/tests/redeem_fees.rs b/token-lending/program/tests/redeem_fees.rs new file mode 100644 index 00000000000..3cb4a75c415 --- /dev/null +++ b/token-lending/program/tests/redeem_fees.rs @@ -0,0 +1,295 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use std::str::FromStr; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solend_program::{ + instruction::{redeem_fees, refresh_reserve}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, + processor::process_instruction, + state::SLOTS_PER_YEAR, +}; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "solend_program", + solend_program::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(228_000); + + const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100000000 * LAMPORTS_TO_SOL; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100000 * FRACTIONAL_TO_USDC; + const BORROW_AMOUNT: u64 = 100000; + const SLOTS_ELAPSED: u64 = 69420; + + let user_accounts_owner = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mut usdc_reserve_config = test_reserve_config(); + usdc_reserve_config.loan_to_value_ratio = 80; + + // Configure reserve to a fixed borrow rate of 200% + const BORROW_RATE: u8 = 250; + usdc_reserve_config.min_borrow_rate = BORROW_RATE; + usdc_reserve_config.optimal_borrow_rate = BORROW_RATE; + usdc_reserve_config.optimal_utilization_rate = 100; + + let usdc_mint = add_usdc_mint(&mut test); + let usdc_oracle = add_oracle( + &mut test, + Pubkey::from_str(SRM_PYTH_PRODUCT).unwrap(), + Pubkey::from_str(SRM_PYTH_PRICE).unwrap(), + Pubkey::from_str(SRM_SWITCHBOARD_FEED).unwrap(), + // Set USDC price to $1 + Decimal::from(1u64), + SLOTS_ELAPSED, + ); + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &usdc_oracle, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: BORROW_AMOUNT, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_decimals: usdc_mint.decimals, + liquidity_mint_pubkey: usdc_mint.pubkey, + config: usdc_reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let mut sol_reserve_config = test_reserve_config(); + sol_reserve_config.loan_to_value_ratio = 80; + + // Configure reserve to a fixed borrow rate of 1% + sol_reserve_config.min_borrow_rate = BORROW_RATE; + sol_reserve_config.optimal_borrow_rate = BORROW_RATE; + sol_reserve_config.optimal_utilization_rate = 100; + let sol_oracle = add_oracle( + &mut test, + Pubkey::from_str(SOL_PYTH_PRODUCT).unwrap(), + Pubkey::from_str(SOL_PYTH_PRICE).unwrap(), + Pubkey::from_str(SOL_SWITCHBOARD_FEED).unwrap(), + // Set SOL price to $20 + Decimal::from(20u64), + SLOTS_ELAPSED, + ); + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &sol_oracle, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: BORROW_AMOUNT, + liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, + liquidity_mint_decimals: 9, + liquidity_mint_pubkey: spl_token::native_mint::id(), + config: sol_reserve_config, + slots_elapsed: 1, // elapsed from 1; clock.slot = 2 + ..AddReserveArgs::default() + }, + ); + + let mut test_context = test.start_with_context().await; + test_context.warp_to_slot(2 + SLOTS_ELAPSED).unwrap(); // clock.slot = 100 + + let ProgramTestContext { + mut banks_client, + payer, + last_blockhash: recent_blockhash, + .. + } = test_context; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_reserve( + solend_program::id(), + usdc_test_reserve.pubkey, + usdc_oracle.pyth_price_pubkey, + usdc_oracle.switchboard_feed_pubkey, + ), + refresh_reserve( + solend_program::id(), + sol_test_reserve.pubkey, + sol_oracle.pyth_price_pubkey, + sol_oracle.switchboard_feed_pubkey, + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve_before = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve_before = usdc_test_reserve.get_state(&mut banks_client).await; + let sol_balance_before = + get_token_balance(&mut banks_client, sol_reserve_before.config.fee_receiver).await; + let usdc_balance_before = + get_token_balance(&mut banks_client, usdc_reserve_before.config.fee_receiver).await; + + let mut transaction2 = Transaction::new_with_payer( + &[ + redeem_fees( + solend_program::id(), + usdc_test_reserve.pubkey, + usdc_test_reserve.config.fee_receiver, + usdc_test_reserve.liquidity_supply_pubkey, + lending_market.pubkey, + ), + redeem_fees( + solend_program::id(), + sol_test_reserve.pubkey, + sol_test_reserve.config.fee_receiver, + sol_test_reserve.liquidity_supply_pubkey, + lending_market.pubkey, + ), + ], + Some(&payer.pubkey()), + ); + + transaction2.sign(&[&payer], recent_blockhash); + assert!(banks_client.process_transaction(transaction2).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await; + let sol_balance_after = + get_token_balance(&mut banks_client, sol_reserve.config.fee_receiver).await; + let usdc_balance_after = + get_token_balance(&mut banks_client, usdc_reserve.config.fee_receiver).await; + + let slot_rate = Rate::from_percent(BORROW_RATE) + .try_div(SLOTS_PER_YEAR) + .unwrap(); + let compound_rate = Rate::one() + .try_add(slot_rate) + .unwrap() + .try_pow(SLOTS_ELAPSED) + .unwrap(); + let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + + let net_new_debt = compound_borrow + .try_sub(Decimal::from(BORROW_AMOUNT)) + .unwrap(); + let protocol_take_rate = Rate::from_percent(sol_test_reserve.config.protocol_take_rate); + let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); + + assert_eq!( + usdc_reserve_before.liquidity.total_supply(), + usdc_reserve.liquidity.total_supply(), + ); + assert_eq!( + sol_reserve_before.liquidity.total_supply(), + sol_reserve.liquidity.total_supply(), + ); + assert_eq!( + Rate::from(usdc_reserve_before.collateral_exchange_rate().unwrap()), + Rate::from(usdc_reserve.collateral_exchange_rate().unwrap()), + ); + assert_eq!( + Rate::from(sol_reserve_before.collateral_exchange_rate().unwrap()), + Rate::from(sol_reserve.collateral_exchange_rate().unwrap()), + ); + + // utilization increases because redeeming adds to borrows and takes from availible + assert_eq!( + usdc_reserve_before.liquidity.utilization_rate().unwrap(), + usdc_reserve.liquidity.utilization_rate().unwrap(), + ); + assert_eq!( + sol_reserve_before.liquidity.utilization_rate().unwrap(), + sol_reserve.liquidity.utilization_rate().unwrap(), + ); + assert_eq!( + sol_reserve.liquidity.cumulative_borrow_rate_wads, + compound_rate.into() + ); + assert_eq!( + sol_reserve.liquidity.cumulative_borrow_rate_wads, + usdc_reserve.liquidity.cumulative_borrow_rate_wads + ); + assert_eq!(sol_reserve.liquidity.borrowed_amount_wads, compound_borrow); + assert_eq!(usdc_reserve.liquidity.borrowed_amount_wads, compound_borrow); + assert_eq!( + Decimal::from(delta_accumulated_protocol_fees.try_floor_u64().unwrap()), + usdc_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_sub(usdc_reserve.liquidity.accumulated_protocol_fees_wads) + .unwrap() + ); + assert_eq!( + Decimal::from(delta_accumulated_protocol_fees.try_floor_u64().unwrap()), + sol_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_sub(sol_reserve.liquidity.accumulated_protocol_fees_wads) + .unwrap() + ); + assert_eq!( + usdc_reserve_before.liquidity.accumulated_protocol_fees_wads, + delta_accumulated_protocol_fees + ); + assert_eq!( + sol_reserve_before.liquidity.accumulated_protocol_fees_wads, + delta_accumulated_protocol_fees + ); + assert_eq!( + usdc_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_floor_u64() + .unwrap(), + usdc_balance_after - usdc_balance_before + ); + assert_eq!( + usdc_reserve.liquidity.accumulated_protocol_fees_wads, + usdc_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_sub(Decimal::from(usdc_balance_after - usdc_balance_before)) + .unwrap() + ); + assert_eq!( + sol_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_floor_u64() + .unwrap(), + sol_balance_after - sol_balance_before + ); + assert_eq!( + sol_reserve.liquidity.accumulated_protocol_fees_wads, + sol_reserve_before + .liquidity + .accumulated_protocol_fees_wads + .try_sub(Decimal::from(sol_balance_after - sol_balance_before)) + .unwrap() + ); + assert_eq!( + sol_reserve.liquidity.borrowed_amount_wads, + usdc_reserve.liquidity.borrowed_amount_wads + ); + assert_eq!( + sol_reserve.liquidity.market_price, + sol_test_reserve.market_price + ); + assert_eq!( + usdc_reserve.liquidity.market_price, + usdc_test_reserve.market_price + ); +} diff --git a/token-lending/program/tests/redeem_reserve_collateral.rs b/token-lending/program/tests/redeem_reserve_collateral.rs index 5cbfdab2485..003233a2a2b 100644 --- a/token-lending/program/tests/redeem_reserve_collateral.rs +++ b/token-lending/program/tests/redeem_reserve_collateral.rs @@ -24,7 +24,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(47_000); + test.set_bpf_compute_max_units(48_000); let user_accounts_owner = Keypair::new(); let lending_market = add_lending_market(&mut test); diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 48f976b840d..6384a299b38 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -9,7 +9,7 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::Transaction, }; -use solend_program::math::{Rate, TryAdd, TryMul}; +use solend_program::math::{Rate, TryAdd, TryMul, TrySub}; use solend_program::state::SLOTS_PER_YEAR; use solend_program::{ instruction::{refresh_obligation, refresh_reserve}, @@ -27,7 +27,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(43_000); + test.set_bpf_compute_max_units(45_000); const SOL_DEPOSIT_AMOUNT: u64 = 100; const USDC_BORROW_AMOUNT: u64 = 1_000; @@ -150,6 +150,14 @@ async fn test_success() { let compound_borrow_wads = Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL) .try_mul(compound_rate) .unwrap(); + let net_new_debt = compound_borrow_wads + .try_sub(Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL)) + .unwrap(); + let protocol_take_rate = Rate::from_percent(usdc_reserve.config.protocol_take_rate); + let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); + let new_borrow_amount_wads = Decimal::from(USDC_BORROW_AMOUNT_FRACTIONAL) + .try_add(net_new_debt) + .unwrap(); let liquidity_price = liquidity.market_value.try_div(compound_borrow).unwrap(); @@ -162,7 +170,15 @@ async fn test_success() { usdc_reserve.liquidity.borrowed_amount_wads, liquidity.borrowed_amount_wads ); - assert_eq!(liquidity.borrowed_amount_wads, compound_borrow_wads); + assert_eq!(liquidity.borrowed_amount_wads, new_borrow_amount_wads); + assert_eq!( + usdc_reserve.liquidity.accumulated_protocol_fees_wads, + delta_accumulated_protocol_fees + ); + assert_eq!( + sol_reserve.liquidity.accumulated_protocol_fees_wads, + Decimal::from(0u64) + ); assert_eq!(sol_reserve.liquidity.market_price, collateral_price,); assert_eq!(usdc_reserve.liquidity.market_price, liquidity_price,); } diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs index 349a46947e1..2aa0773c97f 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -11,7 +11,7 @@ use solana_sdk::{ }; use solend_program::{ instruction::refresh_reserve, - math::{Decimal, Rate, TryAdd, TryDiv, TryMul}, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, processor::process_instruction, state::SLOTS_PER_YEAR, }; @@ -25,7 +25,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(28_000); + test.set_bpf_compute_max_units(31_000); const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL; const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC; @@ -117,6 +117,11 @@ async fn test_success() { .unwrap(); let compound_rate = Rate::one().try_add(slot_rate).unwrap(); let compound_borrow = Decimal::from(BORROW_AMOUNT).try_mul(compound_rate).unwrap(); + let net_new_debt = compound_borrow + .try_sub(Decimal::from(BORROW_AMOUNT)) + .unwrap(); + let protocol_take_rate = Rate::from_percent(usdc_reserve.config.protocol_take_rate); + let delta_accumulated_protocol_fees = net_new_debt.try_mul(protocol_take_rate).unwrap(); assert_eq!( sol_reserve.liquidity.cumulative_borrow_rate_wads, @@ -139,4 +144,12 @@ async fn test_success() { usdc_reserve.liquidity.market_price, usdc_test_reserve.market_price ); + assert_eq!( + delta_accumulated_protocol_fees, + usdc_reserve.liquidity.accumulated_protocol_fees_wads + ); + assert_eq!( + delta_accumulated_protocol_fees, + sol_reserve.liquidity.accumulated_protocol_fees_wads + ); }