From f9606649458541fa5291df55c38bf2a2fa01e3f6 Mon Sep 17 00:00:00 2001 From: nope <83512286+nope-finance@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:57:32 -0700 Subject: [PATCH] added liquidate and redeem (with protocol fee) (#75) * added liquidated and redeem (with protocol fee) * pr comments * working tests * protocol liquidation fee configurable * adding protocol liquidation fee to cli * justin feedback * setting fee to 0 for initial deploy --- token-lending/cli/src/main.rs | 16 ++ token-lending/program/src/instruction.rs | 122 +++++++++-- token-lending/program/src/processor.rs | 136 +++++++++++- token-lending/program/src/state/reserve.rs | 30 ++- token-lending/program/tests/helpers/mod.rs | 10 +- token-lending/program/tests/init_reserve.rs | 1 + ...uidate_obligation_and_redeem_collateral.rs | 201 ++++++++++++++++++ 7 files changed, 494 insertions(+), 22 deletions(-) create mode 100644 token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 45885081a2a..829aefe8f7b 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -66,6 +66,8 @@ struct PartialReserveConfig { pub borrow_limit: Option, /// Liquidity fee receiver pub fee_receiver: Option, + /// Cut of the liquidation bonus that the protocol receives, as a percentage + pub protocol_liquidation_fee: Option, } /// Reserve Fees with optional fields @@ -646,6 +648,8 @@ fn main() { let flash_loan_fee_wad = (flash_loan_fee * WAD as f64) as u64; let liquidity_fee_receiver_keypair = Keypair::new(); + let protocol_liquidation_fee = + value_of(arg_matches, "protocol_liquidation_fee").unwrap(); let source_liquidity_account = config .rpc_client @@ -683,6 +687,7 @@ fn main() { deposit_limit, borrow_limit, fee_receiver: liquidity_fee_receiver_keypair.pubkey(), + protocol_liquidation_fee, }, source_liquidity_pubkey, source_liquidity_owner_keypair, @@ -713,6 +718,7 @@ fn main() { let deposit_limit = value_of(arg_matches, "deposit_limit"); 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 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"); @@ -738,6 +744,7 @@ fn main() { deposit_limit, borrow_limit, fee_receiver, + protocol_liquidation_fee, }, pyth_product_pubkey, pyth_price_pubkey, @@ -1164,6 +1171,15 @@ fn command_update_reserve( reserve.config.fee_receiver = reserve_config.fee_receiver.unwrap(); } + if reserve_config.protocol_liquidation_fee.is_some() { + println!( + "Updating protocol_liquidation_fee from {} to {}", + reserve.config.protocol_liquidation_fee, + reserve_config.protocol_liquidation_fee.unwrap(), + ); + reserve.config.protocol_liquidation_fee = reserve_config.protocol_liquidation_fee.unwrap(); + } + let mut new_pyth_product_pubkey = spl_token_lending::NULL_PUBKEY; if pyth_price_pubkey.is_some() { println!( diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 0e26cde4f9b..f46e05bccda 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -379,6 +379,35 @@ pub enum LendingInstruction { /// Reserve config to update to config: ReserveConfig, }, + + // 17 + /// Repay borrowed liquidity to a reserve to receive collateral at a discount from an unhealthy + /// obligation. Requires a refreshed obligation and reserves. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Source liquidity token account. + /// Minted by repay reserve liquidity mint. + /// $authority can transfer $liquidity_amount. + /// 1. `[writable]` Destination collateral token account. + /// Minted by withdraw reserve collateral mint. + /// 2. `[writable]` Destination liquidity token account. + /// 3. `[writable]` Repay reserve account - refreshed. + /// 4. `[writable]` Repay reserve liquidity supply SPL Token account. + /// 5. `[writable]` Withdraw reserve account - refreshed. + /// 6. `[writable]` Withdraw reserve collateral SPL Token mint. + /// 7. `[writable]` Withdraw reserve collateral supply SPL Token account. + /// 8. `[writable]` Withdraw reserve liquidity supply SPL Token account. + /// 9. `[writable]` Withdraw reserve liquidity fee receiver account. + /// 10 `[writable]` Obligation account - refreshed. + /// 11 `[]` Lending market account. + /// 12 `[]` Derived lending market authority. + /// 13 `[signer]` User transfer authority ($authority). + /// 14 `[]` Token program id. + LiquidateObligationAndRedeemReserveCollateral { + /// Amount of liquidity to repay - u64::MAX for up to 100% of borrowed amount + liquidity_amount: u64, + }, } impl LendingInstruction { @@ -414,7 +443,8 @@ impl LendingInstruction { let (host_fee_percentage, rest) = Self::unpack_u8(rest)?; let (deposit_limit, rest) = Self::unpack_u64(rest)?; let (borrow_limit, rest) = Self::unpack_u64(rest)?; - let (fee_receiver, _) = Self::unpack_pubkey(rest)?; + let (fee_receiver, rest) = Self::unpack_pubkey(rest)?; + let (protocol_liquidation_fee, _rest) = Self::unpack_u8(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { @@ -433,6 +463,7 @@ impl LendingInstruction { deposit_limit, borrow_limit, fee_receiver, + protocol_liquidation_fee, }, } } @@ -480,20 +511,20 @@ impl LendingInstruction { Self::WithdrawObligationCollateralAndRedeemReserveCollateral { collateral_amount } } 16 => { - let (optimal_utilization_rate, _rest) = Self::unpack_u8(rest)?; - let (loan_to_value_ratio, _rest) = Self::unpack_u8(_rest)?; - let (liquidation_bonus, _rest) = Self::unpack_u8(_rest)?; - let (liquidation_threshold, _rest) = Self::unpack_u8(_rest)?; - let (min_borrow_rate, _rest) = Self::unpack_u8(_rest)?; - let (optimal_borrow_rate, _rest) = Self::unpack_u8(_rest)?; - let (max_borrow_rate, _rest) = Self::unpack_u8(_rest)?; - let (borrow_fee_wad, _rest) = Self::unpack_u64(_rest)?; - let (flash_loan_fee_wad, _rest) = Self::unpack_u64(_rest)?; - let (host_fee_percentage, _rest) = Self::unpack_u8(_rest)?; - let (deposit_limit, _rest) = Self::unpack_u64(_rest)?; - let (borrow_limit, _rest) = Self::unpack_u64(_rest)?; - let (fee_receiver, _) = Self::unpack_pubkey(_rest)?; - + let (optimal_utilization_rate, rest) = Self::unpack_u8(rest)?; + let (loan_to_value_ratio, rest) = Self::unpack_u8(rest)?; + let (liquidation_bonus, rest) = Self::unpack_u8(rest)?; + let (liquidation_threshold, rest) = Self::unpack_u8(rest)?; + let (min_borrow_rate, rest) = Self::unpack_u8(rest)?; + let (optimal_borrow_rate, rest) = Self::unpack_u8(rest)?; + let (max_borrow_rate, rest) = Self::unpack_u8(rest)?; + let (borrow_fee_wad, rest) = Self::unpack_u64(rest)?; + let (flash_loan_fee_wad, rest) = Self::unpack_u64(rest)?; + let (host_fee_percentage, rest) = Self::unpack_u8(rest)?; + 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)?; Self::UpdateReserveConfig { config: ReserveConfig { optimal_utilization_rate, @@ -511,9 +542,14 @@ impl LendingInstruction { deposit_limit, borrow_limit, fee_receiver, + protocol_liquidation_fee, }, } } + 17 => { + let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; + Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } + } _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -609,6 +645,7 @@ impl LendingInstruction { deposit_limit, borrow_limit, fee_receiver, + protocol_liquidation_fee, }, } => { buf.push(2); @@ -626,6 +663,7 @@ impl LendingInstruction { buf.extend_from_slice(&deposit_limit.to_le_bytes()); 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()); } Self::RefreshReserve => { buf.push(3); @@ -691,6 +729,11 @@ impl LendingInstruction { buf.extend_from_slice(&config.deposit_limit.to_le_bytes()); 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()); + } + Self::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => { + buf.push(17); + buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } } buf @@ -1216,3 +1259,52 @@ pub fn update_reserve_config( data: LendingInstruction::UpdateReserveConfig { config }.pack(), } } + +/// Creates a `LiquidateObligationAndRedeemReserveCollateral` instruction +#[allow(clippy::too_many_arguments)] +pub fn liquidate_obligation_and_redeem_reserve_collateral( + program_id: Pubkey, + liquidity_amount: u64, + source_liquidity_pubkey: Pubkey, + destination_collateral_pubkey: Pubkey, + destination_liquidity_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + repay_reserve_liquidity_supply_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + withdraw_reserve_collateral_mint_pubkey: Pubkey, + withdraw_reserve_collateral_supply_pubkey: Pubkey, + withdraw_reserve_liquidity_supply_pubkey: Pubkey, + withdraw_reserve_liquidity_fee_receiver_pubkey: Pubkey, + obligation_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + user_transfer_authority_pubkey: Pubkey, +) -> Instruction { + let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( + &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], + &program_id, + ); + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(source_liquidity_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(repay_reserve_pubkey, false), + AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new(withdraw_reserve_collateral_mint_pubkey, false), + AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_liquidity_supply_pubkey, false), + AccountMeta::new(withdraw_reserve_liquidity_fee_receiver_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: LendingInstruction::LiquidateObligationAndRedeemReserveCollateral { + liquidity_amount, + } + .pack(), + } +} diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c21d1ef13cc..3e0a802ec39 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -125,6 +125,14 @@ pub fn process_instruction( msg!("Instruction: UpdateReserveConfig"); process_update_reserve_config(program_id, config, accounts) } + LendingInstruction::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => { + msg!("Instruction: Liquidate Obligation and Redeem Reserve Collateral"); + process_liquidate_obligation_and_redeem_reserve_collateral( + program_id, + liquidity_amount, + accounts, + ) + } } } @@ -637,7 +645,7 @@ fn _redeem_reserve_collateral<'a>( user_transfer_authority_info: &AccountInfo<'a>, clock: &Clock, token_program_id: &AccountInfo<'a>, -) -> ProgramResult { +) -> Result { 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"); @@ -713,7 +721,7 @@ fn _redeem_reserve_collateral<'a>( token_program: token_program_id.clone(), })?; - Ok(()) + Ok(liquidity_amount) } #[inline(never)] // avoid stack frame limit @@ -1577,6 +1585,42 @@ fn process_liquidate_obligation( let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; + _liquidate_obligation( + program_id, + liquidity_amount, + source_liquidity_info, + destination_collateral_info, + repay_reserve_info, + repay_reserve_liquidity_supply_info, + withdraw_reserve_info, + withdraw_reserve_collateral_supply_info, + obligation_info, + lending_market_info, + lending_market_authority_info, + user_transfer_authority_info, + clock, + token_program_id, + )?; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn _liquidate_obligation<'a>( + program_id: &Pubkey, + liquidity_amount: u64, + source_liquidity_info: &AccountInfo<'a>, + destination_collateral_info: &AccountInfo<'a>, + repay_reserve_info: &AccountInfo<'a>, + repay_reserve_liquidity_supply_info: &AccountInfo<'a>, + withdraw_reserve_info: &AccountInfo<'a>, + withdraw_reserve_collateral_supply_info: &AccountInfo<'a>, + obligation_info: &AccountInfo<'a>, + lending_market_info: &AccountInfo<'a>, + lending_market_authority_info: &AccountInfo<'a>, + user_transfer_authority_info: &AccountInfo<'a>, + clock: &Clock, + token_program_id: &AccountInfo<'a>, +) -> Result { 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"); @@ -1741,6 +1785,87 @@ fn process_liquidate_obligation( token_program: token_program_id.clone(), })?; + Ok(withdraw_amount) +} + +#[inline(never)] // avoid stack frame limit +fn process_liquidate_obligation_and_redeem_reserve_collateral( + program_id: &Pubkey, + liquidity_amount: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + if liquidity_amount == 0 { + msg!("Liquidity amount provided cannot be zero"); + return Err(LendingError::InvalidAmount.into()); + } + + let account_info_iter = &mut accounts.iter(); + let source_liquidity_info = next_account_info(account_info_iter)?; + let destination_collateral_info = next_account_info(account_info_iter)?; + let destination_liquidity_info = next_account_info(account_info_iter)?; + let repay_reserve_info = next_account_info(account_info_iter)?; + let repay_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let withdraw_reserve_info = next_account_info(account_info_iter)?; + let withdraw_reserve_collateral_mint_info = next_account_info(account_info_iter)?; + let withdraw_reserve_collateral_supply_info = next_account_info(account_info_iter)?; + let withdraw_reserve_liquidity_supply_info = next_account_info(account_info_iter)?; + let withdraw_reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; + let obligation_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 user_transfer_authority_info = next_account_info(account_info_iter)?; + let token_program_id = next_account_info(account_info_iter)?; + let clock = &Clock::get()?; + + let withdraw_collateral_amount = _liquidate_obligation( + program_id, + liquidity_amount, + source_liquidity_info, + destination_collateral_info, + repay_reserve_info, + repay_reserve_liquidity_supply_info, + withdraw_reserve_info, + withdraw_reserve_collateral_supply_info, + obligation_info, + lending_market_info, + lending_market_authority_info, + user_transfer_authority_info, + clock, + token_program_id, + )?; + + _refresh_reserve_interest(program_id, withdraw_reserve_info, clock)?; + let withdraw_liquidity_amount = _redeem_reserve_collateral( + program_id, + withdraw_collateral_amount, + destination_collateral_info, + destination_liquidity_info, + withdraw_reserve_info, + withdraw_reserve_collateral_mint_info, + withdraw_reserve_liquidity_supply_info, + lending_market_info, + lending_market_authority_info, + user_transfer_authority_info, + clock, + token_program_id, + )?; + let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; + if &withdraw_reserve.config.fee_receiver != withdraw_reserve_liquidity_fee_receiver_info.key { + msg!("Withdraw reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + let protocol_fee = + withdraw_reserve.calculate_protocol_liquidation_fee(withdraw_liquidity_amount)?; + + spl_token_transfer(TokenTransferParams { + source: destination_liquidity_info.clone(), + destination: withdraw_reserve_liquidity_fee_receiver_info.clone(), + amount: protocol_fee, + authority: user_transfer_authority_info.clone(), + authority_signer_seeds: &[], + token_program: token_program_id.clone(), + })?; + Ok(()) } @@ -1971,7 +2096,8 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( user_transfer_authority_info, clock, token_program_id, - ) + )?; + Ok(()) } #[inline(never)] // avoid stack frame limit @@ -2430,6 +2556,10 @@ fn validate_reserve_config(config: ReserveConfig) -> ProgramResult { msg!("Host fee percentage must be in range [0, 100]"); return Err(LendingError::InvalidConfig.into()); } + if config.protocol_liquidation_fee > 100 { + msg!("Protocol liquidation fee 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 2ce5eb471d1..0802293b467 100644 --- a/token-lending/program/src/state/reserve.rs +++ b/token-lending/program/src/state/reserve.rs @@ -296,6 +296,22 @@ impl Reserve { withdraw_amount, }) } + + /// Calculate protocol cut of liquidation bonus always at least 1 lamport + pub fn calculate_protocol_liquidation_fee( + &self, + amount_liquidated: u64, + ) -> Result { + let bonus_rate = Rate::from_percent(self.config.liquidation_bonus).try_add(Rate::one())?; + let amount_liquidated_wads = Decimal::from(amount_liquidated); + + let bonus = amount_liquidated_wads.try_sub(amount_liquidated_wads.try_div(bonus_rate)?)?; + + // After deploying must update all reserves to set liquidation fee then redeploy with this line instead of hardcode + // let protocol_fee = max(bonus.try_mul(Rate::from_percent(self.config.protocol_liquidation_fee))?.try_ceil_u64()?, 1); + let protocol_fee = std::cmp::max(bonus.try_mul(Rate::from_percent(0))?.try_ceil_u64()?, 1); + Ok(protocol_fee) + } } /// Initialize a reserve @@ -615,6 +631,8 @@ pub struct ReserveConfig { pub borrow_limit: u64, /// Reserve liquidity fee receiver address pub fee_receiver: Pubkey, + /// Cut of the liquidation bonus that the protocol receives, as a percentage + pub protocol_liquidation_fee: u8, } /// Additional fee information on a reserve @@ -725,7 +743,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 + 248 +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 impl Pack for Reserve { const LEN: usize = RESERVE_LEN; @@ -763,6 +781,7 @@ impl Pack for Reserve { config_deposit_limit, config_borrow_limit, config_fee_receiver, + config_protocol_liquidation_fee, _padding, ) = mut_array_refs![ output, @@ -795,7 +814,8 @@ impl Pack for Reserve { 8, 8, PUBKEY_BYTES, - 248 + 1, + 247 ]; // reserve @@ -841,6 +861,7 @@ impl Pack for Reserve { *config_deposit_limit = self.config.deposit_limit.to_le_bytes(); *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(); } /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). @@ -877,6 +898,7 @@ impl Pack for Reserve { config_deposit_limit, config_borrow_limit, config_fee_receiver, + config_protocol_liquidation_fee, _padding, ) = array_refs![ input, @@ -909,7 +931,8 @@ impl Pack for Reserve { 8, 8, PUBKEY_BYTES, - 248 + 1, + 247 ]; let version = u8::from_le_bytes(*version); @@ -959,6 +982,7 @@ impl Pack for Reserve { deposit_limit: u64::from_le_bytes(*config_deposit_limit), 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), }, }) } diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 6afa0412f92..2b5db31ae6b 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -56,6 +56,7 @@ pub fn test_reserve_config() -> ReserveConfig { deposit_limit: 100_000_000_000, borrow_limit: u64::MAX, fee_receiver: Keypair::new().pubkey(), + protocol_liquidation_fee: 30, } } @@ -313,13 +314,20 @@ pub fn add_reserve( &spl_token::id(), ); + let amount = if let COption::Some(rent_reserve) = is_native { + rent_reserve + } else { + u32::MAX as u64 + }; + test.add_packable_account( config.fee_receiver, - u32::MAX as u64, + amount, &Token { mint: liquidity_mint_pubkey, owner: lending_market.owner.pubkey(), amount: 0, + is_native, state: AccountState::Initialized, ..Token::default() }, diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 39a9f8618fa..b80c2a93e3e 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -418,6 +418,7 @@ async fn test_update_reserve_config() { deposit_limit: 1_000_000, borrow_limit: 300_000, fee_receiver: Keypair::new().pubkey(), + protocol_liquidation_fee: 30, }; 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 new file mode 100644 index 00000000000..b31b02a8694 --- /dev/null +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -0,0 +1,201 @@ +#![cfg(feature = "test-bpf")] + +mod helpers; + +use helpers::*; +use solana_program_test::*; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token_lending::{ + instruction::{liquidate_obligation_and_redeem_reserve_collateral, refresh_obligation}, + processor::process_instruction, + state::INITIAL_COLLATERAL_RATIO, +}; +use std::cmp::max; + +#[tokio::test] +async fn test_success() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_bpf_compute_max_units(77_000); + + // 100 SOL collateral + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + // 100 SOL * 80% LTV -> 80 SOL * 20 USDC -> 1600 USDC borrow + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_600 * FRACTIONAL_TO_USDC; + // 1600 USDC * 20% -> 320 USDC liquidation + const USDC_LIQUIDATION_AMOUNT_FRACTIONAL: u64 = USDC_BORROW_AMOUNT_FRACTIONAL / 5; + // 320 USDC / 20 USDC per SOL -> 16 SOL + 10% bonus -> 17.6 SOL (88/5) + const SOL_LIQUIDATION_AMOUNT_LAMPORTS: u64 = + LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO * 88 / 5; + + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL; + + let user_accounts_owner = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mut reserve_config = test_reserve_config(); + reserve_config.loan_to_value_ratio = 50; + reserve_config.liquidation_threshold = 80; + reserve_config.liquidation_bonus = 10; + + let sol_oracle = add_sol_oracle(&mut test); + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &sol_oracle, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_amount: SOL_DEPOSIT_AMOUNT_LAMPORTS / INITIAL_COLLATERAL_RATIO, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let mut reserve_config = test_reserve_config(); + reserve_config.loan_to_value_ratio = 50; + reserve_config.liquidation_threshold = 80; + reserve_config.liquidation_bonus = 10; + let usdc_mint = add_usdc_mint(&mut test); + let usdc_oracle = add_usdc_oracle(&mut test); + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &usdc_oracle, + &user_accounts_owner, + AddReserveArgs { + borrow_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + user_liquidity_amount: USDC_BORROW_AMOUNT_FRACTIONAL, + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + borrows: &[(&usdc_test_reserve, USDC_BORROW_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + let initial_liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + let initial_user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + let initial_collateral_supply_balance = + 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 = + get_token_balance(&mut banks_client, sol_test_reserve.config.fee_receiver).await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey, usdc_test_reserve.pubkey], + ), + liquidate_obligation_and_redeem_reserve_collateral( + spl_token_lending::id(), + USDC_LIQUIDATION_AMOUNT_FRACTIONAL, + usdc_test_reserve.user_liquidity_pubkey, + sol_test_reserve.user_collateral_pubkey, + sol_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.pubkey, + sol_test_reserve.collateral_mint_pubkey, + sol_test_reserve.collateral_supply_pubkey, + sol_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.config.fee_receiver, + test_obligation.pubkey, + lending_market.pubkey, + user_accounts_owner.pubkey(), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let user_liquidity_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await; + assert_eq!( + user_liquidity_balance, + initial_user_liquidity_balance - USDC_LIQUIDATION_AMOUNT_FRACTIONAL + ); + + let liquidity_supply_balance = + get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply_balance, + initial_liquidity_supply_balance + USDC_LIQUIDATION_AMOUNT_FRACTIONAL + ); + + let user_collateral_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_collateral_pubkey).await; + assert_eq!(user_collateral_balance, initial_user_collateral_balance); + + let user_withdraw_liquidity_balance = + get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; + let fee_reciever_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, + initial_user_withdraw_liquidity_balance + + initial_fee_reciever_withdraw_liquidity_balance + + SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + + assert_eq!( + // 30% of the bonus + // 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) + ); + + let collateral_supply_balance = + get_token_balance(&mut banks_client, sol_test_reserve.collateral_supply_pubkey).await; + assert_eq!( + collateral_supply_balance, + initial_collateral_supply_balance - SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + + let obligation = test_obligation.get_state(&mut banks_client).await; + assert_eq!( + obligation.deposits[0].deposited_amount, + SOL_DEPOSIT_AMOUNT_LAMPORTS - SOL_LIQUIDATION_AMOUNT_LAMPORTS + ); + assert_eq!( + obligation.borrows[0].borrowed_amount_wads, + (USDC_BORROW_AMOUNT_FRACTIONAL - USDC_LIQUIDATION_AMOUNT_FRACTIONAL).into() + ) +}