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 + ); }