From 7d72f6f25debbed2a767024b5d056ddcf629f91d Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Fri, 24 May 2024 01:03:10 -0700 Subject: [PATCH] sys-lend: add solend pool support --- Cargo.lock | 5 + Cargo.toml | 5 + src/bin/sys-lend.rs | 378 ++++- src/vendor/mod.rs | 1 + src/vendor/solend/error.rs | 227 +++ src/vendor/solend/math/common.rs | 38 + src/vendor/solend/math/decimal.rs | 222 +++ src/vendor/solend/math/mod.rs | 9 + src/vendor/solend/math/rate.rs | 179 ++ src/vendor/solend/mod.rs | 36 + src/vendor/solend/state/last_update.rs | 58 + src/vendor/solend/state/lending_market.rs | 199 +++ .../solend/state/lending_market_metadata.rs | 64 + src/vendor/solend/state/mod.rs | 61 + src/vendor/solend/state/obligation.rs | 617 +++++++ src/vendor/solend/state/rate_limiter.rs | 225 +++ src/vendor/solend/state/reserve.rs | 1469 +++++++++++++++++ 17 files changed, 3790 insertions(+), 3 deletions(-) create mode 100644 src/vendor/solend/error.rs create mode 100644 src/vendor/solend/math/common.rs create mode 100644 src/vendor/solend/math/decimal.rs create mode 100644 src/vendor/solend/math/mod.rs create mode 100644 src/vendor/solend/math/rate.rs create mode 100644 src/vendor/solend/mod.rs create mode 100644 src/vendor/solend/state/last_update.rs create mode 100644 src/vendor/solend/state/lending_market.rs create mode 100644 src/vendor/solend/state/lending_market_metadata.rs create mode 100644 src/vendor/solend/state/mod.rs create mode 100644 src/vendor/solend/state/obligation.rs create mode 100644 src/vendor/solend/state/rate_limiter.rs create mode 100644 src/vendor/solend/state/reserve.rs diff --git a/Cargo.lock b/Cargo.lock index 2fdaab9..06f5949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4988,11 +4988,13 @@ dependencies = [ name = "sys" version = "0.1.0" dependencies = [ + "arrayref", "async-recursion", "async-trait", "binance-rs-async", "bincode", "bs58", + "bytemuck", "chrono", "chrono-humanize", "clap 2.33.3", @@ -5009,6 +5011,8 @@ dependencies = [ "kraken_sdk_rest", "lazy_static", "log", + "num-derive 0.3.3", + "num-traits", "pickledb", "reqwest", "rust_decimal", @@ -5023,6 +5027,7 @@ dependencies = [ "solana-cli-output", "solana-client", "solana-logger", + "solana-program", "solana-remote-wallet", "solana-sdk", "solana-transaction-status", diff --git a/Cargo.toml b/Cargo.toml index aa1bfa9..9937666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ publish = false default-run = "sys" [dependencies] +arrayref = "0.3.6" async-recursion = "1.0.0" async-trait = "0.1.50" #binance-rs-async = { version = "1.2.0", features = ["wallet_api"]} @@ -14,6 +15,7 @@ async-trait = "0.1.50" binance-rs-async = { git = "https://github.com/mvines/binance-rs-async", rev = "bca0331675f39924a06c4c1fbfacc143d3774735", features = ["wallet_api"]} bincode = "1.3" bs58 = "0.4.0" +bytemuck = "1.5.1" chrono = "0.4" chrono-humanize = "0.2.1" clap = "2.33" @@ -36,6 +38,8 @@ kraken_sdk_rest = { git = "https://github.com/mvines/kraken_sdk_rust", rev = "80 #kraken_sdk_rest = "0.18.0" lazy_static = "1.4.0" log = "0.4.17" +num-derive = "0.3" +num-traits = "0.2" pickledb = { git = "https://github.com/seladb/pickledb-rs.git", rev = "0.5.0" } #pickledb = { path = "../pickledb-rs" } reqwest = "0.11" @@ -52,6 +56,7 @@ solana-cli-output = "=1.17.26" solana-client = "=1.17.26" solana-logger = "=1.17.26" solana-remote-wallet = "=1.17.26" +solana-program = "=1.17.26" solana-sdk = "=1.17.26" solana-transaction-status = "=1.17.26" solana-vote-program = "=1.17.26" # Remove `solana-vote-program` dependency upon update to Solana 1.16 diff --git a/src/bin/sys-lend.rs b/src/bin/sys-lend.rs index abc77c7..88a435b 100644 --- a/src/bin/sys-lend.rs +++ b/src/bin/sys-lend.rs @@ -13,6 +13,7 @@ use { instruction::{AccountMeta, Instruction}, message::{self, Message, VersionedMessage}, native_token::sol_to_lamports, + program_pack::Pack, pubkey, pubkey::Pubkey, system_program, sysvar, @@ -25,12 +26,25 @@ use { priority_fee::{apply_priority_fee, PriorityFee}, send_transaction_until_expired, token::*, - vendor::{kamino, marginfi_v2}, + vendor::{ + kamino, marginfi_v2, + solend::{self, math::TryMul}, + }, }, }; lazy_static::lazy_static! { static ref SUPPORTED_TOKENS: HashMap<&'static str, HashSet::> = HashMap::from([ + ("solend-main", HashSet::from([ + Token::USDC, + Token::USDT, + ])) , + ("solend-turbosol", HashSet::from([ + Token::USDC, + ])) , + ("solend-jlp", HashSet::from([ + Token::USDC, + ])) , ("mfi", HashSet::from([ Token::USDC, Token::USDT, @@ -57,7 +71,7 @@ lazy_static::lazy_static! { ]); } -#[derive(PartialEq, Clone, Copy)] +#[derive(PartialEq, Clone, Copy, Debug)] enum Operation { Deposit, Withdraw, @@ -392,6 +406,8 @@ async fn main() -> Result<(), Box> { ) -> Result> { Ok(if pool.starts_with("kamino-") { kamino_apr(rpc_client, pool, token)? + } else if pool.starts_with("solend-") { + solend_apr(rpc_client, pool, token)? } else if pool == "mfi" { mfi_apr(rpc_client, token)? } else { @@ -407,6 +423,8 @@ async fn main() -> Result<(), Box> { ) -> Result> { Ok(if pool.starts_with("kamino-") { kamino_deposited_amount(rpc_client, pool, address, token)? + } else if pool.starts_with("solend-") { + solend_deposited_amount(rpc_client, pool, address, token)? } else if pool == "mfi" { mfi_balance(rpc_client, address, token)?.0 } else { @@ -538,6 +556,7 @@ async fn main() -> Result<(), Box> { .map(|pool| { let supply_balance = pool_supply_balance(&rpc_client, pool, token, address) .unwrap_or_else(|err| panic!("Unable to read balance for {pool}: {err}")); + (pool.clone(), supply_balance) }) .collect::>(); @@ -577,7 +596,7 @@ async fn main() -> Result<(), Box> { balance >= amount } }) - .expect("withdraw_pool"); + .unwrap_or(deposit_pool); let apy_improvement = apr_to_apy( supply_apr.get(deposit_pool).unwrap() - supply_apr.get(withdraw_pool).unwrap(), @@ -615,6 +634,8 @@ async fn main() -> Result<(), Box> { for (op, pool) in ops { let result = if pool.starts_with("kamino-") { kamino_deposit_or_withdraw(op, &rpc_client, pool, address, token, amount)? + } else if pool.starts_with("solend-") { + solend_deposit_or_withdraw(op, &rpc_client, pool, address, token, amount)? } else if pool == "mfi" { mfi_deposit_or_withdraw(op, &rpc_client, address, token, amount, false)? } else { @@ -727,6 +748,7 @@ async fn main() -> Result<(), Box> { _ => unreachable!(), } + // Only send metrics on success metrics::send(metrics::env_config()).await; Ok(()) } @@ -1479,3 +1501,353 @@ fn kamino_deposit_or_withdraw( }), }) } + +////////////////////////////////////////////////////////////////////////////// +///[ Solend Stuff ] ////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// + +fn solend_load_reserve( + rpc_client: &RpcClient, + reserve_address: Pubkey, +) -> Result> { + let account_data = rpc_client.get_account_data(&reserve_address)?; + Ok(solend::state::Reserve::unpack(&account_data)?) +} + +fn solend_load_reserve_for_pool( + rpc_client: &RpcClient, + pool: &str, + token: Token, +) -> Result<(Pubkey, solend::state::Reserve), Box> { + let market_reserve_map = match pool { + "solend-main" => HashMap::from([ + ( + Token::USDC, + pubkey!["BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw"], + ), + ( + Token::USDT, + pubkey!["8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE"], + ), + ]), + "solend-turbosol" => HashMap::from([( + Token::USDC, + pubkey!["EjUgEaPpKMg2nqex9obb46gZQ6Ar9mWSdVKbw9A6PyXA"], + )]), + "solend-jlp" => HashMap::from([( + Token::USDC, + pubkey!["GShhnkfbaYy41Fd8vSEk9zoiwZSKqbH1j16jZ2afV2GG"], + )]), + _ => unreachable!(), + }; + let market_reserve_address = *market_reserve_map + .get(&token) + .ok_or_else(|| format!("{pool}: {token} is not supported"))?; + + let reserve = solend_load_reserve(rpc_client, market_reserve_address)?; + + Ok((market_reserve_address, reserve)) +} + +fn solend_apr( + rpc_client: &RpcClient, + pool: &str, + token: Token, +) -> Result> { + let (_market_reserve_address, reserve) = solend_load_reserve_for_pool(rpc_client, pool, token)?; + + let utilization_rate = reserve.liquidity.utilization_rate()?; + let current_borrow_rate = reserve.current_borrow_rate().unwrap(); + + let supply_apr = format!( + "{}", + utilization_rate.try_mul(current_borrow_rate)?.try_mul( + solend::math::Rate::from_percent(100 - reserve.config.protocol_take_rate) + )? + ); + + Ok(supply_apr.parse::()?) +} + +fn solend_find_obligation_address(wallet_address: Pubkey, lending_market: Pubkey) -> Pubkey { + Pubkey::create_with_seed( + &wallet_address, + &lending_market.to_string()[0..32], + &solend::solend_mainnet::ID, + ) + .unwrap() +} + +fn solend_load_obligation( + rpc_client: &RpcClient, + obligation_address: Pubkey, +) -> Result> { + let account_data = rpc_client.get_account_data(&obligation_address)?; + Ok(solend::state::Obligation::unpack(&account_data)?) +} + +fn solend_load_lending_market( + rpc_client: &RpcClient, + lending_market_address: Pubkey, +) -> Result> { + let account_data = rpc_client.get_account_data(&lending_market_address)?; + Ok(solend::state::LendingMarket::unpack(&account_data)?) +} + +fn solend_deposited_amount( + rpc_client: &RpcClient, + pool: &str, + wallet_address: Pubkey, + token: Token, +) -> Result> { + let (market_reserve_address, reserve) = solend_load_reserve_for_pool(rpc_client, pool, token)?; + + let lending_market = reserve.lending_market; + let market_obligation = solend_find_obligation_address(wallet_address, lending_market); + if matches!(rpc_client.get_balance(&market_obligation), Ok(0)) { + return Ok(0); + } + + let obligation = solend_load_obligation(rpc_client, market_obligation)?; + + let collateral_deposited_amount = obligation + .deposits + .iter() + .find(|collateral| collateral.deposit_reserve == market_reserve_address) + .map(|collateral| collateral.deposited_amount) + .unwrap_or_default(); + + let collateral_exchange_rate = reserve.collateral_exchange_rate()?; + Ok(collateral_exchange_rate.collateral_to_liquidity(collateral_deposited_amount)?) +} + +fn solend_deposit_or_withdraw( + op: Operation, + rpc_client: &RpcClient, + pool: &str, + wallet_address: Pubkey, + token: Token, + amount: u64, +) -> Result> { + let (market_reserve_address, reserve) = solend_load_reserve_for_pool(rpc_client, pool, token)?; + + let market_obligation = solend_find_obligation_address(wallet_address, reserve.lending_market); + let obligation = solend_load_obligation(rpc_client, market_obligation)?; + + let lending_market = solend_load_lending_market(rpc_client, reserve.lending_market)?; + + let lending_market_authority = Pubkey::create_program_address( + &[ + &reserve.lending_market.to_bytes(), + &[lending_market.bump_seed], + ], + &solend::solend_mainnet::ID, + )?; + + let mut instructions = vec![]; + + let (amount, required_compute_units) = match op { + Operation::Deposit => { + // Solend: Deposit Reserve Liquidity and Obligation Collateral + let solend_deposit_reserve_liquidity_and_obligation_collateral_data = { + let mut v = vec![0x0e]; + v.extend(amount.to_le_bytes()); + v + }; + + instructions.push(Instruction::new_with_bytes( + solend::solend_mainnet::ID, + &solend_deposit_reserve_liquidity_and_obligation_collateral_data, + vec![ + // User Liquidity Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &token.mint(), + ), + false, + ), + // User Collateral Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &reserve.collateral.mint_pubkey, + ), + false, + ), + // Lending Market + AccountMeta::new(market_reserve_address, false), + // Reserve Liquidity Supply + AccountMeta::new(reserve.liquidity.supply_pubkey, false), + // Reserve Collateral Mint + AccountMeta::new(reserve.collateral.mint_pubkey, false), + // Lending Market + AccountMeta::new(reserve.lending_market, false), + // Lending Market Authority + AccountMeta::new_readonly(lending_market_authority, false), + // Reserve Destination Deposit Collateral + AccountMeta::new(reserve.collateral.supply_pubkey, false), + // Obligation + AccountMeta::new(market_obligation, false), + // Obligation Owner + AccountMeta::new(wallet_address, true), + // Pyth Oracle + AccountMeta::new_readonly(reserve.liquidity.pyth_oracle_pubkey, false), + // Switchboard Oracle + AccountMeta::new_readonly(reserve.liquidity.switchboard_oracle_pubkey, false), + // User Transfer Authority + AccountMeta::new(wallet_address, true), + // Token Program + AccountMeta::new_readonly(spl_token::id(), false), + ], + )); + (amount, 100_000) + } + Operation::Withdraw => { + let withdraw_amount = if amount == u64::MAX { + let collateral_deposited_amount = obligation + .deposits + .iter() + .find(|collateral| collateral.deposit_reserve == market_reserve_address) + .map(|collateral| collateral.deposited_amount) + .unwrap_or_default(); + + let collateral_exchange_rate = reserve.collateral_exchange_rate()?; + collateral_exchange_rate.collateral_to_liquidity(collateral_deposited_amount)? + } else { + amount + }; + + // Instruction: Solend: Refresh Reserve + let obligation_market_reserves = obligation + .deposits + .iter() + .filter(|c| c.deposit_reserve != Pubkey::default()) + .map(|c| c.deposit_reserve) + .collect::>(); + + let mut refresh_reserves = obligation_market_reserves + .iter() + .filter_map(|reserve_address| { + if *reserve_address != market_reserve_address { + Some(( + *reserve_address, + solend_load_reserve(rpc_client, *reserve_address).unwrap_or_else( + |err| { + // TODO: propagate failure up instead of panic.. + panic!("unable to load reserve {reserve_address}: {err}") + }, + ), + )) + } else { + None + } + }) + .collect::>(); + refresh_reserves.push((market_reserve_address, reserve.clone())); + + for (reserve_address, reserve) in refresh_reserves { + instructions.push(Instruction::new_with_bytes( + solend::solend_mainnet::ID, + &[3], + vec![ + // Reserve + AccountMeta::new(reserve_address, false), + // Pyth Oracle + AccountMeta::new_readonly(reserve.liquidity.pyth_oracle_pubkey, false), + // Switchboard Oracle + AccountMeta::new_readonly( + reserve.liquidity.switchboard_oracle_pubkey, + false, + ), + ], + )); + } + + // Instruction: Solend: Refresh Obligation + let mut refresh_obligation_account_metas = vec![ + // Obligation + AccountMeta::new(market_obligation, false), + ]; + + for obligation_market_reserve in &obligation_market_reserves { + refresh_obligation_account_metas + .push(AccountMeta::new(*obligation_market_reserve, false)); + } + + instructions.push(Instruction::new_with_bytes( + solend::solend_mainnet::ID, + &[0x7], + refresh_obligation_account_metas, + )); + + // Instruction: Solend: Withdraw Obligation Collateral And Redeem Reserve Collateral + + let collateral_exchange_rate = reserve.collateral_exchange_rate()?; + let solend_withdraw_obligation_collateral_and_redeem_reserve_collateral_data = { + let mut v = vec![0x0f]; + v.extend( + collateral_exchange_rate + .liquidity_to_collateral(amount)? + .to_le_bytes(), + ); + v + }; + + instructions.push(Instruction::new_with_bytes( + solend::solend_mainnet::ID, + &solend_withdraw_obligation_collateral_and_redeem_reserve_collateral_data, + vec![ + // Reserve Collateral Supply + AccountMeta::new(reserve.collateral.supply_pubkey, false), + // User Collateral Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &reserve.collateral.mint_pubkey, + ), + false, + ), + // Lending Market + AccountMeta::new(market_reserve_address, false), + // Obligation + AccountMeta::new(market_obligation, false), + // Lending Market + AccountMeta::new(reserve.lending_market, false), + // Lending Market Authority + AccountMeta::new_readonly(lending_market_authority, false), + // User Liquidity Token Account + AccountMeta::new( + spl_associated_token_account::get_associated_token_address( + &wallet_address, + &token.mint(), + ), + false, + ), + // Reserve Collateral Mint + AccountMeta::new(reserve.collateral.mint_pubkey, false), + // Reserve Liquidity Supply + AccountMeta::new(reserve.liquidity.supply_pubkey, false), + // Obligation Owner + AccountMeta::new(wallet_address, true), + // User Transfer Authority + AccountMeta::new(wallet_address, true), + // Token Program + AccountMeta::new_readonly(spl_token::id(), false), + ], + )); + + ( + withdraw_amount - 1, // HACK!! Sometimes Solend loses a lamport? This breaks `rebalance`... + 150_000, + ) + } + }; + + Ok(DepositOrWithdrawResult { + instructions, + required_compute_units, + amount, + address_lookup_table: Some(pubkey!["89ig7Cu6Roi9mJMqpY8sBkPYL2cnqzpgP16sJxSUbvct"]), + }) +} diff --git a/src/vendor/mod.rs b/src/vendor/mod.rs index 086121a..db0e99b 100644 --- a/src/vendor/mod.rs +++ b/src/vendor/mod.rs @@ -1,3 +1,4 @@ /// These projects don't provide a usable Rust SDK.. pub mod kamino; pub mod marginfi_v2; +pub mod solend; diff --git a/src/vendor/solend/error.rs b/src/vendor/solend/error.rs new file mode 100644 index 0000000..23afcb6 --- /dev/null +++ b/src/vendor/solend/error.rs @@ -0,0 +1,227 @@ +//! Error types + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use solana_program::{decode_error::DecodeError, program_error::ProgramError}; +use solana_program::{msg, program_error::PrintProgramError}; +use thiserror::Error; + +/// Errors that may be returned by the TokenLending program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum LendingError { + // 0 + /// Invalid instruction data passed in. + #[error("Failed to unpack instruction data")] + InstructionUnpackError, + /// The account cannot be initialized because it is already in use. + #[error("Account is already initialized")] + AlreadyInitialized, + /// Lamport balance below rent-exempt threshold. + #[error("Lamport balance below rent-exempt threshold")] + NotRentExempt, + /// The program address provided doesn't match the value generated by the program. + #[error("Market authority is invalid")] + InvalidMarketAuthority, + /// Expected a different market owner + #[error("Market owner is invalid")] + InvalidMarketOwner, + + // 5 + /// The owner of the input isn't set to the program address generated by the program. + #[error("Input account owner is not the program address")] + InvalidAccountOwner, + /// The owner of the account input isn't set to the correct token program id. + #[error("Input token account is not owned by the correct token program id")] + InvalidTokenOwner, + /// Expected an SPL Token account + #[error("Input token account is not valid")] + InvalidTokenAccount, + /// Expected an SPL Token mint + #[error("Input token mint account is not valid")] + InvalidTokenMint, + /// Expected a different SPL Token program + #[error("Input token program account is not valid")] + InvalidTokenProgram, + + // 10 + /// Invalid amount, must be greater than zero + #[error("Input amount is invalid")] + InvalidAmount, + /// Invalid config value + #[error("Input config value is invalid")] + InvalidConfig, + /// Invalid config value + #[error("Input account must be a signer")] + InvalidSigner, + /// Invalid account input + #[error("Invalid account input")] + InvalidAccountInput, + /// Math operation overflow + #[error("Math operation overflow")] + MathOverflow, + + // 15 + /// Token initialize mint failed + #[error("Token initialize mint failed")] + TokenInitializeMintFailed, + /// Token initialize account failed + #[error("Token initialize account failed")] + TokenInitializeAccountFailed, + /// Token transfer failed + #[error("Token transfer failed")] + TokenTransferFailed, + /// Token mint to failed + #[error("Token mint to failed")] + TokenMintToFailed, + /// Token burn failed + #[error("Token burn failed")] + TokenBurnFailed, + + // 20 + /// Insufficient liquidity available + #[error("Insufficient liquidity available")] + InsufficientLiquidity, + /// This reserve's collateral cannot be used for borrows + #[error("Input reserve has collateral disabled")] + ReserveCollateralDisabled, + /// Reserve state stale + #[error("Reserve state needs to be refreshed")] + ReserveStale, + /// Withdraw amount too small + #[error("Withdraw amount too small")] + WithdrawTooSmall, + /// Withdraw amount too large + #[error("Withdraw amount too large")] + WithdrawTooLarge, + + // 25 + /// Borrow amount too small + #[error("Borrow amount too small to receive liquidity after fees")] + BorrowTooSmall, + /// Borrow amount too large + #[error("Borrow amount too large for deposited collateral")] + BorrowTooLarge, + /// Repay amount too small + #[error("Repay amount too small to transfer liquidity")] + RepayTooSmall, + /// Liquidation amount too small + #[error("Liquidation amount too small to receive collateral")] + LiquidationTooSmall, + /// Cannot liquidate healthy obligations + #[error("Cannot liquidate healthy obligations")] + ObligationHealthy, + + // 30 + /// Obligation state stale + #[error("Obligation state needs to be refreshed")] + ObligationStale, + /// Obligation reserve limit exceeded + #[error("Obligation reserve limit exceeded")] + ObligationReserveLimit, + /// Expected a different obligation owner + #[error("Obligation owner is invalid")] + InvalidObligationOwner, + /// Obligation deposits are empty + #[error("Obligation deposits are empty")] + ObligationDepositsEmpty, + /// Obligation borrows are empty + #[error("Obligation borrows are empty")] + ObligationBorrowsEmpty, + + // 35 + /// Obligation deposits have zero value + #[error("Obligation deposits have zero value")] + ObligationDepositsZero, + /// Obligation borrows have zero value + #[error("Obligation borrows have zero value")] + ObligationBorrowsZero, + /// Invalid obligation collateral + #[error("Invalid obligation collateral")] + InvalidObligationCollateral, + /// Invalid obligation liquidity + #[error("Invalid obligation liquidity")] + InvalidObligationLiquidity, + /// Obligation collateral is empty + #[error("Obligation collateral is empty")] + ObligationCollateralEmpty, + + // 40 + /// Obligation liquidity is empty + #[error("Obligation liquidity is empty")] + ObligationLiquidityEmpty, + /// Negative interest rate + #[error("Interest rate is negative")] + NegativeInterestRate, + /// Oracle config is invalid + #[error("Input oracle config is invalid")] + InvalidOracleConfig, + /// Expected a different flash loan receiver program + #[error("Input flash loan receiver program account is not valid")] + InvalidFlashLoanReceiverProgram, + /// Not enough liquidity after flash loan + #[error("Not enough liquidity after flash loan")] + NotEnoughLiquidityAfterFlashLoan, + + // 45 + /// Null oracle config + #[error("Null oracle config")] + NullOracleConfig, + /// Insufficent protocol fees to redeem or no liquidity availible to process redeem + #[error("Insufficent protocol fees to claim or no liquidity availible")] + InsufficientProtocolFeesToRedeem, + /// No cpi flash borrows allowed + #[error("No cpi flash borrows allowed")] + FlashBorrowCpi, + /// No corresponding repay found for flash borrow + #[error("No corresponding repay found for flash borrow")] + NoFlashRepayFound, + /// Invalid flash repay found for borrow + #[error("Invalid repay found")] + InvalidFlashRepay, + + // 50 + /// No cpi flash repays allowed + #[error("No cpi flash repays allowed")] + FlashRepayCpi, + /// Multiple flash borrows not allowed in the same transaction + #[error("Multiple flash borrows not allowed in the same transaction")] + MultipleFlashBorrows, + /// Flash loans are disabled for this reserve + #[error("Flash loans are disabled for this reserve")] + FlashLoansDisabled, + /// Deprecated instruction + #[error("Instruction is deprecated")] + DeprecatedInstruction, + /// Outflow Rate Limit Exceeded + #[error("Outflow Rate Limit Exceeded")] + OutflowRateLimitExceeded, + + // 55 + /// Not a whitelisted liquidator + #[error("Not a whitelisted liquidator")] + NotWhitelistedLiquidator, + /// Isolated Tier Asset Violation + #[error("Isolated Tier Asset Violation")] + IsolatedTierAssetViolation, +} + +impl From for ProgramError { + fn from(e: LendingError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for LendingError { + fn type_of() -> &'static str { + "Lending Error" + } +} + +impl PrintProgramError for LendingError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, + { + msg!(&self.to_string()); + } +} diff --git a/src/vendor/solend/math/common.rs b/src/vendor/solend/math/common.rs new file mode 100644 index 0000000..081ee56 --- /dev/null +++ b/src/vendor/solend/math/common.rs @@ -0,0 +1,38 @@ +//! Common module for Decimal and Rate + +use solana_program::program_error::ProgramError; + +/// Scale of precision +pub const SCALE: usize = 18; +/// Identity +pub const WAD: u64 = 1_000_000_000_000_000_000; +/// Half of identity +pub const HALF_WAD: u64 = 500_000_000_000_000_000; +/// Scale for percentages +pub const PERCENT_SCALER: u64 = 10_000_000_000_000_000; +/// Scale for basis points +pub const BPS_SCALER: u64 = 100_000_000_000_000; + +/// Try to subtract, return an error on underflow +pub trait TrySub: Sized { + /// Subtract + fn try_sub(self, rhs: Self) -> Result; +} + +/// Try to subtract, return an error on overflow +pub trait TryAdd: Sized { + /// Add + fn try_add(self, rhs: Self) -> Result; +} + +/// Try to divide, return an error on overflow or divide by zero +pub trait TryDiv: Sized { + /// Divide + fn try_div(self, rhs: RHS) -> Result; +} + +/// Try to multiply, return an error on overflow +pub trait TryMul: Sized { + /// Multiply + fn try_mul(self, rhs: RHS) -> Result; +} diff --git a/src/vendor/solend/math/decimal.rs b/src/vendor/solend/math/decimal.rs new file mode 100644 index 0000000..49f5a12 --- /dev/null +++ b/src/vendor/solend/math/decimal.rs @@ -0,0 +1,222 @@ +//! Math for preserving precision of token amounts which are limited +//! by the SPL Token program to be at most u64::MAX. +//! +//! Decimals are internally scaled by a WAD (10^18) to preserve +//! precision up to 18 decimal places. Decimals are sized to support +//! both serialization and precise math for the full range of +//! unsigned 64-bit integers. The underlying representation is a +//! u192 rather than u256 to reduce compute cost while losing +//! support for arithmetic operations at the high end of u64 range. + +#![allow(clippy::assign_op_pattern)] +#![allow(clippy::ptr_offset_with_cast)] +#![allow(clippy::manual_range_contains)] + +use crate::vendor::solend::{ + error::LendingError, + math::{common::*, Rate}, +}; +use solana_program::program_error::ProgramError; +use std::{convert::TryFrom, fmt}; +use uint::construct_uint; + +// U192 with 192 bits consisting of 3 x 64-bit words +construct_uint! { + pub struct U192(3); +} + +/// Large decimal values, precise to 18 digits +#[derive(Clone, Copy, Default, PartialEq, PartialOrd, Eq, Ord)] +pub struct Decimal(pub U192); + +impl Decimal { + /// One + pub fn one() -> Self { + Self(Self::wad()) + } + + /// Zero + pub fn zero() -> Self { + Self(U192::zero()) + } + + // OPTIMIZE: use const slice when fixed in BPF toolchain + fn wad() -> U192 { + U192::from(WAD) + } + + // OPTIMIZE: use const slice when fixed in BPF toolchain + fn half_wad() -> U192 { + U192::from(HALF_WAD) + } + + /// Create scaled decimal from percent value + pub fn from_percent(percent: u8) -> Self { + Self(U192::from(percent as u64 * PERCENT_SCALER)) + } + + /// Create scaled decimal from deca bps value + pub fn from_deca_bps(deca_bps: u8) -> Self { + Self::from(deca_bps as u64).try_div(1000).unwrap() + } + + /// Create scaled decimal from bps value + pub fn from_bps(bps: u64) -> Self { + Self::from(bps).try_div(10_000).unwrap() + } + + /// Return raw scaled value if it fits within u128 + #[allow(clippy::wrong_self_convention)] + pub fn to_scaled_val(&self) -> Result { + Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?) + } + + /// Create decimal from scaled value + pub fn from_scaled_val(scaled_val: u128) -> Self { + Self(U192::from(scaled_val)) + } + + /// Round scaled decimal to u64 + pub fn try_round_u64(&self) -> Result { + let rounded_val = Self::half_wad() + .checked_add(self.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?; + Ok(u64::try_from(rounded_val).map_err(|_| LendingError::MathOverflow)?) + } + + /// Ceiling scaled decimal to u64 + pub fn try_ceil_u64(&self) -> Result { + let ceil_val = Self::wad() + .checked_sub(U192::from(1u64)) + .ok_or(LendingError::MathOverflow)? + .checked_add(self.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?; + Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?) + } + + /// Floor scaled decimal to u64 + pub fn try_floor_u64(&self) -> Result { + let ceil_val = self + .0 + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?; + Ok(u64::try_from(ceil_val).map_err(|_| LendingError::MathOverflow)?) + } +} + +impl fmt::Display for Decimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut scaled_val = self.0.to_string(); + if scaled_val.len() <= SCALE { + scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join("")); + scaled_val.insert_str(0, "0."); + } else { + scaled_val.insert(scaled_val.len() - SCALE, '.'); + } + f.write_str(&scaled_val) + } +} + +impl fmt::Debug for Decimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl From for Decimal { + fn from(val: u64) -> Self { + Self(Self::wad() * U192::from(val)) + } +} + +impl From for Decimal { + fn from(val: u128) -> Self { + Self(Self::wad() * U192::from(val)) + } +} + +impl From for Decimal { + fn from(val: Rate) -> Self { + Self(U192::from(val.to_scaled_val())) + } +} + +impl TryAdd for Decimal { + fn try_add(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_add(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TrySub for Decimal { + fn try_sub(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_sub(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryDiv for Decimal { + fn try_div(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_div(U192::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryDiv for Decimal { + fn try_div(self, rhs: Rate) -> Result { + self.try_div(Self::from(rhs)) + } +} + +impl TryDiv for Decimal { + fn try_div(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(Self::wad()) + .ok_or(LendingError::MathOverflow)? + .checked_div(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryMul for Decimal { + fn try_mul(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_mul(U192::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryMul for Decimal { + fn try_mul(self, rhs: Rate) -> Result { + self.try_mul(Self::from(rhs)) + } +} + +impl TryMul for Decimal { + fn try_mul(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(rhs.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?, + )) + } +} diff --git a/src/vendor/solend/math/mod.rs b/src/vendor/solend/math/mod.rs new file mode 100644 index 0000000..4274427 --- /dev/null +++ b/src/vendor/solend/math/mod.rs @@ -0,0 +1,9 @@ +//! Math for preserving precision + +mod common; +mod decimal; +mod rate; + +pub use common::*; +pub use decimal::*; +pub use rate::*; diff --git a/src/vendor/solend/math/rate.rs b/src/vendor/solend/math/rate.rs new file mode 100644 index 0000000..e20cf1e --- /dev/null +++ b/src/vendor/solend/math/rate.rs @@ -0,0 +1,179 @@ +//! Math for preserving precision of ratios and percentages. +//! +//! Usages and their ranges include: +//! - Collateral exchange ratio <= 5.0 +//! - Loan to value ratio <= 0.9 +//! - Max borrow rate <= 2.56 +//! - Percentages <= 1.0 +//! +//! Rates are internally scaled by a WAD (10^18) to preserve +//! precision up to 18 decimal places. Rates are sized to support +//! both serialization and precise math for the full range of +//! unsigned 8-bit integers. The underlying representation is a +//! u128 rather than u192 to reduce compute cost while losing +//! support for arithmetic operations at the high end of u8 range. + +#![allow(clippy::assign_op_pattern)] +#![allow(clippy::ptr_offset_with_cast)] +#![allow(clippy::reversed_empty_ranges)] +#![allow(clippy::manual_range_contains)] + +use crate::vendor::solend::{ + error::LendingError, + math::{common::*, decimal::Decimal}, +}; +use solana_program::program_error::ProgramError; +use std::{convert::TryFrom, fmt}; +use uint::construct_uint; + +// U128 with 128 bits consisting of 2 x 64-bit words +construct_uint! { + pub struct U128(2); +} + +/// Small decimal values, precise to 18 digits +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Eq, Ord)] +pub struct Rate(pub U128); + +impl Rate { + /// One + pub fn one() -> Self { + Self(Self::wad()) + } + + /// Zero + pub fn zero() -> Self { + Self(U128::from(0)) + } + + // OPTIMIZE: use const slice when fixed in BPF toolchain + fn wad() -> U128 { + U128::from(WAD) + } + + /// Create scaled decimal from percent value + pub fn from_percent(percent: u8) -> Self { + Self(U128::from(percent as u64 * PERCENT_SCALER)) + } + + /// Create scaled decimal from percent value + pub fn from_percent_u64(percent: u64) -> Self { + Self(U128::from(percent) * PERCENT_SCALER) + } + + /// Return raw scaled value + #[allow(clippy::wrong_self_convention)] + pub fn to_scaled_val(&self) -> u128 { + self.0.as_u128() + } + + /// Create decimal from scaled value + pub fn from_scaled_val(scaled_val: u64) -> Self { + Self(U128::from(scaled_val)) + } + + /// Calculates base^exp + pub fn try_pow(&self, mut exp: u64) -> Result { + let mut base = *self; + let mut ret = if exp % 2 != 0 { + base + } else { + Rate(Self::wad()) + }; + + while exp > 0 { + exp /= 2; + base = base.try_mul(base)?; + + if exp % 2 != 0 { + ret = ret.try_mul(base)?; + } + } + + Ok(ret) + } +} + +impl fmt::Display for Rate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut scaled_val = self.0.to_string(); + if scaled_val.len() <= SCALE { + scaled_val.insert_str(0, &vec!["0"; SCALE - scaled_val.len()].join("")); + scaled_val.insert_str(0, "0."); + } else { + scaled_val.insert(scaled_val.len() - SCALE, '.'); + } + f.write_str(&scaled_val) + } +} + +impl TryFrom for Rate { + type Error = ProgramError; + fn try_from(decimal: Decimal) -> Result { + Ok(Self(U128::from(decimal.to_scaled_val()?))) + } +} + +impl TryAdd for Rate { + fn try_add(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_add(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TrySub for Rate { + fn try_sub(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_sub(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryDiv for Rate { + fn try_div(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_div(U128::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryDiv for Rate { + fn try_div(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(Self::wad()) + .ok_or(LendingError::MathOverflow)? + .checked_div(rhs.0) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryMul for Rate { + fn try_mul(self, rhs: u64) -> Result { + Ok(Self( + self.0 + .checked_mul(U128::from(rhs)) + .ok_or(LendingError::MathOverflow)?, + )) + } +} + +impl TryMul for Rate { + fn try_mul(self, rhs: Self) -> Result { + Ok(Self( + self.0 + .checked_mul(rhs.0) + .ok_or(LendingError::MathOverflow)? + .checked_div(Self::wad()) + .ok_or(LendingError::MathOverflow)?, + )) + } +} diff --git a/src/vendor/solend/mod.rs b/src/vendor/solend/mod.rs new file mode 100644 index 0000000..67af28a --- /dev/null +++ b/src/vendor/solend/mod.rs @@ -0,0 +1,36 @@ +#![deny(missing_docs)] + +//! A lending program for the Solana blockchain. + +pub mod error; +//pub mod instruction; +pub mod math; +//pub mod oracles; +pub mod state; + +/// mainnet program id +pub mod solend_mainnet { + solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); +} + +/// devnet program id +pub mod solend_devnet { + solana_program::declare_id!("So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo"); +} + +/// Canonical null pubkey. Prints out as "nu11111111111111111111111111111111111111111" +pub const NULL_PUBKEY: solana_program::pubkey::Pubkey = + solana_program::pubkey::Pubkey::new_from_array([ + 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, + 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, + ]); + +/// Mainnet program id for Switchboard v2. +pub mod switchboard_v2_mainnet { + solana_program::declare_id!("SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"); +} + +/// Devnet program id for Switchboard v2. +pub mod switchboard_v2_devnet { + solana_program::declare_id!("2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"); +} diff --git a/src/vendor/solend/state/last_update.rs b/src/vendor/solend/state/last_update.rs new file mode 100644 index 0000000..bf260b5 --- /dev/null +++ b/src/vendor/solend/state/last_update.rs @@ -0,0 +1,58 @@ +use crate::vendor::solend::error::LendingError; +use solana_program::{clock::Slot, program_error::ProgramError}; +use std::cmp::Ordering; + +/// Number of slots to consider stale after +pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1; + +/// Last update state +#[derive(Clone, Debug, Default)] +pub struct LastUpdate { + /// Last slot when updated + pub slot: Slot, + /// True when marked stale, false when slot updated + pub stale: bool, +} + +impl LastUpdate { + /// Create new last update + pub fn new(slot: Slot) -> Self { + Self { slot, stale: true } + } + + /// Return slots elapsed since given slot + pub fn slots_elapsed(&self, slot: Slot) -> Result { + let slots_elapsed = slot + .checked_sub(self.slot) + .ok_or(LendingError::MathOverflow)?; + Ok(slots_elapsed) + } + + /// Set last update slot + pub fn update_slot(&mut self, slot: Slot) { + self.slot = slot; + self.stale = false; + } + + /// Set stale to true + pub fn mark_stale(&mut self) { + self.stale = true; + } + + /// Check if marked stale or last update slot is too long ago + pub fn is_stale(&self, slot: Slot) -> Result { + Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED) + } +} + +impl PartialEq for LastUpdate { + fn eq(&self, other: &Self) -> bool { + self.slot == other.slot + } +} + +impl PartialOrd for LastUpdate { + fn partial_cmp(&self, other: &Self) -> Option { + self.slot.partial_cmp(&other.slot) + } +} diff --git a/src/vendor/solend/state/lending_market.rs b/src/vendor/solend/state/lending_market.rs new file mode 100644 index 0000000..8dd5868 --- /dev/null +++ b/src/vendor/solend/state/lending_market.rs @@ -0,0 +1,199 @@ +use crate::vendor::solend::state::*; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::{ + msg, + program_error::ProgramError, + program_pack::{IsInitialized, Pack, Sealed}, + pubkey::{Pubkey, PUBKEY_BYTES}, +}; + +/// Lending market state +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LendingMarket { + /// Version of lending market + pub version: u8, + /// Bump seed for derived authority address + pub bump_seed: u8, + /// Owner authority which can add new reserves + pub owner: Pubkey, + /// Currency market prices are quoted in + /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey + pub quote_currency: [u8; 32], + /// Token program id + pub token_program_id: Pubkey, + /// Oracle (Pyth) program id + pub oracle_program_id: Pubkey, + /// Oracle (Switchboard) program id + pub switchboard_oracle_program_id: Pubkey, + /// Outflow rate limiter denominated in dollars + pub rate_limiter: RateLimiter, + /// whitelisted liquidator + pub whitelisted_liquidator: Option, + /// risk authority (additional pubkey used for setting params) + pub risk_authority: Pubkey, +} + +impl LendingMarket { + /// Create a new lending market + pub fn new(params: InitLendingMarketParams) -> Self { + let mut lending_market = Self::default(); + Self::init(&mut lending_market, params); + lending_market + } + + /// Initialize a lending market + pub fn init(&mut self, params: InitLendingMarketParams) { + self.version = PROGRAM_VERSION; + self.bump_seed = params.bump_seed; + self.owner = params.owner; + self.quote_currency = params.quote_currency; + self.token_program_id = params.token_program_id; + self.oracle_program_id = params.oracle_program_id; + self.switchboard_oracle_program_id = params.switchboard_oracle_program_id; + self.rate_limiter = RateLimiter::default(); + self.whitelisted_liquidator = None; + self.risk_authority = params.owner; + } +} + +/// Initialize a lending market +pub struct InitLendingMarketParams { + /// Bump seed for derived authority address + pub bump_seed: u8, + /// Owner authority which can add new reserves + pub owner: Pubkey, + /// Currency market prices are quoted in + /// e.g. "USD" null padded (`*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"`) or a SPL token mint pubkey + pub quote_currency: [u8; 32], + /// Token program id + pub token_program_id: Pubkey, + /// Oracle (Pyth) program id + pub oracle_program_id: Pubkey, + /// Oracle (Switchboard) program id + pub switchboard_oracle_program_id: Pubkey, +} + +impl Sealed for LendingMarket {} +impl IsInitialized for LendingMarket { + fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +const LENDING_MARKET_LEN: usize = 290; // 1 + 1 + 32 + 32 + 32 + 32 + 32 + 56 + 32 + 40 +impl Pack for LendingMarket { + const LEN: usize = LENDING_MARKET_LEN; + + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, LENDING_MARKET_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + bump_seed, + owner, + quote_currency, + token_program_id, + oracle_program_id, + switchboard_oracle_program_id, + rate_limiter, + whitelisted_liquidator, + risk_authority, + _padding, + ) = mut_array_refs![ + output, + 1, + 1, + PUBKEY_BYTES, + 32, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + RATE_LIMITER_LEN, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8 + ]; + + *version = self.version.to_le_bytes(); + *bump_seed = self.bump_seed.to_le_bytes(); + owner.copy_from_slice(self.owner.as_ref()); + quote_currency.copy_from_slice(self.quote_currency.as_ref()); + token_program_id.copy_from_slice(self.token_program_id.as_ref()); + oracle_program_id.copy_from_slice(self.oracle_program_id.as_ref()); + switchboard_oracle_program_id.copy_from_slice(self.switchboard_oracle_program_id.as_ref()); + self.rate_limiter.pack_into_slice(rate_limiter); + match self.whitelisted_liquidator { + Some(pubkey) => { + whitelisted_liquidator.copy_from_slice(pubkey.as_ref()); + } + None => { + whitelisted_liquidator.copy_from_slice(&[0u8; 32]); + } + } + risk_authority.copy_from_slice(self.risk_authority.as_ref()); + } + + /// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html) + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, LENDING_MARKET_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + bump_seed, + owner, + quote_currency, + token_program_id, + oracle_program_id, + switchboard_oracle_program_id, + rate_limiter, + whitelisted_liquidator, + risk_authority, + _padding, + ) = array_refs![ + input, + 1, + 1, + PUBKEY_BYTES, + 32, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + RATE_LIMITER_LEN, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8 + ]; + + let version = u8::from_le_bytes(*version); + if version > PROGRAM_VERSION { + msg!("Lending market version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + + let owner_pubkey = Pubkey::new_from_array(*owner); + Ok(Self { + version, + bump_seed: u8::from_le_bytes(*bump_seed), + owner: owner_pubkey, + quote_currency: *quote_currency, + token_program_id: Pubkey::new_from_array(*token_program_id), + oracle_program_id: Pubkey::new_from_array(*oracle_program_id), + switchboard_oracle_program_id: Pubkey::new_from_array(*switchboard_oracle_program_id), + rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, + whitelisted_liquidator: if whitelisted_liquidator == &[0u8; 32] { + None + } else { + Some(Pubkey::new_from_array(*whitelisted_liquidator)) + }, + // the risk authority can equal [0; 32] when the program is upgraded to v2.0.2. in that + // case, we set the risk authority to be the owner. This isn't strictly necessary, but + // better to be safe i guess. + risk_authority: if *risk_authority == [0; 32] { + owner_pubkey + } else { + Pubkey::new_from_array(*risk_authority) + }, + }) + } +} + diff --git a/src/vendor/solend/state/lending_market_metadata.rs b/src/vendor/solend/state/lending_market_metadata.rs new file mode 100644 index 0000000..4d14ee0 --- /dev/null +++ b/src/vendor/solend/state/lending_market_metadata.rs @@ -0,0 +1,64 @@ +use super::*; + +use crate::vendor::solend::error::LendingError; +use bytemuck::checked::try_from_bytes; +use bytemuck::{Pod, Zeroable}; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use static_assertions::{assert_eq_size, const_assert}; + +/// market name size +pub const MARKET_NAME_SIZE: usize = 50; + +/// market description size +pub const MARKET_DESCRIPTION_SIZE: usize = 300; + +/// market image url size +pub const MARKET_IMAGE_URL_SIZE: usize = 250; + +/// padding size +pub const PADDING_SIZE: usize = 100; + +/// Lending market state +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct LendingMarketMetadata { + /// Bump seed + pub bump_seed: u8, + /// Market name null padded + pub market_name: [u8; MARKET_NAME_SIZE], + /// Market description null padded + pub market_description: [u8; MARKET_DESCRIPTION_SIZE], + /// Market image url + pub market_image_url: [u8; MARKET_IMAGE_URL_SIZE], + /// Lookup Tables + pub lookup_tables: [Pubkey; 4], + /// Padding + pub padding: [u8; PADDING_SIZE], +} + +impl LendingMarketMetadata { + /// Create a LendingMarketMetadata referernce from a slice + pub fn new_from_bytes(data: &[u8]) -> Result<&LendingMarketMetadata, ProgramError> { + try_from_bytes::(&data[1..]).map_err(|_| { + msg!("Failed to deserialize LendingMarketMetadata"); + LendingError::InstructionUnpackError.into() + }) + } +} + +unsafe impl Zeroable for LendingMarketMetadata {} +unsafe impl Pod for LendingMarketMetadata {} + +assert_eq_size!( + LendingMarketMetadata, + [u8; MARKET_NAME_SIZE + + MARKET_DESCRIPTION_SIZE + + MARKET_IMAGE_URL_SIZE + + 4 * 32 + + PADDING_SIZE + + 1], +); + +// transaction size limit check +const_assert!(std::mem::size_of::() <= 850); diff --git a/src/vendor/solend/state/mod.rs b/src/vendor/solend/state/mod.rs new file mode 100644 index 0000000..38d07a9 --- /dev/null +++ b/src/vendor/solend/state/mod.rs @@ -0,0 +1,61 @@ +//! State types + +mod last_update; +mod lending_market; +mod lending_market_metadata; +mod obligation; +mod rate_limiter; +mod reserve; + +pub use last_update::*; +pub use lending_market::*; +pub use lending_market_metadata::*; +pub use obligation::*; +pub use rate_limiter::*; +pub use reserve::*; + +use crate::vendor::solend::math::{Decimal, WAD}; +use solana_program::{msg, program_error::ProgramError}; + +/// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) +// @FIXME: restore to 5 +pub const INITIAL_COLLATERAL_RATIO: u64 = 1; +const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; + +/// Current version of the program and all new accounts created +pub const PROGRAM_VERSION: u8 = 1; + +/// Accounts are created with data zeroed out, so uninitialized state instances +/// will have the version set to 0. +pub const UNINITIALIZED_VERSION: u8 = 0; + +/// Number of slots per year +// 2 (slots per second) * 60 * 60 * 24 * 365 = 63072000 +pub const SLOTS_PER_YEAR: u64 = 63072000; + +// Helpers +fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { + *dst = decimal + .to_scaled_val() + .expect("Decimal cannot be packed") + .to_le_bytes(); +} + +fn unpack_decimal(src: &[u8; 16]) -> Decimal { + Decimal::from_scaled_val(u128::from_le_bytes(*src)) +} + +fn pack_bool(boolean: bool, dst: &mut [u8; 1]) { + *dst = (boolean as u8).to_le_bytes() +} + +fn unpack_bool(src: &[u8; 1]) -> Result { + match u8::from_le_bytes(*src) { + 0 => Ok(false), + 1 => Ok(true), + _ => { + msg!("Boolean cannot be unpacked"); + Err(ProgramError::InvalidAccountData) + } + } +} diff --git a/src/vendor/solend/state/obligation.rs b/src/vendor/solend/state/obligation.rs new file mode 100644 index 0000000..3ecdf95 --- /dev/null +++ b/src/vendor/solend/state/obligation.rs @@ -0,0 +1,617 @@ +use super::*; +use crate::vendor::solend::{ + error::LendingError, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::{ + clock::Slot, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_pack::{IsInitialized, Pack, Sealed}, + pubkey::{Pubkey, PUBKEY_BYTES}, +}; +use std::{ + cmp::{min, Ordering}, + convert::{TryFrom, TryInto}, +}; + +/// Max number of collateral and liquidity reserve accounts combined for an obligation +pub const MAX_OBLIGATION_RESERVES: usize = 10; + +/// Lending market obligation state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Obligation { + /// Version of the struct + pub version: u8, + /// Last update to collateral, liquidity, or their market values + pub last_update: LastUpdate, + /// Lending market address + pub lending_market: Pubkey, + /// Owner authority which can borrow liquidity + pub owner: Pubkey, + /// Deposited collateral for the obligation, unique by deposit reserve address + pub deposits: Vec, + /// Borrowed liquidity for the obligation, unique by borrow reserve address + pub borrows: Vec, + /// Market value of deposits + pub deposited_value: Decimal, + /// Risk-adjusted market value of borrows. + /// ie sum(b.borrowed_amount * b.current_spot_price * b.borrow_weight for b in borrows) + pub borrowed_value: Decimal, + /// Risk-adjusted upper bound market value of borrows. + /// ie sum(b.borrowed_amount * max(b.current_spot_price, b.smoothed_price) * b.borrow_weight for b in borrows) + pub borrowed_value_upper_bound: Decimal, + /// The maximum open borrow value. + /// ie sum(d.deposited_amount * d.ltv * min(d.current_spot_price, d.smoothed_price) for d in deposits) + /// if borrowed_value_upper_bound >= allowed_borrow_value, then the obligation is unhealthy and + /// borrows and withdraws are disabled. + pub allowed_borrow_value: Decimal, + /// The dangerous borrow value at the weighted average liquidation threshold. + /// ie sum(d.deposited_amount * d.liquidation_threshold * d.current_spot_price for d in deposits) + /// if borrowed_value >= unhealthy_borrow_value, the obligation can be liquidated + pub unhealthy_borrow_value: Decimal, + /// ie sum(d.deposited_amount * d.max_liquidation_threshold * d.current_spot_price for d in + /// deposits). This field is used to calculate the liquidator bonus. + /// An obligation with a borrowed value >= super_unhealthy_borrow_value is eligible for the max + /// bonus + pub super_unhealthy_borrow_value: Decimal, + /// True if the obligation is currently borrowing an isolated tier asset + pub borrowing_isolated_asset: bool, +} + +impl Obligation { + /// Create a new obligation + pub fn new(params: InitObligationParams) -> Self { + let mut obligation = Self::default(); + Self::init(&mut obligation, params); + obligation + } + + /// Initialize an obligation + pub fn init(&mut self, params: InitObligationParams) { + self.version = PROGRAM_VERSION; + self.last_update = LastUpdate::new(params.current_slot); + self.lending_market = params.lending_market; + self.owner = params.owner; + self.deposits = params.deposits; + self.borrows = params.borrows; + } + + /// Calculate the current ratio of borrowed value to deposited value + pub fn loan_to_value(&self) -> Result { + self.borrowed_value.try_div(self.deposited_value) + } + + /// Repay liquidity and remove it from borrows if zeroed out + pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult { + let liquidity = &mut self.borrows[liquidity_index]; + if settle_amount == liquidity.borrowed_amount_wads { + self.borrows.remove(liquidity_index); + } else { + liquidity.repay(settle_amount)?; + } + Ok(()) + } + + /// Withdraw collateral and remove it from deposits if zeroed out + pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult { + let collateral = &mut self.deposits[collateral_index]; + if withdraw_amount == collateral.deposited_amount { + self.deposits.remove(collateral_index); + } else { + collateral.withdraw(withdraw_amount)?; + } + Ok(()) + } + + /// calculate the maximum amount of collateral that can be borrowed + pub fn max_withdraw_amount( + &self, + collateral: &ObligationCollateral, + withdraw_reserve: &Reserve, + ) -> Result { + if self.borrows.is_empty() { + return Ok(collateral.deposited_amount); + } + + if self.allowed_borrow_value <= self.borrowed_value_upper_bound { + return Ok(0); + } + + let loan_to_value_ratio = withdraw_reserve.loan_to_value_ratio(); + if loan_to_value_ratio == Rate::zero() { + return Ok(collateral.deposited_amount); + } + + // max usd value that can be withdrawn + let max_withdraw_value = self + .allowed_borrow_value + .try_sub(self.borrowed_value_upper_bound)? + .try_div(loan_to_value_ratio)?; + + // convert max_withdraw_value to max withdraw liquidity amount + + // why is min used and not max? seems scary + // + // the tldr is that allowed borrow value is calculated with the minimum + // of the spot price and the smoothed price, so we have to use the min here to be + // consistent. + // + // note that safety-wise, it doesn't actually matter. if we used the max (which appears safer), + // the initial max withdraw would be lower, but the user can immediately make another max withdraw call + // because allowed_borrow_value is still greater than borrowed_value_upper_bound + // after a large amount of consecutive max withdraw calls, the end state of using max would be the same + // as using min. + // + // therefore, we use min for the better UX. + let price = min( + withdraw_reserve.liquidity.market_price, + withdraw_reserve.liquidity.smoothed_market_price, + ); + + let decimals = 10u64 + .checked_pow(withdraw_reserve.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + + let max_withdraw_liquidity_amount = max_withdraw_value.try_mul(decimals)?.try_div(price)?; + + // convert max withdraw liquidity amount to max withdraw collateral amount + Ok(min( + withdraw_reserve + .collateral_exchange_rate()? + .decimal_liquidity_to_collateral(max_withdraw_liquidity_amount)? + .try_floor_u64()?, + collateral.deposited_amount, + )) + } + + /// Calculate the maximum liquidity value that can be borrowed + pub fn remaining_borrow_value(&self) -> Result { + self.allowed_borrow_value + .try_sub(self.borrowed_value_upper_bound) + } + + /// Calculate the maximum liquidation amount for a given liquidity + pub fn max_liquidation_amount( + &self, + liquidity: &ObligationLiquidity, + ) -> Result { + let max_liquidation_value = self + .borrowed_value + .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))? + .min(liquidity.market_value) + .min(Decimal::from(MAX_LIQUIDATABLE_VALUE_AT_ONCE)); + + let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?; + liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct) + } + + /// Find collateral by deposit reserve + pub fn find_collateral_in_deposits( + &self, + deposit_reserve: Pubkey, + ) -> Result<(&ObligationCollateral, usize), ProgramError> { + if self.deposits.is_empty() { + msg!("Obligation has no deposits"); + return Err(LendingError::ObligationDepositsEmpty.into()); + } + let collateral_index = self + ._find_collateral_index_in_deposits(deposit_reserve) + .ok_or(LendingError::InvalidObligationCollateral)?; + Ok((&self.deposits[collateral_index], collateral_index)) + } + + /// Find or add collateral by deposit reserve + pub fn find_or_add_collateral_to_deposits( + &mut self, + deposit_reserve: Pubkey, + ) -> Result<&mut ObligationCollateral, ProgramError> { + if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) { + return Ok(&mut self.deposits[collateral_index]); + } + if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { + msg!( + "Obligation cannot have more than {} deposits and borrows combined", + MAX_OBLIGATION_RESERVES + ); + return Err(LendingError::ObligationReserveLimit.into()); + } + let collateral = ObligationCollateral::new(deposit_reserve); + self.deposits.push(collateral); + Ok(self.deposits.last_mut().unwrap()) + } + + fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option { + self.deposits + .iter() + .position(|collateral| collateral.deposit_reserve == deposit_reserve) + } + + /// Find liquidity by borrow reserve + pub fn find_liquidity_in_borrows( + &self, + borrow_reserve: Pubkey, + ) -> Result<(&ObligationLiquidity, usize), ProgramError> { + if self.borrows.is_empty() { + msg!("Obligation has no borrows"); + return Err(LendingError::ObligationBorrowsEmpty.into()); + } + let liquidity_index = self + ._find_liquidity_index_in_borrows(borrow_reserve) + .ok_or(LendingError::InvalidObligationLiquidity)?; + Ok((&self.borrows[liquidity_index], liquidity_index)) + } + + /// Find liquidity by borrow reserve mut + pub fn find_liquidity_in_borrows_mut( + &mut self, + borrow_reserve: Pubkey, + ) -> Result<(&mut ObligationLiquidity, usize), ProgramError> { + if self.borrows.is_empty() { + msg!("Obligation has no borrows"); + return Err(LendingError::ObligationBorrowsEmpty.into()); + } + let liquidity_index = self + ._find_liquidity_index_in_borrows(borrow_reserve) + .ok_or(LendingError::InvalidObligationLiquidity)?; + Ok((&mut self.borrows[liquidity_index], liquidity_index)) + } + + /// Find or add liquidity by borrow reserve + pub fn find_or_add_liquidity_to_borrows( + &mut self, + borrow_reserve: Pubkey, + cumulative_borrow_rate_wads: Decimal, + ) -> Result<&mut ObligationLiquidity, ProgramError> { + if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) { + return Ok(&mut self.borrows[liquidity_index]); + } + if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES { + msg!( + "Obligation cannot have more than {} deposits and borrows combined", + MAX_OBLIGATION_RESERVES + ); + return Err(LendingError::ObligationReserveLimit.into()); + } + let liquidity = ObligationLiquidity::new(borrow_reserve, cumulative_borrow_rate_wads); + self.borrows.push(liquidity); + Ok(self.borrows.last_mut().unwrap()) + } + + fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option { + self.borrows + .iter() + .position(|liquidity| liquidity.borrow_reserve == borrow_reserve) + } +} + +/// Initialize an obligation +pub struct InitObligationParams { + /// Last update to collateral, liquidity, or their market values + pub current_slot: Slot, + /// Lending market address + pub lending_market: Pubkey, + /// Owner authority which can borrow liquidity + pub owner: Pubkey, + /// Deposited collateral for the obligation, unique by deposit reserve address + pub deposits: Vec, + /// Borrowed liquidity for the obligation, unique by borrow reserve address + pub borrows: Vec, +} + +impl Sealed for Obligation {} +impl IsInitialized for Obligation { + fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +/// Obligation collateral state +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ObligationCollateral { + /// Reserve collateral is deposited to + pub deposit_reserve: Pubkey, + /// Amount of collateral deposited + pub deposited_amount: u64, + /// Collateral market value in quote currency + pub market_value: Decimal, +} + +impl ObligationCollateral { + /// Create new obligation collateral + pub fn new(deposit_reserve: Pubkey) -> Self { + Self { + deposit_reserve, + deposited_amount: 0, + market_value: Decimal::zero(), + } + } + + /// Increase deposited collateral + pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult { + self.deposited_amount = self + .deposited_amount + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } + + /// Decrease deposited collateral + pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult { + self.deposited_amount = self + .deposited_amount + .checked_sub(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } +} + +/// Obligation liquidity state +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ObligationLiquidity { + /// Reserve liquidity is borrowed from + pub borrow_reserve: Pubkey, + /// Borrow rate used for calculating interest + pub cumulative_borrow_rate_wads: Decimal, + /// Amount of liquidity borrowed plus interest + pub borrowed_amount_wads: Decimal, + /// Liquidity market value in quote currency + pub market_value: Decimal, +} + +impl ObligationLiquidity { + /// Create new obligation liquidity + pub fn new(borrow_reserve: Pubkey, cumulative_borrow_rate_wads: Decimal) -> Self { + Self { + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_amount_wads: Decimal::zero(), + market_value: Decimal::zero(), + } + } + + /// Decrease borrowed liquidity + pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult { + self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?; + Ok(()) + } + + /// Increase borrowed liquidity + pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult { + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?; + Ok(()) + } + + /// Accrue interest + pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult { + match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) { + Ordering::Less => { + msg!("Interest rate cannot be negative"); + return Err(LendingError::NegativeInterestRate.into()); + } + Ordering::Equal => {} + Ordering::Greater => { + let compounded_interest_rate: Rate = cumulative_borrow_rate_wads + .try_div(self.cumulative_borrow_rate_wads)? + .try_into()?; + + self.borrowed_amount_wads = self + .borrowed_amount_wads + .try_mul(compounded_interest_rate)?; + self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads; + } + } + + Ok(()) + } +} + +const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 8 + 16 + 32 +const OBLIGATION_LIQUIDITY_LEN: usize = 112; // 32 + 16 + 16 + 16 + 32 +const OBLIGATION_LEN: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 64 + 1 + 1 + (88 * 1) + (112 * 9) + // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca +impl Pack for Obligation { + const LEN: usize = OBLIGATION_LEN; + + fn pack_into_slice(&self, dst: &mut [u8]) { + let output = array_mut_ref![dst, 0, OBLIGATION_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + last_update_slot, + last_update_stale, + lending_market, + owner, + deposited_value, + borrowed_value, + allowed_borrow_value, + unhealthy_borrow_value, + borrowed_value_upper_bound, + borrowing_isolated_asset, + super_unhealthy_borrow_value, + _padding, + deposits_len, + borrows_len, + data_flat, + ) = mut_array_refs![ + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 16, + 16, + 16, + 16, + 16, + 1, + 16, + 31, + 1, + 1, + OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) + ]; + + // obligation + *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); + pack_bool(self.last_update.stale, last_update_stale); + lending_market.copy_from_slice(self.lending_market.as_ref()); + owner.copy_from_slice(self.owner.as_ref()); + pack_decimal(self.deposited_value, deposited_value); + pack_decimal(self.borrowed_value, borrowed_value); + pack_decimal(self.borrowed_value_upper_bound, borrowed_value_upper_bound); + pack_decimal(self.allowed_borrow_value, allowed_borrow_value); + pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value); + pack_bool(self.borrowing_isolated_asset, borrowing_isolated_asset); + pack_decimal( + self.super_unhealthy_borrow_value, + super_unhealthy_borrow_value, + ); + + *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); + *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); + + let mut offset = 0; + + // deposits + for collateral in &self.deposits { + let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (deposit_reserve, deposited_amount, market_value, _padding_deposit) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32]; + deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); + *deposited_amount = collateral.deposited_amount.to_le_bytes(); + pack_decimal(collateral.market_value, market_value); + offset += OBLIGATION_COLLATERAL_LEN; + } + + // borrows + for liquidity in &self.borrows { + let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_amount_wads, + market_value, + _padding_borrow, + ) = mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16, 32]; + borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref()); + pack_decimal( + liquidity.cumulative_borrow_rate_wads, + cumulative_borrow_rate_wads, + ); + pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads); + pack_decimal(liquidity.market_value, market_value); + offset += OBLIGATION_LIQUIDITY_LEN; + } + } + + /// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html). + fn unpack_from_slice(src: &[u8]) -> Result { + let input = array_ref![src, 0, OBLIGATION_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + last_update_slot, + last_update_stale, + lending_market, + owner, + deposited_value, + borrowed_value, + allowed_borrow_value, + unhealthy_borrow_value, + borrowed_value_upper_bound, + borrowing_isolated_asset, + super_unhealthy_borrow_value, + _padding, + deposits_len, + borrows_len, + data_flat, + ) = array_refs![ + input, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 16, + 16, + 16, + 16, + 16, + 1, + 16, + 31, + 1, + 1, + OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) + ]; + + let version = u8::from_le_bytes(*version); + if version > PROGRAM_VERSION { + msg!("Obligation version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + + let deposits_len = u8::from_le_bytes(*deposits_len); + let borrows_len = u8::from_le_bytes(*borrows_len); + let mut deposits = Vec::with_capacity(deposits_len as usize + 1); + let mut borrows = Vec::with_capacity(borrows_len as usize + 1); + + let mut offset = 0; + for _ in 0..deposits_len { + let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let (deposit_reserve, deposited_amount, market_value, _padding_deposit) = + array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32]; + deposits.push(ObligationCollateral { + deposit_reserve: Pubkey::try_from(*deposit_reserve).unwrap(), + deposited_amount: u64::from_le_bytes(*deposited_amount), + market_value: unpack_decimal(market_value), + }); + offset += OBLIGATION_COLLATERAL_LEN; + } + for _ in 0..borrows_len { + let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + borrow_reserve, + cumulative_borrow_rate_wads, + borrowed_amount_wads, + market_value, + _padding_borrow, + ) = array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16, 32]; + borrows.push(ObligationLiquidity { + borrow_reserve: Pubkey::try_from(*borrow_reserve).unwrap(), + cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads), + borrowed_amount_wads: unpack_decimal(borrowed_amount_wads), + market_value: unpack_decimal(market_value), + }); + offset += OBLIGATION_LIQUIDITY_LEN; + } + + Ok(Self { + version, + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + stale: unpack_bool(last_update_stale)?, + }, + lending_market: Pubkey::new_from_array(*lending_market), + owner: Pubkey::new_from_array(*owner), + deposits, + borrows, + deposited_value: unpack_decimal(deposited_value), + borrowed_value: unpack_decimal(borrowed_value), + borrowed_value_upper_bound: unpack_decimal(borrowed_value_upper_bound), + allowed_borrow_value: unpack_decimal(allowed_borrow_value), + unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), + super_unhealthy_borrow_value: unpack_decimal(super_unhealthy_borrow_value), + borrowing_isolated_asset: unpack_bool(borrowing_isolated_asset)?, + }) + } +} diff --git a/src/vendor/solend/state/rate_limiter.rs b/src/vendor/solend/state/rate_limiter.rs new file mode 100644 index 0000000..9a8153b --- /dev/null +++ b/src/vendor/solend/state/rate_limiter.rs @@ -0,0 +1,225 @@ +use crate::vendor::solend::state::{pack_decimal, unpack_decimal}; +use solana_program::msg; +use solana_program::program_pack::IsInitialized; +use solana_program::{program_error::ProgramError, slot_history::Slot}; + +use crate::vendor::solend::{ + error::LendingError, + math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use solana_program::program_pack::{Pack, Sealed}; + +/// Sliding Window Rate limiter +/// guarantee: at any point, the outflow between [cur_slot - slot.window_duration, cur_slot] +/// is less than 2x max_outflow. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RateLimiter { + /// configuration parameters + pub config: RateLimiterConfig, + + // state + /// prev qty is the sum of all outflows from [window_start - config.window_duration, window_start) + prev_qty: Decimal, + /// window_start is the start of the current window + window_start: Slot, + /// cur qty is the sum of all outflows from [window_start, window_start + config.window_duration) + cur_qty: Decimal, +} + +/// Lending market configuration parameters +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RateLimiterConfig { + /// Rate limiter window size in slots + pub window_duration: u64, + /// Rate limiter param. Max outflow of tokens in a window + pub max_outflow: u64, +} + +impl RateLimiter { + /// initialize rate limiter + pub fn new(config: RateLimiterConfig, cur_slot: u64) -> Self { + let slot_start = if config.window_duration != 0 { + cur_slot / config.window_duration * config.window_duration + } else { + cur_slot + }; + + Self { + config, + prev_qty: Decimal::zero(), + window_start: slot_start, + cur_qty: Decimal::zero(), + } + } + + fn _update(&mut self, cur_slot: u64) -> Result<(), ProgramError> { + if cur_slot < self.window_start { + msg!("Current slot is less than window start, which is impossible"); + return Err(LendingError::InvalidAccountInput.into()); + } + + // floor wrt window duration + let cur_slot_start = cur_slot / self.config.window_duration * self.config.window_duration; + + // update prev window, current window + match cur_slot_start.cmp(&(self.window_start + self.config.window_duration)) { + // |<-prev window->|<-cur window (cur_slot is in here)->| + std::cmp::Ordering::Less => (), + + // |<-prev window->|<-cur window->| (cur_slot is in here) | + std::cmp::Ordering::Equal => { + self.prev_qty = self.cur_qty; + self.window_start = cur_slot_start; + self.cur_qty = Decimal::zero(); + } + + // |<-prev window->|<-cur window->|<-cur window + 1->| ... | (cur_slot is in here) | + std::cmp::Ordering::Greater => { + self.prev_qty = Decimal::zero(); + self.window_start = cur_slot_start; + self.cur_qty = Decimal::zero(); + } + }; + + Ok(()) + } + + /// Calculate current outflow. Must only be called after ._update()! + fn current_outflow(&self, cur_slot: u64) -> Result { + if self.config.window_duration == 0 { + msg!("Window duration cannot be 0"); + return Err(LendingError::InvalidAccountInput.into()); + } + + // assume the prev_window's outflow is even distributed across the window + // this isn't true, but it's a good enough approximation + let prev_weight = Decimal::from(self.config.window_duration) + .try_sub(Decimal::from(cur_slot - self.window_start + 1))? + .try_div(self.config.window_duration)?; + + prev_weight.try_mul(self.prev_qty)?.try_add(self.cur_qty) + } + + /// Calculate remaining outflow for the current window + pub fn remaining_outflow(&mut self, cur_slot: u64) -> Result { + // rate limiter is disabled if window duration == 0. this is here because we don't want to + // brick borrows/withdraws in permissionless pools on program upgrade. + if self.config.window_duration == 0 { + return Ok(Decimal::from(u64::MAX)); + } + + self._update(cur_slot)?; + + let cur_outflow = self.current_outflow(cur_slot)?; + if cur_outflow > Decimal::from(self.config.max_outflow) { + return Ok(Decimal::zero()); + } + + let diff = Decimal::from(self.config.max_outflow).try_sub(cur_outflow)?; + Ok(diff) + } + + /// update rate limiter with new quantity. errors if rate limit has been reached + pub fn update(&mut self, cur_slot: u64, qty: Decimal) -> Result<(), ProgramError> { + // rate limiter is disabled if window duration == 0. this is here because we don't want to + // brick borrows/withdraws in permissionless pools on program upgrade. + if self.config.window_duration == 0 { + return Ok(()); + } + + self._update(cur_slot)?; + + let cur_outflow = self.current_outflow(cur_slot)?; + if cur_outflow.try_add(qty)? > Decimal::from(self.config.max_outflow) { + Err(LendingError::OutflowRateLimitExceeded.into()) + } else { + self.cur_qty = self.cur_qty.try_add(qty)?; + Ok(()) + } + } +} + +impl Default for RateLimiter { + fn default() -> Self { + Self::new( + RateLimiterConfig { + window_duration: 1, + max_outflow: u64::MAX, + }, + 1, + ) + } +} + +impl Sealed for RateLimiter {} + +impl IsInitialized for RateLimiter { + fn is_initialized(&self) -> bool { + true + } +} + +/// Size of RateLimiter when packed into account +pub const RATE_LIMITER_LEN: usize = 56; +impl Pack for RateLimiter { + const LEN: usize = RATE_LIMITER_LEN; + + fn pack_into_slice(&self, dst: &mut [u8]) { + let dst = array_mut_ref![dst, 0, RATE_LIMITER_LEN]; + let ( + config_max_outflow_dst, + config_window_duration_dst, + prev_qty_dst, + window_start_dst, + cur_qty_dst, + ) = mut_array_refs![dst, 8, 8, 16, 8, 16]; + *config_max_outflow_dst = self.config.max_outflow.to_le_bytes(); + *config_window_duration_dst = self.config.window_duration.to_le_bytes(); + pack_decimal(self.prev_qty, prev_qty_dst); + *window_start_dst = self.window_start.to_le_bytes(); + pack_decimal(self.cur_qty, cur_qty_dst); + } + + fn unpack_from_slice(src: &[u8]) -> Result { + let src = array_ref![src, 0, RATE_LIMITER_LEN]; + let ( + config_max_outflow_src, + config_window_duration_src, + prev_qty_src, + window_start_src, + cur_qty_src, + ) = array_refs![src, 8, 8, 16, 8, 16]; + + Ok(Self { + config: RateLimiterConfig { + max_outflow: u64::from_le_bytes(*config_max_outflow_src), + window_duration: u64::from_le_bytes(*config_window_duration_src), + }, + prev_qty: unpack_decimal(prev_qty_src), + window_start: u64::from_le_bytes(*window_start_src), + cur_qty: unpack_decimal(cur_qty_src), + }) + } +} + +#[cfg(test)] +pub fn rand_rate_limiter() -> RateLimiter { + use rand::Rng; + let mut rng = rand::thread_rng(); + + fn rand_decimal() -> Decimal { + Decimal::from_scaled_val(rand::thread_rng().gen()) + } + + RateLimiter { + config: RateLimiterConfig { + window_duration: rng.gen(), + max_outflow: rng.gen(), + }, + prev_qty: rand_decimal(), + window_start: rng.gen(), + cur_qty: rand_decimal(), + } +} diff --git a/src/vendor/solend/state/reserve.rs b/src/vendor/solend/state/reserve.rs new file mode 100644 index 0000000..af80ccc --- /dev/null +++ b/src/vendor/solend/state/reserve.rs @@ -0,0 +1,1469 @@ +use super::*; +use crate::vendor::solend::{ + error::LendingError, + math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub}, +}; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use solana_program::{ + clock::Slot, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_pack::{IsInitialized, Pack, Sealed}, + pubkey::{Pubkey, PUBKEY_BYTES}, +}; +use std::str::FromStr; +use std::{ + cmp::{max, min, Ordering}, + convert::{TryFrom, TryInto}, +}; + +/// Percentage of an obligation that can be repaid during each liquidation call +pub const LIQUIDATION_CLOSE_FACTOR: u8 = 20; + +/// Obligation borrow amount that is small enough to close out +pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2; + +/// Maximum quote currency value that can be liquidated in 1 liquidate_obligation call +pub const MAX_LIQUIDATABLE_VALUE_AT_ONCE: u64 = 500_000; + +/// Maximum bonus received during liquidation. includes protocol fee. +pub const MAX_BONUS_PCT: u8 = 25; + +/// Maximum protocol liquidation fee in deca bps (1 deca bp = 10 bps) +pub const MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS: u8 = 50; + +/// Lending market reserve state +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Reserve { + /// Version of the struct + pub version: u8, + /// Last slot when supply and rates updated + pub last_update: LastUpdate, + /// Lending market address + pub lending_market: Pubkey, + /// Reserve liquidity + pub liquidity: ReserveLiquidity, + /// Reserve collateral + pub collateral: ReserveCollateral, + /// Reserve configuration values + pub config: ReserveConfig, + /// Outflow Rate Limiter (denominated in tokens) + pub rate_limiter: RateLimiter, +} + +impl Reserve { + /// Create a new reserve + pub fn new(params: InitReserveParams) -> Self { + let mut reserve = Self::default(); + Self::init(&mut reserve, params); + reserve + } + + /// Initialize a reserve + pub fn init(&mut self, params: InitReserveParams) { + self.version = PROGRAM_VERSION; + self.last_update = LastUpdate::new(params.current_slot); + self.lending_market = params.lending_market; + self.liquidity = params.liquidity; + self.collateral = params.collateral; + self.config = params.config; + self.rate_limiter = RateLimiter::new(params.rate_limiter_config, params.current_slot); + } + + /// get borrow weight. Guaranteed to be greater than 1 + pub fn borrow_weight(&self) -> Decimal { + Decimal::one() + .try_add(Decimal::from_bps(self.config.added_borrow_weight_bps)) + .unwrap() + } + + /// get loan to value ratio as a Rate + pub fn loan_to_value_ratio(&self) -> Rate { + Rate::from_percent(self.config.loan_to_value_ratio) + } + + /// Convert USD to liquidity tokens. + /// eg how much SOL can you get for 100USD? + pub fn usd_to_liquidity_amount_lower_bound( + &self, + quote_amount: Decimal, + ) -> Result { + // quote amount / max(market price, smoothed price) * 10**decimals + quote_amount + .try_mul(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + ))? + .try_div(max( + self.liquidity.smoothed_market_price, + self.liquidity.market_price, + )) + } + + /// find current market value of tokens + pub fn market_value(&self, liquidity_amount: Decimal) -> Result { + self.liquidity + .market_price + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) + } + + /// find the current upper bound market value of tokens. + /// ie max(market_price, smoothed_market_price) * liquidity_amount + pub fn market_value_upper_bound( + &self, + liquidity_amount: Decimal, + ) -> Result { + let price_upper_bound = std::cmp::max( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ); + + price_upper_bound + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) + } + + /// find the current lower bound market value of tokens. + /// ie min(market_price, smoothed_market_price) * liquidity_amount + pub fn market_value_lower_bound( + &self, + liquidity_amount: Decimal, + ) -> Result { + let price_lower_bound = std::cmp::min( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ); + + price_lower_bound + .try_mul(liquidity_amount)? + .try_div(Decimal::from( + (10u128) + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?, + )) + } + + /// Record deposited liquidity and return amount of collateral tokens to mint + pub fn deposit_liquidity(&mut self, liquidity_amount: u64) -> Result { + let collateral_amount = self + .collateral_exchange_rate()? + .liquidity_to_collateral(liquidity_amount)?; + + self.liquidity.deposit(liquidity_amount)?; + self.collateral.mint(collateral_amount)?; + + Ok(collateral_amount) + } + + /// Record redeemed collateral and return amount of liquidity to withdraw + pub fn redeem_collateral(&mut self, collateral_amount: u64) -> Result { + let collateral_exchange_rate = self.collateral_exchange_rate()?; + let liquidity_amount = + collateral_exchange_rate.collateral_to_liquidity(collateral_amount)?; + + self.collateral.burn(collateral_amount)?; + self.liquidity.withdraw(liquidity_amount)?; + + Ok(liquidity_amount) + } + + /// Calculate the current borrow rate + pub fn current_borrow_rate(&self) -> Result { + let utilization_rate = self.liquidity.utilization_rate()?; + let optimal_utilization_rate = Rate::from_percent(self.config.optimal_utilization_rate); + let max_utilization_rate = Rate::from_percent(self.config.max_utilization_rate); + if utilization_rate <= optimal_utilization_rate { + let min_rate = Rate::from_percent(self.config.min_borrow_rate); + + if optimal_utilization_rate == Rate::zero() { + return Ok(min_rate); + } + + let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?; + let rate_range = Rate::from_percent( + self.config + .optimal_borrow_rate + .checked_sub(self.config.min_borrow_rate) + .ok_or(LendingError::MathOverflow)?, + ); + + Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?) + } else if utilization_rate <= max_utilization_rate { + let weight = utilization_rate + .try_sub(optimal_utilization_rate)? + .try_div(max_utilization_rate.try_sub(optimal_utilization_rate)?)?; + + let optimal_borrow_rate = Rate::from_percent(self.config.optimal_borrow_rate); + let max_borrow_rate = Rate::from_percent(self.config.max_borrow_rate); + let rate_range = max_borrow_rate.try_sub(optimal_borrow_rate)?; + + weight.try_mul(rate_range)?.try_add(optimal_borrow_rate) + } else { + let weight: Decimal = utilization_rate + .try_sub(max_utilization_rate)? + .try_div(Rate::from_percent( + 100u8 + .checked_sub(self.config.max_utilization_rate) + .ok_or(LendingError::MathOverflow)?, + ))? + .into(); + + let max_borrow_rate = Rate::from_percent(self.config.max_borrow_rate); + let super_max_borrow_rate = Rate::from_percent_u64(self.config.super_max_borrow_rate); + let rate_range: Decimal = super_max_borrow_rate.try_sub(max_borrow_rate)?.into(); + + // if done with just Rates, this computation can overflow. so we temporarily convert to Decimal + // and back to Rate + weight + .try_mul(rate_range)? + .try_add(max_borrow_rate.into())? + .try_into() + } + } + + /// Collateral exchange rate + pub fn collateral_exchange_rate(&self) -> Result { + let total_liquidity = self.liquidity.total_supply()?; + self.collateral.exchange_rate(total_liquidity) + } + + /// Update borrow rate and accrue interest + pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult { + let slots_elapsed = self.last_update.slots_elapsed(current_slot)?; + if slots_elapsed > 0 { + let current_borrow_rate = self.current_borrow_rate()?; + let take_rate = Rate::from_percent(self.config.protocol_take_rate); + self.liquidity + .compound_interest(current_borrow_rate, slots_elapsed, take_rate)?; + } + Ok(()) + } + + /// Borrow liquidity up to a maximum market value + pub fn calculate_borrow( + &self, + amount_to_borrow: u64, + max_borrow_value: Decimal, + remaining_reserve_borrow: Decimal, + ) -> Result { + // @TODO: add lookup table https://git.io/JOCYq + let decimals = 10u64 + .checked_pow(self.liquidity.mint_decimals as u32) + .ok_or(LendingError::MathOverflow)?; + if amount_to_borrow == u64::MAX { + let borrow_amount = max_borrow_value + .try_mul(decimals)? + .try_div(max( + self.liquidity.market_price, + self.liquidity.smoothed_market_price, + ))? + .try_div(self.borrow_weight())? + .min(remaining_reserve_borrow) + .min(self.liquidity.available_amount.into()); + let (borrow_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(borrow_amount, FeeCalculation::Inclusive)?; + let receive_amount = borrow_amount + .try_floor_u64()? + .checked_sub(borrow_fee) + .ok_or(LendingError::MathOverflow)?; + + Ok(CalculateBorrowResult { + borrow_amount, + receive_amount, + borrow_fee, + host_fee, + }) + } else { + let receive_amount = amount_to_borrow; + let borrow_amount = Decimal::from(receive_amount); + let (borrow_fee, host_fee) = self + .config + .fees + .calculate_borrow_fees(borrow_amount, FeeCalculation::Exclusive)?; + + let borrow_amount = borrow_amount.try_add(borrow_fee.into())?; + let borrow_value = self + .market_value_upper_bound(borrow_amount)? + .try_mul(self.borrow_weight())?; + if borrow_value > max_borrow_value { + msg!("Borrow value cannot exceed maximum borrow value"); + return Err(LendingError::BorrowTooLarge.into()); + } + + Ok(CalculateBorrowResult { + borrow_amount, + receive_amount, + borrow_fee, + host_fee, + }) + } + } + + /// Repay liquidity up to the borrowed amount + pub fn calculate_repay( + &self, + amount_to_repay: u64, + borrowed_amount: Decimal, + ) -> Result { + let settle_amount = if amount_to_repay == u64::MAX { + borrowed_amount + } else { + Decimal::from(amount_to_repay).min(borrowed_amount) + }; + let repay_amount = settle_amount.try_ceil_u64()?; + + Ok(CalculateRepayResult { + settle_amount, + repay_amount, + }) + } + + /// Calculate bonus as a percentage + /// the value will be in range [0, MAX_BONUS_PCT] + pub fn calculate_bonus(&self, obligation: &Obligation) -> Result { + if obligation.borrowed_value < obligation.unhealthy_borrow_value { + msg!("Obligation is healthy so a liquidation bonus can't be calculated"); + return Err(LendingError::ObligationHealthy.into()); + } + + let liquidation_bonus = Decimal::from_percent(self.config.liquidation_bonus); + let max_liquidation_bonus = Decimal::from_percent(self.config.max_liquidation_bonus); + let protocol_liquidation_fee = Decimal::from_deca_bps(self.config.protocol_liquidation_fee); + + // could also return the average of liquidation bonus and max liquidation bonus here, but + // i don't think it matters + if obligation.unhealthy_borrow_value == obligation.super_unhealthy_borrow_value { + return Ok(min( + liquidation_bonus.try_add(protocol_liquidation_fee)?, + Decimal::from_percent(MAX_BONUS_PCT), + )); + } + + // safety: + // - super_unhealthy_borrow value > unhealthy borrow value because we verify + // the ge condition in Reserve::unpack and then verify that they're not equal from check + // above + // - borrowed_value is >= unhealthy_borrow_value bc of the check above + // => weight is always between 0 and 1 + let weight = min( + obligation + .borrowed_value + .try_sub(obligation.unhealthy_borrow_value)? + .try_div( + obligation + .super_unhealthy_borrow_value + .try_sub(obligation.unhealthy_borrow_value)?, + ) + // the division above can potentially overflow if super_unhealthy_borrow_value and + // unhealthy_borrow_value are really close to each other. in that case, we want the + // weight to be one. + .unwrap_or_else(|_| Decimal::one()), + Decimal::one(), + ); + + let bonus = liquidation_bonus + .try_add(weight.try_mul(max_liquidation_bonus.try_sub(liquidation_bonus)?)?)? + .try_add(protocol_liquidation_fee)?; + + Ok(min(bonus, Decimal::from_percent(MAX_BONUS_PCT))) + } + + /// Liquidate some or all of an unhealthy obligation + pub fn calculate_liquidation( + &self, + amount_to_liquidate: u64, + obligation: &Obligation, + liquidity: &ObligationLiquidity, + collateral: &ObligationCollateral, + ) -> Result { + let bonus_rate = self.calculate_bonus(obligation)?.try_add(Decimal::one())?; + + let max_amount = if amount_to_liquidate == u64::MAX { + liquidity.borrowed_amount_wads + } else { + Decimal::from(amount_to_liquidate).min(liquidity.borrowed_amount_wads) + }; + + let settle_amount; + let repay_amount; + let withdraw_amount; + + // do a full liquidation if the market value of the borrow is less than one. + if liquidity.market_value <= Decimal::one() { + let liquidation_value = liquidity.market_value.try_mul(bonus_rate)?; + match liquidation_value.cmp(&collateral.market_value) { + Ordering::Greater => { + let repay_pct = collateral.market_value.try_div(liquidation_value)?; + settle_amount = liquidity.borrowed_amount_wads.try_mul(repay_pct)?; + repay_amount = settle_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Equal => { + settle_amount = liquidity.borrowed_amount_wads; + repay_amount = settle_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Less => { + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + + settle_amount = liquidity.borrowed_amount_wads; + repay_amount = settle_amount.try_ceil_u64()?; + if repay_amount == 0 { + msg!("repay amount is zero"); + return Err(LendingError::LiquidationTooSmall.into()); + } + + withdraw_amount = max( + Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_floor_u64()?, + // if withdraw_amount gets floored to zero and repay amount is non-zero, + // we set the withdraw_amount to 1. We do this so dust obligations get + // cleaned up. + // + // safety: technically this gives the liquidator more of a bonus, but this + // can happen at most once per ObligationLiquidity so I don't think this + // can be exploited to cause bad debt or anything. + 1, + ); + } + } + } else { + // partial liquidation + // calculate settle_amount and withdraw_amount, repay_amount is settle_amount rounded + let liquidation_amount = obligation + .max_liquidation_amount(liquidity)? + .min(max_amount); + let liquidation_pct = liquidation_amount.try_div(liquidity.borrowed_amount_wads)?; + let liquidation_value = liquidity + .market_value + .try_mul(liquidation_pct)? + .try_mul(bonus_rate)?; + + match liquidation_value.cmp(&collateral.market_value) { + Ordering::Greater => { + let repay_pct = collateral.market_value.try_div(liquidation_value)?; + settle_amount = liquidation_amount.try_mul(repay_pct)?; + repay_amount = settle_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Equal => { + settle_amount = liquidation_amount; + repay_amount = settle_amount.try_ceil_u64()?; + withdraw_amount = collateral.deposited_amount; + } + Ordering::Less => { + let withdraw_pct = liquidation_value.try_div(collateral.market_value)?; + settle_amount = liquidation_amount; + repay_amount = settle_amount.try_ceil_u64()?; + withdraw_amount = Decimal::from(collateral.deposited_amount) + .try_mul(withdraw_pct)? + .try_floor_u64()?; + } + } + } + + Ok(CalculateLiquidationResult { + settle_amount, + repay_amount, + withdraw_amount, + bonus_rate, + }) + } + + /// Calculate protocol cut of liquidation bonus always at least 1 lamport + /// the bonus rate is always >=1 and includes both liquidator bonus and protocol fee. + /// the bonus rate has to be passed into this function because bonus calculations are dynamic + /// and can't be recalculated after liquidation. + pub fn calculate_protocol_liquidation_fee( + &self, + amount_liquidated: u64, + bonus_rate: Decimal, + ) -> Result { + let amount_liquidated_wads = Decimal::from(amount_liquidated); + let nonbonus_amount = amount_liquidated_wads.try_div(bonus_rate)?; + // After deploying must update all reserves to set liquidation fee then redeploy with this line instead of hardcode + let protocol_fee = std::cmp::max( + nonbonus_amount + .try_mul(Decimal::from_deca_bps(self.config.protocol_liquidation_fee))? + .try_ceil_u64()?, + 1, + ); + Ok(protocol_fee) + } + + /// Calculate protocol fee redemption accounting for availible liquidity and accumulated fees + pub fn calculate_redeem_fees(&self) -> Result { + Ok(min( + self.liquidity.available_amount, + self.liquidity + .accumulated_protocol_fees_wads + .try_floor_u64()?, + )) + } +} + +/// Initialize a reserve +pub struct InitReserveParams { + /// Last slot when supply and rates updated + pub current_slot: Slot, + /// Lending market address + pub lending_market: Pubkey, + /// Reserve liquidity + pub liquidity: ReserveLiquidity, + /// Reserve collateral + pub collateral: ReserveCollateral, + /// Reserve configuration values + pub config: ReserveConfig, + /// rate limiter config + pub rate_limiter_config: RateLimiterConfig, +} + +/// Calculate borrow result +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CalculateBorrowResult { + /// Total amount of borrow including fees + pub borrow_amount: Decimal, + /// Borrow amount portion of total amount + pub receive_amount: u64, + /// Loan origination fee + pub borrow_fee: u64, + /// Host fee portion of origination fee + pub host_fee: u64, +} + +/// Calculate repay result +#[derive(Debug)] +pub struct CalculateRepayResult { + /// Amount of liquidity that is settled from the obligation. + pub settle_amount: Decimal, + /// Amount that will be repaid as u64 + pub repay_amount: u64, +} + +/// Calculate liquidation result +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CalculateLiquidationResult { + /// Amount of liquidity that is settled from the obligation. It includes + /// the amount of loan that was defaulted if collateral is depleted. + pub settle_amount: Decimal, + /// Amount that will be repaid as u64 + pub repay_amount: u64, + /// Amount of collateral to withdraw in exchange for repay amount + pub withdraw_amount: u64, + /// Liquidator bonus as a percentage, including the protocol fee + /// always greater than or equal to 1. + pub bonus_rate: Decimal, +} + +/// Reserve liquidity +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ReserveLiquidity { + /// Reserve liquidity mint address + pub mint_pubkey: Pubkey, + /// Reserve liquidity mint decimals + pub mint_decimals: u8, + /// Reserve liquidity supply address + pub supply_pubkey: Pubkey, + /// Reserve liquidity pyth oracle account + pub pyth_oracle_pubkey: Pubkey, + /// Reserve liquidity switchboard oracle account + pub switchboard_oracle_pubkey: Pubkey, + /// Reserve liquidity available + pub available_amount: u64, + /// Reserve liquidity borrowed + pub borrowed_amount_wads: Decimal, + /// Reserve liquidity cumulative borrow rate + pub cumulative_borrow_rate_wads: Decimal, + /// Reserve cumulative protocol fees + pub accumulated_protocol_fees_wads: Decimal, + /// Reserve liquidity market price in quote currency + pub market_price: Decimal, + /// Smoothed reserve liquidity market price for the liquidity (eg TWAP, VWAP, EMA) + pub smoothed_market_price: Decimal, +} + +impl ReserveLiquidity { + /// Create a new reserve liquidity + pub fn new(params: NewReserveLiquidityParams) -> Self { + Self { + mint_pubkey: params.mint_pubkey, + mint_decimals: params.mint_decimals, + supply_pubkey: params.supply_pubkey, + pyth_oracle_pubkey: params.pyth_oracle_pubkey, + switchboard_oracle_pubkey: params.switchboard_oracle_pubkey, + available_amount: 0, + borrowed_amount_wads: Decimal::zero(), + cumulative_borrow_rate_wads: Decimal::one(), + accumulated_protocol_fees_wads: Decimal::zero(), + market_price: params.market_price, + smoothed_market_price: params.smoothed_market_price, + } + } + + /// Calculate the total reserve supply including active loans + pub fn total_supply(&self) -> Result { + Decimal::from(self.available_amount) + .try_add(self.borrowed_amount_wads)? + .try_sub(self.accumulated_protocol_fees_wads) + } + + /// Add liquidity to available amount + pub fn deposit(&mut self, liquidity_amount: u64) -> ProgramResult { + self.available_amount = self + .available_amount + .checked_add(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } + + /// Remove liquidity from available amount + pub fn withdraw(&mut self, liquidity_amount: u64) -> ProgramResult { + if liquidity_amount > self.available_amount { + msg!("Withdraw amount cannot exceed available amount"); + return Err(LendingError::InsufficientLiquidity.into()); + } + self.available_amount = self + .available_amount + .checked_sub(liquidity_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } + + /// Subtract borrow amount from available liquidity and add to borrows + pub fn borrow(&mut self, borrow_decimal: Decimal) -> ProgramResult { + let borrow_amount = borrow_decimal.try_floor_u64()?; + if borrow_amount > self.available_amount { + msg!("Borrow amount cannot exceed available amount"); + return Err(LendingError::InsufficientLiquidity.into()); + } + + self.available_amount = self + .available_amount + .checked_sub(borrow_amount) + .ok_or(LendingError::MathOverflow)?; + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_decimal)?; + + Ok(()) + } + + /// Add repay amount to available liquidity and subtract settle amount from total borrows + pub fn repay(&mut self, repay_amount: u64, settle_amount: Decimal) -> ProgramResult { + self.available_amount = self + .available_amount + .checked_add(repay_amount) + .ok_or(LendingError::MathOverflow)?; + let safe_settle_amount = settle_amount.min(self.borrowed_amount_wads); + self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(safe_settle_amount)?; + + Ok(()) + } + + /// Forgive bad debt. This essentially socializes the loss across all ctoken holders of + /// this reserve. + pub fn forgive_debt(&mut self, liquidity_amount: Decimal) -> ProgramResult { + self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(liquidity_amount)?; + + Ok(()) + } + + /// Subtract settle amount from accumulated_protocol_fees_wads and withdraw_amount from available liquidity + pub fn redeem_fees(&mut self, withdraw_amount: u64) -> ProgramResult { + self.available_amount = self + .available_amount + .checked_sub(withdraw_amount) + .ok_or(LendingError::MathOverflow)?; + self.accumulated_protocol_fees_wads = self + .accumulated_protocol_fees_wads + .try_sub(Decimal::from(withdraw_amount))?; + + Ok(()) + } + + /// Calculate the liquidity utilization rate of the reserve + pub fn utilization_rate(&self) -> Result { + let total_supply = self.total_supply()?; + if total_supply == Decimal::zero() || self.borrowed_amount_wads == Decimal::zero() { + return Ok(Rate::zero()); + } + let denominator = self + .borrowed_amount_wads + .try_add(Decimal::from(self.available_amount))?; + self.borrowed_amount_wads.try_div(denominator)?.try_into() + } + + /// Compound current borrow rate over elapsed slots + fn compound_interest( + &mut self, + current_borrow_rate: Rate, + slots_elapsed: u64, + take_rate: Rate, + ) -> ProgramResult { + let slot_interest_rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?; + let compounded_interest_rate = Rate::one() + .try_add(slot_interest_rate)? + .try_pow(slots_elapsed)?; + self.cumulative_borrow_rate_wads = self + .cumulative_borrow_rate_wads + .try_mul(compounded_interest_rate)?; + + let net_new_debt = self + .borrowed_amount_wads + .try_mul(compounded_interest_rate)? + .try_sub(self.borrowed_amount_wads)?; + + self.accumulated_protocol_fees_wads = net_new_debt + .try_mul(take_rate)? + .try_add(self.accumulated_protocol_fees_wads)?; + + self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(net_new_debt)?; + Ok(()) + } +} + +/// Create a new reserve liquidity +pub struct NewReserveLiquidityParams { + /// Reserve liquidity mint address + pub mint_pubkey: Pubkey, + /// Reserve liquidity mint decimals + pub mint_decimals: u8, + /// Reserve liquidity supply address + pub supply_pubkey: Pubkey, + /// Reserve liquidity pyth oracle account + pub pyth_oracle_pubkey: Pubkey, + /// Reserve liquidity switchboard oracle account + pub switchboard_oracle_pubkey: Pubkey, + /// Reserve liquidity market price in quote currency + pub market_price: Decimal, + /// Smoothed reserve liquidity market price in quote currency + pub smoothed_market_price: Decimal, +} + +/// Reserve collateral +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ReserveCollateral { + /// Reserve collateral mint address + pub mint_pubkey: Pubkey, + /// Reserve collateral mint supply, used for exchange rate + pub mint_total_supply: u64, + /// Reserve collateral supply address + pub supply_pubkey: Pubkey, +} + +impl ReserveCollateral { + /// Create a new reserve collateral + pub fn new(params: NewReserveCollateralParams) -> Self { + Self { + mint_pubkey: params.mint_pubkey, + mint_total_supply: 0, + supply_pubkey: params.supply_pubkey, + } + } + + /// Add collateral to total supply + pub fn mint(&mut self, collateral_amount: u64) -> ProgramResult { + self.mint_total_supply = self + .mint_total_supply + .checked_add(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } + + /// Remove collateral from total supply + pub fn burn(&mut self, collateral_amount: u64) -> ProgramResult { + self.mint_total_supply = self + .mint_total_supply + .checked_sub(collateral_amount) + .ok_or(LendingError::MathOverflow)?; + Ok(()) + } + + /// Return the current collateral exchange rate. + fn exchange_rate( + &self, + total_liquidity: Decimal, + ) -> Result { + let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() { + Rate::from_scaled_val(INITIAL_COLLATERAL_RATE) + } else { + let mint_total_supply = Decimal::from(self.mint_total_supply); + Rate::try_from(mint_total_supply.try_div(total_liquidity)?)? + }; + + Ok(CollateralExchangeRate(rate)) + } +} + +/// Create a new reserve collateral +pub struct NewReserveCollateralParams { + /// Reserve collateral mint address + pub mint_pubkey: Pubkey, + /// Reserve collateral supply address + pub supply_pubkey: Pubkey, +} + +/// Collateral exchange rate +#[derive(Clone, Copy, Debug)] +pub struct CollateralExchangeRate(Rate); + +impl CollateralExchangeRate { + /// Convert reserve collateral to liquidity + pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result { + self.decimal_collateral_to_liquidity(collateral_amount.into())? + .try_floor_u64() + } + + /// Convert reserve collateral to liquidity + pub fn decimal_collateral_to_liquidity( + &self, + collateral_amount: Decimal, + ) -> Result { + collateral_amount.try_div(self.0) + } + + /// Convert reserve liquidity to collateral + pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> Result { + self.decimal_liquidity_to_collateral(liquidity_amount.into())? + .try_floor_u64() + } + + /// Convert reserve liquidity to collateral + pub fn decimal_liquidity_to_collateral( + &self, + liquidity_amount: Decimal, + ) -> Result { + liquidity_amount.try_mul(self.0) + } +} + +impl From for Rate { + fn from(exchange_rate: CollateralExchangeRate) -> Self { + exchange_rate.0 + } +} + +/// Reserve configuration values +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReserveConfig { + /// Optimal utilization rate, as a percentage + pub optimal_utilization_rate: u8, + /// Unhealthy utilization rate, as a percentage + pub max_utilization_rate: u8, + /// Target ratio of the value of borrows to deposits, as a percentage + /// 0 if use as collateral is disabled + pub loan_to_value_ratio: u8, + /// The minimum bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage + pub liquidation_bonus: u8, + /// The maximum bonus a liquidator gets when repaying part of an unhealthy obligation, as a percentage + pub max_liquidation_bonus: u8, + /// Loan to value ratio at which an obligation can be liquidated, as a percentage + pub liquidation_threshold: u8, + /// Loan to value ratio at which the obligation can be liquidated for the maximum bonus + pub max_liquidation_threshold: u8, + /// Min borrow APY + pub min_borrow_rate: u8, + /// Optimal (utilization) borrow APY + pub optimal_borrow_rate: u8, + /// Max borrow APY + pub max_borrow_rate: u8, + /// Supermax borrow APY + pub super_max_borrow_rate: u64, + /// Program owner fees assessed, separate from gains due to interest accrual + pub fees: ReserveFees, + /// Maximum deposit limit of liquidity in native units, u64::MAX for inf + pub deposit_limit: u64, + /// Borrows disabled + pub borrow_limit: u64, + /// Reserve liquidity fee receiver address + pub fee_receiver: Pubkey, + /// Cut of the liquidation bonus that the protocol receives, in deca bps + pub protocol_liquidation_fee: u8, + /// Protocol take rate is the amount borrowed interest protocol recieves, as a percentage + pub protocol_take_rate: u8, + /// Added borrow weight in basis points. THIS FIELD SHOULD NEVER BE USED DIRECTLY. Always use + /// borrow_weight() + pub added_borrow_weight_bps: u64, + /// Type of the reserve (Regular, Isolated) + pub reserve_type: ReserveType, +} + +/// validates reserve configs +#[inline(always)] +pub 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.max_utilization_rate < config.optimal_utilization_rate + || config.max_utilization_rate > 100 + { + msg!("Unhealthy utilization rate must be in range [optimal_utilization_rate, 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.max_liquidation_bonus < config.liquidation_bonus || config.max_liquidation_bonus > 100 + { + msg!("Max liquidation bonus must be in range [liquidation_bonus, 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.max_liquidation_threshold < config.liquidation_threshold + || config.max_liquidation_threshold > 100 + { + msg!("Max liquidation threshold must be in range [liquidation threshold, 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.super_max_borrow_rate < config.max_borrow_rate as u64 { + msg!("Super max 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()); + } + if config.protocol_liquidation_fee > MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS { + msg!( + "Protocol liquidation fee must be in range [0, {}] deca bps", + MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS + ); + return Err(LendingError::InvalidConfig.into()); + } + if config.max_liquidation_bonus as u64 * 100 + config.protocol_liquidation_fee as u64 * 10 + > MAX_BONUS_PCT as u64 * 100 + { + msg!( + "Max liquidation bonus + protocol liquidation fee must be in pct range [0, {}]", + MAX_BONUS_PCT + ); + return Err(LendingError::InvalidConfig.into()); + } + if config.protocol_take_rate > 100 { + msg!("Protocol take rate must be in range [0, 100]"); + return Err(LendingError::InvalidConfig.into()); + } + + if config.reserve_type == ReserveType::Isolated + && !(config.loan_to_value_ratio == 0 && config.liquidation_threshold == 0) + { + msg!("open/close LTV must be 0 for isolated reserves"); + return Err(LendingError::InvalidConfig.into()); + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, FromPrimitive)] +/// Asset Type of the reserve +pub enum ReserveType { + #[default] + /// this asset can be used as collateral + Regular = 0, + /// this asset cannot be used as collateral and can only be borrowed in isolation + Isolated = 1, +} + +impl FromStr for ReserveType { + type Err = ProgramError; + fn from_str(input: &str) -> Result { + match input { + "Regular" => Ok(ReserveType::Regular), + "Isolated" => Ok(ReserveType::Isolated), + _ => Err(LendingError::InvalidConfig.into()), + } + } +} + +/// Additional fee information on a reserve +/// +/// These exist separately from interest accrual fees, and are specifically for the program owner +/// and frontend host. The fees are paid out as a percentage of liquidity token amounts during +/// repayments and liquidations. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReserveFees { + /// Fee assessed on `BorrowObligationLiquidity`, expressed as a Wad. + /// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for + /// clarity: + /// 1% = 10_000_000_000_000_000 + /// 0.01% (1 basis point) = 100_000_000_000_000 + /// 0.00001% (Aave borrow fee) = 100_000_000_000 + pub borrow_fee_wad: u64, + /// Fee for flash loan, expressed as a Wad. + /// 0.3% (Aave flash loan fee) = 3_000_000_000_000_000 + pub flash_loan_fee_wad: u64, + /// Amount of fee going to host account, if provided in liquidate and repay + pub host_fee_percentage: u8, +} + +impl ReserveFees { + /// Calculate the owner and host fees on borrow + pub fn calculate_borrow_fees( + &self, + borrow_amount: Decimal, + fee_calculation: FeeCalculation, + ) -> Result<(u64, u64), ProgramError> { + self.calculate_fees(borrow_amount, self.borrow_fee_wad, fee_calculation) + } + + /// Calculate the owner and host fees on flash loan + pub fn calculate_flash_loan_fees( + &self, + flash_loan_amount: Decimal, + ) -> Result<(u64, u64), ProgramError> { + let (total_fees, host_fee) = self.calculate_fees( + flash_loan_amount, + self.flash_loan_fee_wad, + FeeCalculation::Exclusive, + )?; + + let origination_fee = total_fees + .checked_sub(host_fee) + .ok_or(LendingError::MathOverflow)?; + Ok((origination_fee, host_fee)) + } + + fn calculate_fees( + &self, + amount: Decimal, + fee_wad: u64, + fee_calculation: FeeCalculation, + ) -> Result<(u64, u64), ProgramError> { + let borrow_fee_rate = Rate::from_scaled_val(fee_wad); + let host_fee_rate = Rate::from_percent(self.host_fee_percentage); + if borrow_fee_rate > Rate::zero() && amount > Decimal::zero() { + let need_to_assess_host_fee = host_fee_rate > Rate::zero(); + let minimum_fee = if need_to_assess_host_fee { + 2u64 // 1 token to owner, 1 to host + } else { + 1u64 // 1 token to owner, nothing else + }; + + let borrow_fee_amount = match fee_calculation { + // Calculate fee to be added to borrow: fee = amount * rate + FeeCalculation::Exclusive => amount.try_mul(borrow_fee_rate)?, + // Calculate fee to be subtracted from borrow: fee = amount * (rate / (rate + 1)) + FeeCalculation::Inclusive => { + let borrow_fee_rate = + borrow_fee_rate.try_div(borrow_fee_rate.try_add(Rate::one())?)?; + amount.try_mul(borrow_fee_rate)? + } + }; + + let borrow_fee_decimal = borrow_fee_amount.max(minimum_fee.into()); + if borrow_fee_decimal >= amount { + msg!("Borrow amount is too small to receive liquidity after fees"); + return Err(LendingError::BorrowTooSmall.into()); + } + + let borrow_fee = borrow_fee_decimal.try_round_u64()?; + let host_fee = if need_to_assess_host_fee { + borrow_fee_decimal + .try_mul(host_fee_rate)? + .try_round_u64()? + .max(1u64) + } else { + 0 + }; + + Ok((borrow_fee, host_fee)) + } else { + Ok((0, 0)) + } + } +} + +/// Calculate fees exlusive or inclusive of an amount +pub enum FeeCalculation { + /// Fee added to amount: fee = rate * amount + Exclusive, + /// Fee included in amount: fee = (rate / (1 + rate)) * amount + Inclusive, +} + +impl Sealed for Reserve {} +impl IsInitialized for Reserve { + fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +const RESERVE_LEN: usize = 619; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 8 + 8 + 32 + 1 + 1 + 16 + 230 +impl Pack for Reserve { + const LEN: usize = RESERVE_LEN; + + // @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, RESERVE_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + last_update_slot, + last_update_stale, + lending_market, + liquidity_mint_pubkey, + liquidity_mint_decimals, + liquidity_supply_pubkey, + liquidity_pyth_oracle_pubkey, + liquidity_switchboard_oracle_pubkey, + liquidity_available_amount, + liquidity_borrowed_amount_wads, + liquidity_cumulative_borrow_rate_wads, + liquidity_market_price, + collateral_mint_pubkey, + collateral_mint_total_supply, + collateral_supply_pubkey, + config_optimal_utilization_rate, + config_loan_to_value_ratio, + config_liquidation_bonus, + config_liquidation_threshold, + config_min_borrow_rate, + config_optimal_borrow_rate, + config_max_borrow_rate, + config_fees_borrow_fee_wad, + config_fees_flash_loan_fee_wad, + config_fees_host_fee_percentage, + config_deposit_limit, + config_borrow_limit, + config_fee_receiver, + config_protocol_liquidation_fee, + config_protocol_take_rate, + liquidity_accumulated_protocol_fees_wads, + rate_limiter, + config_added_borrow_weight_bps, + liquidity_smoothed_market_price, + config_asset_type, + config_max_utilization_rate, + config_super_max_borrow_rate, + config_max_liquidation_bonus, + config_max_liquidation_threshold, + _padding, + ) = mut_array_refs![ + output, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8, + 16, + 16, + 16, + PUBKEY_BYTES, + 8, + PUBKEY_BYTES, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 8, + 8, + 1, + 8, + 8, + PUBKEY_BYTES, + 1, + 1, + 16, + RATE_LIMITER_LEN, + 8, + 16, + 1, + 1, + 8, + 1, + 1, + 138 + ]; + + // reserve + *version = self.version.to_le_bytes(); + *last_update_slot = self.last_update.slot.to_le_bytes(); + pack_bool(self.last_update.stale, last_update_stale); + lending_market.copy_from_slice(self.lending_market.as_ref()); + + // liquidity + liquidity_mint_pubkey.copy_from_slice(self.liquidity.mint_pubkey.as_ref()); + *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes(); + liquidity_supply_pubkey.copy_from_slice(self.liquidity.supply_pubkey.as_ref()); + liquidity_pyth_oracle_pubkey.copy_from_slice(self.liquidity.pyth_oracle_pubkey.as_ref()); + liquidity_switchboard_oracle_pubkey + .copy_from_slice(self.liquidity.switchboard_oracle_pubkey.as_ref()); + *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes(); + pack_decimal( + self.liquidity.borrowed_amount_wads, + liquidity_borrowed_amount_wads, + ); + pack_decimal( + self.liquidity.cumulative_borrow_rate_wads, + liquidity_cumulative_borrow_rate_wads, + ); + pack_decimal( + self.liquidity.accumulated_protocol_fees_wads, + liquidity_accumulated_protocol_fees_wads, + ); + pack_decimal(self.liquidity.market_price, liquidity_market_price); + pack_decimal( + self.liquidity.smoothed_market_price, + liquidity_smoothed_market_price, + ); + + // collateral + collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref()); + *collateral_mint_total_supply = self.collateral.mint_total_supply.to_le_bytes(); + collateral_supply_pubkey.copy_from_slice(self.collateral.supply_pubkey.as_ref()); + + // config + *config_optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes(); + *config_max_utilization_rate = self.config.max_utilization_rate.to_le_bytes(); + *config_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes(); + *config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes(); + *config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes(); + *config_min_borrow_rate = self.config.min_borrow_rate.to_le_bytes(); + *config_optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes(); + *config_max_borrow_rate = self.config.max_borrow_rate.to_le_bytes(); + *config_super_max_borrow_rate = self.config.super_max_borrow_rate.to_le_bytes(); + *config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes(); + *config_fees_flash_loan_fee_wad = self.config.fees.flash_loan_fee_wad.to_le_bytes(); + *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes(); + *config_deposit_limit = self.config.deposit_limit.to_le_bytes(); + *config_borrow_limit = self.config.borrow_limit.to_le_bytes(); + config_fee_receiver.copy_from_slice(self.config.fee_receiver.as_ref()); + *config_protocol_liquidation_fee = self.config.protocol_liquidation_fee.to_le_bytes(); + *config_protocol_take_rate = self.config.protocol_take_rate.to_le_bytes(); + *config_asset_type = (self.config.reserve_type as u8).to_le_bytes(); + + self.rate_limiter.pack_into_slice(rate_limiter); + + *config_added_borrow_weight_bps = self.config.added_borrow_weight_bps.to_le_bytes(); + *config_max_liquidation_bonus = self.config.max_liquidation_bonus.to_le_bytes(); + *config_max_liquidation_threshold = self.config.max_liquidation_threshold.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, RESERVE_LEN]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + version, + last_update_slot, + last_update_stale, + lending_market, + liquidity_mint_pubkey, + liquidity_mint_decimals, + liquidity_supply_pubkey, + liquidity_pyth_oracle_pubkey, + liquidity_switchboard_oracle_pubkey, + liquidity_available_amount, + liquidity_borrowed_amount_wads, + liquidity_cumulative_borrow_rate_wads, + liquidity_market_price, + collateral_mint_pubkey, + collateral_mint_total_supply, + collateral_supply_pubkey, + config_optimal_utilization_rate, + config_loan_to_value_ratio, + config_liquidation_bonus, + config_liquidation_threshold, + config_min_borrow_rate, + config_optimal_borrow_rate, + config_max_borrow_rate, + config_fees_borrow_fee_wad, + config_fees_flash_loan_fee_wad, + config_fees_host_fee_percentage, + config_deposit_limit, + config_borrow_limit, + config_fee_receiver, + config_protocol_liquidation_fee, + config_protocol_take_rate, + liquidity_accumulated_protocol_fees_wads, + rate_limiter, + config_added_borrow_weight_bps, + liquidity_smoothed_market_price, + config_asset_type, + config_max_utilization_rate, + config_super_max_borrow_rate, + config_max_liquidation_bonus, + config_max_liquidation_threshold, + _padding, + ) = array_refs![ + input, + 1, + 8, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + 1, + PUBKEY_BYTES, + PUBKEY_BYTES, + PUBKEY_BYTES, + 8, + 16, + 16, + 16, + PUBKEY_BYTES, + 8, + PUBKEY_BYTES, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 8, + 8, + 1, + 8, + 8, + PUBKEY_BYTES, + 1, + 1, + 16, + RATE_LIMITER_LEN, + 8, + 16, + 1, + 1, + 8, + 1, + 1, + 138 + ]; + + let version = u8::from_le_bytes(*version); + if version > PROGRAM_VERSION { + msg!("Reserve version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + + let optimal_utilization_rate = u8::from_le_bytes(*config_optimal_utilization_rate); + let max_borrow_rate = u8::from_le_bytes(*config_max_borrow_rate); + + // on program upgrade, the max_* values are zero, so we need to safely account for that. + let liquidation_bonus = u8::from_le_bytes(*config_liquidation_bonus); + let max_liquidation_bonus = max( + liquidation_bonus, + u8::from_le_bytes(*config_max_liquidation_bonus), + ); + let liquidation_threshold = u8::from_le_bytes(*config_liquidation_threshold); + let max_liquidation_threshold = max( + liquidation_threshold, + u8::from_le_bytes(*config_max_liquidation_threshold), + ); + + Ok(Self { + version, + last_update: LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + stale: unpack_bool(last_update_stale)?, + }, + lending_market: Pubkey::new_from_array(*lending_market), + liquidity: ReserveLiquidity { + mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey), + mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), + supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey), + pyth_oracle_pubkey: Pubkey::new_from_array(*liquidity_pyth_oracle_pubkey), + switchboard_oracle_pubkey: Pubkey::new_from_array( + *liquidity_switchboard_oracle_pubkey, + ), + available_amount: u64::from_le_bytes(*liquidity_available_amount), + borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), + cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), + accumulated_protocol_fees_wads: unpack_decimal( + liquidity_accumulated_protocol_fees_wads, + ), + market_price: unpack_decimal(liquidity_market_price), + smoothed_market_price: unpack_decimal(liquidity_smoothed_market_price), + }, + collateral: ReserveCollateral { + mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), + mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply), + supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey), + }, + config: ReserveConfig { + optimal_utilization_rate, + max_utilization_rate: max( + optimal_utilization_rate, + u8::from_le_bytes(*config_max_utilization_rate), + ), + loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), + liquidation_bonus, + max_liquidation_bonus, + liquidation_threshold, + max_liquidation_threshold, + min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate), + optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate), + max_borrow_rate, + super_max_borrow_rate: max( + max_borrow_rate as u64, + u64::from_le_bytes(*config_super_max_borrow_rate), + ), + fees: ReserveFees { + borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), + flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad), + host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), + }, + deposit_limit: u64::from_le_bytes(*config_deposit_limit), + borrow_limit: u64::from_le_bytes(*config_borrow_limit), + fee_receiver: Pubkey::new_from_array(*config_fee_receiver), + protocol_liquidation_fee: min( + u8::from_le_bytes(*config_protocol_liquidation_fee), + // the behaviour of this variable changed in v2.0.2 and now represents a + // fraction of the total liquidation value that the protocol receives as + // a bonus. Prior to v2.0.2, this variable used to represent a percentage of of + // the liquidator's bonus that would be sent to the protocol. For safety, we + // cap the value here to MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS. + MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS, + ), + protocol_take_rate: u8::from_le_bytes(*config_protocol_take_rate), + added_borrow_weight_bps: u64::from_le_bytes(*config_added_borrow_weight_bps), + reserve_type: ReserveType::from_u8(config_asset_type[0]).unwrap(), + }, + rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, + }) + } +}