//! Program state processor use crate::{ self as spl_token_lending, error::LendingError, instruction::LendingInstruction, math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub, WAD}, pyth, state::{ CalculateBorrowResult, CalculateLiquidationResult, CalculateRepayResult, InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket, NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, Reserve, ReserveCollateral, ReserveConfig, ReserveLiquidity, }, }; use num_traits::FromPrimitive; use solana_program::{ account_info::{next_account_info, AccountInfo}, decode_error::DecodeError, entrypoint::ProgramResult, instruction::Instruction, msg, program::{invoke, invoke_signed}, program_error::{PrintProgramError, ProgramError}, program_pack::{IsInitialized, Pack}, pubkey::Pubkey, sysvar::{clock::Clock, rent::Rent, Sysvar}, }; use spl_token::solana_program::instruction::AccountMeta; use spl_token::state::{Account, Mint}; use std::{convert::TryInto, result::Result}; use switchboard_program::{ get_aggregator, get_aggregator_result, AggregatorState, RoundResult, SwitchboardAccountType, }; /// Processes an instruction pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8], ) -> ProgramResult { let instruction = LendingInstruction::unpack(input)?; match instruction { LendingInstruction::InitLendingMarket { owner, quote_currency, } => { msg!("Instruction: Init Lending Market"); process_init_lending_market(program_id, owner, quote_currency, accounts) } LendingInstruction::SetLendingMarketOwner { new_owner } => { msg!("Instruction: Set Lending Market Owner"); process_set_lending_market_owner(program_id, new_owner, accounts) } LendingInstruction::InitReserve { liquidity_amount, config, } => { msg!("Instruction: Init Reserve"); process_init_reserve(program_id, liquidity_amount, config, accounts) } LendingInstruction::RefreshReserve => { msg!("Instruction: Refresh Reserve"); process_refresh_reserve(program_id, accounts) } LendingInstruction::DepositReserveLiquidity { liquidity_amount } => { msg!("Instruction: Deposit Reserve Liquidity"); process_deposit_reserve_liquidity(program_id, liquidity_amount, accounts) } LendingInstruction::RedeemReserveCollateral { collateral_amount } => { msg!("Instruction: Redeem Reserve Collateral"); process_redeem_reserve_collateral(program_id, collateral_amount, accounts) } LendingInstruction::InitObligation => { msg!("Instruction: Init Obligation"); process_init_obligation(program_id, accounts) } LendingInstruction::RefreshObligation => { msg!("Instruction: Refresh Obligation"); process_refresh_obligation(program_id, accounts) } LendingInstruction::DepositObligationCollateral { collateral_amount } => { msg!("Instruction: Deposit Obligation Collateral"); process_deposit_obligation_collateral(program_id, collateral_amount, accounts) } LendingInstruction::WithdrawObligationCollateral { collateral_amount } => { msg!("Instruction: Withdraw Obligation Collateral"); process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) } LendingInstruction::BorrowObligationLiquidity { liquidity_amount } => { msg!("Instruction: Borrow Obligation Liquidity"); process_borrow_obligation_liquidity(program_id, liquidity_amount, accounts) } LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { msg!("Instruction: Repay Obligation Liquidity"); process_repay_obligation_liquidity(program_id, liquidity_amount, accounts) } LendingInstruction::LiquidateObligation { liquidity_amount } => { msg!("Instruction: Liquidate Obligation"); process_liquidate_obligation(program_id, liquidity_amount, accounts) } LendingInstruction::FlashLoan { amount } => { msg!("Instruction: Flash Loan"); process_flash_loan(program_id, amount, accounts) } LendingInstruction::DepositReserveLiquidityAndObligationCollateral { liquidity_amount } => { msg!("Instruction: Deposit Reserve Liquidity and Obligation Collateral"); process_deposit_reserve_liquidity_and_obligation_collateral( program_id, liquidity_amount, accounts, ) } LendingInstruction::WithdrawObligationCollateralAndRedeemReserveCollateral { collateral_amount, } => { 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) } } } fn process_init_lending_market( program_id: &Pubkey, owner: Pubkey, quote_currency: [u8; 32], accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let lending_market_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let oracle_program_id = next_account_info(account_info_iter)?; let switchboard_oracle_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, lending_market_info)?; let mut lending_market = assert_uninitialized::(lending_market_info)?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } lending_market.init(InitLendingMarketParams { bump_seed: Pubkey::find_program_address(&[lending_market_info.key.as_ref()], program_id).1, owner, quote_currency, token_program_id: *token_program_id.key, oracle_program_id: *oracle_program_id.key, switchboard_oracle_program_id: *switchboard_oracle_program_id.key, }); LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_set_lending_market_owner( program_id: &Pubkey, new_owner: Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let lending_market_info = next_account_info(account_info_iter)?; let lending_market_owner_info = next_account_info(account_info_iter)?; let mut 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.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()); } lending_market.owner = new_owner; LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; Ok(()) } fn process_init_reserve( program_id: &Pubkey, liquidity_amount: u64, config: ReserveConfig, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { msg!("Reserve must be initialized with liquidity"); return Err(LendingError::InvalidAmount.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)?; let reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_mint_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_collateral_supply_info = next_account_info(account_info_iter)?; let pyth_product_info = next_account_info(account_info_iter)?; let pyth_price_info = next_account_info(account_info_iter)?; let switchboard_feed_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 user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent_info = next_account_info(account_info_iter)?; let rent = &Rent::from_account_info(rent_info)?; let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, reserve_info)?; let mut reserve = assert_uninitialized::(reserve_info)?; 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_liquidity_supply_info.key == source_liquidity_info.key { msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.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.token_program_id != token_program_id.key { msg!("Lending market token program does not match the token program provided"); return Err(LendingError::InvalidTokenProgram.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()); } if *switchboard_feed_info.key == spl_token_lending::NULL_PUBKEY && (*pyth_price_info.key == spl_token_lending::NULL_PUBKEY || *pyth_product_info.key == spl_token_lending::NULL_PUBKEY) { msg!("Both price oracles are null. At least one must be non-null"); return Err(LendingError::InvalidOracleConfig.into()); } validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?; validate_switchboard_keys(&lending_market, switchboard_feed_info)?; let market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?; 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 reserve_liquidity_mint = unpack_mint(&reserve_liquidity_mint_info.data.borrow())?; if reserve_liquidity_mint_info.owner != token_program_id.key { msg!("Reserve liquidity mint is not owned by the token program provided"); return Err(LendingError::InvalidTokenOwner.into()); } reserve.init(InitReserveParams { current_slot: clock.slot, lending_market: *lending_market_info.key, liquidity: ReserveLiquidity::new(NewReserveLiquidityParams { mint_pubkey: *reserve_liquidity_mint_info.key, mint_decimals: reserve_liquidity_mint.decimals, supply_pubkey: *reserve_liquidity_supply_info.key, pyth_oracle_pubkey: *pyth_price_info.key, switchboard_oracle_pubkey: *switchboard_feed_info.key, market_price, }), collateral: ReserveCollateral::new(NewReserveCollateralParams { mint_pubkey: *reserve_collateral_mint_info.key, supply_pubkey: *reserve_collateral_supply_info.key, }), config, }); let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_init_account(TokenInitializeAccountParams { account: reserve_liquidity_supply_info.clone(), mint: reserve_liquidity_mint_info.clone(), owner: lending_market_authority_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; spl_token_init_account(TokenInitializeAccountParams { account: reserve_liquidity_fee_receiver_info.clone(), mint: reserve_liquidity_mint_info.clone(), owner: lending_market_authority_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; spl_token_init_mint(TokenInitializeMintParams { mint: reserve_collateral_mint_info.clone(), authority: lending_market_authority_info.key, rent: rent_info.clone(), decimals: reserve_liquidity_mint.decimals, token_program: token_program_id.clone(), })?; spl_token_init_account(TokenInitializeAccountParams { account: reserve_collateral_supply_info.clone(), mint: reserve_collateral_mint_info.clone(), owner: lending_market_authority_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; spl_token_init_account(TokenInitializeAccountParams { account: destination_collateral_info.clone(), mint: reserve_collateral_mint_info.clone(), owner: user_transfer_authority_info.clone(), rent: rent_info.clone(), token_program: token_program_id.clone(), })?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: reserve_liquidity_supply_info.clone(), amount: liquidity_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; spl_token_mint_to(TokenMintToParams { mint: reserve_collateral_mint_info.clone(), destination: destination_collateral_info.clone(), amount: collateral_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(()) } fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); let reserve_info = next_account_info(account_info_iter)?; let pyth_price_info = next_account_info(account_info_iter)?; let switchboard_feed_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; _refresh_reserve( program_id, reserve_info, pyth_price_info, switchboard_feed_info, clock, ) } fn _refresh_reserve<'a>( program_id: &Pubkey, reserve_info: &AccountInfo<'a>, pyth_price_info: &AccountInfo<'a>, switchboard_feed_info: &AccountInfo<'a>, clock: &Clock, ) -> ProgramResult { 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"); return Err(LendingError::InvalidAccountOwner.into()); } if &reserve.liquidity.pyth_oracle_pubkey != pyth_price_info.key { msg!("Reserve liquidity pyth oracle does not match the reserve liquidity pyth oracle provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.switchboard_oracle_pubkey != switchboard_feed_info.key { msg!("Reserve liquidity switchboard oracle does not match the reserve liquidity switchboard oracle provided"); return Err(LendingError::InvalidOracleConfig.into()); } reserve.liquidity.market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?; reserve.accrue_interest(clock.slot)?; reserve.last_update.update_slot(clock.slot); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; Ok(()) } fn process_deposit_reserve_liquidity( 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 reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_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 clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; // We don't care about the return value here, so just ignore it. _deposit_reserve_liquidity( program_id, liquidity_amount, source_liquidity_info, destination_collateral_info, reserve_info, reserve_liquidity_supply_info, reserve_collateral_mint_info, lending_market_info, lending_market_authority_info, user_transfer_authority_info, clock, token_program_id, )?; Ok(()) } #[allow(clippy::too_many_arguments)] fn _deposit_reserve_liquidity<'a>( program_id: &Pubkey, liquidity_amount: u64, source_liquidity_info: &AccountInfo<'a>, destination_collateral_info: &AccountInfo<'a>, reserve_info: &AccountInfo<'a>, reserve_liquidity_supply_info: &AccountInfo<'a>, reserve_collateral_mint_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"); 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 mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.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.liquidity.supply_pubkey != reserve_liquidity_supply_info.key { msg!("Reserve liquidity supply does not match the reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.mint_pubkey != reserve_collateral_mint_info.key { msg!("Reserve collateral mint does not match the reserve collateral mint provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey == source_liquidity_info.key { msg!("Reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.supply_pubkey == destination_collateral_info.key { msg!("Reserve collateral supply cannot be used as the destination collateral 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 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 {}", &lending_market_authority_pubkey.to_string(), &lending_market_authority_info.key.to_string(), ); return Err(LendingError::InvalidMarketAuthority.into()); } if Decimal::from(liquidity_amount) .try_add(reserve.liquidity.total_supply()?)? .try_floor_u64()? > reserve.config.deposit_limit { msg!("Cannot deposit liquidity above the reserve deposit limit"); return Err(LendingError::InvalidAmount.into()); } let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?; reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: reserve_liquidity_supply_info.clone(), amount: liquidity_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; spl_token_mint_to(TokenMintToParams { mint: reserve_collateral_mint_info.clone(), destination: destination_collateral_info.clone(), amount: collateral_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(collateral_amount) } fn process_redeem_reserve_collateral( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_liquidity_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_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 clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; _redeem_reserve_collateral( program_id, collateral_amount, source_collateral_info, destination_liquidity_info, reserve_info, reserve_collateral_mint_info, reserve_liquidity_supply_info, lending_market_info, lending_market_authority_info, user_transfer_authority_info, clock, token_program_id, ) } #[allow(clippy::too_many_arguments)] fn _redeem_reserve_collateral<'a>( program_id: &Pubkey, collateral_amount: u64, source_collateral_info: &AccountInfo<'a>, destination_liquidity_info: &AccountInfo<'a>, reserve_info: &AccountInfo<'a>, reserve_collateral_mint_info: &AccountInfo<'a>, reserve_liquidity_supply_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>, ) -> ProgramResult { 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 mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.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.collateral.mint_pubkey != reserve_collateral_mint_info.key { msg!("Reserve collateral mint does not match the reserve collateral mint provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.collateral.supply_pubkey == source_collateral_info.key { msg!("Reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey != reserve_liquidity_supply_info.key { msg!("Reserve liquidity supply does not match the reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey == destination_liquidity_info.key { msg!("Reserve liquidity supply cannot be used as the destination liquidity 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 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 liquidity_amount = reserve.redeem_collateral(collateral_amount)?; reserve.last_update.mark_stale(); Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_burn(TokenBurnParams { mint: reserve_collateral_mint_info.clone(), source: source_collateral_info.clone(), amount: collateral_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; spl_token_transfer(TokenTransferParams { source: reserve_liquidity_supply_info.clone(), destination: destination_liquidity_info.clone(), amount: liquidity_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let obligation_info = next_account_info(account_info_iter)?; let lending_market_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, obligation_info)?; let mut obligation = assert_uninitialized::(obligation_info)?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); 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"); 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()); } if !obligation_owner_info.is_signer { msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } obligation.init(InitObligationParams { current_slot: clock.slot, lending_market: *lending_market_info.key, owner: *obligation_owner_info.key, deposits: vec![], borrows: vec![], }); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } let mut deposited_value = Decimal::zero(); let mut borrowed_value = Decimal::zero(); let mut allowed_borrow_value = Decimal::zero(); let mut unhealthy_borrow_value = Decimal::zero(); for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { msg!( "Deposit reserve provided for collateral {} is not owned by the lending program", index ); return Err(LendingError::InvalidAccountOwner.into()); } if collateral.deposit_reserve != *deposit_reserve_info.key { msg!( "Deposit reserve of collateral {} does not match the deposit reserve provided", index ); return Err(LendingError::InvalidAccountInput.into()); } let deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; if deposit_reserve.last_update.is_stale(clock.slot)? { msg!( "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot", index ); return Err(LendingError::ReserveStale.into()); } // @TODO: add lookup table https://git.io/JOCYq let decimals = 10u64 .checked_pow(deposit_reserve.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; let market_value = deposit_reserve .collateral_exchange_rate()? .decimal_collateral_to_liquidity(collateral.deposited_amount.into())? .try_mul(deposit_reserve.liquidity.market_price)? .try_div(decimals)?; collateral.market_value = market_value; let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio); let liquidation_threshold_rate = Rate::from_percent(deposit_reserve.config.liquidation_threshold); deposited_value = deposited_value.try_add(market_value)?; allowed_borrow_value = allowed_borrow_value.try_add(market_value.try_mul(loan_to_value_rate)?)?; unhealthy_borrow_value = unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; } for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { msg!( "Borrow reserve provided for liquidity {} is not owned by the lending program", index ); return Err(LendingError::InvalidAccountOwner.into()); } if liquidity.borrow_reserve != *borrow_reserve_info.key { msg!( "Borrow reserve of liquidity {} does not match the borrow reserve provided", index ); return Err(LendingError::InvalidAccountInput.into()); } let borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve.last_update.is_stale(clock.slot)? { msg!( "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot", index ); return Err(LendingError::ReserveStale.into()); } liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; // @TODO: add lookup table https://git.io/JOCYq let decimals = 10u64 .checked_pow(borrow_reserve.liquidity.mint_decimals as u32) .ok_or(LendingError::MathOverflow)?; let market_value = liquidity .borrowed_amount_wads .try_mul(borrow_reserve.liquidity.market_price)? .try_div(decimals)?; liquidity.market_value = market_value; borrowed_value = borrowed_value.try_add(market_value)?; } if account_info_iter.peek().is_some() { msg!("Too many obligation deposit or borrow reserves provided"); return Err(LendingError::InvalidAccountInput.into()); } obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; obligation.allowed_borrow_value = allowed_borrow_value; obligation.unhealthy_borrow_value = unhealthy_borrow_value; obligation.last_update.update_slot(clock.slot); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_deposit_obligation_collateral( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let deposit_reserve_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 obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; _deposit_obligation_collateral( program_id, collateral_amount, source_collateral_info, destination_collateral_info, deposit_reserve_info, obligation_info, lending_market_info, obligation_owner_info, user_transfer_authority_info, clock, token_program_id, ) } #[allow(clippy::too_many_arguments)] fn _deposit_obligation_collateral<'a>( program_id: &Pubkey, collateral_amount: u64, source_collateral_info: &AccountInfo<'a>, destination_collateral_info: &AccountInfo<'a>, deposit_reserve_info: &AccountInfo<'a>, obligation_info: &AccountInfo<'a>, lending_market_info: &AccountInfo<'a>, obligation_owner_info: &AccountInfo<'a>, user_transfer_authority_info: &AccountInfo<'a>, clock: &Clock, token_program_id: &AccountInfo<'a>, ) -> ProgramResult { 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 deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; if deposit_reserve_info.owner != program_id { msg!("Deposit reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &deposit_reserve.lending_market != lending_market_info.key { msg!("Deposit reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &deposit_reserve.collateral.supply_pubkey == source_collateral_info.key { msg!("Deposit reserve collateral supply cannot be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if &deposit_reserve.collateral.supply_pubkey != destination_collateral_info.key { msg!( "Deposit reserve collateral supply must be used as the destination collateral provided" ); return Err(LendingError::InvalidAccountInput.into()); } if deposit_reserve.last_update.is_stale(clock.slot)? { msg!("Deposit reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } if deposit_reserve.config.loan_to_value_ratio == 0 { msg!("Deposit reserve has collateral disabled for borrowing"); return Err(LendingError::ReserveCollateralDisabled.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &obligation.owner != obligation_owner_info.key { msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } if !obligation_owner_info.is_signer { msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } obligation .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)? .deposit(collateral_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), amount: collateral_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_deposit_reserve_liquidity_and_obligation_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 user_collateral_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_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 destination_collateral_info = next_account_info(account_info_iter)?; let obligation_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let pyth_price_info = next_account_info(account_info_iter)?; let switchboard_feed_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let collateral_amount = _deposit_reserve_liquidity( program_id, liquidity_amount, source_liquidity_info, user_collateral_info, reserve_info, reserve_liquidity_supply_info, reserve_collateral_mint_info, lending_market_info, lending_market_authority_info, user_transfer_authority_info, clock, token_program_id, )?; _refresh_reserve( program_id, reserve_info, pyth_price_info, switchboard_feed_info, clock, )?; _deposit_obligation_collateral( program_id, collateral_amount, user_collateral_info, destination_collateral_info, reserve_info, obligation_info, lending_market_info, obligation_owner_info, user_transfer_authority_info, clock, token_program_id, )?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { if collateral_amount == 0 { msg!("Collateral amount provided cannot be zero"); return Err(LendingError::InvalidAmount.into()); } let account_info_iter = &mut accounts.iter(); let source_collateral_info = next_account_info(account_info_iter)?; let destination_collateral_info = next_account_info(account_info_iter)?; let withdraw_reserve_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 obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; _withdraw_obligation_collateral( program_id, collateral_amount, source_collateral_info, destination_collateral_info, withdraw_reserve_info, obligation_info, lending_market_info, lending_market_authority_info, obligation_owner_info, clock, token_program_id, )?; Ok(()) } #[allow(clippy::too_many_arguments)] fn _withdraw_obligation_collateral<'a>( program_id: &Pubkey, collateral_amount: u64, source_collateral_info: &AccountInfo<'a>, destination_collateral_info: &AccountInfo<'a>, withdraw_reserve_info: &AccountInfo<'a>, obligation_info: &AccountInfo<'a>, lending_market_info: &AccountInfo<'a>, lending_market_authority_info: &AccountInfo<'a>, obligation_owner_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"); 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 withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &withdraw_reserve.lending_market != lending_market_info.key { msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey != source_collateral_info.key { msg!("Withdraw reserve collateral supply must be used as the source collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if withdraw_reserve.last_update.is_stale(clock.slot)? { msg!("Withdraw reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &obligation.owner != obligation_owner_info.key { msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } if !obligation_owner_info.is_signer { msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; if collateral.deposited_amount == 0 { msg!("Collateral deposited amount is zero"); return Err(LendingError::ObligationCollateralEmpty.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 = if obligation.borrows.is_empty() { if collateral_amount == u64::MAX { collateral.deposited_amount } else { collateral.deposited_amount.min(collateral_amount) } } else if obligation.deposited_value == Decimal::zero() { msg!("Obligation deposited value is zero"); return Err(LendingError::ObligationDepositsZero.into()); } else { let max_withdraw_value = obligation.max_withdraw_value()?; if max_withdraw_value == Decimal::zero() { msg!("Maximum withdraw value is zero"); return Err(LendingError::WithdrawTooLarge.into()); } let withdraw_amount = if collateral_amount == u64::MAX { let withdraw_value = max_withdraw_value.min(collateral.market_value); let withdraw_pct = withdraw_value.try_div(collateral.market_value)?; withdraw_pct .try_mul(collateral.deposited_amount)? .try_floor_u64()? .min(collateral.deposited_amount) } else { let withdraw_amount = collateral_amount.min(collateral.deposited_amount); let withdraw_pct = Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?; let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?; if withdraw_value > max_withdraw_value { msg!("Withdraw value cannot exceed maximum withdraw value"); return Err(LendingError::WithdrawTooLarge.into()); } withdraw_amount }; if withdraw_amount == 0 { msg!("Withdraw amount is too small to transfer collateral"); return Err(LendingError::WithdrawTooSmall.into()); } withdraw_amount }; obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_collateral_info.clone(), destination: destination_collateral_info.clone(), amount: withdraw_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(withdraw_amount) } #[inline(never)] // avoid stack frame limit fn process_borrow_obligation_liquidity( 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_liquidity_info = next_account_info(account_info_iter)?; let borrow_reserve_info = next_account_info(account_info_iter)?; let borrow_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 obligation_owner_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; 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 mut borrow_reserve = Reserve::unpack(&borrow_reserve_info.data.borrow())?; if borrow_reserve_info.owner != program_id { msg!("Borrow reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &borrow_reserve.lending_market != lending_market_info.key { msg!("Borrow reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.supply_pubkey != source_liquidity_info.key { msg!("Borrow reserve liquidity supply must be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.liquidity.supply_pubkey == destination_liquidity_info.key { msg!( "Borrow reserve liquidity supply cannot be used as the destination liquidity provided" ); return Err(LendingError::InvalidAccountInput.into()); } if &borrow_reserve.config.fee_receiver != borrow_reserve_liquidity_fee_receiver_info.key { msg!("Borrow reserve liquidity fee receiver does not match the borrow reserve liquidity fee receiver provided"); return Err(LendingError::InvalidAccountInput.into()); } if borrow_reserve.last_update.is_stale(clock.slot)? { msg!("Borrow reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } if liquidity_amount != u64::MAX && Decimal::from(liquidity_amount) .try_add(borrow_reserve.liquidity.borrowed_amount_wads)? .try_floor_u64()? > borrow_reserve.config.borrow_limit { msg!("Cannot borrow above the borrow limit"); return Err(LendingError::InvalidAmount.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &obligation.owner != obligation_owner_info.key { msg!("Obligation owner does not match the obligation owner provided"); return Err(LendingError::InvalidObligationOwner.into()); } if !obligation_owner_info.is_signer { msg!("Obligation owner provided must be a signer"); return Err(LendingError::InvalidSigner.into()); } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } if obligation.deposits.is_empty() { msg!("Obligation has no deposits to borrow against"); return Err(LendingError::ObligationDepositsEmpty.into()); } if obligation.deposited_value == Decimal::zero() { msg!("Obligation deposits have zero value"); return Err(LendingError::ObligationDepositsZero.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 remaining_borrow_value = obligation.remaining_borrow_value()?; if remaining_borrow_value == Decimal::zero() { msg!("Remaining borrow value is zero"); return Err(LendingError::BorrowTooLarge.into()); } let remaining_reserve_capacity = Decimal::from(borrow_reserve.config.borrow_limit) .try_sub(borrow_reserve.liquidity.borrowed_amount_wads) .unwrap_or_else(|_| Decimal::zero()); let CalculateBorrowResult { borrow_amount, receive_amount, borrow_fee, host_fee, } = borrow_reserve.calculate_borrow( liquidity_amount, remaining_borrow_value, remaining_reserve_capacity, )?; if receive_amount == 0 { msg!("Borrow amount is too small to receive liquidity after fees"); return Err(LendingError::BorrowTooSmall.into()); } let cumulative_borrow_rate_wads = borrow_reserve.liquidity.cumulative_borrow_rate_wads; borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; let obligation_liquidity = obligation .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key, cumulative_borrow_rate_wads)?; obligation_liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; let mut owner_fee = borrow_fee; if let Ok(host_fee_receiver_info) = next_account_info(account_info_iter) { if host_fee > 0 { owner_fee = owner_fee .checked_sub(host_fee) .ok_or(LendingError::MathOverflow)?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: host_fee_receiver_info.clone(), amount: host_fee, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; } } if owner_fee > 0 { spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: borrow_reserve_liquidity_fee_receiver_info.clone(), amount: owner_fee, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; } spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: destination_liquidity_info.clone(), amount: receive_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_repay_obligation_liquidity( 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_liquidity_info = next_account_info(account_info_iter)?; let repay_reserve_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 user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; 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 mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; if repay_reserve_info.owner != program_id { msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &repay_reserve.lending_market != lending_market_info.key { msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey != destination_liquidity_info.key { msg!("Repay reserve liquidity supply must be used as the destination liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if repay_reserve.last_update.is_stale(clock.slot)? { msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; if liquidity.borrowed_amount_wads == Decimal::zero() { msg!("Liquidity borrowed amount is zero"); return Err(LendingError::ObligationLiquidityEmpty.into()); } let CalculateRepayResult { settle_amount, repay_amount, } = repay_reserve.calculate_repay(liquidity_amount, liquidity.borrowed_amount_wads)?; if repay_amount == 0 { msg!("Repay amount is too small to transfer liquidity"); return Err(LendingError::RepayTooSmall.into()); } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; repay_reserve.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; obligation.repay(settle_amount, liquidity_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: destination_liquidity_info.clone(), amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_liquidate_obligation( 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 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_supply_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 clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; 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 mut repay_reserve = Reserve::unpack(&repay_reserve_info.data.borrow())?; if repay_reserve_info.owner != program_id { msg!("Repay reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &repay_reserve.lending_market != lending_market_info.key { msg!("Repay reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey != repay_reserve_liquidity_supply_info.key { msg!("Repay reserve liquidity supply does not match the repay reserve liquidity supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.liquidity.supply_pubkey == source_liquidity_info.key { msg!("Repay reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &repay_reserve.collateral.supply_pubkey == destination_collateral_info.key { msg!( "Repay reserve collateral supply cannot be used as the destination collateral provided" ); return Err(LendingError::InvalidAccountInput.into()); } if repay_reserve.last_update.is_stale(clock.slot)? { msg!("Repay reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?; if withdraw_reserve_info.owner != program_id { msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &withdraw_reserve.lending_market != lending_market_info.key { msg!("Withdraw reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey != withdraw_reserve_collateral_supply_info.key { msg!("Withdraw reserve collateral supply does not match the withdraw reserve collateral supply provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.liquidity.supply_pubkey == source_liquidity_info.key { msg!("Withdraw reserve liquidity supply cannot be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.into()); } if &withdraw_reserve.collateral.supply_pubkey == destination_collateral_info.key { msg!("Withdraw reserve collateral supply cannot be used as the destination collateral provided"); return Err(LendingError::InvalidAccountInput.into()); } if withdraw_reserve.last_update.is_stale(clock.slot)? { msg!("Withdraw reserve is stale and must be refreshed in the current slot"); return Err(LendingError::ReserveStale.into()); } let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &obligation.lending_market != lending_market_info.key { msg!("Obligation lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.into()); } if obligation.last_update.is_stale(clock.slot)? { msg!("Obligation is stale and must be refreshed in the current slot"); return Err(LendingError::ObligationStale.into()); } if obligation.deposited_value == Decimal::zero() { msg!("Obligation deposited value is zero"); return Err(LendingError::ObligationDepositsZero.into()); } if obligation.borrowed_value == Decimal::zero() { msg!("Obligation borrowed value is zero"); return Err(LendingError::ObligationBorrowsZero.into()); } if obligation.borrowed_value < obligation.unhealthy_borrow_value { msg!("Obligation is healthy and cannot be liquidated"); return Err(LendingError::ObligationHealthy.into()); } let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?; if liquidity.market_value == Decimal::zero() { msg!("Obligation borrow value is zero"); return Err(LendingError::ObligationLiquidityEmpty.into()); } let (collateral, collateral_index) = obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?; if collateral.market_value == Decimal::zero() { msg!("Obligation deposit value is zero"); return Err(LendingError::ObligationCollateralEmpty.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 CalculateLiquidationResult { settle_amount, repay_amount, withdraw_amount, } = withdraw_reserve.calculate_liquidation( liquidity_amount, &obligation, liquidity, collateral, )?; if repay_amount == 0 { msg!("Liquidation is too small to transfer liquidity"); return Err(LendingError::LiquidationTooSmall.into()); } if withdraw_amount == 0 { msg!("Liquidation is too small to receive collateral"); return Err(LendingError::LiquidationTooSmall.into()); } repay_reserve.liquidity.repay(repay_amount, settle_amount)?; repay_reserve.last_update.mark_stale(); Reserve::pack(repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; obligation.repay(settle_amount, liquidity_index)?; obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: repay_reserve_liquidity_supply_info.clone(), amount: repay_amount, authority: user_transfer_authority_info.clone(), authority_signer_seeds: &[], token_program: token_program_id.clone(), })?; spl_token_transfer(TokenTransferParams { source: withdraw_reserve_collateral_supply_info.clone(), destination: destination_collateral_info.clone(), amount: withdraw_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; Ok(()) } #[inline(never)] // avoid stack frame limit fn process_flash_loan( 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_liquidity_info = next_account_info(account_info_iter)?; let reserve_info = next_account_info(account_info_iter)?; let reserve_liquidity_fee_receiver_info = next_account_info(account_info_iter)?; let host_fee_receiver_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 flash_loan_receiver_program_id = next_account_info(account_info_iter)?; if program_id == flash_loan_receiver_program_id.key { msg!("Lending program cannot be used as the flash loan receiver program provided"); return Err(LendingError::InvalidFlashLoanReceiverProgram.into()); } let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { 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 mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; if reserve_info.owner != program_id { msg!("Reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); } if &reserve.lending_market != lending_market_info.key { msg!("Invalid reserve lending market account"); return Err(LendingError::InvalidAccountInput.into()); } if &reserve.liquidity.supply_pubkey != source_liquidity_info.key { msg!("Reserve liquidity supply must be used as the source liquidity provided"); return Err(LendingError::InvalidAccountInput.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()); } // @FIXME: if u64::MAX is flash loaned, fees should be inclusive as with ordinary borrows let flash_loan_amount = if liquidity_amount == u64::MAX { reserve.liquidity.available_amount } else { liquidity_amount }; let flash_loan_amount_decimal = Decimal::from(flash_loan_amount); let (origination_fee, host_fee) = reserve .config .fees .calculate_flash_loan_fees(flash_loan_amount_decimal)?; let balance_before_flash_loan = Account::unpack(&source_liquidity_info.data.borrow())?.amount; let expected_balance_after_flash_loan = balance_before_flash_loan .checked_add(origination_fee) .ok_or(LendingError::MathOverflow)?; let returned_amount_required = flash_loan_amount .checked_add(origination_fee) .ok_or(LendingError::MathOverflow)?; let mut flash_loan_instruction_accounts = vec![ AccountMeta::new(*destination_liquidity_info.key, false), AccountMeta::new(*source_liquidity_info.key, false), AccountMeta::new_readonly(*token_program_id.key, false), ]; let mut flash_loan_instruction_account_infos = vec![ destination_liquidity_info.clone(), flash_loan_receiver_program_id.clone(), source_liquidity_info.clone(), token_program_id.clone(), ]; for account_info in account_info_iter { flash_loan_instruction_accounts.push(AccountMeta { pubkey: *account_info.key, is_signer: account_info.is_signer, is_writable: account_info.is_writable, }); flash_loan_instruction_account_infos.push(account_info.clone()); } reserve.liquidity.borrow(flash_loan_amount_decimal)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: destination_liquidity_info.clone(), amount: flash_loan_amount, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; const RECEIVE_FLASH_LOAN_INSTRUCTION_DATA_SIZE: usize = 9; // @FIXME: don't use 0 to indicate a flash loan receiver instruction https://git.io/JGzz9 const RECEIVE_FLASH_LOAN_INSTRUCTION_TAG: u8 = 0u8; let mut data = Vec::with_capacity(RECEIVE_FLASH_LOAN_INSTRUCTION_DATA_SIZE); data.push(RECEIVE_FLASH_LOAN_INSTRUCTION_TAG); data.extend_from_slice(&returned_amount_required.to_le_bytes()); invoke( &Instruction { program_id: *flash_loan_receiver_program_id.key, accounts: flash_loan_instruction_accounts, data, }, &flash_loan_instruction_account_infos[..], )?; reserve = Reserve::unpack(&reserve_info.data.borrow())?; reserve .liquidity .repay(flash_loan_amount, flash_loan_amount_decimal)?; Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?; let actual_balance_after_flash_loan = Account::unpack(&source_liquidity_info.data.borrow())?.amount; if actual_balance_after_flash_loan < expected_balance_after_flash_loan { msg!("Insufficient reserve liquidity after flash loan"); return Err(LendingError::NotEnoughLiquidityAfterFlashLoan.into()); } let mut owner_fee = origination_fee; if host_fee > 0 { owner_fee = owner_fee .checked_sub(host_fee) .ok_or(LendingError::MathOverflow)?; spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: host_fee_receiver_info.clone(), amount: host_fee, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; } if owner_fee > 0 { spl_token_transfer(TokenTransferParams { source: source_liquidity_info.clone(), destination: reserve_liquidity_fee_receiver_info.clone(), amount: owner_fee, authority: lending_market_authority_info.clone(), authority_signer_seeds, token_program: token_program_id.clone(), })?; } Ok(()) } #[inline(never)] // avoid stack frame limit fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( program_id: &Pubkey, collateral_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let reserve_collateral_info = next_account_info(account_info_iter)?; let user_collateral_info = next_account_info(account_info_iter)?; let reserve_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_liquidity_info = next_account_info(account_info_iter)?; let reserve_collateral_mint_info = next_account_info(account_info_iter)?; let reserve_liquidity_supply_info = next_account_info(account_info_iter)?; let obligation_owner_info = next_account_info(account_info_iter)?; let user_transfer_authority_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(next_account_info(account_info_iter)?)?; let token_program_id = next_account_info(account_info_iter)?; let liquidity_amount = _withdraw_obligation_collateral( program_id, collateral_amount, reserve_collateral_info, user_collateral_info, reserve_info, obligation_info, lending_market_info, lending_market_authority_info, obligation_owner_info, clock, token_program_id, )?; _redeem_reserve_collateral( program_id, liquidity_amount, user_collateral_info, user_liquidity_info, reserve_info, reserve_collateral_mint_info, reserve_liquidity_supply_info, lending_market_info, lending_market_authority_info, user_transfer_authority_info, clock, token_program_id, ) } #[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(); 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 pyth_product_info = next_account_info(account_info_iter)?; let pyth_price_info = next_account_info(account_info_iter)?; let switchboard_feed_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()); } if &reserve.lending_market != lending_market_info.key { msg!("Reserve lending market does not match the lending market provided"); return Err(LendingError::InvalidAccountInput.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()); } if *pyth_price_info.key != reserve.liquidity.pyth_oracle_pubkey { validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?; reserve.liquidity.pyth_oracle_pubkey = *pyth_price_info.key; } if *switchboard_feed_info.key != reserve.liquidity.switchboard_oracle_pubkey { validate_switchboard_keys(&lending_market, switchboard_feed_info)?; reserve.liquidity.switchboard_oracle_pubkey = *switchboard_feed_info.key; } if reserve.liquidity.switchboard_oracle_pubkey == spl_token_lending::NULL_PUBKEY && (*pyth_price_info.key == spl_token_lending::NULL_PUBKEY || *pyth_product_info.key == spl_token_lending::NULL_PUBKEY) { msg!("At least one price oracle must have a non-null pubkey"); return Err(LendingError::InvalidOracleConfig.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!( "Rent exempt balance insufficient got {} expected {}", &account_info.lamports().to_string(), &rent.minimum_balance(account_info.data_len()).to_string(), ); Err(LendingError::NotRentExempt.into()) } else { Ok(()) } } fn assert_uninitialized( account_info: &AccountInfo, ) -> Result { let account: T = T::unpack_unchecked(&account_info.data.borrow())?; if account.is_initialized() { Err(LendingError::AlreadyInitialized.into()) } else { Ok(account) } } /// Unpacks a spl_token `Mint`. fn unpack_mint(data: &[u8]) -> Result { Mint::unpack(data).map_err(|_| LendingError::InvalidTokenMint) } fn get_pyth_product_quote_currency(pyth_product: &pyth::Product) -> Result<[u8; 32], ProgramError> { const LEN: usize = 14; const KEY: &[u8; LEN] = b"quote_currency"; let mut start = 0; while start < pyth::PROD_ATTR_SIZE { let mut length = pyth_product.attr[start] as usize; start += 1; if length == LEN { let mut end = start + length; if end > pyth::PROD_ATTR_SIZE { msg!("Pyth product attribute key length too long"); return Err(LendingError::InvalidOracleConfig.into()); } let key = &pyth_product.attr[start..end]; if key == KEY { start += length; length = pyth_product.attr[start] as usize; start += 1; end = start + length; if length > 32 || end > pyth::PROD_ATTR_SIZE { msg!("Pyth product quote currency value too long"); return Err(LendingError::InvalidOracleConfig.into()); } let mut value = [0u8; 32]; value[0..length].copy_from_slice(&pyth_product.attr[start..end]); return Ok(value); } } start += length; start += 1 + pyth_product.attr[start] as usize; } msg!("Pyth product quote currency not found"); Err(LendingError::InvalidOracleConfig.into()) } fn get_price( switchboard_feed_info: &AccountInfo, pyth_price_account_info: &AccountInfo, clock: &Clock, ) -> Result { let pyth_price = get_pyth_price(pyth_price_account_info, clock).unwrap_or_default(); if pyth_price != Decimal::zero() { return Ok(pyth_price); } get_switchboard_price(switchboard_feed_info, clock) } fn get_pyth_price(pyth_price_info: &AccountInfo, clock: &Clock) -> Result { const STALE_AFTER_SLOTS_ELAPSED: u64 = 20; if *pyth_price_info.key == spl_token_lending::NULL_PUBKEY { return Err(LendingError::NullOracleConfig.into()); } let pyth_price_data = pyth_price_info.try_borrow_data()?; let pyth_price = pyth::load::(&pyth_price_data) .map_err(|_| ProgramError::InvalidAccountData)?; if pyth_price.ptype != pyth::PriceType::Price { msg!("Oracle price type is invalid"); return Err(LendingError::InvalidOracleConfig.into()); } let slots_elapsed = clock .slot .checked_sub(pyth_price.valid_slot) .ok_or(LendingError::MathOverflow)?; if slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { msg!("Pyth oracle price is stale"); return Err(LendingError::InvalidOracleConfig.into()); } let price: u64 = pyth_price.agg.price.try_into().map_err(|_| { msg!("Oracle price cannot be negative"); LendingError::InvalidOracleConfig })?; let market_price = if pyth_price.expo >= 0 { let exponent = pyth_price .expo .try_into() .map_err(|_| LendingError::MathOverflow)?; let zeros = 10u64 .checked_pow(exponent) .ok_or(LendingError::MathOverflow)?; Decimal::from(price).try_mul(zeros)? } else { let exponent = pyth_price .expo .checked_abs() .ok_or(LendingError::MathOverflow)? .try_into() .map_err(|_| LendingError::MathOverflow)?; let decimals = 10u64 .checked_pow(exponent) .ok_or(LendingError::MathOverflow)?; Decimal::from(price).try_div(decimals)? }; Ok(market_price) } fn get_switchboard_price( switchboard_feed_info: &AccountInfo, clock: &Clock, ) -> Result { const STALE_AFTER_SLOTS_ELAPSED: u64 = 100; if *switchboard_feed_info.key == spl_token_lending::NULL_PUBKEY { return Err(LendingError::NullOracleConfig.into()); } let account_buf = switchboard_feed_info.try_borrow_data()?; // first byte type discriminator if account_buf[0] != SwitchboardAccountType::TYPE_AGGREGATOR as u8 { msg!("switchboard address not of type aggregator"); return Err(LendingError::InvalidAccountInput.into()); } let aggregator: AggregatorState = get_aggregator(switchboard_feed_info)?; // if aggregator.version != 1 { // msg!("switchboard version incorrect"); // return Err(LendingError::InvalidAccountInput.into()); // } let round_result: RoundResult = get_aggregator_result(&aggregator)?; let slots_elapsed = clock .slot .checked_sub(round_result.round_open_slot.unwrap()) .ok_or(LendingError::MathOverflow)?; if slots_elapsed >= STALE_AFTER_SLOTS_ELAPSED { msg!("Switchboard oracle price is stale"); return Err(LendingError::InvalidOracleConfig.into()); } let price_float = round_result.result.unwrap_or(0.0); // we just do this so we can parse coins with low usd value // it might be better to just extract the mantissa and exponent from the float directly let price_quotient = 10u64.pow(9); let price = ((price_quotient as f64) * price_float) as u128; Decimal::from(price).try_div(price_quotient) } /// Issue a spl_token `InitializeAccount` instruction. #[inline(always)] fn spl_token_init_account(params: TokenInitializeAccountParams<'_>) -> ProgramResult { let TokenInitializeAccountParams { account, mint, owner, rent, token_program, } = params; let ix = spl_token::instruction::initialize_account( token_program.key, account.key, mint.key, owner.key, )?; let result = invoke(&ix, &[account, mint, owner, rent, token_program]); result.map_err(|_| LendingError::TokenInitializeAccountFailed.into()) } /// Issue a spl_token `InitializeMint` instruction. #[inline(always)] fn spl_token_init_mint(params: TokenInitializeMintParams<'_, '_>) -> ProgramResult { let TokenInitializeMintParams { mint, rent, authority, token_program, decimals, } = params; let ix = spl_token::instruction::initialize_mint( token_program.key, mint.key, authority, None, decimals, )?; let result = invoke(&ix, &[mint, rent, token_program]); result.map_err(|_| LendingError::TokenInitializeMintFailed.into()) } /// Invoke signed unless signers seeds are empty #[inline(always)] fn invoke_optionally_signed( instruction: &Instruction, account_infos: &[AccountInfo], authority_signer_seeds: &[&[u8]], ) -> ProgramResult { if authority_signer_seeds.is_empty() { invoke(instruction, account_infos) } else { invoke_signed(instruction, account_infos, &[authority_signer_seeds]) } } /// Issue a spl_token `Transfer` instruction. #[inline(always)] fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult { let TokenTransferParams { source, destination, authority, token_program, amount, authority_signer_seeds, } = params; let result = invoke_optionally_signed( &spl_token::instruction::transfer( token_program.key, source.key, destination.key, authority.key, &[], amount, )?, &[source, destination, authority, token_program], authority_signer_seeds, ); result.map_err(|_| LendingError::TokenTransferFailed.into()) } /// Issue a spl_token `MintTo` instruction. fn spl_token_mint_to(params: TokenMintToParams<'_, '_>) -> ProgramResult { let TokenMintToParams { mint, destination, authority, token_program, amount, authority_signer_seeds, } = params; let result = invoke_optionally_signed( &spl_token::instruction::mint_to( token_program.key, mint.key, destination.key, authority.key, &[], amount, )?, &[mint, destination, authority, token_program], authority_signer_seeds, ); result.map_err(|_| LendingError::TokenMintToFailed.into()) } /// Issue a spl_token `Burn` instruction. #[inline(always)] fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult { let TokenBurnParams { mint, source, authority, token_program, amount, authority_signer_seeds, } = params; let result = invoke_optionally_signed( &spl_token::instruction::burn( token_program.key, source.key, mint.key, authority.key, &[], amount, )?, &[source, mint, authority, token_program], authority_signer_seeds, ); 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(()) } /// validates pyth AccountInfos #[inline(always)] fn validate_pyth_keys( lending_market: &LendingMarket, pyth_product_info: &AccountInfo, pyth_price_info: &AccountInfo, ) -> ProgramResult { if *pyth_price_info.key == spl_token_lending::NULL_PUBKEY { return Ok(()); } if &lending_market.oracle_program_id != pyth_product_info.owner { msg!("Pyth product account provided is not owned by the lending market oracle program"); return Err(LendingError::InvalidOracleConfig.into()); } if &lending_market.oracle_program_id != pyth_price_info.owner { msg!("Pyth price account provided is not owned by the lending market oracle program"); return Err(LendingError::InvalidOracleConfig.into()); } let pyth_product_data = pyth_product_info.try_borrow_data()?; let pyth_product = pyth::load::(&pyth_product_data) .map_err(|_| ProgramError::InvalidAccountData)?; if pyth_product.magic != pyth::MAGIC { msg!("Pyth product account provided is not a valid Pyth account"); return Err(LendingError::InvalidOracleConfig.into()); } if pyth_product.ver != pyth::VERSION_2 { msg!("Pyth product account provided has a different version than expected"); return Err(LendingError::InvalidOracleConfig.into()); } if pyth_product.atype != pyth::AccountType::Product as u32 { msg!("Pyth product account provided is not a valid Pyth product account"); return Err(LendingError::InvalidOracleConfig.into()); } let pyth_price_pubkey_bytes: &[u8; 32] = pyth_price_info .key .as_ref() .try_into() .map_err(|_| LendingError::InvalidAccountInput)?; if &pyth_product.px_acc.val != pyth_price_pubkey_bytes { msg!("Pyth product price account does not match the Pyth price provided"); return Err(LendingError::InvalidOracleConfig.into()); } let quote_currency = get_pyth_product_quote_currency(pyth_product)?; if lending_market.quote_currency != quote_currency { msg!("Lending market quote currency does not match the oracle quote currency"); return Err(LendingError::InvalidOracleConfig.into()); } Ok(()) } /// validates switchboard AccountInfo fn validate_switchboard_keys( lending_market: &LendingMarket, switchboard_feed_info: &AccountInfo, ) -> ProgramResult { if *switchboard_feed_info.key == spl_token_lending::NULL_PUBKEY { return Ok(()); } if &lending_market.switchboard_oracle_program_id != switchboard_feed_info.owner { msg!("Switchboard account provided is not owned by the switchboard oracle program"); return Err(LendingError::InvalidOracleConfig.into()); } Ok(()) } struct TokenInitializeMintParams<'a: 'b, 'b> { mint: AccountInfo<'a>, rent: AccountInfo<'a>, authority: &'b Pubkey, decimals: u8, token_program: AccountInfo<'a>, } struct TokenInitializeAccountParams<'a> { account: AccountInfo<'a>, mint: AccountInfo<'a>, owner: AccountInfo<'a>, rent: AccountInfo<'a>, token_program: AccountInfo<'a>, } struct TokenTransferParams<'a: 'b, 'b> { source: AccountInfo<'a>, destination: AccountInfo<'a>, amount: u64, authority: AccountInfo<'a>, authority_signer_seeds: &'b [&'b [u8]], token_program: AccountInfo<'a>, } struct TokenMintToParams<'a: 'b, 'b> { mint: AccountInfo<'a>, destination: AccountInfo<'a>, amount: u64, authority: AccountInfo<'a>, authority_signer_seeds: &'b [&'b [u8]], token_program: AccountInfo<'a>, } struct TokenBurnParams<'a: 'b, 'b> { mint: AccountInfo<'a>, source: AccountInfo<'a>, amount: u64, authority: AccountInfo<'a>, authority_signer_seeds: &'b [&'b [u8]], token_program: AccountInfo<'a>, } impl PrintProgramError for LendingError { fn print(&self) where E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, { msg!(&self.to_string()); } }