From aaef4f58aefaecbed8f29da9639bdf7ad3d98a1a Mon Sep 17 00:00:00 2001 From: dasichuan <0xdasichuan@gmail.com> Date: Wed, 14 Jul 2021 16:34:03 -0700 Subject: [PATCH] Add instruction to update the LTV of a reserve --- token-lending/program/src/instruction.rs | 82 ++++++++++ token-lending/program/src/processor.rs | 141 +++++++++++++----- .../program/tests/init_lending_market.rs | 2 +- token-lending/program/tests/init_reserve.rs | 106 ++++++++++++- 4 files changed, 292 insertions(+), 39 deletions(-) diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 1eab8cdf4cc..3a952845a89 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -362,6 +362,20 @@ pub enum LendingInstruction { /// liquidity_amount is the amount of collateral tokens to withdraw collateral_amount: u64, }, + + // 16 + /// Updates a reserve config parameter + /// + /// Accounts expected by this instruction: + /// + /// 1. `[writable]` Reserve account - refreshed + /// 2 `[]` Lending market account. + /// 3 `[]` Derived lending market authority. + /// 4 `[signer]` Lending market owner. + UpdateReserveConfig { + /// Reserve config to update to + config: ReserveConfig, + }, } impl LendingInstruction { @@ -456,6 +470,35 @@ impl LendingInstruction { let (collateral_amount, _rest) = Self::unpack_u64(rest)?; 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)?; + + Self::UpdateReserveConfig { + config: ReserveConfig { + optimal_utilization_rate, + loan_to_value_ratio, + liquidation_bonus, + liquidation_threshold, + min_borrow_rate, + optimal_borrow_rate, + max_borrow_rate, + fees: ReserveFees { + borrow_fee_wad, + flash_loan_fee_wad, + host_fee_percentage, + }, + }, + } + } _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -612,6 +655,19 @@ impl LendingInstruction { buf.push(15); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } + Self::UpdateReserveConfig { config } => { + buf.push(16); + buf.extend_from_slice(&config.optimal_utilization_rate.to_le_bytes()); + buf.extend_from_slice(&config.loan_to_value_ratio.to_le_bytes()); + buf.extend_from_slice(&config.liquidation_bonus.to_le_bytes()); + buf.extend_from_slice(&config.liquidation_threshold.to_le_bytes()); + buf.extend_from_slice(&config.min_borrow_rate.to_le_bytes()); + buf.extend_from_slice(&config.optimal_borrow_rate.to_le_bytes()); + buf.extend_from_slice(&config.max_borrow_rate.to_le_bytes()); + buf.extend_from_slice(&config.fees.borrow_fee_wad.to_le_bytes()); + buf.extend_from_slice(&config.fees.flash_loan_fee_wad.to_le_bytes()); + buf.extend_from_slice(&config.fees.host_fee_percentage.to_le_bytes()); + } } buf } @@ -1108,3 +1164,29 @@ pub fn flash_loan( data: LendingInstruction::FlashLoan { amount }.pack(), } } + +/// Creates an 'UpdateReserveConfig' instruction. +#[allow(clippy::too_many_arguments)] +pub fn update_reserve_config( + program_id: Pubkey, + config: ReserveConfig, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + lending_market_owner_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_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(lending_market_owner_pubkey, true), + ]; + Instruction { + program_id, + accounts, + data: LendingInstruction::UpdateReserveConfig { config }.pack(), + } +} diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 08bf33c400b..2452b760453 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -114,13 +114,17 @@ pub fn process_instruction( LendingInstruction::WithdrawObligationCollateralAndRedeemReserveCollateral { collateral_amount, } => { - msg!("Instruction: Withdraw Obligation Collateral and Redeem Reserve Collateral "); + msg!("Instruction: Withdraw Obligation Collateral and Redeem Reserve Collateral"); process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( program_id, collateral_amount, accounts, ) } + LendingInstruction::UpdateReserveConfig { config } => { + msg!("Instruction: UpdateReserveConfig"); + process_update_reserve_config(program_id, config, accounts) + } } } @@ -195,41 +199,7 @@ fn process_init_reserve( msg!("Reserve must be initialized with liquidity"); return Err(LendingError::InvalidAmount.into()); } - if config.optimal_utilization_rate > 100 { - msg!("Optimal utilization rate must be in range [0, 100]"); - return Err(LendingError::InvalidConfig.into()); - } - if config.loan_to_value_ratio >= 100 { - msg!("Loan to value ratio must be in range [0, 100)"); - return Err(LendingError::InvalidConfig.into()); - } - if config.liquidation_bonus > 100 { - msg!("Liquidation bonus must be in range [0, 100]"); - return Err(LendingError::InvalidConfig.into()); - } - if config.liquidation_threshold <= config.loan_to_value_ratio - || config.liquidation_threshold > 100 - { - msg!("Liquidation threshold must be in range (LTV, 100]"); - return Err(LendingError::InvalidConfig.into()); - } - if config.optimal_borrow_rate < config.min_borrow_rate { - msg!("Optimal borrow rate must be >= min borrow rate"); - return Err(LendingError::InvalidConfig.into()); - } - if config.optimal_borrow_rate > config.max_borrow_rate { - msg!("Optimal borrow rate must be <= max borrow rate"); - return Err(LendingError::InvalidConfig.into()); - } - if config.fees.borrow_fee_wad >= WAD { - msg!("Borrow fee must be in range [0, 1_000_000_000_000_000_000)"); - return Err(LendingError::InvalidConfig.into()); - } - if config.fees.host_fee_percentage > 100 { - msg!("Host fee percentage must be in range [0, 100]"); - return Err(LendingError::InvalidConfig.into()); - } - + validate_reserve_config(config)?; let account_info_iter = &mut accounts.iter().peekable(); let source_liquidity_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; @@ -1994,6 +1964,65 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( ) } +#[allow(clippy::too_many_arguments)] +#[inline(never)] // avoid stack frame limit +fn process_update_reserve_config( + program_id: &Pubkey, + config: ReserveConfig, + accounts: &[AccountInfo], +) -> ProgramResult { + validate_reserve_config(config)?; + let account_info_iter = &mut accounts.iter().peekable(); + let reserve_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 lending_market_owner_info = next_account_info(account_info_iter)?; + + 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()); + } + + 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 {} != {}", + &lending_market_info.owner.to_string(), + &program_id.to_string(), + ); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &lending_market.owner != lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); + return Err(LendingError::InvalidMarketOwner.into()); + } + if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); + return Err(LendingError::InvalidSigner.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()); + } + reserve.config = config; + Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; + Ok(()) +} + fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult { if !rent.is_exempt(account_info.lamports(), account_info.data_len()) { msg!( @@ -2298,6 +2327,46 @@ fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult { result.map_err(|_| LendingError::TokenBurnFailed.into()) } +/// validates reserve configs +#[inline(always)] +fn validate_reserve_config(config: ReserveConfig) -> ProgramResult { + if config.optimal_utilization_rate > 100 { + msg!("Optimal utilization rate must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + if config.loan_to_value_ratio >= 100 { + msg!("Loan to value ratio must be in range [0, 100)"); + return Err(LendingError::InvalidConfig.into()); + } + if config.liquidation_bonus > 100 { + msg!("Liquidation bonus must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + if config.liquidation_threshold <= config.loan_to_value_ratio + || config.liquidation_threshold > 100 + { + msg!("Liquidation threshold must be in range (LTV, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + if config.optimal_borrow_rate < config.min_borrow_rate { + msg!("Optimal borrow rate must be >= min borrow rate"); + return Err(LendingError::InvalidConfig.into()); + } + if config.optimal_borrow_rate > config.max_borrow_rate { + msg!("Optimal borrow rate must be <= max borrow rate"); + return Err(LendingError::InvalidConfig.into()); + } + if config.fees.borrow_fee_wad >= WAD { + msg!("Borrow fee must be in range [0, 1_000_000_000_000_000_000)"); + return Err(LendingError::InvalidConfig.into()); + } + if config.fees.host_fee_percentage > 100 { + msg!("Host fee percentage must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + Ok(()) +} + struct TokenInitializeMintParams<'a: 'b, 'b> { mint: AccountInfo<'a>, rent: AccountInfo<'a>, diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index 467512882a4..a8370e48d6b 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -23,7 +23,7 @@ async fn test_success() { ); // limit to track compute unit increase - test.set_bpf_compute_max_units(15_000); + test.set_bpf_compute_max_units(17_000); let (mut banks_client, payer, _recent_blockhash) = test.start().await; diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 19e56c9e0f6..642b2267536 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -12,9 +12,9 @@ use solana_sdk::{ }; use spl_token_lending::{ error::LendingError, - instruction::init_reserve, + instruction::{init_reserve, update_reserve_config}, processor::process_instruction, - state::{ReserveFees, INITIAL_COLLATERAL_RATIO}, + state::{ReserveConfig, ReserveFees, INITIAL_COLLATERAL_RATIO}, }; #[tokio::test] @@ -234,3 +234,105 @@ async fn test_invalid_fees() { ); } } + +#[tokio::test] +async fn test_update_reserve_config() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + let user_accounts_owner = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mint = add_usdc_mint(&mut test); + let oracle = add_usdc_oracle(&mut test); + let test_reserve = add_reserve( + &mut test, + &lending_market, + &oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: 42, + liquidity_mint_decimals: mint.decimals, + liquidity_mint_pubkey: mint.pubkey, + config: TEST_RESERVE_CONFIG, + ..AddReserveArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + // Create a reserve + let mut transaction = Transaction::new_with_payer( + &[init_reserve( + spl_token_lending::id(), + 42, + test_reserve.config, + test_reserve.user_liquidity_pubkey, + test_reserve.user_collateral_pubkey, + test_reserve.pubkey, + test_reserve.liquidity_mint_pubkey, + test_reserve.liquidity_supply_pubkey, + test_reserve.liquidity_fee_receiver_pubkey, + test_reserve.collateral_mint_pubkey, + test_reserve.collateral_supply_pubkey, + oracle.pyth_product_pubkey, + oracle.pyth_price_pubkey, + oracle.switchboard_feed_pubkey, + lending_market.pubkey, + lending_market.owner.pubkey(), + user_transfer_authority.pubkey(), + )], + Some(&payer.pubkey()), + ); + transaction.sign( + &[&payer, &lending_market.owner, &user_transfer_authority], + recent_blockhash, + ); + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::AlreadyInitialized as u32) + ) + ); + + // Update the reserve config + let new_config: ReserveConfig = ReserveConfig { + optimal_utilization_rate: 75, + loan_to_value_ratio: 45, + liquidation_bonus: 10, + liquidation_threshold: 65, + min_borrow_rate: 1, + optimal_borrow_rate: 5, + max_borrow_rate: 45, + fees: ReserveFees { + borrow_fee_wad: 200_000_000_000, + flash_loan_fee_wad: 5_000_000_000_000_000, + host_fee_percentage: 15, + }, + }; + + let mut transaction = Transaction::new_with_payer( + &[update_reserve_config( + spl_token_lending::id(), + new_config, + test_reserve.pubkey, + lending_market.pubkey, + lending_market.owner.pubkey(), + )], + Some(&payer.pubkey()), + ); + transaction.sign(&[&payer, &lending_market.owner], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let test_reserve = test_reserve.get_state(&mut banks_client).await; + assert_eq!(test_reserve.config, new_config); +}